From 49c9e3aa4c872cc6a89c21c33ae3b9ca345a97e5 Mon Sep 17 00:00:00 2001 From: Nilstrieb <48135649+Nilstrieb@users.noreply.github.com> Date: Wed, 31 Jan 2024 21:44:14 +0100 Subject: [PATCH] db --- src/cli/commands/db.rs | 17 ++++++-- src/cli/mod.rs | 7 +++- src/cli/utils.rs | 18 +++++++- src/db.rs | 93 +++++++++++++++++++++++++++++++++++++++++- src/workspace.rs | 6 ++- tests/init.rs | 6 --- 6 files changed, 130 insertions(+), 17 deletions(-) diff --git a/src/cli/commands/db.rs b/src/cli/commands/db.rs index 3018c60..1daefa8 100644 --- a/src/cli/commands/db.rs +++ b/src/cli/commands/db.rs @@ -1,12 +1,21 @@ //! `fuf db` commands. -use color_eyre::Result; -use std::{path::Path}; +use color_eyre::{eyre::Context, Result}; +use std::path::PathBuf; use crate::{cli::utils, workspace::Workspace}; -pub fn save_file(workspace: &Workspace, path: &Path) -> Result<()> { - let file = utils::read_file(path)?; +pub fn save_file(workspace: &Workspace, paths: &[PathBuf]) -> Result<()> { + for path in paths { + let file = utils::read_file(path)?; + + let address = workspace + .db + .save_file(&file) + .wrap_err("saving file to database")?; + + utils::print(format_args!("{address}\n")).wrap_err("printing output")?; + } Ok(()) } diff --git a/src/cli/mod.rs b/src/cli/mod.rs index e7b50d0..7e16c72 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -37,7 +37,10 @@ pub enum Command { #[derive(Debug, Subcommand)] pub enum DbCommand { /// Save a file into the database and get the hash. - SaveFile { path: PathBuf }, + SaveFile { + #[arg(required = true)] + paths: Vec, + }, } pub fn main() -> Result<()> { @@ -65,7 +68,7 @@ pub fn run_command(cwd: &Path, cli: Cli) -> Result<()> { workspace::Workspace::find(&cwd).wrap_err("failed to open workspace")?; match command { - DbCommand::SaveFile { path } => commands::db::save_file(&workspace, &path), + DbCommand::SaveFile { paths } => commands::db::save_file(&workspace, &paths), } } } diff --git a/src/cli/utils.rs b/src/cli/utils.rs index ca9987d..2b6b551 100644 --- a/src/cli/utils.rs +++ b/src/cli/utils.rs @@ -1,9 +1,23 @@ ///! Utilities for the CLI functions. These should *not* be used outside of the CLI-specific code! - use color_eyre::{eyre::Context, Result}; -use std::path::Path; +use std::{ + fmt::Display, + io::{self, Write}, + path::Path, +}; /// [`std::fs::read`], adding the path to the error message. pub(super) fn read_file(path: &Path) -> Result> { std::fs::read(path).wrap_err_with(|| format!("trying to open {}", path.display())) } + +/// Prints the content to stdout. Handles `BrokenPipe` by ignoring the rror. +/// Does *not* exit for `BrokenPipe` +pub(super) fn print(content: impl Display) -> io::Result<()> { + let result = write!(std::io::stdout().lock(), "{}", content); + match result { + Ok(()) => Ok(()), + Err(e) if e.kind() == io::ErrorKind::BrokenPipe => Ok(()), + Err(e) => Err(e), + } +} diff --git a/src/db.rs b/src/db.rs index e11f020..95c56b7 100644 --- a/src/db.rs +++ b/src/db.rs @@ -13,14 +13,26 @@ //! |-- 60d4301d00238a934f94eacd0d4963ca7810afd9f198256e9f6aea2d8c101793 //! ``` +use std::io::{self, Read}; +use std::{fmt::Display, io::Write}; + use cap_std::fs::Dir; -use color_eyre::{eyre::Context, Result}; +use color_eyre::{ + eyre::{bail, Context}, + Result, +}; pub struct Db { db: Dir, objects: Dir, } +#[derive(Debug, Clone, Copy)] +pub struct Address(blake3::Hash); + +/// How often we bother retrying things when we race. +const RETRY_TOLERANCE: usize = 5; + impl Db { pub fn init(dir: &Dir) -> Result<()> { dir.create_dir(".fuf/db").wrap_err("creating .fuf/db")?; @@ -28,6 +40,7 @@ impl Db { .wrap_err("creating .fuf/db/objects")?; Ok(()) } + pub fn open(dir: &Dir) -> Result { let db = dir .open_dir(".fuf/db") @@ -38,4 +51,82 @@ impl Db { Ok(Self { db, objects }) } + + pub fn save_file(&self, content: &[u8]) -> Result
{ + let hash = Address(blake3::hash(content)); + + let prefix_dir = self + .open_prefix_dir(hash) + .wrap_err("opening prefix dir for saving file")?; + + let suffix = &hash.0.to_hex()[2..]; + match prefix_dir.create(suffix) { + Ok(mut file) => { + file.write_all(content) + .wrap_err("writing contents of file")?; + } + Err(e) if e.kind() == io::ErrorKind::AlreadyExists => { + // Ok, it already exists. Do nothing. + // But make sure it reaaaaally is what we want. + if cfg!(debug_assertions) { + let mut existing_content = Vec::new(); + let _: usize = prefix_dir + .open(suffix) + .wrap_err("opening file")? + .read_to_end(&mut existing_content) + .wrap_err("reading existing content")?; + if existing_content != content { + let _: Result<_, _> = std::fs::write(".fuf-inconsistenty-new!", content); + panic!("inconsistency file {hash} already exists with a different content. dumping new content to .fuf-inconsistenty-new!"); + } + } + } + Err(e) => return Err(e).wrap_err("opening file"), + } + + Ok(hash) + } + + fn open_prefix_dir(&self, hash: Address) -> Result { + let hash_string = hash.0.to_hex(); + let prefix = &hash_string[..2]; + + open_or_create( + || self.objects.open_dir(prefix), + || self.objects.create_dir(prefix), + ) + } +} + +fn open_or_create( + open: impl Fn() -> io::Result, + create: impl Fn() -> io::Result<()>, +) -> Result { + for _ in 0..RETRY_TOLERANCE { + let dir = open(); + + match dir { + Ok(dir) => return Ok(dir), + Err(e) if e.kind() == io::ErrorKind::NotFound => { + let dir = create(); + match dir { + Ok(()) => { + // Try opening again now that it's been created... + } + Err(e) if e.kind() == io::ErrorKind::AlreadyExists => { + // A race! Ok, try opening the other directory now... + } + Err(e) => return Err(e).wrap_err("failed to create file"), + } + } + Err(e) => return Err(e).wrap_err("failed to open file"), + } + } + bail!("repeated creations and deletions of file") +} + +impl Display for Address { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0.to_hex()) + } } diff --git a/src/workspace.rs b/src/workspace.rs index aec0e2a..2e9a99e 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -12,7 +12,7 @@ use color_eyre::{ pub struct Workspace { root: Dir, - db: Db, + pub db: Db, } impl Workspace { @@ -49,7 +49,9 @@ pub fn find_workspace_dir(mut dir: &Path) -> Result> { for child in readdir { let child = child .wrap_err_with(|| format!("failed to read entry in directory {}", dir.display()))?; - if child.path().ends_with(".fuf") {} + if child.path().ends_with(".fuf") { + return Ok(Some(dir.to_owned())); + } } let Some(parent) = dir.parent() else { diff --git a/tests/init.rs b/tests/init.rs index e5c967f..0fec436 100644 --- a/tests/init.rs +++ b/tests/init.rs @@ -17,11 +17,5 @@ fn double_init() { let dir = tmpdir(); run_in(&dir, ["init"]).unwrap(); - - assert!(dir.path().join(".fuf").is_dir()); - assert!(dir.path().join(".fuf").join("db").is_dir()); - assert!(dir.path().join(".fuf").join("db").join("objects").is_dir()); - - assert!(run_in(&dir, ["init"]).is_err()); }