use std::fs::File;
use std::io::{self, Read};
use std::path::{Path, PathBuf};
use opys_core::{Artifact, HashEntry, Integrity, Source};
use serde::{Deserialize, Serialize};
use sha1::{Digest, Sha1};
use sha2::Sha256;
use crate::contribution::{BuildArtifact, Contribution};
#[derive(Debug, thiserror::Error)]
pub enum ScanError {
#[error("cannot read {path}: {source}")]
Io {
path: String,
#[source]
source: io::Error,
},
}
fn io_at(path: &Path) -> impl FnOnce(io::Error) -> ScanError + '_ {
move |source| ScanError::Io {
path: path.display().to_string(),
source,
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ScannedFile {
pub rel: String,
pub dir: String,
pub filename: String,
pub abs: PathBuf,
pub size: u64,
}
fn walk(root: &Path, dir: &Path, out: &mut Vec<ScannedFile>) -> Result<(), ScanError> {
for entry in std::fs::read_dir(dir).map_err(io_at(dir))? {
let entry = entry.map_err(io_at(dir))?;
let abs = entry.path();
let kind = entry.file_type().map_err(io_at(&abs))?;
if kind.is_dir() {
walk(root, &abs, out)?;
} else if kind.is_file() {
let size = entry.metadata().map_err(io_at(&abs))?.len();
let parts: Vec<String> = abs
.strip_prefix(root)
.unwrap_or(&abs)
.components()
.map(|part| part.as_os_str().to_string_lossy().into_owned())
.collect();
let (filename, dirs) = parts.split_last().expect("a file has a name");
out.push(ScannedFile {
rel: parts.join("/"),
dir: dirs.join("/"),
filename: filename.clone(),
abs,
size,
});
}
}
Ok(())
}
pub fn scan_directory(root: &Path) -> Result<Vec<ScannedFile>, ScanError> {
let mut files = Vec::new();
walk(root, root, &mut files)?;
files.sort_by(|a, b| a.rel.cmp(&b.rel));
Ok(files)
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ScanHash {
#[default]
Sha1,
Sha256,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PlacedFile {
pub abs: PathBuf,
pub path: String,
#[serde(default)]
pub url: Option<String>,
}
fn digest<D: Digest>(path: &Path) -> Result<(String, u64), ScanError> {
let mut file = File::open(path).map_err(io_at(path))?;
let mut hasher = D::new();
let mut buffer = [0u8; 64 * 1024];
let mut size = 0u64;
loop {
let read = file.read(&mut buffer).map_err(io_at(path))?;
if read == 0 {
return Ok((hex::encode(hasher.finalize()), size));
}
hasher.update(&buffer[..read]);
size += read as u64;
}
}
pub fn scanned_files(files: &[PlacedFile], hash: ScanHash) -> Result<Contribution, ScanError> {
let mut contribution = Contribution::default();
for file in files {
let artifact = match &file.url {
None => BuildArtifact::file(&file.path, &file.abs),
Some(url) => {
let (entry, size) = match hash {
ScanHash::Sha1 => {
let (sha1, size) = digest::<Sha1>(&file.abs)?;
(HashEntry::Sha1 { sha1 }, size)
}
ScanHash::Sha256 => {
let (sha256, size) = digest::<Sha256>(&file.abs)?;
(HashEntry::Sha256 { sha256 }, size)
}
};
Artifact {
path: file.path.clone(),
source: Source::Url { url: url.clone() },
size: Some(size),
rules: Vec::new(),
integrity: Some(Integrity::One(entry)),
metadata: None,
extract: None,
overwrite: Default::default(),
}
.into()
}
};
contribution.artifacts.push(artifact);
}
Ok(contribution)
}