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.
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.
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.
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}