extern crate flate2;
extern crate regex;
extern crate serde;
extern crate tar;
extern crate toml;
extern crate walkdir;
use self::serde::{Deserialize, Serialize};
use std::env;
use std::fs;
use std::io;
use std::path;
use std::time;
#[derive(Debug, PartialEq, Deserialize, Serialize)]
pub enum HeaderType {
Old,
Gnu,
UStar,
}
#[derive(Debug, PartialEq, Deserialize, Serialize)]
pub enum FileMode {
Directory,
File,
}
#[derive(Debug)]
pub struct Condition {
pub mode: Option<FileMode>,
pub path: Option<regex::Regex>,
}
#[derive(Debug)]
pub struct Rule {
pub when: Condition,
pub skip: bool,
pub mtime: Option<u64>,
pub uid: Option<u64>,
pub gid: Option<u64>,
pub username: Option<String>,
pub groupname: Option<String>,
pub permissions: Option<u32>,
}
impl Rule {
pub fn is_match(&self, filemode: &FileMode, pth: &str) -> bool {
if let Some(when_mode) = &self.when.mode {
if when_mode != filemode {
return false;
}
}
if let Some(when_path) = &self.when.path {
if !when_path.is_match(pth) {
return false;
}
}
true
}
pub fn is_skip(&self, filemode: &FileMode, pth: &str) -> bool {
self.is_match(filemode, pth) && self.skip
}
pub fn apply(&self, header: &mut tar::Header) -> Result<(), io::Error> {
if let Some(mtime) = self.mtime {
header.set_mtime(mtime);
}
if let Some(uid) = self.uid {
header.set_uid(uid);
}
if let Some(gid) = self.gid {
header.set_gid(gid);
}
if let Some(username) = &self.username {
header.set_username(username)?;
}
if let Some(groupname) = &self.groupname {
header.set_groupname(groupname)?;
}
if let Some(permissions) = &self.permissions {
header.set_mode(*permissions);
}
Ok(())
}
}
#[derive(Debug)]
pub struct Chandler {
pub verbose: bool,
pub header_type: HeaderType,
pub cwd: Option<path::PathBuf>,
pub rules: Vec<Rule>,
}
pub fn permissions_to_u32(permissions: fs::Permissions) -> u32 {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
permissions.mode()
}
#[cfg(windows)]
{
if permissions.readonly() {
0o444u32
} else {
0o666u32
}
}
}
impl Default for Chandler {
fn default() -> Self {
Chandler {
verbose: false,
header_type: HeaderType::UStar,
cwd: None,
rules: vec![
Rule{
when: Condition{ mode: None, path: Some(regex::Regex::new(r"^(\.DS_Store)|(Thumbs\.db)$").unwrap()) },
skip: true,
mtime: None,
uid: None,
gid: None,
username: None,
groupname: None,
permissions: None,
},
Rule{
when: Condition{ mode: None, path: None },
skip: false,
mtime: None,
uid: Some(1000u64),
gid: Some(1000u64),
username: None,
groupname: None,
permissions: Some(0o755u32),
},
Rule{
when: Condition{ mode: None, path: Some(regex::Regex::new(r"(?i)^(.*\.(bat|cmd|exe|jar|md|ps1|tar|tar\.gz|tgz|txt|wasm|zip))|(.*[/\\]js[/|\\]wasm[/|\\].+)$"
).unwrap()) },
skip: false,
mtime: None,
uid: None,
gid: None,
username: None,
groupname: None,
permissions: Some(0o644u32),
},
],
}
}
}
impl Chandler {
pub fn archive(&self, target: &path::Path, source: &path::Path) -> Result<(), io::Error> {
if let Some(cwd_pathbuf) = &self.cwd {
env::set_current_dir(cwd_pathbuf.as_path())?;
}
let file = fs::File::create(target)?;
let gz_encoder = flate2::write::GzEncoder::new(file, flate2::Compression::default());
let mut builder = tar::Builder::new(gz_encoder);
let walker = walkdir::WalkDir::new(source).sort_by(
|a: &walkdir::DirEntry, b: &walkdir::DirEntry| a.file_name().cmp(b.file_name()),
);
for entry in walker {
let entry = entry?;
let pth = entry.path();
if pth.as_os_str().is_empty() {
continue;
}
let pth_str = pth
.to_str()
.ok_or_else(|| io::Error::other(format!("unable to render path {:?}", pth)))?;
let metadata = entry.metadata()?;
let mut header = match self.header_type {
HeaderType::Old => tar::Header::new_old(),
HeaderType::Gnu => tar::Header::new_gnu(),
HeaderType::UStar => tar::Header::new_ustar(),
};
header.set_path(pth)?;
let mtime = metadata
.modified()?
.duration_since(time::UNIX_EPOCH)
.map(|e| e.as_secs())
.map_err(io::Error::other)?;
header.set_mtime(mtime);
header.set_mode(permissions_to_u32(metadata.permissions()));
let filemode = if metadata.is_dir() {
FileMode::Directory
} else if metadata.is_file() {
FileMode::File
} else {
return Err(io::Error::other(format!(
"unsupported file type: {pth_str}"
)));
};
if filemode == FileMode::Directory {
header.set_entry_type(tar::EntryType::Directory);
header.set_size(0);
} else if filemode == FileMode::File {
header.set_size(metadata.len());
}
if self.verbose {
eprintln!("a {pth_str}");
}
if self.rules.iter().any(|e| e.is_skip(&filemode, pth_str)) {
continue;
}
for rule in &self.rules {
if !rule.is_match(&filemode, pth_str) {
continue;
}
rule.apply(&mut header)?;
}
header.set_cksum();
if filemode == FileMode::Directory {
builder.append(&header, &[] as &[u8])?;
} else if filemode == FileMode::File {
let mut source_file = fs::File::open(pth)?;
builder.append(&header, &mut source_file)?;
}
}
builder.into_inner()?.finish().map(|_| ())
}
}