Bootstrap geth Rust workspace
This commit is contained in:
commit
26f81ff1ef
73 changed files with 4835 additions and 0 deletions
15
crates/geth-cas/Cargo.toml
Normal file
15
crates/geth-cas/Cargo.toml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
[package]
|
||||
name = "geth-cas"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
blake3.workspace = true
|
||||
hex.workspace = true
|
||||
thiserror.workspace = true
|
||||
geth-types = { path = "../geth-types" }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile.workspace = true
|
||||
157
crates/geth-cas/src/lib.rs
Normal file
157
crates/geth-cas/src/lib.rs
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
use geth_types::BlobHash;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum CasError {
|
||||
#[error("io error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("invalid blob hash: {0}")]
|
||||
InvalidHash(String),
|
||||
#[error("blob not found: {0}")]
|
||||
NotFound(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct LocalCas {
|
||||
root: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct BlobInfo {
|
||||
pub hash: BlobHash,
|
||||
pub size_bytes: u64,
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
impl LocalCas {
|
||||
#[must_use]
|
||||
pub fn new(root: impl Into<PathBuf>) -> Self {
|
||||
Self { root: root.into() }
|
||||
}
|
||||
|
||||
pub fn add_path(&self, path: &Path) -> Result<BlobInfo, CasError> {
|
||||
let bytes = std::fs::read(path)?;
|
||||
self.add_bytes(&bytes)
|
||||
}
|
||||
|
||||
pub fn add_bytes(&self, bytes: &[u8]) -> Result<BlobInfo, CasError> {
|
||||
let hash = hash_bytes(bytes);
|
||||
let path = self.blob_path(&hash)?;
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
if !path.exists() {
|
||||
let tmp = path.with_extension("tmp");
|
||||
std::fs::write(&tmp, bytes)?;
|
||||
std::fs::rename(tmp, &path)?;
|
||||
}
|
||||
Ok(BlobInfo {
|
||||
hash,
|
||||
size_bytes: bytes.len() as u64,
|
||||
path,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_to_path(&self, hash: &BlobHash, out: &Path) -> Result<u64, CasError> {
|
||||
let path = self.blob_path(hash)?;
|
||||
if !path.exists() {
|
||||
return Err(CasError::NotFound(hash.to_string()));
|
||||
}
|
||||
if let Some(parent) = out.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
std::fs::copy(path, out).map_err(CasError::from)
|
||||
}
|
||||
|
||||
pub fn has(&self, hash: &BlobHash) -> Result<bool, CasError> {
|
||||
Ok(self.blob_path(hash)?.exists())
|
||||
}
|
||||
|
||||
pub fn list(&self) -> Result<Vec<BlobInfo>, CasError> {
|
||||
let blobs = self.root.join("blobs");
|
||||
if !blobs.exists() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let mut infos = Vec::new();
|
||||
for first in std::fs::read_dir(blobs)? {
|
||||
let first = first?;
|
||||
if !first.file_type()?.is_dir() {
|
||||
continue;
|
||||
}
|
||||
for second in std::fs::read_dir(first.path())? {
|
||||
let second = second?;
|
||||
if !second.file_type()?.is_dir() {
|
||||
continue;
|
||||
}
|
||||
for entry in std::fs::read_dir(second.path())? {
|
||||
let entry = entry?;
|
||||
if !entry.file_type()?.is_file() {
|
||||
continue;
|
||||
}
|
||||
let hash = entry.file_name().to_string_lossy().to_string();
|
||||
if is_valid_hash(&hash) {
|
||||
let meta = entry.metadata()?;
|
||||
infos.push(BlobInfo {
|
||||
hash: BlobHash::new(hash),
|
||||
size_bytes: meta.len(),
|
||||
path: entry.path(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
infos.sort_by(|a, b| a.hash.as_str().cmp(b.hash.as_str()));
|
||||
Ok(infos)
|
||||
}
|
||||
|
||||
pub fn blob_path(&self, hash: &BlobHash) -> Result<PathBuf, CasError> {
|
||||
validate_hash(hash)?;
|
||||
let hash = hash.as_str();
|
||||
Ok(self
|
||||
.root
|
||||
.join("blobs")
|
||||
.join(&hash[0..2])
|
||||
.join(&hash[2..4])
|
||||
.join(hash))
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn hash_bytes(bytes: &[u8]) -> BlobHash {
|
||||
BlobHash::new(blake3::hash(bytes).to_hex().to_string())
|
||||
}
|
||||
|
||||
pub fn hash_path(path: &Path) -> Result<BlobHash, CasError> {
|
||||
let bytes = std::fs::read(path)?;
|
||||
Ok(hash_bytes(&bytes))
|
||||
}
|
||||
|
||||
pub fn validate_hash(hash: &BlobHash) -> Result<(), CasError> {
|
||||
if is_valid_hash(hash.as_str()) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(CasError::InvalidHash(hash.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_valid_hash(hash: &str) -> bool {
|
||||
hash.len() == 64 && hash.bytes().all(|byte| byte.is_ascii_hexdigit())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cas_add_get_has_list() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let cas = LocalCas::new(dir.path());
|
||||
let info = cas.add_bytes(b"hello geth").expect("add");
|
||||
assert!(cas.has(&info.hash).expect("has"));
|
||||
assert_eq!(cas.list().expect("list").len(), 1);
|
||||
let out = dir.path().join("out.txt");
|
||||
cas.get_to_path(&info.hash, &out).expect("get");
|
||||
assert_eq!(std::fs::read(out).expect("read"), b"hello geth");
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue