commit f8ba5c1f0bdd5d4f13e2380dec89eeebb80ab0a6 Author: Joseph Montanaro Date: Tue Feb 20 07:37:35 2024 -0800 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8ad3c7e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/tests/output diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..747685f --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,477 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "anstream" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "ark" +version = "0.1.0" +dependencies = [ + "bzip2", + "clap", + "flate2", + "tar", + "tempdir", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" + +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80c21025abd42669a92efc996ef13cfb2c5c627858421ea58d5c3b331a6c134f" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "458bf1f341769dfcf849846f65dffdf9146daa56bcd2a47cb4e1de9915567c99" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "crc32fast" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "filetime" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "windows-sys", +] + +[[package]] +name = "flate2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + +[[package]] +name = "miniz_oxide" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +dependencies = [ + "adler", +] + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "proc-macro2" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "rustix" +version = "0.38.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +dependencies = [ + "bitflags 2.4.2", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "strsim" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" + +[[package]] +name = "syn" +version = "2.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tar" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "tempdir" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" +dependencies = [ + "rand", + "remove_dir_all", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "xattr" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" +dependencies = [ + "libc", + "linux-raw-sys", + "rustix", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9b4110a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "ark" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bzip2 = "0.4.4" +clap = { version = "4.5.0", features = ["derive"] } +flate2 = "1.0.28" +tar = "0.4.40" + +[dev-dependencies] +tempdir = "0.3.7" diff --git a/README.md b/README.md new file mode 100644 index 0000000..a035863 --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +# `ark` - Archiving and compression toolkit with a simple, obvious interface + +`ark` is a tool for creating and extracting archives, and (eventually) for compressing and decompressing files. It aims to support the most common operations on a broad variety of formats: `tar`, `zip`, `7z`, `ar`, etc. + +## Features + +* Intuitive, subcommand-based interface +* Automatically infers archive format and compression algorithm from filenames +* Cross-platform, supporting Windows/Mac/Linux (only tested on Linux so far) +* Optionally respects `.gitignore` files (not yet implemented) +* Optionally deduplicates top-level directory when extracting, in the event that the archive is being extracted into a directory with the same name as the top-level directory _inside_ the archive. (not yet implemented) +* Tree view when listing contents of an archive (not yet implemented) + +## Examples + +```sh +# Create an archive +ark pack somedir archive.tar.gz + +# Like mv or cp, the last argument is interpreted as the destination +ark pack file1 file2 anotherdir archive.zip + +# You can explicitly specify the format/compression, in case you need to do something weird +ark pack --format zip --compression deflate files/* archive.vl2 + +# Unpack an archive into the current directory +ark unpack archive.tar.gz + +# Automatically creates destination directory if not extant +ark unpack archive.tar.gz somedir + +# Deduplicate top-level directory (i.e. if "targetdir" is the root of all paths in the archive, +# this will prevent the output tree from starting with targetdir/targetdir) +ark unpack archive.tar.xz --deduplicate targetdir +``` diff --git a/src/compression.rs b/src/compression.rs new file mode 100644 index 0000000..54f48cb --- /dev/null +++ b/src/compression.rs @@ -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 { + Gzip(GzEncoder), + Bzip2(BzEncoder), +} + +impl Encoder { + 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 Write for Encoder { + fn write(&mut self, data: &[u8]) -> io::Result { + 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 { + Gzip(GzDecoder), + Bzip2(BzDecoder), +} + +impl Decoder { + 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 Read for Decoder { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + match self { + Self::Gzip(inner) => inner.read(buf), + Self::Bzip2(inner) => inner.read(buf), + } + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..d59f8a9 --- /dev/null +++ b/src/lib.rs @@ -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>(archive_path: P) -> Option { + 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(targets: I, archive_path: Q) -> io::Result<()> +where + I: IntoIterator, + P: AsRef, + Q: AsRef, +{ + let alg = infer_formats(&archive_path).unwrap(); + pack_with(targets, archive_path, alg) +} + +pub fn pack_with(targets: I, archive_path: Q, alg: CompressionType) -> io::Result<()> +where + I: IntoIterator, + P: AsRef, + Q: AsRef, +{ + 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(archive: &mut Builder, path: &P, archive_path: &Q) -> io::Result<()> +where + W: Write, + P: AsRef, + Q: AsRef, +{ + 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(archive_path: P, output_path: Q) -> io::Result<()> +where + P: AsRef, + Q: AsRef, +{ + let alg = infer_formats(&archive_path).unwrap(); + unpack_with(archive_path, output_path, alg) +} + +pub fn unpack_with(archive_path: P, output_path: Q, alg: CompressionType) -> io::Result<()> +where + P: AsRef, + Q: AsRef, +{ + 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); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..1b0fc05 --- /dev/null +++ b/src/main.rs @@ -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, + + /// Where to create archive file + archive_path: PathBuf, + + /// Compression type (inferred from path if not specified) + #[arg(short, long, value_enum)] + compression: Option, +} + +/// 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, +} + +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) + }, + } +} diff --git a/tests/expected/simple.tar b/tests/expected/simple.tar new file mode 100644 index 0000000..54d4cf1 Binary files /dev/null and b/tests/expected/simple.tar differ diff --git a/tests/pack.rs b/tests/pack.rs new file mode 100644 index 0000000..5b88514 --- /dev/null +++ b/tests/pack.rs @@ -0,0 +1,78 @@ +use std::fs; +use std::process::Command; +use std::path::PathBuf; +use tempdir::TempDir; + +fn verify(targets: &[&str], archive_path: &str, compress_flag: &str) +{ + let unpack_dir = TempDir::new("ark_pack").unwrap(); + dbg!(Command::new("tar") + .arg(compress_flag) + .args(["-xf", archive_path, "-C"]) + .arg(unpack_dir.path()) + .output() + .unwrap()); + + for target in targets { + let unpack_path = unpack_dir.path().join(&target); + compare_trees(target.into(), unpack_path); + } +} + +fn compare_trees(src: PathBuf, dst: PathBuf) { + // we don't want to follow symlinks, because if the symlink points + // within the archive then we'll be looking at it anyway, and if + // points outside then it's out of scope for us + let src_meta = src.symlink_metadata().unwrap(); + let dst_meta = dst.symlink_metadata().unwrap(); + assert_eq!(src_meta.file_type(), dst_meta.file_type()); + assert_eq!(src_meta.permissions(), dst_meta.permissions()); + if src_meta.is_symlink() { + let src_target = fs::read_link(&src).unwrap(); + let dst_target = fs::read_link(&dst).unwrap(); + assert_eq!(src_target, dst_target); + } + // eventually compare content of files here + else if src_meta.is_dir() { + for entry in fs::read_dir(&src).unwrap() { + let src_child = entry.unwrap().path(); + let dst_child = dst.join(src_child.file_name().unwrap()); + compare_trees(src_child, dst_child); + } + } +} + +#[test] +fn test_pack_simple_files() { + let targets = [ + "tests/samples/simple/a.txt", + "tests/samples/simple/b.txt", + ]; + let archive_path = "tests/output/pack_simple_files.tar.gz"; + ark::pack(&targets, &archive_path).unwrap(); + verify(&targets, &archive_path, "-z"); +} + +#[test] +fn test_pack_simple_dir() { + let targets = ["tests/samples/simple"]; + let archive_path = "tests/output/pack_simple_dir.tar.gz"; + ark::pack(&targets, &archive_path).unwrap(); + verify(&targets, &archive_path, "-z"); +} + +#[test] +fn test_pack_symlinks() { + let targets = ["tests/samples/symlinks"]; + let archive_path = "tests/output/pack_symlinks.tar.gz"; + ark::pack(&targets, &archive_path).unwrap(); + verify(&targets, &archive_path, "-z"); +} + +#[test] +fn test_bz2() { + let targets = ["tests/samples/simple"]; + let archive_path = "tests/output/pack_simple_dir.tar.bz2"; + ark::pack(&targets, &archive_path).unwrap(); + verify(&targets, &archive_path, "-j"); +} diff --git a/tests/samples/simple.tar.bz2 b/tests/samples/simple.tar.bz2 new file mode 100644 index 0000000..c22038a Binary files /dev/null and b/tests/samples/simple.tar.bz2 differ diff --git a/tests/samples/simple.tar.gz b/tests/samples/simple.tar.gz new file mode 100644 index 0000000..25b42a9 Binary files /dev/null and b/tests/samples/simple.tar.gz differ diff --git a/tests/samples/simple/a.txt b/tests/samples/simple/a.txt new file mode 100644 index 0000000..5804254 --- /dev/null +++ b/tests/samples/simple/a.txt @@ -0,0 +1 @@ +this is file A diff --git a/tests/samples/simple/b.txt b/tests/samples/simple/b.txt new file mode 100644 index 0000000..f14749f --- /dev/null +++ b/tests/samples/simple/b.txt @@ -0,0 +1 @@ +this is file B diff --git a/tests/samples/symlinks.tar.gz b/tests/samples/symlinks.tar.gz new file mode 100644 index 0000000..a2e570d Binary files /dev/null and b/tests/samples/symlinks.tar.gz differ diff --git a/tests/samples/symlinks/one.txt b/tests/samples/symlinks/one.txt new file mode 100644 index 0000000..c2a1599 --- /dev/null +++ b/tests/samples/symlinks/one.txt @@ -0,0 +1 @@ +this is file one diff --git a/tests/samples/symlinks/somedir/two.txt b/tests/samples/symlinks/somedir/two.txt new file mode 100644 index 0000000..f8156fa --- /dev/null +++ b/tests/samples/symlinks/somedir/two.txt @@ -0,0 +1 @@ +this is file two diff --git a/tests/samples/symlinks/two.txt b/tests/samples/symlinks/two.txt new file mode 120000 index 0000000..d9ad031 --- /dev/null +++ b/tests/samples/symlinks/two.txt @@ -0,0 +1 @@ +somedir/two.txt \ No newline at end of file diff --git a/tests/unpack.rs b/tests/unpack.rs new file mode 100644 index 0000000..afc2d5f --- /dev/null +++ b/tests/unpack.rs @@ -0,0 +1,50 @@ +use std::fs; +use std::path::PathBuf; +use tempdir::TempDir; + +#[test] +fn test_unpack_simple() { + let unpack_dir = TempDir::new("ark_unpack").unwrap(); + ark::unpack("tests/samples/simple.tar.gz", unpack_dir.path()).unwrap(); + let unpack_path = unpack_dir.path().join("tests/samples/simple"); + compare_trees("tests/samples/simple".into(), unpack_path); +} + +#[test] +fn test_unpack_symlinks() { + let unpack_dir = TempDir::new("ark_unpack").unwrap(); + ark::unpack("tests/samples/symlinks.tar.gz", unpack_dir.path()).unwrap(); + let unpack_path = unpack_dir.path().join("tests/samples/symlinks"); + compare_trees("tests/samples/symlinks".into(), unpack_path); +} + +#[test] +fn test_unpack_bz2() { + let unpack_dir = TempDir::new("ark_unpack").unwrap(); + ark::unpack("tests/samples/simple.tar.bz2", unpack_dir.path()).unwrap(); + let unpack_path = unpack_dir.path().join("tests/samples/simple"); + compare_trees("tests/samples/simple".into(), unpack_path); +} + +fn compare_trees(src: PathBuf, dst: PathBuf) { + // we don't want to follow symlinks, because if the symlink points + // within the archive then we'll be looking at it anyway, and if + // points outside then it's out of scope for us + let src_meta = src.symlink_metadata().unwrap(); + let dst_meta = dst.symlink_metadata().unwrap(); + assert_eq!(src_meta.file_type(), dst_meta.file_type()); + assert_eq!(src_meta.permissions(), dst_meta.permissions()); + if src_meta.is_symlink() { + let src_target = fs::read_link(&src).unwrap(); + let dst_target = fs::read_link(&dst).unwrap(); + assert_eq!(src_target, dst_target); + } + // eventually compare content of files here + else if src_meta.is_dir() { + for entry in fs::read_dir(&src).unwrap() { + let src_child = entry.unwrap().path(); + let dst_child = dst.join(src_child.file_name().unwrap()); + compare_trees(src_child, dst_child); + } + } +}