use anyhow::{Context, Result};
use blake3;
use serde::{Deserialize, Serialize};
use std::fs;
use std::io::Read;
use std::path::Path;
pub mod downloader;
pub mod manifest;
pub use downloader::{DownloadCache, DownloadOptions};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChecksumPolicy {
#[serde(rename = "type")]
pub policy_type: ChecksumPolicyType,
#[serde(skip_serializing_if = "Option::is_none")]
pub expected_hash: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ChecksumPolicyType {
Required, Preferred, Optional, }
#[derive(Debug, Clone)]
pub struct CacheStrategy {
pub check_remote: bool,
}
impl Default for CacheStrategy {
fn default() -> Self {
Self { check_remote: true }
}
}
pub fn calculate_blake3(path: &Path) -> Result<String> {
let mut file = fs::File::open(path)
.with_context(|| format!("Failed to open file for checksum: {}", path.display()))?;
let metadata = file.metadata()?;
let file_size = metadata.len();
if file_size > 100 * 1024 * 1024 {
let mut hasher = blake3::Hasher::new();
let mut buffer = vec![0; 64 * 1024 * 1024];
loop {
let bytes_read = file.read(&mut buffer)?;
if bytes_read == 0 {
break;
}
hasher.update_rayon(&buffer[..bytes_read]);
}
Ok(hasher.finalize().to_hex().to_string())
} else {
let mut hasher = blake3::Hasher::new();
let mut buffer = vec![0; 8 * 1024 * 1024];
loop {
let bytes_read = file.read(&mut buffer)?;
if bytes_read == 0 {
break;
}
hasher.update(&buffer[..bytes_read]);
}
Ok(hasher.finalize().to_hex().to_string())
}
}
pub fn link_or_copy(source: &Path, target: &Path) -> Result<()> {
if let Some(parent) = target.parent() {
fs::create_dir_all(parent).with_context(|| {
format!("Failed to create parent directory for {}", target.display())
})?;
}
if target.exists() {
fs::remove_file(target)
.with_context(|| format!("Failed to remove existing file at {}", target.display()))?;
}
#[cfg(unix)]
{
if fs::hard_link(source, target).is_ok() {
return Ok(());
}
use std::os::unix::fs::symlink;
symlink(source, target).with_context(|| {
format!(
"Failed to create symlink from {} to {}",
source.display(),
target.display()
)
})?;
}
#[cfg(windows)]
{
fs::copy(source, target).with_context(|| {
format!(
"Failed to copy file from {} to {}",
source.display(),
target.display()
)
})?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_calculate_blake3() {
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("test.txt");
fs::write(&test_file, b"Hello, World!").unwrap();
let hash = calculate_blake3(&test_file).unwrap();
assert!(!hash.is_empty());
assert_eq!(hash.len(), 64); }
#[test]
fn test_link_or_copy() {
let temp_dir = TempDir::new().unwrap();
let source = temp_dir.path().join("source.txt");
let target = temp_dir.path().join("subdir/target.txt");
fs::write(&source, b"test content").unwrap();
link_or_copy(&source, &target).unwrap();
assert!(target.exists());
assert_eq!(fs::read(&target).unwrap(), b"test content");
}
}