1use 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#[derive(Debug, Clone)]
18pub struct FileStore {
19 root: PathBuf,
20}
21
22impl FileStore {
23 pub fn open(root: &Path) -> Result<Self, StoreError> {
30 journal::recover(root)?;
31 Ok(Self {
32 root: root.to_owned(),
33 })
34 }
35
36 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 pub fn write_files(&self, files: Files) -> Result<(), StoreError> {
57 journal::save(&self.root, &Changes::of_tree(files, &[]))
58 }
59
60 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 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 self.write_files(files)
88 }
89
90 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 pub fn publish(&self, files: Files, lock: &Lock) -> Result<(), StoreError> {
114 self.write_with_beside(files, (LOCK, lock.to_bytes()))
115 }
116
117 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 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(¤t, &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
152fn 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}