use super::sevenzip::{ArchiveType, tool};
use anyhow::{Context, Result};
use sevenz_rust2::encoder_options::Lzma2Options;
use sevenz_rust2::{ArchiveEntry, ArchiveReader, ArchiveWriter, Password};
use std::fs::File;
use std::io;
use std::path::Path;
use std::str::FromStr;
use tokio::task::spawn_blocking;
use zip::write::SimpleFileOptions;
use zip::{CompressionMethod, ZipArchive, ZipWriter};
fn archive_type(path: &Path) -> Result<ArchiveType> {
let extension = path
.extension()
.and_then(|extension| extension.to_str())
.unwrap_or_default()
.to_lowercase();
ArchiveType::from_str(&extension).context("Not a valid archive")
}
pub async fn parse<P: AsRef<Path>>(
path: P,
name: Option<&str>,
) -> Result<Vec<(String, u64, String)>> {
let path = path.as_ref().to_path_buf();
let name = name.map(|name| name.to_string());
spawn_blocking(move || {
let wanted = |entry: &str| name.as_ref().is_none_or(|name| name == entry);
match archive_type(&path)? {
ArchiveType::Sevenzip => {
let archive = sevenz_rust2::Archive::open(&path)
.with_context(|| format!("Failed to open \"{}\"", path.display()))?;
Ok(archive
.files
.iter()
.filter(|entry| !entry.is_directory && wanted(&entry.name))
.map(|entry| {
(
entry.name.clone(),
entry.size,
format!("{:08x}", entry.crc as u32),
)
})
.collect())
}
ArchiveType::Zip => {
let file = File::open(&path)
.with_context(|| format!("Failed to open \"{}\"", path.display()))?;
let mut archive = ZipArchive::new(file)
.with_context(|| format!("Failed to read \"{}\"", path.display()))?;
let mut entries = Vec::with_capacity(archive.len());
for index in 0..archive.len() {
let entry = archive.by_index(index)?;
if entry.is_dir() || !wanted(entry.name()) {
continue;
}
entries.push((
entry.name().to_string(),
entry.size(),
format!("{:08x}", entry.crc32()),
));
}
Ok(entries)
}
}
})
.await
.context("Archive task failed")?
}
pub async fn extract<P: AsRef<Path>, Q: AsRef<Path>>(
path: P,
name: &str,
directory: Q,
) -> Result<()> {
let path = path.as_ref().to_path_buf();
let name = name.to_string();
let directory = directory.as_ref().to_path_buf();
spawn_blocking(move || {
let destination = directory.join(&name);
if let Some(parent) = destination.parent() {
std::fs::create_dir_all(parent)?;
}
match archive_type(&path)? {
ArchiveType::Sevenzip => {
let mut reader = ArchiveReader::open(&path, Password::empty())
.with_context(|| format!("Failed to open \"{}\"", path.display()))?;
reader.set_thread_count(threads());
let data = reader
.read_file(&name)
.with_context(|| format!("Failed to extract \"{}\"", name))?;
std::fs::write(&destination, data)?;
}
ArchiveType::Zip => {
let mut archive = ZipArchive::new(File::open(&path)?)
.with_context(|| format!("Failed to read \"{}\"", path.display()))?;
let mut entry = archive
.by_name(&name)
.with_context(|| format!("Failed to extract \"{}\"", name))?;
io::copy(&mut entry, &mut File::create(&destination)?)?;
}
}
Ok(())
})
.await
.context("Archive task failed")?
}
pub async fn create<P: AsRef<Path>, Q: AsRef<Path>>(
archive_path: P,
working_directory: Q,
entry_path: &Path,
archive_type: &ArchiveType,
compression_level: &Option<usize>,
solid: bool,
) -> Result<()> {
if archive_type == &ArchiveType::Sevenzip && archive_path.as_ref().is_file() {
return tool::create(
archive_path,
working_directory,
entry_path,
archive_type,
compression_level,
solid,
)
.await;
}
let archive_path = archive_path.as_ref().to_path_buf();
let working_directory = working_directory.as_ref().to_path_buf();
let entry_path = entry_path.to_path_buf();
let archive_type = *archive_type;
let compression_level = *compression_level;
spawn_blocking(move || {
let name = entry_path.to_string_lossy().replace('\\', "/");
let source = working_directory.join(&entry_path);
match archive_type {
ArchiveType::Zip => {
let options = SimpleFileOptions::default()
.compression_method(CompressionMethod::Deflated)
.compression_level(compression_level.map(|level| level as i64))
.large_file(true);
let mut writer = if archive_path.is_file() {
ZipWriter::new_append(
File::options().read(true).write(true).open(&archive_path)?,
)?
} else {
ZipWriter::new(File::create(&archive_path)?)
};
writer.start_file(&name, options)?;
io::copy(&mut File::open(&source)?, &mut writer)?;
writer.finish()?;
}
ArchiveType::Sevenzip => {
write_sevenzip(
&archive_path,
vec![(name, std::fs::read(&source)?)],
compression_level,
solid,
)?;
}
}
Ok(())
})
.await
.context("Archive task failed")?
}
pub async fn rename<P: AsRef<Path>>(path: P, from: &str, to: &str) -> Result<()> {
if archive_type(path.as_ref())? == ArchiveType::Sevenzip {
return tool::rename(path, from, to).await;
}
let (from, to) = (from.to_string(), to.to_string());
rebuild(path, move |name| {
if name == from {
Some(to.clone())
} else {
Some(name.to_string())
}
})
.await
}
pub async fn delete<P: AsRef<Path>>(path: P, name: &str) -> Result<()> {
if archive_type(path.as_ref())? == ArchiveType::Sevenzip {
return tool::delete(path, name).await;
}
let name = name.to_string();
rebuild(path, move |entry| {
if entry == name {
None
} else {
Some(entry.to_string())
}
})
.await
}
async fn rebuild<P, F>(path: P, map: F) -> Result<()>
where
P: AsRef<Path>,
F: Fn(&str) -> Option<String> + Send + 'static,
{
let path = path.as_ref().to_path_buf();
spawn_blocking(move || {
let temporary = path.with_extension("oxyromon-tmp");
match archive_type(&path)? {
ArchiveType::Zip => {
let mut archive = ZipArchive::new(File::open(&path)?)
.with_context(|| format!("Failed to read \"{}\"", path.display()))?;
let mut writer = ZipWriter::new(File::create(&temporary)?);
for index in 0..archive.len() {
let entry = archive.by_index_raw(index)?;
match map(entry.name()) {
Some(name) if name == entry.name() => writer.raw_copy_file(entry)?,
Some(name) => writer.raw_copy_file_rename(entry, name)?,
None => {}
}
}
writer.finish()?;
}
ArchiveType::Sevenzip => {
let solid = sevenz_rust2::Archive::open(&path)?.is_solid;
let entries: Vec<(String, Vec<u8>)> = read_sevenzip(&path)?
.into_iter()
.filter_map(|(name, data)| map(&name).map(|name| (name, data)))
.collect();
if entries.is_empty() {
std::fs::remove_file(&path)?;
return Ok(());
}
write_sevenzip(&temporary, entries, None, solid)?;
}
}
std::fs::rename(&temporary, &path)?;
Ok(())
})
.await
.context("Archive task failed")?
}
const DEFAULT_COMPRESSION_LEVEL: usize = 9;
fn threads() -> u32 {
std::thread::available_parallelism()
.map(|available| available.get() as u32)
.unwrap_or(1)
}
fn read_sevenzip(path: &Path) -> Result<Vec<(String, Vec<u8>)>> {
let mut reader = ArchiveReader::open(path, Password::empty())
.with_context(|| format!("Failed to open \"{}\"", path.display()))?;
reader.set_thread_count(threads());
let mut entries = Vec::new();
reader.for_each_entries(|entry, reader| {
if entry.is_directory {
return Ok(true);
}
let mut data = Vec::with_capacity(entry.size as usize);
io::copy(reader, &mut data)?;
entries.push((entry.name.clone(), data));
Ok(true)
})?;
Ok(entries)
}
fn write_sevenzip(
path: &Path,
entries: Vec<(String, Vec<u8>)>,
compression_level: Option<usize>,
solid: bool,
) -> Result<()> {
let mut writer = ArchiveWriter::create(path)
.with_context(|| format!("Failed to create \"{}\"", path.display()))?;
writer.set_content_methods(vec![
Lzma2Options::from_level_mt(
compression_level.unwrap_or(DEFAULT_COMPRESSION_LEVEL) as u32,
threads(),
1,
)
.into(),
]);
if solid {
writer.push_archive_entries(
entries
.iter()
.map(|(name, _)| ArchiveEntry::new_file(name))
.collect(),
entries
.iter()
.map(|(_, data)| io::Cursor::new(data.as_slice()).into())
.collect(),
)?;
} else {
for (name, data) in &entries {
writer.push_archive_entry(
ArchiveEntry::new_file(name),
Some(io::Cursor::new(data.as_slice())),
)?;
}
}
writer.finish()?;
Ok(())
}