1use std::path::PathBuf;
4use std::sync::Arc;
5
6#[cfg(unix)]
7use crate::files::flush_batch::BatchFolder;
8use crate::files::protection::Protection;
9use crate::files::{
10 lock, settings, AtomicFile, CacheWriteLock, ClosedStoreLock, FileError, FileWriter, FlushBatch,
11 SettingsError, StoreDeletionLock, StoreLock, StoreLockError, StoreReadLock, StoreSettings,
12 WriterFolder,
13};
14use crate::id_source::StoreId;
15use crate::task::BugSinkRef;
16
17#[derive(Debug, PartialEq, Eq)]
20pub struct TemporaryFolder {
21 path: PathBuf,
22}
23
24impl TemporaryFolder {
25 pub fn at(path: &std::path::Path) -> Option<Self> {
28 std::fs::metadata(path)
29 .is_ok_and(|metadata| metadata.is_dir())
30 .then(|| Self { path: path.to_owned() })
31 }
32
33 pub fn path(&self) -> &std::path::Path {
35 &self.path
36 }
37}
38
39pub(super) const DATABASE: &str = "store.db";
41
42#[derive(Clone, Copy, Debug)]
45pub enum StoreFile {
46 StorageSettings,
48 StoreKeys,
50 MemberKeys,
52 Bootstrap,
54}
55
56impl StoreFile {
57 pub(super) const fn name(self) -> &'static str {
59 match self {
60 Self::StorageSettings => "storage.json",
61 Self::StoreKeys => "store-keys.sealed",
62 Self::MemberKeys => "member-keys.sealed",
63 Self::Bootstrap => "bootstrap.sealed",
64 }
65 }
66}
67
68#[derive(Clone, Copy, Debug)]
70pub enum FileArea {
71 AppProvided,
74 Cache,
76}
77
78impl FileArea {
79 const ALL: [Self; 2] = [Self::AppProvided, Self::Cache];
80
81 const fn name(self) -> &'static str {
83 match self {
84 Self::AppProvided => "files",
85 Self::Cache => "cache",
86 }
87 }
88}
89
90#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
93pub struct FileName(String);
94
95#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
97pub enum FileNameError {
98 #[error("file name must contain 1 to 255 bytes")]
100 Length,
101 #[error("file name requires ASCII letters, digits, hyphens, underscores or interior dots")]
103 Character,
104 #[error("file name is reserved for a Windows device")]
106 Reserved,
107}
108
109impl FileName {
110 pub fn as_str(&self) -> &str {
112 &self.0
113 }
114
115 pub fn new(name: impl Into<String>) -> Result<Self, FileNameError> {
117 let name = name.into();
118 if name.is_empty() || name.len() > super::relative_path::NAME_LIMIT {
119 return Err(FileNameError::Length);
120 }
121 if name.starts_with('.')
122 || name.ends_with('.')
123 || !name.bytes().all(|b| b.is_ascii_alphanumeric() || b"-_.".contains(&b))
124 {
125 return Err(FileNameError::Character);
126 }
127 let stem = name.split('.').next().expect("a nonempty name has a stem");
128 if super::relative_path::windows_device(stem) {
129 return Err(FileNameError::Reserved);
130 }
131 Ok(Self(name))
132 }
133}
134
135#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
139pub struct StagedName(String);
140
141impl StagedName {
142 pub fn new(folder: &FileName, name: &FileName) -> Self {
144 Self(format!("{}/{}", folder.0, name.0))
145 }
146
147 pub fn parse(recorded: &str) -> Option<Self> {
150 let (folder, name) = recorded.split_once('/')?;
151 Some(Self::new(&FileName::new(folder).ok()?, &FileName::new(name).ok()?))
152 }
153
154 pub fn as_str(&self) -> &str {
156 &self.0
157 }
158
159 pub fn folder(&self) -> FileName {
161 FileName(self.parts().0.to_owned())
162 }
163
164 pub fn copy(&self) -> FileName {
166 FileName(self.parts().1.to_owned())
167 }
168
169 fn parts(&self) -> (&str, &str) {
171 self.0.split_once('/').expect("a staged name holds its folder")
172 }
173}
174
175#[derive(Debug, thiserror::Error)]
177pub enum StagingFolderError {
178 #[error("staging folder {} is already there", .0.as_str())]
181 Taken(FileName),
182 #[error(transparent)]
184 Writer(#[from] WriterError),
185}
186
187#[derive(Clone)]
190pub struct StoreDir {
191 path: PathBuf,
192 id: StoreId,
193 bootstrap: Option<lock::LockInstance>,
196 protection: Protection,
198 bugs: BugSinkRef,
200}
201
202impl std::fmt::Debug for StoreDir {
203 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
204 f.debug_struct("StoreDir")
205 .field("path", &self.path)
206 .field("id", &self.id)
207 .field("bootstrap", &self.bootstrap)
208 .field("protection", &self.protection)
209 .finish_non_exhaustive()
210 }
211}
212
213impl StoreDir {
214 pub(crate) fn new(
215 path: PathBuf,
216 id: StoreId,
217 protection: Protection,
218 bugs: BugSinkRef,
219 ) -> Self {
220 Self { path, id, bootstrap: None, protection, bugs }
221 }
222
223 pub(super) fn bootstrapping(
224 path: PathBuf,
225 id: StoreId,
226 bootstrap: lock::LockInstance,
227 protection: Protection,
228 bugs: BugSinkRef,
229 ) -> Self {
230 Self { path, id, bootstrap: Some(bootstrap), protection, bugs }
231 }
232
233 pub fn is_unpublished(&self) -> Result<bool, FileError> {
236 super::bootstrap::is_pending(&self.path)
237 }
238
239 fn is_bootstrapping(&self) -> bool {
240 self.bootstrap
241 .is_some_and(|instance| lock::bootstrap_running(&self.path, self.id, instance))
242 }
243
244 fn check_published(&self) -> Result<(), StoreLockError> {
248 if self.is_bootstrapping() {
249 return Ok(());
250 }
251 let unfinished =
252 super::device_reset::unfinished(&self.path, self.id).map_err(|error| match error {
253 SettingsError::File(error) => StoreLockError::from(error),
254 SettingsError::Bug(bug) => StoreLockError::Bug(bug),
255 error => FileError::at(
256 "read the store's settings beside its replacement record",
257 &self.path,
258 std::io::Error::other(error),
259 )
260 .into(),
261 })?;
262 if let Some(record) = unfinished {
263 return Err(StoreLockError::ReplacementUnfinished { store: self.id, by: record.by });
264 }
265 if super::bootstrap::is_pending(&self.path)? {
266 return Err(StoreLockError::BootstrapPending(self.id));
267 }
268 Ok(())
269 }
270
271 pub fn delete_discarded_devices(&self) -> Result<(), FileError> {
275 super::device_reset::delete_archives(&self.path)
276 }
277
278 pub fn local_bytes(&self) -> Result<super::DirectoryBytes, FileError> {
281 super::local_bytes::measure(&self.path, FileArea::Cache.name())
282 }
283
284 pub fn id(&self) -> StoreId {
286 self.id
287 }
288
289 #[cfg(any(test, feature = "test-utils"))]
292 pub fn area_path(&self, area: FileArea) -> std::path::PathBuf {
293 self.path.join(area.name())
294 }
295}
296
297pub(crate) fn initialize(
298 path: &std::path::Path,
299 settings: &StoreSettings,
300) -> Result<(), SettingsError> {
301 for area in FileArea::ALL {
302 let directory = path.join(area.name());
303 std::fs::create_dir(&directory).map_err(|source| {
304 crate::files::FileError::at("create store files directory", &directory, source)
305 })?;
306 #[cfg(unix)]
307 crate::files::atomic_file::sync_directory(&directory).map_err(|source| {
308 crate::files::FileError::at("sync store files directory", &directory, source)
309 })?;
310 }
311 settings::write(path, settings)
312}
313
314#[derive(Debug, thiserror::Error)]
317pub enum WriterError {
318 #[error(transparent)]
320 Lock(#[from] StoreLockError),
321 #[error(transparent)]
323 File(#[from] FileError),
324}
325
326#[cfg(any(test, feature = "test-utils"))]
329pub trait StoreLockProbe {
330 fn lock_exclusive(&self) -> Result<StoreLock, StoreLockError>;
332}
333
334#[cfg(any(test, feature = "test-utils"))]
335impl StoreLockProbe for StoreDir {
336 fn lock_exclusive(&self) -> Result<StoreLock, StoreLockError> {
337 let guard = lock::acquire(&self.path, self.id)?;
338 self.check_published()?;
339 Ok(guard)
340 }
341}
342
343pub trait StoreDirAccess: Sized {
345 fn database_path(&self) -> PathBuf;
348 fn input_database_path(&self) -> PathBuf;
351 fn unfinished_replacement(&self) -> Result<Option<super::ReplacementRecord>, SettingsError>;
356 fn unfinished_deletion(&self) -> Result<Option<super::IfUnsent>, FileError>;
360 fn record_deletion(&self, unsent: super::IfUnsent) -> Result<(), FileError>;
364 fn forget_deletion(&self) -> Result<(), FileError>;
367 fn settings(&self) -> Result<StoreSettings, SettingsError>;
370 fn bugs(&self) -> &BugSinkRef;
373 fn lock_for_open(
376 &self,
377 ) -> impl std::future::Future<Output = Result<StoreLock, StoreLockError>> + Send;
378 fn lock_closed(
383 &self,
384 ) -> impl std::future::Future<Output = Result<ClosedStoreLock, StoreLockError>> + Send;
385 fn lock_read_only(&self) -> Result<StoreReadLock, StoreLockError>;
388 fn lock_cache_write(&self) -> Result<CacheWriteLock, FileError>;
392 fn exclude_cache_writes(&self) -> Result<CacheWriteLock, FileError>;
396 fn lock_for_deletion(&self) -> Result<Option<StoreDeletionLock>, StoreLockError>;
405 fn closed(&self) -> impl std::future::Future<Output = Result<(), FileError>> + Send;
409 fn owned_file(&self, file: StoreFile) -> AtomicFile;
412 fn file(&self, area: FileArea, name: &FileName) -> AtomicFile;
415 fn create_writer(&self, area: FileArea, name: &FileName) -> Result<FileWriter, WriterError>;
421 fn writer_folder(&self, area: FileArea) -> Result<WriterFolder, WriterError>;
425 fn staged_file(&self, name: &StagedName) -> AtomicFile;
427 fn staging_folder(
433 &self,
434 folder: &FileName,
435 batch: &mut FlushBatch,
436 ) -> Result<WriterFolder, StagingFolderError>;
437 fn remove_staged(&self, name: &StagedName) -> Result<(), FileError>;
441 fn resume_writer(
447 &self,
448 area: FileArea,
449 name: &FileName,
450 length: u64,
451 ) -> Result<FileWriter, WriterError>;
452}
453
454pub fn verify_lock(directory: &StoreDir, lock: &StoreLock) -> Result<(), StoreLockError> {
456 lock.verify(&directory.path, directory.id)
457}
458
459pub fn settle_files(directory: &StoreDir, lock: &StoreLock) -> Result<(), FileError> {
467 super::atomic_file::remove_temporaries(lock.directory())?;
468 if !super::bootstrap::is_pending(lock.directory())?
470 && super::atomic_file::exists(
471 "inspect the joining keys",
472 &lock.directory().join(StoreFile::Bootstrap.name()),
473 )?
474 {
475 directory.owned_file(StoreFile::Bootstrap).remove_with_temporaries()?;
476 }
477 directory.protection.apply(lock.directory())
478}
479
480impl StoreDirAccess for StoreDir {
481 fn database_path(&self) -> PathBuf {
482 self.path.join(DATABASE)
483 }
484 fn input_database_path(&self) -> PathBuf {
485 self.path.join("inputs.db")
486 }
487 fn unfinished_replacement(&self) -> Result<Option<super::ReplacementRecord>, SettingsError> {
488 super::device_reset::unfinished(&self.path, self.id)
489 }
490 fn unfinished_deletion(&self) -> Result<Option<super::IfUnsent>, FileError> {
491 super::deletion::read(&self.path)
492 }
493 fn record_deletion(&self, unsent: super::IfUnsent) -> Result<(), FileError> {
494 super::deletion::record(&self.path, unsent)
495 }
496 fn forget_deletion(&self) -> Result<(), FileError> {
497 super::deletion::forget(&self.path)
498 }
499 fn settings(&self) -> Result<StoreSettings, SettingsError> {
500 settings::read(&self.path, self.id)
501 }
502 fn bugs(&self) -> &BugSinkRef {
503 &self.bugs
504 }
505 async fn lock_for_open(&self) -> Result<StoreLock, StoreLockError> {
506 let guard = lock::acquire_for_open(&self.path, self.id, &self.bugs).await?;
507 self.check_published()?;
508 Ok(guard)
509 }
510 async fn lock_closed(&self) -> Result<ClosedStoreLock, StoreLockError> {
511 let guard = lock::acquire_closed(&self.path, self.id, &self.bugs).await?;
512 self.check_published()?;
513 Ok(guard)
514 }
515 fn lock_read_only(&self) -> Result<StoreReadLock, StoreLockError> {
516 let guard = lock::acquire_reader(&self.path, self.id)?;
517 self.check_published()?;
518 Ok(guard)
519 }
520 fn lock_cache_write(&self) -> Result<CacheWriteLock, FileError> {
521 lock::cache_writes(&self.path, true)
522 }
523 fn exclude_cache_writes(&self) -> Result<CacheWriteLock, FileError> {
524 lock::cache_writes(&self.path, false)
525 }
526 fn lock_for_deletion(&self) -> Result<Option<StoreDeletionLock>, StoreLockError> {
527 let bootstrapping = self.is_bootstrapping();
528 let locked = lock::for_deletion(&self.path, self.id, None);
529 if !bootstrapping
535 && matches!(locked, Ok(Some(_)) | Err(StoreLockError::AlreadyOpen(_)))
536 && super::device_reset::read(&self.path)?.is_none()
537 && super::bootstrap::is_pending(&self.path)?
538 {
539 return Err(StoreLockError::BootstrapPending(self.id));
540 }
541 locked
542 }
543 async fn closed(&self) -> Result<(), FileError> {
544 lock::closed(&self.path, self.id).await
545 }
546 fn owned_file(&self, file: StoreFile) -> AtomicFile {
547 AtomicFile::new(self.path.join(file.name()))
548 }
549 fn file(&self, area: FileArea, name: &FileName) -> AtomicFile {
550 AtomicFile::new(self.path.join(area.name()).join(&name.0))
551 }
552 fn create_writer(&self, area: FileArea, name: &FileName) -> Result<FileWriter, WriterError> {
553 Ok(self.writer_folder(area)?.create(name)?)
554 }
555 fn writer_folder(&self, area: FileArea) -> Result<WriterFolder, WriterError> {
556 let lock = self.lock_read_only()?;
557 Ok(WriterFolder::open(&self.path.join(area.name()), Arc::new(lock), self.bugs.clone())?)
558 }
559 fn resume_writer(
560 &self,
561 area: FileArea,
562 name: &FileName,
563 length: u64,
564 ) -> Result<FileWriter, WriterError> {
565 let lock = self.lock_read_only()?;
566 Ok(FileWriter::resume(&self.file(area, name), lock, self.bugs.clone(), length)?)
567 }
568 fn staged_file(&self, name: &StagedName) -> AtomicFile {
569 let (folder, copy) = name.parts();
570 AtomicFile::new(self.path.join(FileArea::AppProvided.name()).join(folder).join(copy))
571 }
572 fn staging_folder(
573 &self,
574 folder: &FileName,
575 #[cfg_attr(
576 not(unix),
577 expect(unused_variables, reason = "Windows writes a folder's entries through")
578 )]
579 batch: &mut FlushBatch,
580 ) -> Result<WriterFolder, StagingFolderError> {
581 let lock = Arc::new(self.lock_read_only().map_err(WriterError::from)?);
582 let files = self.path.join(FileArea::AppProvided.name());
583 let path = files.join(&folder.0);
584 match std::fs::create_dir(&path) {
585 Ok(()) => {}
586 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
587 return Err(StagingFolderError::Taken(folder.clone()));
588 }
589 Err(source) => {
590 let error = FileError::at("create staging folder", &path, source);
591 return Err(WriterError::from(error).into());
592 }
593 }
594 #[cfg(unix)]
595 batch.add_folder(
596 BatchFolder::open(files.clone(), Some(lock.clone())).map_err(|source| {
597 WriterError::from(FileError::at("open folder", &files, source))
598 })?,
599 );
600 Ok(WriterFolder::open(&path, lock, self.bugs.clone()).map_err(WriterError::from)?)
601 }
602 fn remove_staged(&self, name: &StagedName) -> Result<(), FileError> {
603 let copy = self.staged_file(name);
604 copy.remove()?;
605 let (folder, _) = name.parts();
606 super::atomic_file::remove_empty_folder(
607 &self.path.join(FileArea::AppProvided.name()).join(folder),
608 )
609 }
610}
611
612#[cfg(test)]
613#[path = "directory_tests.rs"]
614mod tests;