initial commit
This commit is contained in:
@@ -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
@@ -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
@@ -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)
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user