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
.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 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<()> {
for path in paths {
@ -11,7 +11,7 @@ pub fn save_file(workspace: &Workspace, paths: &[PathBuf]) -> Result<()> {
let address = workspace
.db
.save_file(&file)
.save_blob(&file)
.wrap_err("saving file to database")?;
utils::print(format_args!("{address}\n")).wrap_err("printing output")?;
@ -19,3 +19,23 @@ pub fn save_file(workspace: &Workspace, paths: &[PathBuf]) -> Result<()> {
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 color_eyre::{eyre::Context, Result};
use crate::workspace;
use crate::{db::Address, workspace};
#[derive(Debug, Parser)]
#[command(name = "fuf")]
@ -41,6 +41,15 @@ pub enum DbCommand {
#[arg(required = true)]
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<()> {
@ -69,6 +78,8 @@ pub fn run_command(cwd: &Path, cli: Cli) -> Result<()> {
match command {
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!
use color_eyre::{eyre::Context, Result};
use color_eyre::{
eyre::{bail, Context},
Result,
};
use std::{
fmt::Display,
fs::File,
io::{self, Write},
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()))
}
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.
/// Does *not* exit for `BrokenPipe`
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),
}
}
/// 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::str::FromStr;
use std::{fmt::Display, io::Write};
use cap_std::fs::Dir;
@ -51,15 +52,16 @@ impl Db {
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 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) {
let full_name = hash.0.to_hex();
let full_name = full_name.as_str();
match prefix_dir.create(full_name) {
Ok(mut file) => {
file.write_all(content)
.wrap_err("writing contents of file")?;
@ -70,7 +72,7 @@ impl Db {
if cfg!(debug_assertions) {
let mut existing_content = Vec::new();
let _: usize = prefix_dir
.open(suffix)
.open(full_name)
.wrap_err("opening file")?
.read_to_end(&mut existing_content)
.wrap_err("reading existing content")?;
@ -86,6 +88,11 @@ impl Db {
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> {
let hash_string = hash.0.to_hex();
let prefix = &hash_string[..2];
@ -129,3 +136,11 @@ impl Display for Address {
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;
mod db;
mod tree;
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();
}
}
}