use std::io::Write as _;
use std::path::{Path, PathBuf};
use crate as cindy;
use crate::Context;
use super::Format;
#[derive(Clone, Default, PartialEq, Eq)]
#[crate::wire]
pub struct State {
pub sources: Vec<PathBuf>,
pub dest: PathBuf,
pub format: Option<Format>,
pub mode: Option<super::path::Mode>,
}
fn entry_name(path: &Path) -> crate::Result<&Path> {
path.file_name()
.map(Path::new)
.context(format!("source path {} has no file name", path.display()))
}
fn append_to_tar<W: std::io::Write>(
builder: &mut tar::Builder<W>,
source: &Path,
) -> crate::Result<()> {
let name = entry_name(source)?;
let meta =
std::fs::symlink_metadata(source).context(format!("Couldn't stat {}", source.display()))?;
if meta.is_dir() {
builder.append_dir_all(name, source).context(format!(
"Couldn't add directory {} to archive",
source.display()
))
} else {
builder
.append_path_with_name(source, name)
.context(format!("Couldn't add file {} to archive", source.display()))
}
}
fn build_tar(sources: &[PathBuf]) -> crate::Result<Vec<u8>> {
let mut builder = tar::Builder::new(Vec::new());
for source in sources {
append_to_tar(&mut builder, source)?;
}
builder
.into_inner()
.context("Couldn't finalise the tar stream")
}
fn build_zip(sources: &[PathBuf]) -> crate::Result<Vec<u8>> {
let mut zip = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
let options: zip::write::FileOptions<()> =
zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Deflated);
for source in sources {
add_to_zip(&mut zip, source, entry_name(source)?, options)?;
}
let cursor = zip.finish().context("Couldn't finalise the zip archive")?;
Ok(cursor.into_inner())
}
fn add_to_zip<W: std::io::Write + std::io::Seek>(
zip: &mut zip::ZipWriter<W>,
source: &Path,
name: &Path,
options: zip::write::FileOptions<()>,
) -> crate::Result<()> {
let name_str = name.to_string_lossy().into_owned();
let meta =
std::fs::symlink_metadata(source).context(format!("Couldn't stat {}", source.display()))?;
if meta.is_dir() {
zip.add_directory(format!("{name_str}/"), options)
.context(format!("Couldn't add dir {name_str} to zip"))?;
for entry in
std::fs::read_dir(source).context(format!("Couldn't read dir {}", source.display()))?
{
let entry = entry.context("Couldn't read directory entry")?;
add_to_zip(zip, &entry.path(), &name.join(entry.file_name()), options)?;
}
} else {
zip.start_file(name_str.clone(), options)
.context(format!("Couldn't start zip entry {name_str}"))?;
let bytes = std::fs::read(source).context(format!("Couldn't read {}", source.display()))?;
zip.write_all(&bytes)
.context(format!("Couldn't write zip entry {name_str}"))?;
}
Ok(())
}
fn add_to_7z<W: std::io::Write + std::io::Seek>(
writer: &mut sevenz_rust2::ArchiveWriter<W>,
source: &Path,
name: &Path,
) -> crate::Result<()> {
let name_str = name.to_string_lossy().into_owned();
let meta =
std::fs::symlink_metadata(source).context(format!("Couldn't stat {}", source.display()))?;
if meta.is_dir() {
let entry = sevenz_rust2::ArchiveEntry::from_path(source, name_str);
writer
.push_archive_entry::<&[u8]>(entry, None)
.context(format!("Couldn't add dir {} to 7z", source.display()))?;
for entry in
std::fs::read_dir(source).context(format!("Couldn't read dir {}", source.display()))?
{
let entry = entry.context("Couldn't read directory entry")?;
add_to_7z(writer, &entry.path(), &name.join(entry.file_name()))?;
}
} else {
let file =
std::fs::File::open(source).context(format!("Couldn't open {}", source.display()))?;
let entry = sevenz_rust2::ArchiveEntry::from_path(source, name_str);
writer
.push_archive_entry(entry, Some(file))
.context(format!("Couldn't add file {} to 7z", source.display()))?;
}
Ok(())
}
fn build_7z(sources: &[PathBuf]) -> crate::Result<Vec<u8>> {
let mut buf = std::io::Cursor::new(Vec::new());
let mut writer =
sevenz_rust2::ArchiveWriter::new(&mut buf).context("Couldn't create the 7z writer")?;
for source in sources {
add_to_7z(&mut writer, source, entry_name(source)?)?;
}
writer
.finish()
.context("Couldn't finalise the 7z archive")?;
Ok(buf.into_inner())
}
#[crate::remote]
pub fn archive(state: State) -> crate::Result<super::Return> {
if state.sources.is_empty() {
crate::bail!("`archive` needs at least one source path");
}
let format = match state.format {
Some(f) => f,
None => Format::from_path(&state.dest)?,
};
eprintln!(
"archive {} source(s) -> {} ({:?})",
state.sources.len(),
state.dest.display(),
format,
);
let bytes = match format {
Format::Tar(codec) => {
let tar_bytes = build_tar(&state.sources)?;
super::compress::compress::inner(tar_bytes, codec.unwrap_or(super::Codec::Store))?
}
Format::Zip => build_zip(&state.sources)?,
Format::SevenZ => build_7z(&state.sources)?,
};
let (user, group) = super::current_owner_names();
super::path::file_raw::inner(
state.dest,
bytes,
user,
group,
state.mode.unwrap_or_else(|| 0o644.into()),
)?;
Ok(super::Return::Changed)
}