diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..1d953f4 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use nix diff --git a/.gitignore b/.gitignore index 4b9ab5a..fbdc12d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target .fuf +/testing diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..856f661 --- /dev/null +++ b/shell.nix @@ -0,0 +1,3 @@ +{ pkgs ? import { } }: pkgs.mkShell { + packages = with pkgs; [ rustup tree ]; +} diff --git a/src/cli/commands/db.rs b/src/cli/commands/db.rs index 1daefa8..5bd1521 100644 --- a/src/cli/commands/db.rs +++ b/src/cli/commands/db.rs @@ -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(()) +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 7e16c72..b8cae54 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -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, }, + /// Save a tree (directory) into the database and get the hash. + SaveTree { + #[arg(required = true)] + paths: Vec, + }, + /// 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), } } } diff --git a/src/cli/utils.rs b/src/cli/utils.rs index 2b6b551..35838f9 100644 --- a/src/cli/utils.rs +++ b/src/cli/utils.rs @@ -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> { std::fs::read(path).wrap_err_with(|| format!("trying to open {}", path.display())) } +pub fn dir_from_path(path: &Path) -> Result { + 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), + } +} diff --git a/src/db.rs b/src/db.rs index 467c7bd..9109be1 100644 --- a/src/db.rs +++ b/src/db.rs @@ -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
{ + pub fn save_blob(&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) { + 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> { + 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 { 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 { + blake3::Hash::from_str(s).map(Self) + } +} diff --git a/src/lib.rs b/src/lib.rs index fc245b6..78e36de 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ pub mod cli; mod db; +mod tree; mod workspace; diff --git a/src/tree.rs b/src/tree.rs new file mode 100644 index 0000000..8af32e0 --- /dev/null +++ b/src/tree.rs @@ -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, +} + +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
{ + 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) { + 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(); + } + } +}