This commit is contained in:
nora 2024-02-14 21:08:00 +01:00
parent 382143e6da
commit d28d923d43
9 changed files with 182 additions and 8 deletions

1
.envrc Normal file
View file

@ -0,0 +1 @@
use nix

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
/target /target
.fuf .fuf
/testing

3
shell.nix Normal file
View file

@ -0,0 +1,3 @@
{ pkgs ? import <nixpkgs> { } }: pkgs.mkShell {
packages = with pkgs; [ rustup tree ];
}

View file

@ -3,7 +3,7 @@
use color_eyre::{eyre::Context, Result}; use color_eyre::{eyre::Context, Result};
use std::path::PathBuf; use std::path::PathBuf;
use crate::{cli::utils, workspace::Workspace}; use crate::{cli::utils, db::Address, workspace::Workspace};
pub fn save_file(workspace: &Workspace, paths: &[PathBuf]) -> Result<()> { pub fn save_file(workspace: &Workspace, paths: &[PathBuf]) -> Result<()> {
for path in paths { for path in paths {
@ -11,7 +11,7 @@ pub fn save_file(workspace: &Workspace, paths: &[PathBuf]) -> Result<()> {
let address = workspace let address = workspace
.db .db
.save_file(&file) .save_blob(&file)
.wrap_err("saving file to database")?; .wrap_err("saving file to database")?;
utils::print(format_args!("{address}\n")).wrap_err("printing output")?; utils::print(format_args!("{address}\n")).wrap_err("printing output")?;
@ -19,3 +19,23 @@ pub fn save_file(workspace: &Workspace, paths: &[PathBuf]) -> Result<()> {
Ok(()) Ok(())
} }
pub fn save_tree(workspace: &Workspace, paths: &[PathBuf]) -> Result<()> {
for path in paths {
let dir = utils::dir_from_path(path)?;
let address = crate::tree::save_tree(workspace, &dir)?;
utils::print(format_args!("{address}\n")).wrap_err("printing output")?;
}
Ok(())
}
pub fn read_blob(workspace: &Workspace, address: Address) -> Result<()> {
let content = workspace
.db
.read_blob(address)
.wrap_err_with(|| format!("reading blog {address}"))?;
utils::print_bytes(&content).wrap_err("printing bytes")?;
Ok(())
}

View file

@ -13,7 +13,7 @@ use cap_std::fs::Dir;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use color_eyre::{eyre::Context, Result}; use color_eyre::{eyre::Context, Result};
use crate::workspace; use crate::{db::Address, workspace};
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
#[command(name = "fuf")] #[command(name = "fuf")]
@ -41,6 +41,15 @@ pub enum DbCommand {
#[arg(required = true)] #[arg(required = true)]
paths: Vec<PathBuf>, paths: Vec<PathBuf>,
}, },
/// Save a tree (directory) into the database and get the hash.
SaveTree {
#[arg(required = true)]
paths: Vec<PathBuf>,
},
/// Reads the blob behind an address.
ReadBlob {
hash: Address,
}
} }
pub fn main() -> Result<()> { pub fn main() -> Result<()> {
@ -69,6 +78,8 @@ pub fn run_command(cwd: &Path, cli: Cli) -> Result<()> {
match command { match command {
DbCommand::SaveFile { paths } => commands::db::save_file(&workspace, &paths), DbCommand::SaveFile { paths } => commands::db::save_file(&workspace, &paths),
DbCommand::SaveTree { paths } => commands::db::save_tree(&workspace, &paths),
DbCommand::ReadBlob { hash: address } => commands::db::read_blob(&workspace, address),
} }
} }
} }

View file

@ -1,7 +1,12 @@
use cap_std::fs::Dir;
///! 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::{bail, Context},
Result,
};
use std::{ use std::{
fmt::Display, fmt::Display,
fs::File,
io::{self, Write}, io::{self, Write},
path::Path, path::Path,
}; };
@ -11,6 +16,14 @@ 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()))
} }
pub fn dir_from_path(path: &Path) -> Result<Dir> {
let dir = File::open(path).wrap_err_with(|| format!("opening directory {}", path.display()))?;
if !dir.metadata()?.is_dir() {
bail!("{} is not a directory", path.display());
}
Ok(Dir::from_std_file(dir))
}
/// Prints the content to stdout. Handles `BrokenPipe` by ignoring the rror. /// Prints the content to stdout. Handles `BrokenPipe` by ignoring the rror.
/// Does *not* exit for `BrokenPipe` /// Does *not* exit for `BrokenPipe`
pub(super) fn print(content: impl Display) -> io::Result<()> { pub(super) fn print(content: impl Display) -> io::Result<()> {
@ -21,3 +34,14 @@ pub(super) fn print(content: impl Display) -> io::Result<()> {
Err(e) => Err(e), Err(e) => Err(e),
} }
} }
/// Prints the bytes to stdout. Handles `BrokenPipe` by ignoring the rror.
/// Does *not* exit for `BrokenPipe`
pub(super) fn print_bytes(content: &[u8]) -> io::Result<()> {
let result = std::io::stdout().lock().write_all(content);
match result {
Ok(()) => Ok(()),
Err(e) if e.kind() == io::ErrorKind::BrokenPipe => Ok(()),
Err(e) => Err(e),
}
}

View file

