initial commit

This commit is contained in:
2024-02-20 07:37:35 -08:00
commit f8ba5c1f0b
18 changed files with 935 additions and 0 deletions
+81
View File
@@ -0,0 +1,81 @@
use std::io::{self, Read, Write};
use clap::ValueEnum;
use flate2::Compression as GzCompression;
use flate2::write::GzEncoder;
use flate2::read::GzDecoder;
use bzip2::Compression as BzCompression;
use bzip2::write::BzEncoder;
use bzip2::read::BzDecoder;
#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)]
pub enum CompressionType {
Gzip,
Bzip2,
}
pub enum Encoder<W: Write> {
Gzip(GzEncoder<W>),
Bzip2(BzEncoder<W>),
}
impl<W: Write> Encoder<W> {
pub fn new(sink: W, alg: CompressionType) -> Self {
match alg {
CompressionType::Gzip => {
let inner = GzEncoder::new(sink, GzCompression::default());
Self::Gzip(inner)
},
CompressionType::Bzip2 => {
let inner = BzEncoder::new(sink, BzCompression::new(5));
Self::Bzip2(inner)
},
}
}
}
impl<W: Write> Write for Encoder<W> {
fn write(&mut self, data: &[u8]) -> io::Result<usize> {
match self {
Self::Gzip(inner) => inner.write(data),
Self::Bzip2(inner) => inner.write(data),
}
}
fn flush(&mut self) -> io::Result<()> {
match self {
Self::Gzip(inner) => inner.flush(),
Self::Bzip2(inner) => inner.flush(),
}
}
}
pub enum Decoder<R: Read> {
Gzip(GzDecoder<R>),
Bzip2(BzDecoder<R>),
}
impl<R: Read> Decoder<R> {
pub fn new(src: R, alg: CompressionType) -> Self {
match alg {
CompressionType::Gzip => {
let inner = GzDecoder::new(src);
Self::Gzip(inner)
},
CompressionType::Bzip2 => {
let inner = BzDecoder::new(src);
Self::Bzip2(inner)
},
}
}
}
impl<R: Read> Read for Decoder<R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self {
Self::Gzip(inner) => inner.read(buf),
Self::Bzip2(inner) => inner.read(buf),
}
}
}
+123
View File
@@ -0,0 +1,123 @@
use std::fs::{self, File};
use std::io::{self, Write};
use std::path::Path;
use tar::{Archive, Builder, EntryType, Header};
mod compression;
pub use crate::compression::CompressionType;
use crate::compression::{Encoder, Decoder};
pub fn infer_formats<P: AsRef<Path>>(archive_path: P) -> Option<CompressionType> {
match archive_path.as_ref().to_string_lossy() {
s if s.ends_with(".tar.gz") => Some(CompressionType::Gzip),
s if s.ends_with(".tar.bz2") => Some(CompressionType::Bzip2),
_ => None,
}
}
pub fn pack<I, P, Q>(targets: I, archive_path: Q) -> io::Result<()>
where
I: IntoIterator<Item = P>,
P: AsRef<Path>,
Q: AsRef<Path>,
{
let alg = infer_formats(&archive_path).unwrap();
pack_with(targets, archive_path, alg)
}
pub fn pack_with<I, P, Q>(targets: I, archive_path: Q, alg: CompressionType) -> io::Result<()>
where
I: IntoIterator<Item = P>,
P: AsRef<Path>,
Q: AsRef<Path>,
{
let dst = File::create(&archive_path)?;
let encoder = Encoder::new(dst, alg);
let mut archive = Builder::new(encoder);
for target in targets {
add_from_path(&mut archive, &target, &archive_path)?;
}
archive.finish()
}
fn add_from_path<W, P, Q>(archive: &mut Builder<W>, path: &P, archive_path: &Q) -> io::Result<()>
where
W: Write,
P: AsRef<Path>,
Q: AsRef<Path>,
{
let meta = path.as_ref().symlink_metadata()?;
if meta.is_file() {
archive.append_path(path)?;
}
else if meta.is_dir() {
for entry in fs::read_dir(path)? {
let child = entry?.path();
if child == archive_path.as_ref() {
continue;
}
add_from_path(archive, &child.as_path(), archive_path)?;
}
}
else if meta.is_symlink() {
let mut header = Header::new_gnu();
header.set_entry_type(EntryType::Symlink);
header.set_metadata(&meta);
let target = fs::read_link(path)?;
archive.append_link(&mut header, path, &target)?;
}
Ok(())
}
pub fn unpack<P, Q>(archive_path: P, output_path: Q) -> io::Result<()>
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
let alg = infer_formats(&archive_path).unwrap();
unpack_with(archive_path, output_path, alg)
}
pub fn unpack_with<P, Q>(archive_path: P, output_path: Q, alg: CompressionType) -> io::Result<()>
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
let archive_file = File::open(&archive_path)?;
let decoder = Decoder::new(archive_file, alg);
let mut archive = Archive::new(decoder);
if !output_path.as_ref().exists() {
fs::create_dir(&output_path)?;
}
archive.unpack(&output_path)
// check for duplicated top-level directory and fix
// let mut iter = fs::read_dir(&output_path).into_iter().next();
// if let (Some(entry), None) = (iter.next(), iter.next()) {
// let child = entry?.path();
// if child.file_name() == output_path.file_name() {
// let mut name = output_path.file_name().to_os_string();
// name.push("_tmp".into())
// }
// }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_infer_formats() {
assert_eq!(infer_formats("test.tar.gz"), Some(CompressionType::Gzip));
assert_eq!(infer_formats("test.tar.bz2"), Some(CompressionType::Bzip2));
assert_eq!(infer_formats("test.nothing"), None);
assert_eq!(infer_formats("test"), None);
}
}
+69
View File
@@ -0,0 +1,69 @@
use std::io;
use std::path::PathBuf;
use clap::{Args, Parser, Subcommand};
use ark::CompressionType;
/// Intuitive CLI archiving/compression toolkit
#[derive(Debug, Parser)]
#[command(version, about)]
struct Cli {
#[command(subcommand)]
op: Command
}
#[derive(Debug, Subcommand)]
enum Command {
Pack(PackCmd),
Unpack(UnpackCmd),
}
/// Create an archive
#[derive(Debug, Args)]
struct PackCmd {
/// Target files/directories to include in archive
#[arg(required = true)]
targets: Vec<PathBuf>,
/// Where to create archive file
archive_path: PathBuf,
/// Compression type (inferred from path if not specified)
#[arg(short, long, value_enum)]
compression: Option<CompressionType>,
}
/// Extract an archive
#[derive(Debug, Args)]
struct UnpackCmd {
/// Archive to unpack
archive_path: PathBuf,
/// Destionation to unpack into (will be created if necessary)
#[arg(default_value = ".")]
output_path: PathBuf,
/// Compression type (inferred from path if not specified)
#[arg(short, long, value_enum)]
compression: Option<CompressionType>,
}
fn main() -> io::Result<()> {
let cli = Cli::parse();
match cli.op {
Command::Pack(pack) => {
let alg = pack.compression
.or_else(|| ark::infer_formats(&pack.archive_path))
.unwrap(); // replace with proper error handling later
ark::pack_with(pack.targets, pack.archive_path, alg)
},
Command::Unpack(unpack) => {
let alg = unpack.compression
.or_else(|| ark::infer_formats(&unpack.archive_path))
.unwrap();
ark::unpack_with(unpack.archive_path, unpack.output_path, alg)
},
}
}