log.rsannotatedlog.rssource163 lines · 6.2 KB · raw
1use std::fs::{self, File, OpenOptions};
2use std::io::Write;
3use std::path::{Path, PathBuf};
4use std::time::{SystemTime, UNIX_EPOCH};
5
6use ::log::{debug, error, trace, warn};
7
8use crate::ports::{Log, LogError, PictureError, Pictures};
9use crate::types::{Entry, Image, PictureId};

One JSON object per line, flushed to disk before append returns, so a turn is never spoken on the strength of a line still in a buffer.

13pub struct JsonlLog {
14    file: File,
15}
17impl JsonlLog {
18    pub fn open(path: &Path) -> Result<Self, LogError> {
19        // A crash can leave a half-written last line. Appending straight after it would glue the
20        // next entry onto the fragment and lose both, so start on a fresh line.
21        debug!("opening the journal {}", path.display());
22        let needs_newline = fs::read(path).map(|b| b.last().is_some_and(|&c| c != b'\n')).unwrap_or(false);
23        let file = OpenOptions::new()
24            .create(true)
25            .append(true)
26            .open(path)
27            .map_err(|e| {
28                error!("cannot open the journal {}: {e}", path.display());
29                LogError(e.to_string())
30            })?;
31        let mut log = Self { file };
32        if needs_newline {
33            warn!("the journal {} ended mid-line (a crash?); starting a fresh line", path.display());
34            log.file.write_all(b"\n").and_then(|()| log.file.sync_data()).map_err(|e| {
35                error!("cannot repair the journal's last line: {e}");
36                LogError(e.to_string())
37            })?;
38        }
39        Ok(log)
40    }
41}
42
43impl Log for JsonlLog {
44    fn append(&mut self, entry: &Entry) -> Result<(), LogError> {
45        let mut line = serde_json::to_string(entry).map_err(|e| {
46            error!("journal entry does not serialize: {e}");
47            LogError(e.to_string())
48        })?;
49        line.push('\n');
50        trace!("journal append of {} bytes", line.len());
51        self.file.write_all(line.as_bytes()).and_then(|()| self.file.sync_data()).map_err(|e| {
52            error!("journal append of {} bytes failed: {e}", line.len());
53            LogError(e.to_string())
54        })
55    }
56}

Pictures as files in one directory, named by time so they sort, never overwriting one.

60pub struct DirPictures {
61    dir: PathBuf,
62}
64impl DirPictures {
65    pub fn open(dir: &Path) -> Result<Self, PictureError> {
66        debug!("picture directory {}", dir.display());
67        fs::create_dir_all(dir).map_err(|e| {
68            error!("cannot create the picture directory {}: {e}", dir.display());
69            PictureError(e.to_string())
70        })?;
71        Ok(Self { dir: dir.to_owned() })
72    }

Whether a picture of this name is kept here. An unsafe name is simply not there.

75    pub fn has(&self, id: &PictureId) -> bool {
76        id.is_safe() && self.dir.join(&id.0).is_file()
77    }
79    pub fn read(&self, id: &PictureId) -> Option<Vec<u8>> {
80        if !id.is_safe() {
81            warn!("refusing to read a picture with an unsafe name ({} bytes)", id.0.len());
82            return None;
83        }
84        match fs::read(self.dir.join(&id.0)) {
85            Ok(b) => {
86                trace!("read picture {} ({} bytes)", id.0, b.len());
87                Some(b)
88            }
89            Err(e) => {
90                debug!("picture {} not readable: {e}", id.0);
91                None
92            }
93        }
94    }

Keeps a picture that came from another device under the name it was given there. Names are made from the time and a counter, so the same name means the same picture.

98    pub fn store(&self, id: &PictureId, bytes: &[u8]) -> Result<(), PictureError> {
99        if !id.is_safe() {
100            warn!("refusing to store a picture with an unsafe name ({} chars)", id.0.len());
101            return Err(PictureError(format!("not a picture name: {:?}", id.0)));
102        }
103        match OpenOptions::new().write(true).create_new(true).open(self.dir.join(&id.0)) {
104            Ok(mut f) => {
105                debug!("storing picture {} ({} bytes)", id.0, bytes.len());
106                f.write_all(bytes).and_then(|()| f.sync_data()).map_err(|e| {
107                    error!("storing picture {} failed: {e}", id.0);
108                    PictureError(e.to_string())
109                })
110            }
111            Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
112                trace!("picture {} already kept", id.0);
113                Ok(())
114            }
115            Err(e) => {
116                error!("cannot create picture {}: {e}", id.0);
117                Err(PictureError(e.to_string()))
118            }
119        }
120    }
122    pub fn path_of(&self, id: &PictureId) -> PathBuf {
123        // An id is only ever a bare file name made here; refuse anything else.
124        self.dir.join(Path::new(&id.0).file_name().unwrap_or_default())
125    }
126}
127
128fn extension(media_type: &str) -> &'static str {
129    match media_type {
130        "image/jpeg" => "jpg",
131        "image/png" => "png",
132        "image/webp" => "webp",
133        "image/gif" => "gif",
134        _ => "img",
135    }
136}
137
138impl Pictures for DirPictures {
139    fn save(&mut self, image: &Image) -> Result<PictureId, PictureError> {
140        let ms = SystemTime::now().duration_since(UNIX_EPOCH).map_or(0, |d| d.as_millis());
141        for n in 0..10_000u32 {
142            let name = format!("{ms:016}-{n:04}.{}", extension(&image.media_type));
143            let path = self.dir.join(&name);
144            match OpenOptions::new().write(true).create_new(true).open(&path) {
145                Ok(mut f) => {
146                    f.write_all(&image.bytes).and_then(|()| f.sync_data()).map_err(|e| {
147                        error!("saving picture {name} failed: {e}");
148                        PictureError(e.to_string())
149                    })?;
150                    debug!("saved picture {name} ({} bytes, {})", image.bytes.len(), image.media_type);
151                    return Ok(PictureId(name));
152                }
153                Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue,
154                Err(e) => {
155                    error!("cannot create picture {name}: {e}");
156                    return Err(PictureError(e.to_string()));
157                }
158            }
159        }
160        error!("no free picture name after 10000 tries");
161        Err(PictureError("could not find a free picture name".into()))
162    }
163}