local.rs
raw
use std::{
fs::File,
io::Write as _,
path::{Path, PathBuf},
};
use crate::{
ChunkId, Error,
backend::{Backend, ObjectKey, ObjectKind},
};
/// A [`Backend`] storing objects as files under a local directory.
///
/// The directory layout follows [`ObjectKey::segments`]. Writes go to a
/// temporary file that is atomically renamed into place and synced, so a
/// crash never leaves a partially written object under its final name.
#[derive(Debug)]
pub struct LocalBackend {
root: PathBuf,
}
impl LocalBackend {
/// Uses `root` as a repository directory, creating it if necessary.
pub fn new(root: impl Into<PathBuf>) -> Result<LocalBackend, Error> {
let root = root.into();
std::fs::create_dir_all(&root)?;
// Canonical form, so the path can be compared against scanner
// paths (see Backend::local_root)
let root = root.canonicalize()?;
Ok(LocalBackend { root })
}
fn object_path(&self, key: &ObjectKey) -> PathBuf {
let mut path = self.root.clone();
for segment in key.segments() {
path.push(segment);
}
path
}
}
impl Backend for LocalBackend {
fn put(&self, key: &ObjectKey, data: &[u8]) -> Result<(), Error> {
let path = self.object_path(key);
if path.try_exists()? {
// Objects are write-once and content-addressed or unique by
// construction, so an existing object is already this one.
return Ok(());
}
let parent = path
.parent()
.expect("object paths always have a parent directory");
std::fs::create_dir_all(parent)?;
let mut temp = tempfile::NamedTempFile::new_in(parent)?;
temp.write_all(data)?;
temp.as_file().sync_all()?;
temp.persist(&path).map_err(|err| err.error)?;
sync_dir(parent)?;
Ok(())
}
fn get(&self, key: &ObjectKey) -> Result<Option<Vec<u8>>, Error> {
match std::fs::read(self.object_path(key)) {
Ok(data) => Ok(Some(data)),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(err) => Err(err.into()),
}
}
fn contains(&self, key: &ObjectKey) -> Result<bool, Error> {
Ok(self.object_path(key).try_exists()?)
}
fn list(
&self,
kind: ObjectKind,
visit: &mut dyn FnMut(ObjectKey) -> Result<(), Error>,
) -> Result<(), Error> {
match kind {
ObjectKind::Header => {
if self.contains(&ObjectKey::Header)? {
visit(ObjectKey::Header)?;
}
}
// The flat kinds: one directory of hex-named objects each
ObjectKind::Snapshot | ObjectKind::Prune | ObjectKind::Lock => {
for name in dir_file_names(&self.root.join(kind.directory()))? {
// Names that don't parse are not objects (stray temp
// files from an interrupted put, for example)
if let Some(key) = kind.key_for(&name) {
visit(key)?;
}
}
}
ObjectKind::Chunk => {
let chunks = self.root.join("chunks");
for prefix in dir_file_names(&chunks)? {
if prefix.len() != 2 || !prefix.bytes().all(|b| b.is_ascii_hexdigit()) {
continue;
}
for name in dir_file_names(&chunks.join(prefix))? {
if let Ok(id) = ChunkId::from_hex(name) {
visit(ObjectKey::Chunk(id))?;
}
}
}
}
}
Ok(())
}
fn free_space(&self) -> Result<Option<u64>, Error> {
free_space(&self.root)
}
fn used_space(&self) -> Result<Option<u64>, Error> {
Ok(Some(tree_bytes(&self.root)?))
}
fn delete(&self, key: &ObjectKey) -> Result<(), Error> {
match std::fs::remove_file(self.object_path(key)) {
Ok(()) => Ok(()),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(err) => Err(err.into()),
}
}
fn local_root(&self) -> Option<&Path> {
Some(&self.root)
}
}
/// The space left on the filesystem holding `root`, as available to
/// this user: reserved blocks and quotas are already deducted, which is
/// what a backup wanting to leave room behind cares about.
#[cfg(unix)]
fn free_space(root: &Path) -> Result<Option<u64>, Error> {
let stats = rustix::fs::statvfs(root).map_err(std::io::Error::from)?;
Ok(Some(stats.f_bavail.saturating_mul(stats.f_frsize)))
}
#[cfg(not(unix))]
fn free_space(_root: &Path) -> Result<Option<u64>, Error> {
Ok(None)
}
/// How many bytes of file content a directory tree holds. A directory
/// that vanishes mid-walk contributes nothing, which is the truth by
/// the time the walk ends.
fn tree_bytes(path: &Path) -> Result<u64, Error> {
let entries = match std::fs::read_dir(path) {
Ok(entries) => entries,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(0),
Err(err) => return Err(err.into()),
};
let mut total = 0;
for entry in entries {
let entry = entry?;
let metadata = match entry.metadata() {
Ok(metadata) => metadata,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => continue,
Err(err) => return Err(err.into()),
};
total += if metadata.is_dir() {
tree_bytes(&entry.path())?
} else {
metadata.len()
};
}
Ok(total)
}
/// The file names within a directory, treating a missing directory as
/// empty. Names that are not valid UTF-8 cannot be object names, so they
/// are omitted.
fn dir_file_names(path: &Path) -> Result<Vec<String>, Error> {
let entries = match std::fs::read_dir(path) {
Ok(entries) => entries,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(err) => return Err(err.into()),
};
let mut names = Vec::new();
for entry in entries {
if let Ok(name) = entry?.file_name().into_string() {
names.push(name);
}
}
Ok(names)
}
#[cfg(unix)]
fn sync_dir(path: &Path) -> Result<(), Error> {
File::open(path)?.sync_all()?;
Ok(())
}
#[cfg(not(unix))]
fn sync_dir(_path: &Path) -> Result<(), Error> {
Ok(())
}