This commit is contained in:
nora 2024-01-31 21:44:14 +01:00
parent d964157bfc
commit 49c9e3aa4c
6 changed files with 130 additions and 17 deletions

View file

@ -1,12 +1,21 @@
//! `fuf db` commands. //! `fuf db` commands.
use color_eyre::Result; use color_eyre::{eyre::Context, Result};
use std::{path::Path}; use std::path::PathBuf;
use crate::{cli::utils, workspace::Workspace}; use crate::{cli::utils, workspace::Workspace};
pub fn save_file(workspace: &Workspace, path: &Path) -> Result<()> { pub fn save_file(workspace: &Workspace, paths: &[PathBuf]) -> Result<()> {
for path in paths {
let file = utils::read_file(path)?; 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(()) Ok(())
} }

View file

@ -37,7 +37,10 @@ pub enum Command {
#[derive(Debug, Subcommand)] #[derive(Debug, Subcommand)]
pub enum DbCommand { pub enum DbCommand {
/// Save a file into the database and get the hash. /// Save a file into the database and get the hash.
SaveFile { path: PathBuf }, SaveFile {
#[arg(required = true)]
paths: Vec<PathBuf>,
},
} }
pub fn main() -> Result<()> { 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")?; workspace::Workspace::find(&cwd).wrap_err("failed to open workspace")?;
match command { match command {
DbCommand::SaveFile { path } => commands::db::save_file(&workspace, &path), DbCommand::SaveFile { paths } => commands::db::save_file(&workspace, &paths),
} }
} }
} }

View file

@ -1,9 +1,23 @@
///! Utilities for the CLI functions. These should *not* be used outside of the CLI-specific code! ///! Utilities for the CLI functions. These should *not* be used outside of the CLI-specific code!
use color_eyre::{eyre::Context, Result}; 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. /// [`std::fs::read`], adding the path to the error message.
pub(super) fn read_file(path: &Path) -> Result<Vec<u8>> { pub(super) fn read_file(path: &Path) -> Result<Vec<u8>> {
std::fs::read(path).wrap_err_with(|| format!("trying to open {}", path.display())) 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),
}
}

View file

@ -13,14 +13,26 @@
//! |-- 60d4301d00238a934f94eacd0d4963ca7810afd9f198256e9f6aea2d8c101793 //! |-- 60d4301d00238a934f94eacd0d4963ca7810afd9f198256e9f6aea2d8c101793
//! ``` //! ```
use std::io::{self, Read};
use std::{fmt::Display, io::Write};
use cap_std::fs::Dir; use cap_std::fs::Dir;
use color_eyre::{eyre::Context, Result}; use color_eyre::{
eyre::{bail, Context},
Result,
};
pub struct Db { pub struct Db {
db: Dir, db: Dir,
objects: 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 { impl Db {
pub fn init(dir: &Dir) -> Result<()> { pub fn init(dir: &Dir) -> Result<()> {
dir.create_dir(".fuf/db").wrap_err("creating .fuf/db")?; dir.create_dir(".fuf/db").wrap_err("creating .fuf/db")?;
@ -28,6 +40,7 @@ impl Db {
.wrap_err("creating .fuf/db/objects")?; .wrap_err("creating .fuf/db/objects")?;
Ok(()) Ok(())
} }
pub fn open(dir: &Dir) -> Result<Self> { pub fn open(dir: &Dir) -> Result<Self> {
let db = dir let db = dir
.open_dir(".fuf/db") .open_dir(".fuf/db")
@ -38,4 +51,82 @@ impl Db {
Ok(Self { db, objects }) Ok(Self { db, objects })
} }
pub fn save_file(&self, content: &[u8]) -> Result<Address> {
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<Dir> {
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<T>(
open: impl Fn() -> io::Result<T>,
create: impl Fn() -> io::Result<()>,
) -> Result<T> {
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())
}
} }

View file

@ -12,7 +12,7 @@ use color_eyre::{
pub struct Workspace { pub struct Workspace {
root: Dir, root: Dir,
db: Db, pub db: Db,
} }
impl Workspace { impl Workspace {
@ -49,7 +49,9 @@ pub fn find_workspace_dir(mut dir: &Path) -> Result<Option<PathBuf>> {
for child in readdir { for child in readdir {
let child = child let child = child
.wrap_err_with(|| format!("failed to read entry in directory {}", dir.display()))?; .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 { let Some(parent) = dir.parent() else {

View file

@ -17,11 +17,5 @@ fn double_init() {
let dir = tmpdir(); let dir = tmpdir();
run_in(&dir, ["init"]).unwrap(); 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()); assert!(run_in(&dir, ["init"]).is_err());
} }