Implement zip command

This commit is contained in:
Nikita Medvedev 2022-02-12 21:54:44 +07:00
parent a24d4729a0
commit 10a647dcae
8 changed files with 373 additions and 4 deletions

50
Cargo.lock generated
View file

@ -95,12 +95,39 @@ version = "3.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d"
[[package]]
name = "byteorder"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "bytes"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db"
[[package]]
name = "bzip2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6afcd980b5f3a45017c57e57a2fcccbb351cc43a356ce117ef760ef8052b89b0"
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.73"
@ -298,6 +325,7 @@ dependencies = [
"walkdir",
"which",
"whoami",
"zip",
]
[[package]]
@ -1074,18 +1102,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.37"
version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e"
checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.37"
version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb"
checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
dependencies = [
"proc-macro2",
"quote",
@ -1398,3 +1426,17 @@ name = "windows_x86_64_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
[[package]]
name = "zip"
version = "0.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93ab48844d61251bb3835145c521d88aa4031d7139e8485990f60ca911fa0815"
dependencies = [
"byteorder",
"bzip2",
"crc32fast",
"flate2",
"thiserror",
"time",
]

View file

@ -47,6 +47,7 @@ suppaftp = "^4.5"
walkdir = "^2"
which = { version = "^4", default-features = false }
whoami = "^1"
zip = "^0.5"
[target.'cfg(not(windows))'.dependencies]
uname = "^0.1"

View file

@ -28,6 +28,7 @@ mod test;
mod thread;
mod time;
mod var;
mod zip;
use duckscript::types::command::Commands;
use duckscript::types::error::ScriptError;
@ -66,6 +67,7 @@ pub(crate) fn load(commands: &mut Commands) -> Result<(), ScriptError> {
thread::load(commands, PACKAGE)?;
time::load(commands, PACKAGE)?;
var::load(commands, PACKAGE)?;
zip::load(commands, PACKAGE)?;
Ok(())
}

View file

@ -0,0 +1,15 @@
mod pack;
use duckscript::types::command::Commands;
use duckscript::types::error::ScriptError;
use crate::utils::pckg;
static PACKAGE: &str = "zip";
pub(crate) fn load(commands: &mut Commands, parent: &str) -> Result<(), ScriptError> {
let package = pckg::concat(parent, PACKAGE);
commands.set(pack::create(&package))?;
Ok(())
}

View file

@ -0,0 +1,35 @@
```
zip [--base basedir] [--compression comp] [--append] <zipfile> <files>
```
Packs the provided files into a ZIP archive.
File paths in the archive will be relative to current working directory.
### Parameters
- zipfile - The path to the ZIP archive to be created.
- files - One or more file paths to pack. No globbing is performed.
- Optional base directory via `--base <basedir>` --- this directory will be used
as a base for the file paths inside the archive.
- Optional append flag via `--append` --- if set, the files will be added to the
existing archive. If the archive does not exist, it will be created.
- Optional compression mode via `--compression comp` where `comp` is one of
`deflate`, `bzip2`, `none`. If not specified, `deflate` is used by default.
### Return value
true if successful
### Examples
```sh
# create some files for the example
mkdir ./test
touch ./test/foo/bar.txt
touch ./test/baz/qux.png
touch ./test/folder/another/file.doc
# pack the files
zipped = zip ./archive.zip ./test/foo/bar.txt ./test/folder/another/file.doc
```

View file

@ -0,0 +1,193 @@
use std::fs::{File, OpenOptions};
use std::io::{Read, Write};
use std::path::Path;
use zip::write::FileOptions;
use zip::{CompressionMethod, ZipWriter};
use duckscript::types::command::{Command, CommandResult};
use crate::utils::pckg;
#[derive(Clone)]
pub(crate) struct CommandImpl {
package: String,
}
enum LookingFor {
Options,
Base,
Compression,
Files,
}
impl Command for CommandImpl {
fn name(&self) -> String {
pckg::concat(&self.package, "Pack")
}
fn aliases(&self) -> Vec<String> {
vec!["zip".to_string()]
}
fn help(&self) -> String {
include_str!("help.md").to_string()
}
fn clone_and_box(&self) -> Box<dyn Command> {
Box::new((*self).clone())
}
fn run(&self, arguments: Vec<String>) -> CommandResult {
if arguments.len() < 2 {
return CommandResult::Error(
"Paths to the ZIP file and/or files to pack are not provided.".to_string());
}
let mut looking_for = LookingFor::Options;
let mut append = false;
let mut base = None;
let mut compression = CompressionMethod::Deflated;
let mut zipfile = None;
let mut files = vec![];
for argument in &arguments {
match looking_for {
LookingFor::Options => match argument.as_str() {
"--compression" => looking_for = LookingFor::Compression,
"--base" => looking_for = LookingFor::Base,
"--append" => append = true,
_ => {
zipfile = Some(argument.as_str());
looking_for = LookingFor::Files;
}
},
LookingFor::Compression => {
match argument.as_str() {
"deflate" => compression = CompressionMethod::Deflated,
"bzip2" => compression = CompressionMethod::Bzip2,
"none" => compression = CompressionMethod::Stored,
_ => return CommandResult::Error("Unknown compression method.".to_string()),
}
looking_for = LookingFor::Options;
},
LookingFor::Base => {
let base_str = argument.as_str();
base = Some(base_str.strip_prefix("./").unwrap_or(base_str));
looking_for = LookingFor::Options;
},
LookingFor::Files => files.push(argument.as_str()),
}
}
if files.is_empty() {
return CommandResult::Error("Input files not provided.".to_string());
}
let zipfile = match zipfile {
Some(filename) => filename,
None => return CommandResult::Error("ZIP file name not provided.".to_string()),
};
let zipfile = Path::new(zipfile);
let zipfile_dir = match zipfile.parent() {
Some(path) => path,
None => return CommandResult::Error("Couldn't parse ZIP file directory.".to_string()),
};
match std::fs::create_dir_all(zipfile_dir) {
Ok(_) => (),
Err(err) =>
return CommandResult::Error(format!("Couldn't create ZIP file directory: {}", err)),
};
let zip_file_existed = zipfile.exists();
let zip_file = match OpenOptions::new()
.read(true)
.write(true)
.create(!zip_file_existed)
.truncate(!append)
.open(zipfile)
{
Ok(file) => file,
Err(err) =>
return CommandResult::Error(format!("Couldn't create/open ZIP file: {}", err)),
};
let mut zip = if append && zip_file_existed {
match ZipWriter::new_append(zip_file) {
Ok(writer) => writer,
Err(err) =>
return CommandResult::Error(format!("Couldn't open ZIP file: {}", err)),
}
} else {
ZipWriter::new(zip_file)
};
let zip_options = FileOptions::default()
.compression_method(compression)
.unix_permissions(0o755);
for file_to_add_str in files {
let file_to_add_path = Path::new(file_to_add_str);
let mut file_to_add = match File::open(file_to_add_path) {
Ok(file) => file,
Err(err) => return CommandResult::Error(
format!("File does not exist or can't be opened: {} ({})",
file_to_add_str,
err)),
};
let mut file_to_add_contents = Vec::new();
match file_to_add.read_to_end(&mut file_to_add_contents) {
Ok(_) => (),
Err(err) =>
return CommandResult::Error(
format!("Could not read file {}: {}", file_to_add_str, err)),
};
let file_to_add_path_stripped = file_to_add_path
.strip_prefix("./").unwrap_or(file_to_add_path);
let file_to_add_path_stripped = match base {
Some(base) =>
file_to_add_path_stripped.strip_prefix(base)
.unwrap_or(file_to_add_path_stripped),
None => file_to_add_path_stripped,
};
let file_to_add_path_str = match file_to_add_path_stripped.to_str() {
Some(str) => str,
None => return CommandResult::Error("Invalid file path".to_string()),
};
match zip.start_file(file_to_add_path_str, zip_options) {
Ok(_) => (),
Err(err) =>
return CommandResult::Error(
format!("Could not write file to archive: {} ({})", file_to_add_str, err)),
};
match zip.write_all(&file_to_add_contents) {
Ok(_) => (),
Err(err) =>
return CommandResult::Error(
format!("Could not write file to archive: {} ({})", file_to_add_str, err)),
};
}
match zip.finish() {
Ok(_) => (),
Err(err) =>
return CommandResult::Error(
format!("Could not finish the archive: {}", err)),
};
CommandResult::Continue(Some("true".to_string()))
}
}
pub(crate) fn create(package: &str) -> Box<dyn Command> {
Box::new(CommandImpl {
package: package.to_string(),
})
}

View file

@ -0,0 +1,23 @@
```
unzip <zipfile> [target]
```
Unpacks the ZIP file into the `target` directory, if provided, or
into current working directory otherwise.
### Parameters
- zipfile - The path to the ZIP archive to be created.
- target - The directory to unpack files into. If not provided,
current working directory is used.
### Return value
true if successful (i.e. the zip file exists and there are no existing file conflicts)
### Examples
```sh
# ./stuff directory will be created automatically
unzipped = unzip ./archive.zip ./stuff
```

58
test/std/zip/pack_test.ds Normal file
View file

@ -0,0 +1,58 @@
fn test_create_simple
zipped = zip ./target/_duckscript_test/zip/test_simple.zip ./Cargo.toml ./Cargo.lock
assert ${zipped}
exists = is_file ./target/_duckscript_test/zip/test_simple.zip
assert ${exists}
end
fn test_create_base
touch ./target/_duckscript_test/zip/foo.txt
zipped = zip --base ./target/_duckscript_test ./target/_duckscript_test/zip/test_base.zip ./target/_duckscript_test/zip/foo.txt
assert ${zipped}
exists = is_file ./target/_duckscript_test/zip/test_base.zip
assert ${exists}
end
fn test_append
touch ./target/_duckscript_test/zip/foo.txt
touch ./target/_duckscript_test/zip/bar.txt
touch ./target/_duckscript_test/zip/baz/qux.txt
zipped = zip --base ./target/_duckscript_test ./target/_duckscript_test/zip/test_append.zip ./target/_duckscript_test/zip/foo.txt
assert ${zipped}
zipped = zip --append --base ./target/_duckscript_test ./target/_duckscript_test/zip/test_append.zip ./target/_duckscript_test/zip/bar.txt
assert ${zipped}
zipped = zip --append --base ./target/_duckscript_test ./target/_duckscript_test/zip/test_append.zip ./target/_duckscript_test/zip/baz/qux.txt
assert ${zipped}
exists = is_file ./target/_duckscript_test/zip/test_append.zip
assert ${exists}
end
fn test_compression_mode
touch ./target/_duckscript_test/zip/foo.txt
touch ./target/_duckscript_test/zip/bar.txt
zipped = zip --compression deflate ./target/_duckscript_test/zip/test_deflate.zip ./target/_duckscript_test/zip/foo.txt ./target/_duckscript_test/zip/bar.txt
assert ${zipped}
exists = is_file ./target/_duckscript_test/zip/test_deflate.zip
assert ${exists}
zipped = zip --compression bzip2 ./target/_duckscript_test/zip/test_bzip2.zip ./target/_duckscript_test/zip/foo.txt ./target/_duckscript_test/zip/bar.txt
assert ${zipped}
exists = is_file ./target/_duckscript_test/zip/test_bzip2.zip
assert ${exists}
zipped = zip --compression none ./target/_duckscript_test/zip/test_none.zip ./target/_duckscript_test/zip/foo.txt ./target/_duckscript_test/zip/bar.txt
assert ${zipped}
exists = is_file ./target/_duckscript_test/zip/test_none.zip
assert ${exists}
end