use std::{ffi::CStr, mem::MaybeUninit, time::SystemTime};
use rustix::{
fd::OwnedFd,
fs::{self, AtFlags, FileType, FlockOperation, Mode, OFlags, RawDir, SeekFrom},
io::{self, Errno},
};
use super::FileStream;
const ACTIVE_NAMES: [&[u8]; 4] = [
b"saddle-access.jsonl",
b"saddle-trace.jsonl",
b"saddle-event.jsonl",
b"saddle-system.jsonl",
];
const PREFIXES: [&[u8]; 4] = [
b"saddle-access.",
b"saddle-trace.",
b"saddle-event.",
b"saddle-system.",
];
const STAGED_PREFIXES: [&[u8]; 4] = [
b".saddle-access.staged-",
b".saddle-trace.staged-",
b".saddle-event.staged-",
b".saddle-system.staged-",
];
const LOCK_NAME: &[u8] = b".saddle-observability.lock";
pub(super) const ROTATION_RETRIES: u64 = 8;
pub(super) const WRITE_INTERRUPT_RETRIES: u64 = 8;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) enum BackendError {
Directory,
DirectoryExceeded,
PathExceeded,
InvalidPath,
Lock,
Recovery(FileStream),
Open(FileStream),
Write(FileStream),
Sync(FileStream),
Rotate(FileStream),
Retention(FileStream),
}
#[derive(Clone, Copy)]
struct FixedPath<const PATH: usize> {
bytes: [u8; PATH],
length_with_nul: usize,
}
impl<const PATH: usize> FixedPath<PATH> {
const fn empty() -> Self {
Self {
bytes: [0; PATH],
length_with_nul: 1,
}
}
fn new(value: &[u8]) -> Result<Self, BackendError> {
let length_with_nul = value
.len()
.checked_add(1)
.ok_or(BackendError::PathExceeded)?;
if length_with_nul > PATH {
return Err(BackendError::PathExceeded);
}
if value.contains(&0) {
return Err(BackendError::InvalidPath);
}
let mut path = Self::empty();
path.bytes[..value.len()].copy_from_slice(value);
path.length_with_nul = length_with_nul;
Ok(path)
}
fn as_bytes(&self) -> &[u8] {
&self.bytes[..self.length_with_nul - 1]
}
fn as_c_str(&self) -> &CStr {
CStr::from_bytes_with_nul(&self.bytes[..self.length_with_nul])
.expect("FixedPath constructor preserves one trailing NUL")
}
}
#[derive(Clone, Copy)]
struct DirectoryEntry<const PATH: usize> {
name: FixedPath<PATH>,
file_type: FileType,
device: u64,
inode: u64,
}
impl<const PATH: usize> DirectoryEntry<PATH> {
const fn empty() -> Self {
Self {
name: FixedPath::empty(),
file_type: FileType::Unknown,
device: 0,
inode: 0,
}
}
}
struct DirectoryScratch<const PATH: usize, const DIRENT_BYTES: usize, const ENTRIES: usize> {
dirent: [MaybeUninit<u8>; DIRENT_BYTES],
entries: [DirectoryEntry<PATH>; ENTRIES],
entry_count: usize,
generated: FixedPath<PATH>,
}
impl<const PATH: usize, const DIRENT_BYTES: usize, const ENTRIES: usize>
DirectoryScratch<PATH, DIRENT_BYTES, ENTRIES>
{
const fn new() -> Self {
Self {
dirent: [MaybeUninit::uninit(); DIRENT_BYTES],
entries: [DirectoryEntry::empty(); ENTRIES],
entry_count: 0,
generated: FixedPath::empty(),
}
}
}
pub(super) struct LinuxFileBackend<
const PATH: usize,
const DIRENT_BYTES: usize,
const ENTRIES: usize,
> {
_directory_path: FixedPath<PATH>,
directory: OwnedFd,
_lock: OwnedFd,
active: [OwnedFd; 4],
scratch: DirectoryScratch<PATH, DIRENT_BYTES, ENTRIES>,
suffix_counter: u64,
}
impl<const PATH: usize, const DIRENT_BYTES: usize, const ENTRIES: usize>
LinuxFileBackend<PATH, DIRENT_BYTES, ENTRIES>
{
pub(super) fn prepare(directory: &[u8]) -> Result<Self, BackendError> {
if DIRENT_BYTES < 256 || ENTRIES == 0 {
return Err(BackendError::DirectoryExceeded);
}
let directory_path = FixedPath::new(directory)?;
let directory = open_directory(directory_path.as_c_str())?;
let lock_name = FixedPath::<PATH>::new(LOCK_NAME)?;
let lock = fs::openat(
&directory,
lock_name.as_c_str(),
OFlags::CREATE | OFlags::RDWR | OFlags::CLOEXEC,
Mode::RUSR | Mode::WUSR,
)
.map_err(|_| BackendError::Lock)?;
fs::flock(&lock, FlockOperation::NonBlockingLockExclusive)
.map_err(|_| BackendError::Lock)?;
let mut scratch = DirectoryScratch::new();
scan_directory(&directory, &mut scratch)?;
for stream in FileStream::ALL {
recover_stream(&directory, &mut scratch, stream)?;
scan_directory(&directory, &mut scratch)?;
}
let active = [
open_active::<PATH>(&directory, FileStream::Access)?,
open_active::<PATH>(&directory, FileStream::Trace)?,
open_active::<PATH>(&directory, FileStream::Event)?,
open_active::<PATH>(&directory, FileStream::System)?,
];
Ok(Self {
_directory_path: directory_path,
directory,
_lock: lock,
active,
scratch,
suffix_counter: 0,
})
}
pub(super) fn write_all(
&self,
stream: FileStream,
mut bytes: &[u8],
) -> Result<(), BackendError> {
let mut interrupted = 0_u64;
while !bytes.is_empty() {
match io::write(&self.active[stream.index()], bytes) {
Ok(0) => return Err(BackendError::Write(stream)),
Ok(written) => bytes = &bytes[written..],
Err(Errno::INTR) if interrupted < WRITE_INTERRUPT_RETRIES => {
interrupted += 1;
}
Err(_) => return Err(BackendError::Write(stream)),
}
}
Ok(())
}
pub(super) fn sync_stream(&self, stream: FileStream) -> Result<(), BackendError> {
fs::fdatasync(&self.active[stream.index()]).map_err(|_| BackendError::Sync(stream))
}
pub(super) fn sync_all(&self) -> Result<(), BackendError> {
for stream in FileStream::ALL {
self.sync_stream(stream)?;
}
Ok(())
}
pub(super) fn sync_directory(&self) -> Result<(), BackendError> {
fs::fsync(&self.directory).map_err(|_| BackendError::Directory)
}
pub(super) fn rotate(
&mut self,
stream: FileStream,
retained_files: usize,
retention_age_ms: u64,
) -> Result<(), BackendError> {
self.sync_stream(stream)?;
let active = FixedPath::<PATH>::new(ACTIVE_NAMES[stream.index()])
.map_err(|_| BackendError::Rotate(stream))?;
let mut published = false;
for _ in 0..ROTATION_RETRIES {
generated_rotated(
&mut self.scratch.generated,
stream,
unix_millis(),
self.suffix_counter,
)
.map_err(|_| BackendError::Rotate(stream))?;
self.suffix_counter = self.suffix_counter.wrapping_add(1);
match fs::linkat(
&self.directory,
active.as_c_str(),
&self.directory,
self.scratch.generated.as_c_str(),
AtFlags::empty(),
) {
Ok(()) => {
published = true;
break;
}
Err(Errno::EXIST) => {}
Err(_) => return Err(BackendError::Rotate(stream)),
}
}
if !published {
return Err(BackendError::Rotate(stream));
}
fs::fsync(&self.directory).map_err(|_| BackendError::Rotate(stream))?;
fs::unlinkat(&self.directory, active.as_c_str(), AtFlags::empty())
.map_err(|_| BackendError::Rotate(stream))?;
fs::fsync(&self.directory).map_err(|_| BackendError::Rotate(stream))?;
let replacement = create_active::<PATH>(&self.directory, stream)
.map_err(|_| BackendError::Rotate(stream))?;
fs::fsync(&self.directory).map_err(|_| BackendError::Rotate(stream))?;
self.active[stream.index()] = replacement;
self.apply_retention(stream, retained_files, retention_age_ms)
}
fn apply_retention(
&mut self,
stream: FileStream,
retained_files: usize,
retention_age_ms: u64,
) -> Result<(), BackendError> {
if retained_files == 0 {
return Err(BackendError::Retention(stream));
}
scan_directory(&self.directory, &mut self.scratch)
.map_err(|_| BackendError::Retention(stream))?;
let entries = &self.scratch.entries[..self.scratch.entry_count];
let rotated_count = entries
.iter()
.filter(|entry| parse_rotated(entry.name.as_bytes(), stream).is_some())
.count();
let excess = rotated_count.saturating_sub(retained_files);
let oldest_allowed = unix_millis().saturating_sub(retention_age_ms);
let mut removed = false;
for candidate in entries {
let Some(timestamp) = parse_rotated(candidate.name.as_bytes(), stream) else {
continue;
};
let older_rank = entries
.iter()
.filter_map(|entry| {
parse_rotated(entry.name.as_bytes(), stream)
.map(|other_timestamp| (other_timestamp, entry.name.as_bytes()))
})
.filter(|(other_timestamp, other_name)| {
(*other_timestamp, *other_name) < (timestamp, candidate.name.as_bytes())
})
.count();
if timestamp < oldest_allowed || older_rank < excess {
fs::unlinkat(&self.directory, candidate.name.as_c_str(), AtFlags::empty())
.map_err(|_| BackendError::Retention(stream))?;
removed = true;
}
}
if removed {
fs::fsync(&self.directory).map_err(|_| BackendError::Retention(stream))?;
}
Ok(())
}
}
fn scan_directory<const PATH: usize, const DIRENT_BYTES: usize, const ENTRIES: usize>(
directory: &OwnedFd,
scratch: &mut DirectoryScratch<PATH, DIRENT_BYTES, ENTRIES>,
) -> Result<(), BackendError> {
fs::seek(directory, SeekFrom::Start(0)).map_err(|_| BackendError::Directory)?;
scratch.entry_count = 0;
let mut raw = RawDir::new(directory, &mut scratch.dirent);
while let Some(entry) = raw.next() {
let entry = entry.map_err(|_| BackendError::Directory)?;
let name = entry.file_name().to_bytes();
if name == b"." || name == b".." {
continue;
}
if scratch.entry_count == ENTRIES {
return Err(BackendError::DirectoryExceeded);
}
let name = FixedPath::new(name)?;
let stat = fs::statat(directory, name.as_c_str(), AtFlags::SYMLINK_NOFOLLOW)
.map_err(|_| BackendError::Directory)?;
scratch.entries[scratch.entry_count] = DirectoryEntry {
name,
file_type: FileType::from_raw_mode(stat.st_mode),
device: stat.st_dev,
inode: stat.st_ino,
};
scratch.entry_count += 1;
}
Ok(())
}
fn recover_stream<const PATH: usize, const DIRENT_BYTES: usize, const ENTRIES: usize>(
directory: &OwnedFd,
scratch: &mut DirectoryScratch<PATH, DIRENT_BYTES, ENTRIES>,
stream: FileStream,
) -> Result<(), BackendError> {
let entries = &scratch.entries[..scratch.entry_count];
let active_name = ACTIVE_NAMES[stream.index()];
let mut active = None;
let mut staged = None;
let mut alias = None;
for entry in entries {
let name = entry.name.as_bytes();
if name == active_name {
if entry.file_type != FileType::RegularFile || active.replace(*entry).is_some() {
return Err(BackendError::Recovery(stream));
}
} else if is_staged(name, stream) {
if entry.file_type != FileType::RegularFile || staged.replace(entry.name).is_some() {
return Err(BackendError::Recovery(stream));
}
} else if parse_rotated(name, stream).is_some() {
if entry.file_type != FileType::RegularFile {
return Err(BackendError::Recovery(stream));
}
} else if name.starts_with(PREFIXES[stream.index()])
|| name.starts_with(STAGED_PREFIXES[stream.index()])
{
return Err(BackendError::Recovery(stream));
}
}
if let Some(active_entry) = active {
for entry in entries {
if parse_rotated(entry.name.as_bytes(), stream).is_some()
&& entry.device == active_entry.device
&& entry.inode == active_entry.inode
&& alias.replace(entry.name).is_some()
{
return Err(BackendError::Recovery(stream));
}
}
}
match (active, staged, alias) {
(_, Some(_), Some(_)) | (Some(_), Some(_), None) => Err(BackendError::Recovery(stream)),
(Some(_), None, Some(rotated)) => {
let rotated_fd = fs::openat(
directory,
rotated.as_c_str(),
OFlags::RDONLY | OFlags::CLOEXEC,
Mode::empty(),
)
.map_err(|_| BackendError::Recovery(stream))?;
fs::fsync(&rotated_fd).map_err(|_| BackendError::Recovery(stream))?;
fs::fsync(directory).map_err(|_| BackendError::Recovery(stream))?;
let active =
FixedPath::<PATH>::new(active_name).map_err(|_| BackendError::Recovery(stream))?;
fs::unlinkat(directory, active.as_c_str(), AtFlags::empty())
.map_err(|_| BackendError::Recovery(stream))?;
fs::fsync(directory).map_err(|_| BackendError::Recovery(stream))?;
create_active::<PATH>(directory, stream)?;
fs::fsync(directory).map_err(|_| BackendError::Recovery(stream))
}
(Some(_), None, None) => Ok(()),
(None, Some(staged), None) => {
publish_staged(directory, scratch, staged, stream)?;
create_active::<PATH>(directory, stream)?;
fs::fsync(directory).map_err(|_| BackendError::Recovery(stream))
}
(None, None, None) => {
create_active::<PATH>(directory, stream)?;
fs::fsync(directory).map_err(|_| BackendError::Recovery(stream))
}
(None, None, Some(_)) => Err(BackendError::Recovery(stream)),
}
}
fn publish_staged<const PATH: usize, const DIRENT_BYTES: usize, const ENTRIES: usize>(
directory: &OwnedFd,
scratch: &mut DirectoryScratch<PATH, DIRENT_BYTES, ENTRIES>,
staged: FixedPath<PATH>,
stream: FileStream,
) -> Result<(), BackendError> {
for counter in 0..ROTATION_RETRIES {
generated_rotated(&mut scratch.generated, stream, unix_millis(), counter)
.map_err(|_| BackendError::Recovery(stream))?;
match fs::linkat(
directory,
staged.as_c_str(),
directory,
scratch.generated.as_c_str(),
AtFlags::empty(),
) {
Ok(()) => {
fs::fsync(directory).map_err(|_| BackendError::Recovery(stream))?;
fs::unlinkat(directory, staged.as_c_str(), AtFlags::empty())
.map_err(|_| BackendError::Recovery(stream))?;
fs::fsync(directory).map_err(|_| BackendError::Recovery(stream))?;
return Ok(());
}
Err(Errno::EXIST) => {}
Err(_) => return Err(BackendError::Recovery(stream)),
}
}
Err(BackendError::Recovery(stream))
}
fn open_active<const PATH: usize>(
directory: &OwnedFd,
stream: FileStream,
) -> Result<OwnedFd, BackendError> {
let name = FixedPath::<PATH>::new(ACTIVE_NAMES[stream.index()])
.map_err(|_| BackendError::Open(stream))?;
fs::openat(
directory,
name.as_c_str(),
OFlags::APPEND | OFlags::WRONLY | OFlags::CLOEXEC,
Mode::empty(),
)
.map_err(|_| BackendError::Open(stream))
}
fn create_active<const PATH: usize>(
directory: &OwnedFd,
stream: FileStream,
) -> Result<OwnedFd, BackendError> {
let name = FixedPath::<PATH>::new(ACTIVE_NAMES[stream.index()])
.map_err(|_| BackendError::Open(stream))?;
fs::openat(
directory,
name.as_c_str(),
OFlags::CREATE | OFlags::EXCL | OFlags::APPEND | OFlags::WRONLY | OFlags::CLOEXEC,
Mode::RUSR | Mode::WUSR,
)
.map_err(|_| BackendError::Open(stream))
}
fn open_directory(path: &CStr) -> Result<OwnedFd, BackendError> {
let flags = OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC;
match fs::openat(fs::CWD, path, flags, Mode::empty()) {
Ok(directory) => Ok(directory),
Err(Errno::NOENT) => {
fs::mkdirat(fs::CWD, path, Mode::RUSR | Mode::WUSR | Mode::XUSR)
.map_err(|_| BackendError::Directory)?;
fs::openat(fs::CWD, path, flags, Mode::empty()).map_err(|_| BackendError::Directory)
}
Err(_) => Err(BackendError::Directory),
}
}
fn parse_rotated(name: &[u8], stream: FileStream) -> Option<u64> {
let rest = name.strip_prefix(PREFIXES[stream.index()])?;
let rest = rest.strip_suffix(b".jsonl")?;
if rest.len() != 46 || rest[13] != b'-' {
return None;
}
let timestamp = &rest[..13];
let suffix = &rest[14..];
if !timestamp.iter().all(u8::is_ascii_digit) || !suffix.iter().all(u8::is_ascii_hexdigit) {
return None;
}
let mut value = 0_u64;
for digit in timestamp {
value = value
.checked_mul(10)?
.checked_add(u64::from(digit - b'0'))?;
}
Some(value)
}
fn is_staged(name: &[u8], stream: FileStream) -> bool {
name.strip_prefix(STAGED_PREFIXES[stream.index()])
.is_some_and(|suffix| suffix.len() == 32 && suffix.iter().all(u8::is_ascii_hexdigit))
}
fn generated_rotated<const PATH: usize>(
target: &mut FixedPath<PATH>,
stream: FileStream,
timestamp: u64,
counter: u64,
) -> Result<(), BackendError> {
let prefix = PREFIXES[stream.index()];
let required = prefix
.len()
.checked_add(13 + 1 + 32 + 6 + 1)
.ok_or(BackendError::PathExceeded)?;
if required > PATH {
return Err(BackendError::PathExceeded);
}
let mut cursor = 0;
target.bytes[cursor..cursor + prefix.len()].copy_from_slice(prefix);
cursor += prefix.len();
write_decimal_13(&mut target.bytes[cursor..cursor + 13], timestamp);
cursor += 13;
target.bytes[cursor] = b'-';
cursor += 1;
let mut random = [0_u8; 16];
getrandom::fill(&mut random).map_err(|_| BackendError::Rotate(stream))?;
for (byte, tail) in random[8..].iter_mut().zip(counter.to_be_bytes()) {
*byte ^= tail;
}
for byte in random {
target.bytes[cursor] = hex(byte >> 4);
target.bytes[cursor + 1] = hex(byte & 0x0f);
cursor += 2;
}
target.bytes[cursor..cursor + 6].copy_from_slice(b".jsonl");
cursor += 6;
target.bytes[cursor] = 0;
target.length_with_nul = cursor + 1;
Ok(())
}
fn write_decimal_13(target: &mut [u8], mut value: u64) {
target.fill(b'0');
for index in (0..target.len()).rev() {
target[index] = b'0' + u8::try_from(value % 10).expect("decimal digit fits");
value /= 10;
}
}
const fn hex(value: u8) -> u8 {
match value {
0..=9 => b'0' + value,
_ => b'a' + value - 10,
}
}
fn unix_millis() -> u64 {
SystemTime::UNIX_EPOCH
.elapsed()
.unwrap_or_default()
.as_millis()
.try_into()
.unwrap_or(u64::MAX)
}
#[cfg(test)]
mod tests {
use std::{
os::unix::{ffi::OsStrExt, fs::MetadataExt},
path::PathBuf,
sync::atomic::{AtomicU64, Ordering},
};
use super::*;
type Backend = LinuxFileBackend<256, 4096, 32>;
static NEXT: AtomicU64 = AtomicU64::new(0);
fn directory(name: &str) -> PathBuf {
std::env::temp_dir().join(format!(
"saddle-fixed-backend-{}-{name}",
NEXT.fetch_add(1, Ordering::Relaxed)
))
}
#[test]
fn routes_writes_syncs_and_rotates_four_fixed_files() {
let path = directory("routing");
let mut backend = Backend::prepare(path.as_os_str().as_bytes()).expect("prepare");
for stream in FileStream::ALL {
backend.write_all(stream, b"{\"ok\":true}\n").unwrap();
}
backend.sync_all().unwrap();
backend
.rotate(FileStream::Access, 8, 7 * 24 * 60 * 60 * 1_000)
.unwrap();
backend.sync_directory().unwrap();
for name in ACTIVE_NAMES {
assert!(path.join(std::str::from_utf8(name).unwrap()).is_file());
}
assert!(
std::fs::read_dir(&path)
.unwrap()
.filter_map(Result::ok)
.any(|entry| parse_rotated(
entry.file_name().as_os_str().as_bytes(),
FileStream::Access
)
.is_some())
);
drop(backend);
std::fs::remove_dir_all(path).unwrap();
}
#[test]
fn recovers_protocol_alias_and_rotated_only_without_touching_history() {
let path = directory("recovery");
{
let backend = Backend::prepare(path.as_os_str().as_bytes()).unwrap();
drop(backend);
}
let active = path.join("saddle-access.jsonl");
std::fs::write(&active, b"history\n").unwrap();
let rotated =
path.join("saddle-access.0000000000000-00000000000000000000000000000000.jsonl");
std::fs::hard_link(&active, &rotated).unwrap();
let alias_inode = std::fs::metadata(&rotated).unwrap().ino();
let backend = Backend::prepare(path.as_os_str().as_bytes()).unwrap();
assert_ne!(std::fs::metadata(&active).unwrap().ino(), alias_inode);
assert_eq!(std::fs::read(&rotated).unwrap(), b"history\n");
drop(backend);
std::fs::remove_file(&active).unwrap();
let backend = Backend::prepare(path.as_os_str().as_bytes()).unwrap();
assert!(active.is_file());
assert_eq!(std::fs::read(&rotated).unwrap(), b"history\n");
drop(backend);
std::fs::remove_dir_all(path).unwrap();
}
#[test]
fn recovers_staged_and_rejects_multiple_aliases_without_guessing() {
let path = directory("staged");
std::fs::create_dir_all(&path).unwrap();
let staged = path.join(".saddle-system.staged-00000000000000000000000000000000");
std::fs::write(&staged, b"staged-history\n").unwrap();
let backend = Backend::prepare(path.as_os_str().as_bytes()).unwrap();
assert!(!staged.exists());
let rotated = std::fs::read_dir(&path)
.unwrap()
.filter_map(Result::ok)
.find(|entry| {
parse_rotated(entry.file_name().as_os_str().as_bytes(), FileStream::System)
.is_some()
})
.unwrap();
assert_eq!(std::fs::read(rotated.path()).unwrap(), b"staged-history\n");
drop(backend);
let active = path.join("saddle-access.jsonl");
let first = path.join("saddle-access.0000000000001-11111111111111111111111111111111.jsonl");
let second =
path.join("saddle-access.0000000000002-22222222222222222222222222222222.jsonl");
std::fs::hard_link(&active, first).unwrap();
std::fs::hard_link(&active, second).unwrap();
assert!(matches!(
Backend::prepare(path.as_os_str().as_bytes()),
Err(BackendError::Recovery(FileStream::Access))
));
std::fs::remove_dir_all(path).unwrap();
}
#[test]
fn retention_uses_fixed_directory_table_and_deletes_oldest() {
let path = directory("retention");
let mut backend = Backend::prepare(path.as_os_str().as_bytes()).unwrap();
for _ in 0..3 {
backend
.write_all(FileStream::Event, b"{\"event\":1}\n")
.unwrap();
backend.rotate(FileStream::Event, 2, u64::MAX).unwrap();
}
let retained = std::fs::read_dir(&path)
.unwrap()
.filter_map(Result::ok)
.filter(|entry| {
parse_rotated(entry.file_name().as_os_str().as_bytes(), FileStream::Event).is_some()
})
.count();
assert_eq!(retained, 2);
drop(backend);
std::fs::remove_dir_all(path).unwrap();
}
#[test]
fn rejects_path_directory_overflow_and_lock_collision_without_fallback() {
assert!(matches!(
LinuxFileBackend::<8, 4096, 32>::prepare(b"/path/is/too/long"),
Err(BackendError::PathExceeded)
));
let path = directory("lock");
let first = Backend::prepare(path.as_os_str().as_bytes()).unwrap();
assert!(matches!(
Backend::prepare(path.as_os_str().as_bytes()),
Err(BackendError::Lock)
));
drop(first);
for index in 0..33 {
std::fs::write(path.join(format!("unrelated-{index}")), b"x").unwrap();
}
assert!(matches!(
Backend::prepare(path.as_os_str().as_bytes()),
Err(BackendError::DirectoryExceeded)
));
std::fs::remove_dir_all(path).unwrap();
}
}