use std::fs::{self, File, OpenOptions};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use crate::store::check_leaf;
use crate::{Files, LogWriteError, SessionError, Store, check_name};
const LOGS: &str = "logs";
const FILES: &str = "files";
const LOG: &str = ".jsonl";
const PART: &str = ".rig-part";
const TRASH: &str = ".rig-trash";
const LOCK: &str = ".rig-memory.lock";
#[derive(Clone)]
pub struct DirStore {
dir: PathBuf,
files: DirFiles,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DirFiles(PathBuf);
impl DirFiles {
pub fn new(dir: impl Into<PathBuf>) -> Self {
Self(dir.into())
}
pub fn path(&self) -> &Path {
&self.0
}
fn file(&self, name: &str) -> io::Result<PathBuf> {
check_name(name)?;
refuse_part(name)?;
Ok(self.0.join(name))
}
}
impl DirStore {
pub fn open(dir: impl Into<PathBuf>) -> Result<Self, SessionError> {
let dir = dir.into();
let old = names(&dir)?
.into_iter()
.any(|name| name.ends_with(LOG) && dir.join(&name).is_file());
if old {
return Err(SessionError::OldLayout);
}
Ok(Self {
files: DirFiles::new(dir.join(FILES)),
dir,
})
}
fn lock(&self) -> io::Result<File> {
fs::create_dir_all(&self.dir)?;
let lock = OpenOptions::new()
.create(true)
.write(true)
.truncate(false)
.open(self.dir.join(LOCK))?;
lock.lock()?;
Ok(lock)
}
fn log(&self, name: &str) -> io::Result<PathBuf> {
check_leaf(name)?;
Ok(self.dir.join(LOGS).join(format!("{name}{LOG}")))
}
}
fn refuse_part(name: &str) -> io::Result<()> {
if name.ends_with(PART) {
let why = format!("{name:?} ends as a file being written does");
return Err(io::Error::new(io::ErrorKind::InvalidInput, why));
}
Ok(())
}
fn names(dir: &Path) -> io::Result<Vec<String>> {
let entries = match fs::read_dir(dir) {
Err(failure) if failure.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
entries => entries?,
};
let names = entries.filter_map(|entry| entry.ok()?.file_name().into_string().ok());
Ok(names.collect())
}
fn absent_is_fine(removed: io::Result<()>) -> io::Result<()> {
match removed {
Err(failure) if failure.kind() == io::ErrorKind::NotFound => Ok(()),
removed => removed,
}
}
fn write_atomic(path: &Path, bytes: &[u8]) -> io::Result<()> {
let part = write_part(path, bytes, false)?;
fs::rename(&part, path)
}
fn write_durable(path: &Path, bytes: &[u8]) -> io::Result<()> {
let part = write_part(path, bytes, true)?;
fs::rename(&part, path)?;
if let Some(parent) = path.parent() {
let _ = File::open(parent).and_then(|folder| folder.sync_all());
}
Ok(())
}
fn write_part(path: &Path, bytes: &[u8], sync: bool) -> io::Result<PathBuf> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let mut part = path.as_os_str().to_owned();
part.push(PART);
let part = PathBuf::from(part);
let mut file = File::create(&part)?;
file.write_all(bytes)?;
if sync {
file.sync_all()?;
}
Ok(part)
}
impl Files for DirFiles {
fn write_file(&self, name: &str, bytes: &[u8]) -> io::Result<()> {
write_atomic(&self.file(name)?, bytes)
}
fn append_file(&self, name: &str, bytes: &[u8]) -> io::Result<()> {
let path = self.file(name)?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let mut file = OpenOptions::new().create(true).append(true).open(path)?;
file.write_all(bytes)
}
fn read_file(&self, name: &str) -> io::Result<Vec<u8>> {
fs::read(self.file(name)?)
}
fn remove_file(&self, name: &str) -> io::Result<()> {
absent_is_fine(fs::remove_file(self.file(name)?))
}
fn files(&self) -> io::Result<Vec<String>> {
let mut files = Vec::new();
let mut dirs = vec![(self.0.clone(), String::new())];
while let Some((dir, prefix)) = dirs.pop() {
for name in names(&dir)? {
if name.ends_with(PART) {
continue;
}
let path = dir.join(&name);
let name = format!("{prefix}{name}");
if path.is_dir() {
dirs.push((path, format!("{name}/")));
} else {
files.push(name);
}
}
}
Ok(files)
}
}
fn check_len(path: &Path, len: u64) -> Result<(), LogWriteError> {
let held = match fs::metadata(path) {
Err(failure) if failure.kind() == io::ErrorKind::NotFound => 0,
metadata => metadata?.len(),
};
if held != len {
return Err(LogWriteError::Changed);
}
Ok(())
}
impl Files for DirStore {
fn write_file(&self, name: &str, bytes: &[u8]) -> io::Result<()> {
let _lock = self.lock()?;
self.files.write_file(name, bytes)
}
fn append_file(&self, name: &str, bytes: &[u8]) -> io::Result<()> {
let _lock = self.lock()?;
self.files.append_file(name, bytes)
}
fn read_file(&self, name: &str) -> io::Result<Vec<u8>> {
self.files.read_file(name)
}
fn remove_file(&self, name: &str) -> io::Result<()> {
let _lock = self.lock()?;
self.files.remove_file(name)
}
fn files(&self) -> io::Result<Vec<String>> {
self.files.files()
}
}
impl Store for DirStore {
fn logs(&self) -> io::Result<Vec<String>> {
let names = names(&self.dir.join(LOGS))?.into_iter();
let logs = names.filter_map(|name| Some(name.strip_suffix(LOG)?.to_owned()));
Ok(logs.filter(|name| !name.is_empty()).collect())
}
fn read_log(&self, name: &str) -> io::Result<Vec<u8>> {
fs::read(self.log(name)?)
}
fn append_log(&self, name: &str, len: u64, bytes: &[u8]) -> Result<(), LogWriteError> {
let path = self.log(name)?;
let _lock = self.lock()?;
check_len(&path, len)?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let mut log = OpenOptions::new().create(true).append(true).open(path)?;
Ok(log.write_all(bytes)?)
}
fn replace_log(&self, name: &str, len: u64, bytes: &[u8]) -> Result<(), LogWriteError> {
let path = self.log(name)?;
let _lock = self.lock()?;
check_len(&path, len)?;
Ok(write_durable(&path, bytes)?)
}
fn destroy(&self) -> io::Result<()> {
let _lock = self.lock()?;
for folder in [LOGS, FILES] {
let (path, trash) = (
self.dir.join(folder),
self.dir.join(format!("{folder}{TRASH}")),
);
if path.exists() {
absent_is_fine(fs::remove_dir_all(&trash))?;
fs::rename(&path, &trash)?;
}
absent_is_fine(fs::remove_dir_all(&trash))?;
}
Ok(())
}
}
#[cfg(test)]
mod tests;