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