use std::collections::{BTreeMap, BTreeSet};
use std::io::{self, Read, Write};
use std::sync::{Arc, Condvar, Mutex};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum EntryKind {
File,
Dir,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct DirEntry {
pub name: String,
pub kind: EntryKind,
}
impl DirEntry {
pub fn new(name: impl Into<String>, kind: EntryKind) -> Self {
Self {
name: name.into(),
kind,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum LockKind {
Writer,
BackupShared,
BackupExclusive,
}
pub struct DirLockGuard(#[allow(dead_code)] Box<dyn std::any::Any + Send + Sync>);
impl DirLockGuard {
pub fn new(holder: impl std::any::Any + Send + Sync) -> Self {
Self(Box::new(holder))
}
}
impl std::fmt::Debug for DirLockGuard {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("DirLockGuard")
}
}
pub trait StoreFile: Write + Send + Sync {
fn sync(&mut self) -> io::Result<()>;
fn set_len(&mut self, len: u64) -> io::Result<()>;
}
pub trait StoreDir: Send + Sync + std::fmt::Debug {
fn location(&self) -> String;
fn is_volatile(&self) -> bool {
false
}
#[cfg(not(target_arch = "wasm32"))]
fn local_path(&self) -> Option<&std::path::Path> {
None
}
fn entry_kind(&self, name: &str) -> io::Result<Option<EntryKind>>;
fn list(&self, dir: &str) -> io::Result<Vec<DirEntry>>;
fn file_len(&self, name: &str) -> io::Result<u64>;
fn read(&self, name: &str, max: u64) -> io::Result<Vec<u8>>;
fn read_range(&self, name: &str, offset: u64, len: usize) -> io::Result<Vec<u8>>;
fn open_read(&self, name: &str) -> io::Result<Box<dyn Read + Send>>;
fn create_dir_all(&self, name: &str) -> io::Result<()>;
fn create(&self, name: &str, exclusive: bool) -> io::Result<Box<dyn StoreFile>>;
fn open_write(&self, name: &str, offset: u64) -> io::Result<Box<dyn StoreFile>>;
fn append(&self, name: &str, bytes: &[u8]) -> io::Result<()>;
fn write_atomic(&self, name: &str, bytes: &[u8]) -> io::Result<()>;
fn rename(&self, from: &str, to: &str) -> io::Result<()>;
fn truncate(&self, name: &str, len: u64) -> io::Result<()>;
fn remove_file(&self, name: &str) -> io::Result<()>;
fn remove_tree(&self, name: &str) -> io::Result<()>;
fn sync_dir(&self, name: &str) -> io::Result<()>;
fn lock(&self, kind: LockKind) -> io::Result<Option<DirLockGuard>>;
fn space_usage(&self) -> Option<SpaceUsage> {
None
}
fn compact(&self) -> io::Result<Option<DirCompaction>> {
Ok(None)
}
fn free_space(&self) -> Option<u64> {
None
}
fn check(&self) -> DirCheck {
DirCheck::default()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct SpaceUsage {
pub total_bytes: u64,
pub live_bytes: u64,
}
impl SpaceUsage {
pub fn new(total_bytes: u64, live_bytes: u64) -> Self {
Self {
total_bytes,
live_bytes,
}
}
pub fn garbage_bytes(&self) -> u64 {
self.total_bytes.saturating_sub(self.live_bytes)
}
pub fn garbage_ratio(&self) -> f64 {
if self.total_bytes == 0 {
0.0
} else {
self.garbage_bytes() as f64 / self.total_bytes as f64
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct DirCompaction {
pub bytes_before: u64,
pub bytes_after: u64,
}
impl DirCompaction {
pub fn new(bytes_before: u64, bytes_after: u64) -> Self {
Self {
bytes_before,
bytes_after,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct DirCheck {
pub damage: Vec<String>,
pub warnings: Vec<String>,
}
impl DirCheck {
pub fn is_ok(&self) -> bool {
self.damage.is_empty() && self.warnings.is_empty()
}
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn check_name(name: &str) -> io::Result<()> {
if name.is_empty() {
return Ok(());
}
if name.starts_with('/')
|| name.contains('\\')
|| name.contains('\0')
|| name.contains(':')
|| name
.split('/')
.any(|part| part.is_empty() || part == "." || part == "..")
{
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("not a store name: {name:?}"),
));
}
Ok(())
}
pub trait IntoStoreDir {
fn into_store_dir(self) -> Arc<dyn StoreDir>;
}
impl IntoStoreDir for Arc<dyn StoreDir> {
fn into_store_dir(self) -> Arc<dyn StoreDir> {
self
}
}
impl IntoStoreDir for &Arc<dyn StoreDir> {
fn into_store_dir(self) -> Arc<dyn StoreDir> {
self.clone()
}
}
impl IntoStoreDir for MemDir {
fn into_store_dir(self) -> Arc<dyn StoreDir> {
Arc::new(self)
}
}
impl IntoStoreDir for &MemDir {
fn into_store_dir(self) -> Arc<dyn StoreDir> {
Arc::new(self.clone())
}
}
#[cfg(not(target_arch = "wasm32"))]
mod fs_dir {
use super::*;
use std::fs::{self, File, OpenOptions};
use std::io::{Seek, SeekFrom};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct FsDir {
root: PathBuf,
}
impl FsDir {
pub fn new(root: impl Into<PathBuf>) -> Self {
Self { root: root.into() }
}
pub fn path(&self) -> &Path {
&self.root
}
fn at(&self, name: &str) -> io::Result<PathBuf> {
if name.is_empty() {
return Ok(self.root.clone());
}
check_name(name)?;
Ok(self.root.join(name))
}
fn lock_file(&self, name: &str) -> io::Result<fd_lock::RwLock<File>> {
let file = OpenOptions::new()
.create(true)
.truncate(false)
.write(true)
.open(self.at(name)?)?;
Ok(fd_lock::RwLock::new(file))
}
}
struct FsFile(File);
impl Write for FsFile {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.0.flush()
}
}
impl StoreFile for FsFile {
fn sync(&mut self) -> io::Result<()> {
self.0.sync_data()
}
fn set_len(&mut self, len: u64) -> io::Result<()> {
self.0.set_len(len)?;
self.0.seek(SeekFrom::Start(len))?;
self.0.sync_all()
}
}
struct HeldLock(#[allow(dead_code)] fd_lock::RwLock<File>);
impl StoreDir for FsDir {
fn location(&self) -> String {
self.root.display().to_string()
}
fn local_path(&self) -> Option<&Path> {
Some(&self.root)
}
fn entry_kind(&self, name: &str) -> io::Result<Option<EntryKind>> {
match fs::metadata(self.at(name)?) {
Ok(meta) if meta.is_dir() => Ok(Some(EntryKind::Dir)),
Ok(_) => Ok(Some(EntryKind::File)),
Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
Err(err) => Err(err),
}
}
fn list(&self, dir: &str) -> io::Result<Vec<DirEntry>> {
let mut out = Vec::new();
for entry in fs::read_dir(self.at(dir)?)? {
let entry = entry?;
let Some(name) = entry.file_name().to_str().map(str::to_string) else {
continue;
};
let kind = if entry.path().is_dir() {
EntryKind::Dir
} else {
EntryKind::File
};
out.push(DirEntry { name, kind });
}
Ok(out)
}
fn file_len(&self, name: &str) -> io::Result<u64> {
Ok(fs::metadata(self.at(name)?)?.len())
}
fn read(&self, name: &str, max: u64) -> io::Result<Vec<u8>> {
let file = File::open(self.at(name)?)?;
let mut bytes = Vec::new();
file.take(max.saturating_add(1)).read_to_end(&mut bytes)?;
if bytes.len() as u64 > max {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("{name} is larger than {max} bytes"),
));
}
Ok(bytes)
}
fn read_range(&self, name: &str, offset: u64, len: usize) -> io::Result<Vec<u8>> {
let mut file = File::open(self.at(name)?)?;
file.seek(SeekFrom::Start(offset))?;
let mut bytes = vec![0u8; len];
file.read_exact(&mut bytes)?;
Ok(bytes)
}
fn open_read(&self, name: &str) -> io::Result<Box<dyn Read + Send>> {
Ok(Box::new(io::BufReader::with_capacity(
1 << 16,
File::open(self.at(name)?)?,
)))
}
fn create_dir_all(&self, name: &str) -> io::Result<()> {
fs::create_dir_all(self.at(name)?)
}
fn create(&self, name: &str, exclusive: bool) -> io::Result<Box<dyn StoreFile>> {
let path = self.at(name)?;
let file = if exclusive {
OpenOptions::new().create_new(true).write(true).open(path)?
} else {
File::create(path)?
};
Ok(Box::new(FsFile(file)))
}
fn open_write(&self, name: &str, offset: u64) -> io::Result<Box<dyn StoreFile>> {
let mut file = OpenOptions::new().write(true).open(self.at(name)?)?;
file.seek(SeekFrom::Start(offset))?;
Ok(Box::new(FsFile(file)))
}
fn append(&self, name: &str, bytes: &[u8]) -> io::Result<()> {
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(self.at(name)?)?;
file.write_all(bytes)?;
file.sync_data()
}
fn write_atomic(&self, name: &str, bytes: &[u8]) -> io::Result<()> {
let path = self.at(name)?;
let tmp = self.at(&format!("{name}.tmp"))?;
let result = File::create(&tmp)
.and_then(|mut file| {
file.write_all(bytes)?;
file.sync_all()
})
.and_then(|()| fs::rename(&tmp, &path));
if let Err(err) = result {
let _ = fs::remove_file(&tmp);
return Err(err);
}
let parent = name.rsplit_once('/').map_or("", |(dir, _)| dir);
self.sync_dir(parent)
}
fn rename(&self, from: &str, to: &str) -> io::Result<()> {
let (source, target) = (self.at(from)?, self.at(to)?);
if target.is_dir() && source.is_dir() {
remove_all(&source)?;
return sync_parent(&source);
}
match fs::rename(&source, &target) {
Err(err) if is_cross_device(&err) => move_across_devices(&source, &target),
other => other,
}
}
fn truncate(&self, name: &str, len: u64) -> io::Result<()> {
let file = OpenOptions::new().write(true).open(self.at(name)?)?;
file.set_len(len)?;
file.sync_all()
}
fn remove_file(&self, name: &str) -> io::Result<()> {
fs::remove_file(self.at(name)?)
}
fn remove_tree(&self, name: &str) -> io::Result<()> {
match remove_all(&self.at(name)?) {
Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(()),
other => other,
}
}
fn sync_dir(&self, name: &str) -> io::Result<()> {
#[cfg(unix)]
{
File::open(self.at(name)?).and_then(|dir| dir.sync_all())
}
#[cfg(not(unix))]
{
let _ = name;
Ok(())
}
}
fn lock(&self, kind: LockKind) -> io::Result<Option<DirLockGuard>> {
match kind {
LockKind::Writer | LockKind::BackupExclusive => {
let name = if kind == LockKind::Writer {
"LOCK"
} else {
"BACKUP"
};
let mut lock = self.lock_file(name)?;
match lock.try_write() {
Ok(guard) => std::mem::forget(guard),
Err(_) => return Ok(None),
}
Ok(Some(DirLockGuard::new(HeldLock(lock))))
}
LockKind::BackupShared => {
let lock = self.lock_file("BACKUP")?;
std::mem::forget(lock.read()?);
Ok(Some(DirLockGuard::new(HeldLock(lock))))
}
}
}
}
fn is_cross_device(err: &io::Error) -> bool {
if err.kind() == io::ErrorKind::CrossesDevices {
return true;
}
#[cfg(unix)]
let code = 18;
#[cfg(windows)]
let code = 17;
#[cfg(not(any(unix, windows)))]
let code = -1;
err.raw_os_error() == Some(code)
}
fn sync_parent(path: &Path) -> io::Result<()> {
#[cfg(unix)]
if let Some(parent) = path.parent() {
File::open(parent)?.sync_all()?;
}
#[cfg(not(unix))]
let _ = path;
Ok(())
}
fn remove_all(path: &Path) -> io::Result<()> {
let meta = fs::symlink_metadata(path)?;
if meta.file_type().is_symlink() {
if path.is_dir() {
for entry in fs::read_dir(path)? {
remove_all(&entry?.path())?;
}
}
return fs::remove_file(path);
}
if meta.is_dir() {
for entry in fs::read_dir(path)? {
remove_all(&entry?.path())?;
}
fs::remove_dir(path)
} else {
fs::remove_file(path)
}
}
fn copy_all(source: &Path, target: &Path) -> io::Result<()> {
if source.is_dir() {
fs::create_dir(target)?;
for entry in fs::read_dir(source)? {
let entry = entry?;
copy_all(&entry.path(), &target.join(entry.file_name()))?;
}
#[cfg(unix)]
File::open(target)?.sync_all()?;
Ok(())
} else {
fs::copy(source, target)?;
fs::OpenOptions::new().write(true).open(target)?.sync_all()
}
}
fn move_across_devices(source: &Path, target: &Path) -> io::Result<()> {
let name = target
.file_name()
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "no target name"))?;
let tmp = target.with_file_name(format!("tmp-xdev-{}", name.to_string_lossy()));
if fs::symlink_metadata(&tmp).is_ok() {
remove_all(&tmp)?;
}
copy_all(source, &tmp)?;
fs::rename(&tmp, target)?;
sync_parent(target)?;
remove_all(source)?;
sync_parent(source)
}
impl IntoStoreDir for FsDir {
fn into_store_dir(self) -> Arc<dyn StoreDir> {
Arc::new(self)
}
}
pub fn store_dir_at(path: impl AsRef<Path>) -> Arc<dyn StoreDir> {
let path = path.as_ref();
if crate::persist::single_file::names_single_file(path) {
Arc::new(crate::persist::SingleFileDir::new(path))
} else {
Arc::new(FsDir::new(path))
}
}
impl IntoStoreDir for &Path {
fn into_store_dir(self) -> Arc<dyn StoreDir> {
store_dir_at(self)
}
}
impl IntoStoreDir for PathBuf {
fn into_store_dir(self) -> Arc<dyn StoreDir> {
store_dir_at(self)
}
}
impl IntoStoreDir for &PathBuf {
fn into_store_dir(self) -> Arc<dyn StoreDir> {
store_dir_at(self)
}
}
impl IntoStoreDir for &str {
fn into_store_dir(self) -> Arc<dyn StoreDir> {
store_dir_at(self)
}
}
impl IntoStoreDir for String {
fn into_store_dir(self) -> Arc<dyn StoreDir> {
store_dir_at(self)
}
}
impl IntoStoreDir for &String {
fn into_store_dir(self) -> Arc<dyn StoreDir> {
store_dir_at(self.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn temp(name: &str) -> PathBuf {
let path = std::env::temp_dir()
.join(format!("graphersal-fsdir-{}-{name}", std::process::id()));
let _ = fs::remove_dir_all(&path);
fs::create_dir_all(&path).unwrap();
path
}
#[test]
fn a_move_across_devices_copies_then_removes_and_completes_after_a_crash() {
let root = temp("xdev");
let dir = FsDir::new(&root);
dir.create_dir_all("snapshots/00000000000000000003")
.unwrap();
dir.write_atomic("snapshots/00000000000000000003/manifest", b"m")
.unwrap();
dir.create_dir_all("attic/a/snapshots").unwrap();
move_across_devices(
&root.join("snapshots/00000000000000000003"),
&root.join("attic/a/snapshots/00000000000000000003"),
)
.unwrap();
assert_eq!(
dir.entry_kind("snapshots/00000000000000000003").unwrap(),
None
);
assert_eq!(
dir.read("attic/a/snapshots/00000000000000000003/manifest", 9)
.unwrap(),
b"m"
);
copy_all(
&root.join("attic/a/snapshots/00000000000000000003"),
&root.join("snapshots/00000000000000000003"),
)
.unwrap();
dir.rename(
"snapshots/00000000000000000003",
"attic/a/snapshots/00000000000000000003",
)
.unwrap();
assert_eq!(
dir.entry_kind("snapshots/00000000000000000003").unwrap(),
None
);
assert!(is_cross_device(&io::Error::from(
io::ErrorKind::CrossesDevices
)));
let _ = fs::remove_dir_all(&root);
}
#[cfg(unix)]
#[test]
fn removing_a_linked_directory_removes_its_contents_and_the_link() {
let root = temp("link-root");
let elsewhere = temp("link-target");
fs::write(elsewhere.join("seg"), b"x").unwrap();
std::os::unix::fs::symlink(&elsewhere, root.join("snap")).unwrap();
let dir = FsDir::new(&root);
assert_eq!(dir.entry_kind("snap").unwrap(), Some(EntryKind::Dir));
dir.remove_tree("snap").unwrap();
assert!(fs::symlink_metadata(root.join("snap")).is_err());
assert_eq!(fs::read_dir(&elsewhere).unwrap().count(), 0);
let _ = fs::remove_dir_all(&root);
let _ = fs::remove_dir_all(&elsewhere);
}
}
}
#[cfg(not(target_arch = "wasm32"))]
pub use fs_dir::{FsDir, store_dir_at};
#[derive(Debug, Default, Clone)]
struct MemState {
files: BTreeMap<String, Arc<Vec<u8>>>,
dirs: BTreeSet<String>,
writer: bool,
backups: usize,
pruning: bool,
}
#[derive(Debug, Default)]
struct MemInner {
name: String,
state: Mutex<MemState>,
released: Condvar,
}
#[derive(Debug, Clone)]
pub struct MemDir {
inner: Arc<MemInner>,
}
impl Default for MemDir {
fn default() -> Self {
Self::new()
}
}
fn not_found(name: &str) -> io::Error {
io::Error::new(io::ErrorKind::NotFound, format!("{name}: no such file"))
}
fn parent_of(name: &str) -> &str {
name.rsplit_once('/').map_or("", |(dir, _)| dir)
}
impl MemState {
fn is_dir(&self, name: &str) -> bool {
name.is_empty() || self.dirs.contains(name)
}
fn file(&self, name: &str) -> io::Result<&Arc<Vec<u8>>> {
self.files.get(name).ok_or_else(|| not_found(name))
}
fn check_parent(&self, name: &str) -> io::Result<()> {
if self.is_dir(parent_of(name)) {
Ok(())
} else {
Err(io::Error::new(
io::ErrorKind::NotFound,
format!("{name}: the directory does not exist"),
))
}
}
fn below(&self, dir: &str) -> (Vec<String>, Vec<String>) {
let prefix = format!("{dir}/");
let files = self
.files
.keys()
.filter(|k| k.starts_with(&prefix))
.cloned()
.collect();
let dirs = self
.dirs
.iter()
.filter(|k| k.starts_with(&prefix))
.cloned()
.collect();
(files, dirs)
}
}
impl MemDir {
pub fn new() -> Self {
Self::named("memory")
}
pub fn named(name: impl Into<String>) -> Self {
Self {
inner: Arc::new(MemInner {
name: name.into(),
..MemInner::default()
}),
}
}
fn state(&self) -> std::sync::MutexGuard<'_, MemState> {
self.inner
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
pub fn total_bytes(&self) -> u64 {
self.state().files.values().map(|f| f.len() as u64).sum()
}
pub fn file_names(&self) -> Vec<String> {
self.state().files.keys().cloned().collect()
}
pub fn file(&self, name: &str) -> Option<Vec<u8>> {
self.state().files.get(name).map(|f| f.as_ref().clone())
}
pub fn put_file(&self, name: &str, bytes: Vec<u8>) {
let mut state = self.state();
let mut dir = parent_of(name);
while !dir.is_empty() {
state.dirs.insert(dir.to_string());
dir = parent_of(dir);
}
state.files.insert(name.to_string(), Arc::new(bytes));
}
pub fn deep_copy(&self) -> MemDir {
let state = self.state();
let copy = MemDir::named(self.inner.name.clone());
{
let mut target = copy.state();
target.files = state
.files
.iter()
.map(|(k, v)| (k.clone(), Arc::new(v.as_ref().clone())))
.collect();
target.dirs = state.dirs.clone();
}
copy
}
}
struct MemReader {
bytes: Arc<Vec<u8>>,
at: usize,
}
impl Read for MemReader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let rest = self.bytes.get(self.at..).unwrap_or_default();
let n = rest.len().min(buf.len());
buf[..n].copy_from_slice(&rest[..n]);
self.at += n;
Ok(n)
}
}
struct MemFile {
dir: MemDir,
name: String,
at: u64,
}
impl Write for MemFile {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let mut state = self.dir.state();
let file = state
.files
.get_mut(&self.name)
.ok_or_else(|| not_found(&self.name))?;
let bytes = Arc::make_mut(file);
let at = usize::try_from(self.at).map_err(|_| io::Error::other("file too large"))?;
if bytes.len() < at {
bytes.resize(at, 0);
}
let overlap = (bytes.len() - at).min(buf.len());
bytes[at..at + overlap].copy_from_slice(&buf[..overlap]);
bytes.extend_from_slice(&buf[overlap..]);
self.at += buf.len() as u64;
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl StoreFile for MemFile {
fn sync(&mut self) -> io::Result<()> {
Ok(())
}
fn set_len(&mut self, len: u64) -> io::Result<()> {
self.dir.truncate(&self.name, len)?;
self.at = len;
Ok(())
}
}
struct MemLock {
dir: MemDir,
kind: LockKind,
}
impl Drop for MemLock {
fn drop(&mut self) {
let mut state = self.dir.state();
match self.kind {
LockKind::Writer => state.writer = false,
LockKind::BackupShared => state.backups = state.backups.saturating_sub(1),
LockKind::BackupExclusive => state.pruning = false,
}
drop(state);
self.dir.inner.released.notify_all();
}
}
impl StoreDir for MemDir {
fn location(&self) -> String {
self.inner.name.clone()
}
fn is_volatile(&self) -> bool {
true
}
fn entry_kind(&self, name: &str) -> io::Result<Option<EntryKind>> {
let state = self.state();
Ok(if state.files.contains_key(name) {
Some(EntryKind::File)
} else if state.is_dir(name) {
Some(EntryKind::Dir)
} else {
None
})
}
fn list(&self, dir: &str) -> io::Result<Vec<DirEntry>> {
let state = self.state();
if !state.is_dir(dir) {
return Err(not_found(dir));
}
let prefix = if dir.is_empty() {
String::new()
} else {
format!("{dir}/")
};
let direct = |name: &String| {
name.strip_prefix(&prefix)
.filter(|rest| !rest.is_empty() && !rest.contains('/'))
.map(str::to_string)
};
let mut out: Vec<DirEntry> = state
.files
.keys()
.filter_map(direct)
.map(|name| DirEntry::new(name, EntryKind::File))
.collect();
out.extend(
state
.dirs
.iter()
.filter_map(direct)
.map(|name| DirEntry::new(name, EntryKind::Dir)),
);
Ok(out)
}
fn file_len(&self, name: &str) -> io::Result<u64> {
Ok(self.state().file(name)?.len() as u64)
}
fn read(&self, name: &str, max: u64) -> io::Result<Vec<u8>> {
let state = self.state();
let file = state.file(name)?;
if file.len() as u64 > max {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("{name} is larger than {max} bytes"),
));
}
Ok(file.as_ref().clone())
}
fn read_range(&self, name: &str, offset: u64, len: usize) -> io::Result<Vec<u8>> {
let state = self.state();
let file = state.file(name)?;
let start = usize::try_from(offset).unwrap_or(usize::MAX);
start
.checked_add(len)
.and_then(|end| file.get(start..end))
.map(<[u8]>::to_vec)
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::UnexpectedEof,
format!("{name} is shorter than {offset} + {len} bytes"),
)
})
}
fn open_read(&self, name: &str) -> io::Result<Box<dyn Read + Send>> {
let bytes = self.state().file(name)?.clone();
Ok(Box::new(MemReader { bytes, at: 0 }))
}
fn create_dir_all(&self, name: &str) -> io::Result<()> {
let mut state = self.state();
let mut dir = name;
while !dir.is_empty() {
if state.files.contains_key(dir) {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("{dir} is a file"),
));
}
state.dirs.insert(dir.to_string());
dir = parent_of(dir);
}
Ok(())
}
fn create(&self, name: &str, exclusive: bool) -> io::Result<Box<dyn StoreFile>> {
{
let mut state = self.state();
state.check_parent(name)?;
if state.dirs.contains(name) {
return Err(io::Error::other(format!("{name} is a directory")));
}
if exclusive && state.files.contains_key(name) {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("{name} exists"),
));
}
state.files.insert(name.to_string(), Arc::new(Vec::new()));
}
Ok(Box::new(MemFile {
dir: self.clone(),
name: name.to_string(),
at: 0,
}))
}
fn open_write(&self, name: &str, offset: u64) -> io::Result<Box<dyn StoreFile>> {
self.state().file(name)?;
Ok(Box::new(MemFile {
dir: self.clone(),
name: name.to_string(),
at: offset,
}))
}
fn append(&self, name: &str, bytes: &[u8]) -> io::Result<()> {
let mut state = self.state();
state.check_parent(name)?;
let file = state.files.entry(name.to_string()).or_default();
Arc::make_mut(file).extend_from_slice(bytes);
Ok(())
}
fn write_atomic(&self, name: &str, bytes: &[u8]) -> io::Result<()> {
let mut state = self.state();
state.check_parent(name)?;
state
.files
.insert(name.to_string(), Arc::new(bytes.to_vec()));
Ok(())
}
fn rename(&self, from: &str, to: &str) -> io::Result<()> {
let mut state = self.state();
state.check_parent(to)?;
if let Some(file) = state.files.remove(from) {
if state.dirs.contains(to) {
state.files.insert(from.to_string(), file);
return Err(io::Error::other(format!("{to} is a directory")));
}
state.files.insert(to.to_string(), file);
return Ok(());
}
if !state.dirs.contains(from) {
return Err(not_found(from));
}
if state.dirs.contains(to) {
let (files, dirs) = state.below(from);
for name in files {
state.files.remove(&name);
}
for name in dirs {
state.dirs.remove(&name);
}
state.dirs.remove(from);
return Ok(());
}
if state.files.contains_key(to) {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("{to} exists"),
));
}
if to.starts_with(&format!("{from}/")) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("{to} is inside {from}"),
));
}
let (files, dirs) = state.below(from);
let moved = |name: &str| format!("{to}{}", &name[from.len()..]);
for name in files {
if let Some(file) = state.files.remove(&name) {
state.files.insert(moved(&name), file);
}
}
for name in dirs {
state.dirs.remove(&name);
state.dirs.insert(moved(&name));
}
state.dirs.remove(from);
state.dirs.insert(to.to_string());
Ok(())
}
fn truncate(&self, name: &str, len: u64) -> io::Result<()> {
let mut state = self.state();
let file = state.files.get_mut(name).ok_or_else(|| not_found(name))?;
let len = usize::try_from(len).map_err(|_| io::Error::other("file too large"))?;
Arc::make_mut(file).resize(len, 0);
Ok(())
}
fn remove_file(&self, name: &str) -> io::Result<()> {
self.state()
.files
.remove(name)
.map(|_| ())
.ok_or_else(|| not_found(name))
}
fn remove_tree(&self, name: &str) -> io::Result<()> {
let mut state = self.state();
if state.files.remove(name).is_some() || name.is_empty() {
return Ok(());
}
if state.dirs.remove(name) {
let (files, dirs) = state.below(name);
for file in files {
state.files.remove(&file);
}
for dir in dirs {
state.dirs.remove(&dir);
}
}
Ok(())
}
fn sync_dir(&self, _: &str) -> io::Result<()> {
Ok(())
}
fn lock(&self, kind: LockKind) -> io::Result<Option<DirLockGuard>> {
let mut state = self.state();
match kind {
LockKind::Writer => {
if state.writer {
return Ok(None);
}
state.writer = true;
}
LockKind::BackupExclusive => {
if state.pruning || state.backups > 0 {
return Ok(None);
}
state.pruning = true;
}
LockKind::BackupShared => {
while state.pruning {
state = self
.inner
.released
.wait(state)
.unwrap_or_else(std::sync::PoisonError::into_inner);
}
state.backups += 1;
}
}
drop(state);
Ok(Some(DirLockGuard::new(MemLock {
dir: self.clone(),
kind,
})))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mem_dir_names_files_and_directories() {
let dir = MemDir::new();
assert!(dir.create("a/b", false).is_err(), "no parent directory");
dir.create_dir_all("a/x").unwrap();
let mut file = dir.create("a/b", true).unwrap();
file.write_all(b"hello").unwrap();
assert!(dir.create("a/b", true).is_err());
assert_eq!(dir.read("a/b", 10).unwrap(), b"hello");
assert!(dir.read("a/b", 4).is_err());
assert_eq!(dir.read_range("a/b", 1, 3).unwrap(), b"ell");
assert!(dir.read_range("a/b", 3, 3).is_err());
file.set_len(2).unwrap();
file.write_all(b"y!").unwrap();
assert_eq!(dir.file("a/b").unwrap(), b"hey!");
let mut names: Vec<_> = dir.list("a").unwrap().into_iter().map(|e| e.name).collect();
names.sort();
assert_eq!(names, ["b", "x"]);
dir.append("a/x/m", b"1").unwrap();
dir.append("a/x/m", b"2").unwrap();
dir.rename("a", "c").unwrap();
assert_eq!(dir.entry_kind("a").unwrap(), None);
assert_eq!(dir.file("c/x/m").unwrap(), b"12");
assert_eq!(dir.entry_kind("c/x").unwrap(), Some(EntryKind::Dir));
let copy = dir.deep_copy();
dir.remove_tree("c").unwrap();
assert!(dir.file_names().is_empty());
assert_eq!(copy.file_names(), ["c/b", "c/x/m"]);
assert_eq!(copy.total_bytes(), 6);
}
#[test]
fn mem_dir_locks_are_in_process_flags() {
let dir = MemDir::new();
let writer = dir.lock(LockKind::Writer).unwrap();
assert!(writer.is_some());
assert!(dir.lock(LockKind::Writer).unwrap().is_none());
drop(writer);
assert!(dir.lock(LockKind::Writer).unwrap().is_some());
let backup = dir.lock(LockKind::BackupShared).unwrap();
assert!(dir.lock(LockKind::BackupExclusive).unwrap().is_none());
drop(backup);
assert!(dir.lock(LockKind::BackupExclusive).unwrap().is_some());
}
}