1use std::fs;
5use std::path::{Path, PathBuf};
6
7use loftspec::codec::{self, Files, SpecPath};
8use loftspec::document::Document;
9use loftspec::publish::{LOCK, Lock};
10use loftspec::trace::{TRACE, TestFile, TestFiles};
11
12use crate::error::io_error;
13use crate::fs::{SIDECAR, SPEC, hidden};
14use crate::journal::{self, Changes, RootPath};
15use crate::{DocumentStore, Saved, StoreError};
16
17#[derive(Debug, Clone)]
19pub struct FileStore {
20 root: PathBuf,
21}
22
23impl FileStore {
24 pub fn open(root: &Path) -> Result<Self, StoreError> {
31 journal::recover(root)?;
32 Ok(Self {
33 root: root.to_owned(),
34 })
35 }
36
37 pub fn read_files(&self) -> Result<Files, StoreError> {
43 let spec = self.root.join(SPEC);
44 if !spec.is_dir() {
45 return Err(StoreError::NoTree(spec));
46 }
47 let mut files = Files::new();
48 collect(&spec, "", &mut files)?;
49 Ok(files)
50 }
51
52 pub fn write_files(&self, files: Files) -> Result<(), StoreError> {
58 journal::save(&self.root, &Changes::of_tree(files, &[]))
59 }
60
61 pub fn import(&self, files: Files, sidecar: &[u8]) -> Result<(), StoreError> {
69 let spec = self.root.join(SPEC);
70 if spec.exists() && holds_entries(&spec)? {
71 return Err(StoreError::NotEmpty(spec));
72 }
73 fs::create_dir_all(&spec).map_err(io_error(&spec))?;
74 self.write_with_beside(files, (SIDECAR, sidecar.to_vec()))
75 }
76
77 pub fn create(&self, files: Files) -> Result<(), StoreError> {
83 let spec = self.root.join(SPEC);
84 if spec.exists() {
85 return Err(StoreError::Exists(spec));
86 }
87 self.write_files(files)
89 }
90
91 pub fn lock(&self) -> Result<Option<Lock>, StoreError> {
97 let Some(bytes) = self.beside(LOCK)? else {
98 return Ok(None);
99 };
100 Lock::read(&bytes)
101 .map(Some)
102 .map_err(|error| StoreError::Lock(self.root.join(LOCK), error))
103 }
104
105 pub fn trace(&self) -> Result<Option<Vec<u8>>, StoreError> {
111 self.beside(TRACE)
112 }
113
114 pub fn write_trace(&self, text: String) -> Result<(), StoreError> {
120 self.write_with_beside(Files::new(), (TRACE, text.into_bytes()))
121 }
122
123 #[must_use]
125 pub fn test_files(&self) -> ProjectFiles {
126 ProjectFiles {
127 root: self.root.clone(),
128 }
129 }
130
131 fn beside(&self, name: &str) -> Result<Option<Vec<u8>>, StoreError> {
133 let path = self.root.join(name);
134 match fs::read(&path) {
135 Ok(bytes) => Ok(Some(bytes)),
136 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
137 Err(error) => Err(io_error(&path)(error)),
138 }
139 }
140
141 pub fn publish(&self, files: Files, lock: &Lock) -> Result<(), StoreError> {
148 self.write_with_beside(files, (LOCK, lock.to_bytes()))
149 }
150
151 fn write_with_beside(
153 &self,
154 files: Files,
155 (name, bytes): (&str, Vec<u8>),
156 ) -> Result<(), StoreError> {
157 let mut changes = Changes::of_tree(files, &[]);
158 changes.write.insert(RootPath::beside(name), bytes);
159 journal::save(&self.root, &changes)
160 }
161}
162
163#[derive(Debug, Clone)]
165pub struct ProjectFiles {
166 root: PathBuf,
167}
168
169impl TestFiles for ProjectFiles {
170 fn read(&self, path: &str) -> TestFile {
171 match fs::read(self.root.join(path)) {
172 Ok(bytes) => String::from_utf8(bytes).map_or(TestFile::NotText, TestFile::Text),
173 Err(error) if error.kind() == std::io::ErrorKind::NotFound => TestFile::Missing,
174 Err(error) => TestFile::Unreadable(error.to_string()),
175 }
176 }
177}
178
179impl DocumentStore for FileStore {
180 fn load(&self) -> Result<Document, StoreError> {
181 codec::read(&self.read_files()?).map_err(StoreError::Read)
182 }
183
184 fn save(&mut self, document: &Document) -> Result<Saved, StoreError> {
188 journal::recover(&self.root)?;
189 let current = match self.read_files() {
190 Ok(files) => files,
191 Err(StoreError::NoTree(_)) => Files::new(),
192 Err(error) => return Err(error),
193 };
194 let mut next = codec::write(document);
195 let saved = Saved::between(¤t, &next);
196 next.retain(|path, _| saved.written.binary_search(path).is_ok());
197 journal::save(&self.root, &Changes::of_tree(next, &saved.removed))?;
198 Ok(saved)
199 }
200}
201
202fn holds_entries(dir: &Path) -> Result<bool, StoreError> {
204 for entry in fs::read_dir(dir).map_err(io_error(dir))? {
205 let entry = entry.map_err(io_error(dir))?;
206 if !entry.file_name().to_str().is_some_and(hidden) {
207 return Ok(true);
208 }
209 }
210 Ok(false)
211}
212
213fn collect(dir: &Path, prefix: &str, files: &mut Files) -> Result<(), StoreError> {
214 for entry in fs::read_dir(dir).map_err(io_error(dir))? {
215 let entry = entry.map_err(io_error(dir))?;
216 let path = entry.path();
217 let name = entry
218 .file_name()
219 .into_string()
220 .map_err(|_| StoreError::NotUtf8(path.clone()))?;
221 if hidden(&name) {
222 continue;
223 }
224 let relative = if prefix.is_empty() {
225 name
226 } else {
227 format!("{prefix}/{name}")
228 };
229 if path.is_dir() {
230 collect(&path, &relative, files)?;
231 } else {
232 let bytes = fs::read(&path).map_err(io_error(&path))?;
233 files.insert(SpecPath::new(&relative), bytes);
234 }
235 }
236 Ok(())
237}