Skip to main content

loftspec_store/
file.rs

1//! The file store: the document held as the `spec/` tree under a directory, read through the
2//! codec and saved through the journal.
3
4use std::fs;
5use std::path::{Path, PathBuf};
6
7use loftspec::codec::{self, Files, SpecPath};
8use loftspec::document::Document;
9use loftspec::publish::{LOCK, Lock};
10
11use crate::error::io_error;
12use crate::fs::{SIDECAR, SPEC, hidden};
13use crate::journal::{self, Changes, RootPath};
14use crate::{DocumentStore, Saved, StoreError};
15
16/// The `spec/` tree under a directory.
17#[derive(Debug, Clone)]
18pub struct FileStore {
19    root: PathBuf,
20}
21
22impl FileStore {
23    /// The store of the tree under `root`, after finishing a committed save there that was
24    /// interrupted, or discarding an uncommitted one.
25    ///
26    /// # Errors
27    ///
28    /// An interrupted save that cannot be recovered.
29    pub fn open(root: &Path) -> Result<Self, StoreError> {
30        journal::recover(root)?;
31        Ok(Self {
32            root: root.to_owned(),
33        })
34    }
35
36    /// Every file of the tree, by its path under `spec/`.
37    ///
38    /// # Errors
39    ///
40    /// A missing `spec/` directory, a file that cannot be read, or a name that is not UTF-8.
41    pub fn read_files(&self) -> Result<Files, StoreError> {
42        let spec = self.root.join(SPEC);
43        if !spec.is_dir() {
44            return Err(StoreError::NoTree(spec));
45        }
46        let mut files = Files::new();
47        collect(&spec, "", &mut files)?;
48        Ok(files)
49    }
50
51    /// Writes the given files of the tree in one save and removes none.
52    ///
53    /// # Errors
54    ///
55    /// A file that cannot be staged or put in place.
56    pub fn write_files(&self, files: Files) -> Result<(), StoreError> {
57        journal::save(&self.root, &Changes::of_tree(files, &[]))
58    }
59
60    /// Writes an imported tree into an empty or missing `spec/`, and the sidecar beside it, in
61    /// one save.
62    ///
63    /// # Errors
64    ///
65    /// `spec/` holds a file or directory already (RQ-134), in which case nothing is written; or a
66    /// file cannot be written.
67    pub fn import(&self, files: Files, sidecar: &[u8]) -> Result<(), StoreError> {
68        let spec = self.root.join(SPEC);
69        if spec.exists() && holds_entries(&spec)? {
70            return Err(StoreError::NotEmpty(spec));
71        }
72        fs::create_dir_all(&spec).map_err(io_error(&spec))?;
73        self.write_with_beside(files, (SIDECAR, sidecar.to_vec()))
74    }
75
76    /// Writes a new tree into a `spec/` that does not exist yet, in one save.
77    ///
78    /// # Errors
79    ///
80    /// `spec/` exists already, in which case nothing is written; or a file cannot be written.
81    pub fn create(&self, files: Files) -> Result<(), StoreError> {
82        let spec = self.root.join(SPEC);
83        if spec.exists() {
84            return Err(StoreError::Exists(spec));
85        }
86        // The save creates `spec/` with its files, so an interrupted save leaves none behind.
87        self.write_files(files)
88    }
89
90    /// The lock beside `spec/`, or none.
91    ///
92    /// # Errors
93    ///
94    /// The lock is there but cannot be read, or is not a valid lock record.
95    pub fn lock(&self) -> Result<Option<Lock>, StoreError> {
96        let path = self.root.join(LOCK);
97        let bytes = match fs::read(&path) {
98            Ok(bytes) => bytes,
99            Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
100            Err(error) => return Err(io_error(&path)(error)),
101        };
102        Lock::read(&bytes)
103            .map(Some)
104            .map_err(|error| StoreError::Lock(path, error))
105    }
106
107    /// Writes what a publish gives in one save: the files under `spec/` it changes, and the lock
108    /// beside `spec/`; all of them or none (AC-463).
109    ///
110    /// # Errors
111    ///
112    /// A file cannot be staged or put in place.
113    pub fn publish(&self, files: Files, lock: &Lock) -> Result<(), StoreError> {
114        self.write_with_beside(files, (LOCK, lock.to_bytes()))
115    }
116
117    /// Writes files of the tree and one file beside it in one save, removing none.
118    fn write_with_beside(
119        &self,
120        files: Files,
121        (name, bytes): (&str, Vec<u8>),
122    ) -> Result<(), StoreError> {
123        let mut changes = Changes::of_tree(files, &[]);
124        changes.write.insert(RootPath::beside(name), bytes);
125        journal::save(&self.root, &changes)
126    }
127}
128
129impl DocumentStore for FileStore {
130    fn load(&self) -> Result<Document, StoreError> {
131        codec::read(&self.read_files()?).map_err(StoreError::Read)
132    }
133
134    /// Writes each file whose bytes change and removes each file the document no longer has, in
135    /// one save; a file whose bytes would not change is left alone. A save of this store's that
136    /// failed part-way is finished first, so the tree it compares against is the one it left.
137    fn save(&mut self, document: &Document) -> Result<Saved, StoreError> {
138        journal::recover(&self.root)?;
139        let current = match self.read_files() {
140            Ok(files) => files,
141            Err(StoreError::NoTree(_)) => Files::new(),
142            Err(error) => return Err(error),
143        };
144        let mut next = codec::write(document);
145        let saved = Saved::between(&current, &next);
146        next.retain(|path, _| saved.written.binary_search(path).is_ok());
147        journal::save(&self.root, &Changes::of_tree(next, &saved.removed))?;
148        Ok(saved)
149    }
150}
151
152/// Whether `dir` holds an entry that is not hidden.
153fn holds_entries(dir: &Path) -> Result<bool, StoreError> {
154    for entry in fs::read_dir(dir).map_err(io_error(dir))? {
155        let entry = entry.map_err(io_error(dir))?;
156        if !entry.file_name().to_str().is_some_and(hidden) {
157            return Ok(true);
158        }
159    }
160    Ok(false)
161}
162
163fn collect(dir: &Path, prefix: &str, files: &mut Files) -> Result<(), StoreError> {
164    for entry in fs::read_dir(dir).map_err(io_error(dir))? {
165        let entry = entry.map_err(io_error(dir))?;
166        let path = entry.path();
167        let name = entry
168            .file_name()
169            .into_string()
170            .map_err(|_| StoreError::NotUtf8(path.clone()))?;
171        if hidden(&name) {
172            continue;
173        }
174        let relative = if prefix.is_empty() {
175            name
176        } else {
177            format!("{prefix}/{name}")
178        };
179        if path.is_dir() {
180            collect(&path, &relative, files)?;
181        } else {
182            let bytes = fs::read(&path).map_err(io_error(&path))?;
183            files.insert(SpecPath::new(&relative), bytes);
184        }
185    }
186    Ok(())
187}