1use std::fs::{self, File, OpenOptions};
7use std::io::{self, Write};
8use std::path::{Path, PathBuf};
9
10use crate::store::check_leaf;
11use crate::{Files, LogWriteError, SessionError, Store, check_name};
12
13const LOGS: &str = "logs";
14const FILES: &str = "files";
15
16const LOG: &str = ".jsonl";
18
19const PART: &str = ".rig-part";
22
23const TRASH: &str = ".rig-trash";
25
26const LOCK: &str = ".rig-memory.lock";
30
31#[derive(Clone)]
36pub struct DirStore {
37 dir: PathBuf,
38 files: DirFiles,
39}
40
41#[derive(Clone, Debug, PartialEq, Eq)]
45pub struct DirFiles(PathBuf);
46
47impl DirFiles {
48 pub fn new(dir: impl Into<PathBuf>) -> Self {
50 Self(dir.into())
51 }
52
53 pub fn path(&self) -> &Path {
55 &self.0
56 }
57
58 fn file(&self, name: &str) -> io::Result<PathBuf> {
59 check_name(name)?;
60 refuse_part(name)?;
61 Ok(self.0.join(name))
62 }
63}
64
65impl DirStore {
66 pub fn open(dir: impl Into<PathBuf>) -> Result<Self, SessionError> {
74 let dir = dir.into();
75 let old = names(&dir)?
76 .into_iter()
77 .any(|name| name.ends_with(LOG) && dir.join(&name).is_file());
78 if old {
79 return Err(SessionError::OldLayout);
80 }
81 Ok(Self {
82 files: DirFiles::new(dir.join(FILES)),
83 dir,
84 })
85 }
86
87 fn lock(&self) -> io::Result<File> {
89 fs::create_dir_all(&self.dir)?;
90 let lock = OpenOptions::new()
91 .create(true)
92 .write(true)
93 .truncate(false)
94 .open(self.dir.join(LOCK))?;
95 lock.lock()?;
96 Ok(lock)
97 }
98
99 fn log(&self, name: &str) -> io::Result<PathBuf> {
100 check_leaf(name)?;
101 Ok(self.dir.join(LOGS).join(format!("{name}{LOG}")))
102 }
103}
104
105fn refuse_part(name: &str) -> io::Result<()> {
106 if name.ends_with(PART) {
107 let why = format!("{name:?} ends as a file being written does");
108 return Err(io::Error::new(io::ErrorKind::InvalidInput, why));
109 }
110 Ok(())
111}
112
113fn names(dir: &Path) -> io::Result<Vec<String>> {
115 let entries = match fs::read_dir(dir) {
116 Err(failure) if failure.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
117 entries => entries?,
118 };
119 let names = entries.filter_map(|entry| entry.ok()?.file_name().into_string().ok());
120 Ok(names.collect())
121}
122
123fn absent_is_fine(removed: io::Result<()>) -> io::Result<()> {
125 match removed {
126 Err(failure) if failure.kind() == io::ErrorKind::NotFound => Ok(()),
127 removed => removed,
128 }
129}
130
131fn write_atomic(path: &Path, bytes: &[u8]) -> io::Result<()> {
134 let part = write_part(path, bytes, false)?;
135 fs::rename(&part, path)
136}
137
138fn write_durable(path: &Path, bytes: &[u8]) -> io::Result<()> {
142 let part = write_part(path, bytes, true)?;
143 fs::rename(&part, path)?;
144 if let Some(parent) = path.parent() {
145 let _ = File::open(parent).and_then(|folder| folder.sync_all());
146 }
147 Ok(())
148}
149
150fn write_part(path: &Path, bytes: &[u8], sync: bool) -> io::Result<PathBuf> {
153 if let Some(parent) = path.parent() {
154 fs::create_dir_all(parent)?;
155 }
156 let mut part = path.as_os_str().to_owned();
157 part.push(PART);
158 let part = PathBuf::from(part);
159 let mut file = File::create(&part)?;
160 file.write_all(bytes)?;
161 if sync {
162 file.sync_all()?;
163 }
164 Ok(part)
165}
166
167impl Files for DirFiles {
168 fn write_file(&self, name: &str, bytes: &[u8]) -> io::Result<()> {
169 write_atomic(&self.file(name)?, bytes)
170 }
171
172 fn append_file(&self, name: &str, bytes: &[u8]) -> io::Result<()> {
173 let path = self.file(name)?;
174 if let Some(parent) = path.parent() {
175 fs::create_dir_all(parent)?;
176 }
177 let mut file = OpenOptions::new().create(true).append(true).open(path)?;
178 file.write_all(bytes)
179 }
180
181 fn read_file(&self, name: &str) -> io::Result<Vec<u8>> {
182 fs::read(self.file(name)?)
183 }
184
185 fn remove_file(&self, name: &str) -> io::Result<()> {
186 absent_is_fine(fs::remove_file(self.file(name)?))
187 }
188
189 fn files(&self) -> io::Result<Vec<String>> {
190 let mut files = Vec::new();
191 let mut dirs = vec![(self.0.clone(), String::new())];
192 while let Some((dir, prefix)) = dirs.pop() {
193 for name in names(&dir)? {
194 if name.ends_with(PART) {
195 continue;
196 }
197 let path = dir.join(&name);
198 let name = format!("{prefix}{name}");
199 if path.is_dir() {
200 dirs.push((path, format!("{name}/")));
201 } else {
202 files.push(name);
203 }
204 }
205 }
206 Ok(files)
207 }
208}
209
210fn check_len(path: &Path, len: u64) -> Result<(), LogWriteError> {
213 let held = match fs::metadata(path) {
214 Err(failure) if failure.kind() == io::ErrorKind::NotFound => 0,
215 metadata => metadata?.len(),
216 };
217 if held != len {
218 return Err(LogWriteError::Changed);
219 }
220 Ok(())
221}
222
223impl Files for DirStore {
224 fn write_file(&self, name: &str, bytes: &[u8]) -> io::Result<()> {
225 let _lock = self.lock()?;
226 self.files.write_file(name, bytes)
227 }
228
229 fn append_file(&self, name: &str, bytes: &[u8]) -> io::Result<()> {
230 let _lock = self.lock()?;
231 self.files.append_file(name, bytes)
232 }
233
234 fn read_file(&self, name: &str) -> io::Result<Vec<u8>> {
235 self.files.read_file(name)
236 }
237
238 fn remove_file(&self, name: &str) -> io::Result<()> {
239 let _lock = self.lock()?;
240 self.files.remove_file(name)
241 }
242
243 fn files(&self) -> io::Result<Vec<String>> {
244 self.files.files()
245 }
246}
247
248impl Store for DirStore {
249 fn logs(&self) -> io::Result<Vec<String>> {
250 let names = names(&self.dir.join(LOGS))?.into_iter();
251 let logs = names.filter_map(|name| Some(name.strip_suffix(LOG)?.to_owned()));
252 Ok(logs.filter(|name| !name.is_empty()).collect())
253 }
254
255 fn read_log(&self, name: &str) -> io::Result<Vec<u8>> {
256 fs::read(self.log(name)?)
257 }
258
259 fn append_log(&self, name: &str, len: u64, bytes: &[u8]) -> Result<(), LogWriteError> {
260 let path = self.log(name)?;
261 let _lock = self.lock()?;
262 check_len(&path, len)?;
263 if let Some(parent) = path.parent() {
264 fs::create_dir_all(parent)?;
265 }
266 let mut log = OpenOptions::new().create(true).append(true).open(path)?;
267 Ok(log.write_all(bytes)?)
268 }
269
270 fn replace_log(&self, name: &str, len: u64, bytes: &[u8]) -> Result<(), LogWriteError> {
273 let path = self.log(name)?;
274 let _lock = self.lock()?;
275 check_len(&path, len)?;
276 Ok(write_durable(&path, bytes)?)
277 }
278
279 fn destroy(&self) -> io::Result<()> {
283 let _lock = self.lock()?;
284 for folder in [LOGS, FILES] {
285 let (path, trash) = (
286 self.dir.join(folder),
287 self.dir.join(format!("{folder}{TRASH}")),
288 );
289 if path.exists() {
290 absent_is_fine(fs::remove_dir_all(&trash))?;
291 fs::rename(&path, &trash)?;
292 }
293 absent_is_fine(fs::remove_dir_all(&trash))?;
294 }
295 Ok(())
296 }
297}
298
299#[cfg(test)]
300mod tests;