@ -14,6 +14,7 @@
//! ``` //! ```
use std::io::{self, Read}; use std::io::{self, Read};
use std::str::FromStr;
use std::{fmt::Display, io::Write}; use std::{fmt::Display, io::Write};
use cap_std::fs::Dir; use cap_std::fs::Dir;
@ -51,15 +52,16 @@ impl Db {
Ok(Self { objects }) Ok(Self { objects })
} }
pub fn save_file(&self, content: &[u8]) -> Result<Address> { pub fn save_blob(&self, content: &[u8]) -> Result<Address> {
let hash = Address(blake3::hash(content)); let hash = Address(blake3::hash(content));
let prefix_dir = self let prefix_dir = self
.open_prefix_dir(hash) .open_prefix_dir(hash)
.wrap_err("opening prefix dir for saving file")?; .wrap_err("opening prefix dir for saving file")?;
let suffix = &hash.0.to_hex()[2..]; let full_name = hash.0.to_hex();
match prefix_dir.create(suffix) { let full_name = full_name.as_str();
match prefix_dir.create(full_name) {
Ok(mut file) => { Ok(mut file) => {
file.write_all(content) file.write_all(content)
.wrap_err("writing contents of file")?; .wrap_err("writing contents of file")?;
@ -70,7 +72,7 @@ impl Db {
if cfg!(debug_assertions) { if cfg!(debug_assertions) {
let mut existing_content = Vec::new(); let mut existing_content = Vec::new();
let _: usize = prefix_dir let _: usize = prefix_dir
.open(suffix) .open(full_name)
.wrap_err("opening file")? .wrap_err("opening file")?
.read_to_end(&mut existing_content) .read_to_end(&mut existing_content)
.wrap_err("reading existing content")?; .wrap_err("reading existing content")?;
@ -86,6 +88,11 @@ impl Db {
Ok(hash) Ok(hash)
} }
pub fn read_blob(&self, address: Address) -> Result<Vec<u8>> {
let prefix_dir = self.open_prefix_dir(address).wrap_err("opening prefix dir for reading file")?;
prefix_dir.read(address.0.to_hex().as_str()).wrap_err("reading file")
}
fn open_prefix_dir(&self, hash: Address) -> Result<Dir> { fn open_prefix_dir(&self, hash: Address) -> Result<Dir> {
let hash_string = hash.0.to_hex(); let hash_string = hash.0.to_hex();
let prefix = &hash_string[..2]; let prefix = &hash_string[..2];
@ -129,3 +136,11 @@ impl Display for Address {
f.write_str(&self.0.to_hex()) f.write_str(&self.0.to_hex())
} }
} }
impl FromStr for Address {
type Err = blake3::HexError;
fn from_str(s: &str) -> std::prelude::v1::Result<Self, Self::Err> {
blake3::Hash::from_str(s).map(Self)
}
}

View file

@ -1,3 +1,4 @@
pub mod cli; pub mod cli;
mod db; mod db;
mod tree;
mod workspace; mod workspace;

98
src/tree.rs Normal file
View file

@ -0,0 +1,98 @@
use std::io::Read;
use cap_std::fs::Dir;
use color_eyre::{
eyre::{bail, Context, ContextCompat},
Result,
};
use crate::{db::Address, workspace::Workspace};
pub(crate) struct Tree {
pub(crate) children: Vec<TreeChild>,
}
pub(crate) struct TreeChild {
address: Address,
name: String,
kind: TreeChildType,
}
pub(crate) enum TreeChildType {
Dir,
File,
}
pub(crate) fn save_tree(workspace: &Workspace, directory: &Dir) -> Result<Address> {
let mut tree = vec![];
for entry in directory.entries().wrap_err("reading directory")? {
let entry = entry.wrap_err("reading directory")?;
let filetype = entry.file_type().wrap_err("getting entry file type")?;
let file_name = entry.file_name();
let file_name = file_name.to_str().wrap_err("non-UTF-8 file name")?;
if file_name.contains(['\n', '\r']) {
bail!("file name {file_name} contains a newline, which is not supported");
}
match () {
() if filetype.is_dir() => {
let inner_dir = entry.open_dir().wrap_err("opening directory")?;
let inner_address = save_tree(workspace, &inner_dir)
.wrap_err_with(|| format!("saving directory {file_name}"))?;
tree.push(TreeChild {
address: inner_address,
name: file_name.to_owned(),
kind: TreeChildType::Dir,
});
}
() if filetype.is_file() => {
let wrap_err = || format!("reading {file_name}");
let mut file = entry.open().wrap_err_with(wrap_err)?;
let mut content = vec![];
file.read_to_end(&mut content).wrap_err_with(wrap_err)?;
let address = workspace
.db
.save_blob(&content)
.wrap_err("saving file to database")?;
tree.push(TreeChild {
address,
name: file_name.to_owned(),
kind: TreeChildType::File,
});
}
_ => {
bail!("file {file_name} with type {filetype:?} is not supported")
}
}
}
let mut content = vec![];
Tree { children: tree }.serialize(&mut content);
workspace
.db
.save_blob(&content)
.wrap_err("saving tree blob")
}
impl Tree {
pub(crate) fn serialize(&self, out: &mut Vec<u8>) {
use std::io::Write;
for child in &self.children {
write!(
out,
"{}",
match child.kind {
TreeChildType::Dir => "D",
TreeChildType::File => "F",
}
)
.unwrap();
write!(out, " {}", child.address).unwrap();
write!(out, " {}", child.name).unwrap();
write!(out, "\n").unwrap();
}
}
}