use crate::{cache, format::CodeStr};
use std::{
fs,
fs::{File, Metadata},
io::{Seek, SeekFrom, Write},
os::unix::fs::PermissionsExt,
path::{Path, PathBuf},
};
use tar::{Builder, Header};
use walkdir::WalkDir;
fn add_file<W: Write>(
builder: &mut Builder<W>,
metadata: &Metadata,
path: &Path,
source_dir: &Path,
destination_dir: &Path,
file_hashes: &mut Vec<String>,
) -> Result<(), String> {
let source = source_dir.join(&path);
let mut destination = destination_dir.join(&path);
if destination.starts_with("/") {
destination = destination.strip_prefix("/").unwrap().to_owned();
}
let mode = metadata.permissions().mode();
let executable = mode & 0o1 > 0 || mode & 0o10 > 0 || mode & 0o100 > 0;
let mut header = Header::new_gnu();
header.set_mode(if executable { 0o777 } else { 0o666 });
header.set_size(metadata.len());
let mut file = File::open(&source).map_err(|e| {
format!(
"Unable to open file {}. Details: {}",
&source.to_string_lossy().code_str(),
e
)
})?;
file_hashes.push(cache::extend(
&cache::extend(
&cache::hash_str(&path.to_string_lossy()),
&cache::hash_read(&mut file)?,
),
if executable { "+x" } else { "-x" },
));
file.seek(SeekFrom::Start(0)).map_err(|e| {
format!(
"Unable to seek file {}. Details: {}",
&source.to_string_lossy().code_str(),
e
)
})?;
builder.append_data(&mut header, destination, file).unwrap();
Ok(())
}
pub fn create<W: Write>(
writer: W,
paths: &[PathBuf],
source_dir: &Path,
destination_dir: &Path,
) -> Result<(W, String), String> {
let source_dir = source_dir.canonicalize().map_err(|e| {
format!(
"Unable to canonicalize path {}. Details: {}",
source_dir.to_string_lossy().code_str(),
e
)
})?;
let mut file_hashes = vec![];
let mut builder = Builder::new(writer);
builder.follow_symlinks(false);
for path in paths {
let source_path = source_dir.join(path);
let metadata = fs::metadata(&source_path).map_err(|e| {
format!(
"Unable to fetch filesystem metadata for {}. Details: {}",
&source_path.to_string_lossy().code_str(),
e
)
})?;
if metadata.is_dir() {
for entry in WalkDir::new(&source_path) {
let entry = entry.map_err(|e| {
format!(
"Unable to traverse directory {}. Details: {}",
&source_path.to_string_lossy().code_str(),
e
)
})?;
let entry_metadata = entry.metadata().map_err(|e| {
format!(
"Unable to fetch filesystem metadata for {}. Details: {}",
&source_path.to_string_lossy().code_str(),
e
)
})?;
if entry.file_type().is_file() {
add_file(
&mut builder,
&entry_metadata,
entry
.path()
.canonicalize()
.map_err(|e| {
format!(
"Unable to canonicalize path {}. Details: {}",
&entry.path().to_string_lossy().code_str(),
e
)
})?
.strip_prefix(&source_dir)
.map_err(|e| {
format!(
"Unable to relativize path {} with respect to {}. Details: {}",
&entry.path().to_string_lossy().code_str(),
&source_dir.to_string_lossy().code_str(),
e
)
})?,
&source_dir,
&destination_dir,
&mut file_hashes,
)?;
}
}
} else {
add_file(
&mut builder,
&metadata,
path,
&source_dir,
&destination_dir,
&mut file_hashes,
)?;
}
}
file_hashes.sort();
Ok((
builder
.into_inner()
.map_err(|e| format!("Error writing tar archive. Details: {}", e))?,
file_hashes
.iter()
.fold(cache::hash_str(""), |acc, x| cache::extend(&acc, x)),
))
}