mirror of
https://github.com/Noratrieb/fix-und-fertig.git
synced 2026-01-14 18:55:04 +01:00
db
This commit is contained in:
parent
d964157bfc
commit
49c9e3aa4c
6 changed files with 130 additions and 17 deletions
|
|
@ -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<()> {
|
||||||
let file = utils::read_file(path)?;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
93
src/db.rs
93
src/db.rs
|
|
@ -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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue