use std::fs;
use std::path::{Path, PathBuf};
use loftspec::codec::{self, Files, SpecPath};
use loftspec::document::Document;
use loftspec::publish::{LOCK, Lock};
use loftspec::trace::{TRACE, TestFile, TestFiles};
use crate::error::io_error;
use crate::fs::{SIDECAR, SPEC, hidden};
use crate::journal::{self, Changes, RootPath};
use crate::{DocumentStore, Saved, StoreError};
#[derive(Debug, Clone)]
pub struct FileStore {
root: PathBuf,
}
impl FileStore {
pub fn open(root: &Path) -> Result<Self, StoreError> {
journal::recover(root)?;
Ok(Self {
root: root.to_owned(),
})
}
pub fn read_files(&self) -> Result<Files, StoreError> {
let spec = self.root.join(SPEC);
if !spec.is_dir() {
return Err(StoreError::NoTree(spec));
}
let mut files = Files::new();
collect(&spec, "", &mut files)?;
Ok(files)
}
pub fn write_files(&self, files: Files) -> Result<(), StoreError> {
journal::save(&self.root, &Changes::of_tree(files, &[]))
}
pub fn import(&self, files: Files, sidecar: &[u8]) -> Result<(), StoreError> {
let spec = self.root.join(SPEC);
if spec.exists() && holds_entries(&spec)? {
return Err(StoreError::NotEmpty(spec));
}
fs::create_dir_all(&spec).map_err(io_error(&spec))?;
self.write_with_beside(files, (SIDECAR, sidecar.to_vec()))
}
pub fn create(&self, files: Files) -> Result<(), StoreError> {
let spec = self.root.join(SPEC);
if spec.exists() {
return Err(StoreError::Exists(spec));
}
self.write_files(files)
}
pub fn lock(&self) -> Result<Option<Lock>, StoreError> {
let Some(bytes) = self.beside(LOCK)? else {
return Ok(None);
};
Lock::read(&bytes)
.map(Some)
.map_err(|error| StoreError::Lock(self.root.join(LOCK), error))
}
pub fn trace(&self) -> Result<Option<Vec<u8>>, StoreError> {
self.beside(TRACE)
}
pub fn write_trace(&self, text: String) -> Result<(), StoreError> {
self.write_with_beside(Files::new(), (TRACE, text.into_bytes()))
}
#[must_use]
pub fn test_files(&self) -> ProjectFiles {
ProjectFiles {
root: self.root.clone(),
}
}
fn beside(&self, name: &str) -> Result<Option<Vec<u8>>, StoreError> {
let path = self.root.join(name);
match fs::read(&path) {
Ok(bytes) => Ok(Some(bytes)),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(error) => Err(io_error(&path)(error)),
}
}
pub fn publish(&self, files: Files, lock: &Lock) -> Result<(), StoreError> {
self.write_with_beside(files, (LOCK, lock.to_bytes()))
}
fn write_with_beside(
&self,
files: Files,
(name, bytes): (&str, Vec<u8>),
) -> Result<(), StoreError> {
let mut changes = Changes::of_tree(files, &[]);
changes.write.insert(RootPath::beside(name), bytes);
journal::save(&self.root, &changes)
}
}
#[derive(Debug, Clone)]
pub struct ProjectFiles {
root: PathBuf,
}
impl TestFiles for ProjectFiles {
fn read(&self, path: &str) -> TestFile {
match fs::read(self.root.join(path)) {
Ok(bytes) => String::from_utf8(bytes).map_or(TestFile::NotText, TestFile::Text),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => TestFile::Missing,
Err(error) => TestFile::Unreadable(error.to_string()),
}
}
}
impl DocumentStore for FileStore {
fn load(&self) -> Result<Document, StoreError> {
codec::read(&self.read_files()?).map_err(StoreError::Read)
}
fn save(&mut self, document: &Document) -> Result<Saved, StoreError> {
journal::recover(&self.root)?;
let current = match self.read_files() {
Ok(files) => files,
Err(StoreError::NoTree(_)) => Files::new(),
Err(error) => return Err(error),
};
let mut next = codec::write(document);
let saved = Saved::between(¤t, &next);
next.retain(|path, _| saved.written.binary_search(path).is_ok());
journal::save(&self.root, &Changes::of_tree(next, &saved.removed))?;
Ok(saved)
}
}
fn holds_entries(dir: &Path) -> Result<bool, StoreError> {
for entry in fs::read_dir(dir).map_err(io_error(dir))? {
let entry = entry.map_err(io_error(dir))?;
if !entry.file_name().to_str().is_some_and(hidden) {
return Ok(true);
}
}
Ok(false)
}
fn collect(dir: &Path, prefix: &str, files: &mut Files) -> Result<(), StoreError> {
for entry in fs::read_dir(dir).map_err(io_error(dir))? {
let entry = entry.map_err(io_error(dir))?;
let path = entry.path();
let name = entry
.file_name()
.into_string()
.map_err(|_| StoreError::NotUtf8(path.clone()))?;
if hidden(&name) {
continue;
}
let relative = if prefix.is_empty() {
name
} else {
format!("{prefix}/{name}")
};
if path.is_dir() {
collect(&path, &relative, files)?;
} else {
let bytes = fs::read(&path).map_err(io_error(&path))?;
files.insert(SpecPath::new(&relative), bytes);
}
}
Ok(())
}