whiskers.git / crates / whiskersd / src / pictures.rs

The master copy of the pictures, as files in one directory named by the picture's id. A picture is written to a temporary name and then linked into place, so a reader never sees half of one and a name already kept is never replaced.

5use std::path::{Path, PathBuf};
6use std::sync::atomic::{AtomicU64, Ordering};
8use log::{debug, error, trace, warn};
9use whiskers_ports::{PictureShelf, Put, SafePictureId, StoreError};
10
11pub struct DirShelf {
12    dir: PathBuf,

Makes temporary names that cannot collide, even between threads.

14    counter: AtomicU64,
15}
17impl DirShelf {
18    pub fn open(dir: &Path) -> Self {
19        if let Err(e) = std::fs::create_dir_all(dir) {
20            error!("cannot create the picture directory {}: {e}", dir.display());
21        }
22        Self { dir: dir.to_owned(), counter: AtomicU64::new(0) }
23    }
24
25    pub fn dir(&self) -> &Path {
26        &self.dir
27    }
28}
29
30impl DirShelf {
31    fn put_in_place(&self, id: &SafePictureId, bytes: &[u8]) -> Result<Put, StoreError> {
32        match std::fs::OpenOptions::new().write(true).create_new(true).open(self.dir.join(id.as_str())) {
33            Ok(mut f) => match std::io::Write::write_all(&mut f, bytes).and_then(|()| f.sync_data()) {
34                Ok(()) => Ok(Put::Stored),
35                Err(e) => {
36                    error!("picture {} not written: {e}", id.as_str());
37                    Err(StoreError::Unavailable)
38                }
39            },
40            Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => Ok(Put::AlreadyKept),
41            Err(e) => {
42                error!("picture {} cannot be created: {e}", id.as_str());
43                Err(StoreError::Unavailable)
44            }
45        }
46    }
47}
48
49impl PictureShelf for DirShelf {
50    async fn has(&self, id: &SafePictureId) -> Result<bool, StoreError> {
51        Ok(self.dir.join(id.as_str()).is_file())
52    }
53
54    async fn put(&self, id: &SafePictureId, bytes: &[u8]) -> Result<Put, StoreError> {
55        let target = self.dir.join(id.as_str());
56        // A name that starts with a dot is never a picture id, so this cannot be taken for one.
57        let temp = self.dir.join(format!(".put-{}-{}", std::process::id(), self.counter.fetch_add(1, Ordering::SeqCst)));
58        let written = std::fs::File::create(&temp).and_then(|mut f| std::io::Write::write_all(&mut f, bytes).and_then(|()| f.sync_data()));
59        if let Err(e) = written {
60            error!("picture {} not written: {e}", id.as_str());
61            let _ = std::fs::remove_file(&temp);
62            return Err(StoreError::Unavailable);
63        }
64        let linked = std::fs::hard_link(&temp, &target);
65        let _ = std::fs::remove_file(&temp);
66        match linked {
67            Ok(()) => {
68                debug!("picture {} stored ({} bytes)", id.as_str(), bytes.len());
69                Ok(Put::Stored)
70            }
71            Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
72                trace!("picture {} already kept", id.as_str());
73                Ok(Put::AlreadyKept)
74            }
75            Err(e) => {
76                // A filesystem without hard links: write in place, which never replaces a name but can
77                // be seen half-written by a reader that arrives at that instant.
78                warn!("picture {}: cannot link it into place ({e}); writing it in place", id.as_str());
79                self.put_in_place(id, bytes)
80            }
81        }
82    }
83
84    async fn get(&self, id: &SafePictureId) -> Result<Option<Vec<u8>>, StoreError> {
85        match std::fs::read(self.dir.join(id.as_str())) {
86            Ok(bytes) => {
87                trace!("picture {} read ({} bytes)", id.as_str(), bytes.len());
88                Ok(Some(bytes))
89            }
90            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
91            Err(e) => {
92                error!("picture {} unreadable: {e}", id.as_str());
93                Err(StoreError::Unavailable)
94            }
95        }
96    }
97}