use std::collections::HashSet;
use std::fs::File;
use std::io::{BufReader, BufWriter};
use std::path::Path;
use crate::read::Archive;
use crate::write::{EntryMeta, WriteOptions, Writer};
use crate::{ArchivePath, Error, Result};
#[must_use = "append result should be checked to verify operation completed as expected"]
#[derive(Debug, Clone, Default)]
pub struct AppendResult {
pub entries_added: usize,
pub total_entries: usize,
pub total_bytes: u64,
}
pub struct ArchiveAppender {
path: std::path::PathBuf,
existing_paths: HashSet<String>,
original_entry_count: usize,
options: WriteOptions,
new_entries: Vec<PendingAppendEntry>,
}
#[derive(Debug)]
struct PendingAppendEntry {
path: ArchivePath,
data: Vec<u8>,
is_directory: bool,
}
impl ArchiveAppender {
pub fn open(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref();
let file = File::open(path).map_err(Error::Io)?;
let reader = BufReader::new(file);
let archive = Archive::open(reader)?;
let existing_paths: HashSet<String> = archive
.entries()
.iter()
.map(|e| e.path.as_str().to_string())
.collect();
let original_entry_count = archive.entries().len();
Ok(Self {
path: path.to_path_buf(),
existing_paths,
original_entry_count,
options: WriteOptions::default(),
new_entries: Vec::new(),
})
}
pub fn with_options(mut self, options: WriteOptions) -> Self {
self.options = options;
self
}
pub fn existing_entry_count(&self) -> usize {
self.original_entry_count
}
pub fn pending_entry_count(&self) -> usize {
self.new_entries.len()
}
pub fn path_exists(&self, path: &ArchivePath) -> bool {
let path_str = path.as_str();
self.existing_paths.contains(path_str)
|| self.new_entries.iter().any(|e| e.path.as_str() == path_str)
}
pub fn add_bytes(&mut self, path: ArchivePath, data: impl Into<Vec<u8>>) -> Result<()> {
if self.path_exists(&path) {
return Err(Error::InvalidArchivePath(format!(
"path '{}' already exists in archive",
path.as_str()
)));
}
self.new_entries.push(PendingAppendEntry {
path,
data: data.into(),
is_directory: false,
});
Ok(())
}
pub fn add_path(&mut self, source: impl AsRef<Path>, archive_path: ArchivePath) -> Result<()> {
if self.path_exists(&archive_path) {
return Err(Error::InvalidArchivePath(format!(
"path '{}' already exists in archive",
archive_path.as_str()
)));
}
let data = std::fs::read(source.as_ref()).map_err(Error::Io)?;
self.new_entries.push(PendingAppendEntry {
path: archive_path,
data,
is_directory: false,
});
Ok(())
}
pub fn add_directory(&mut self, path: ArchivePath) -> Result<()> {
if self.path_exists(&path) {
return Err(Error::InvalidArchivePath(format!(
"path '{}' already exists in archive",
path.as_str()
)));
}
self.new_entries.push(PendingAppendEntry {
path,
data: Vec::new(),
is_directory: true,
});
Ok(())
}
pub fn finish(self) -> Result<AppendResult> {
if self.new_entries.is_empty() {
return Ok(AppendResult {
entries_added: 0,
total_entries: self.original_entry_count,
total_bytes: 0,
});
}
let entries_added = self.new_entries.len();
let total_entries = self.original_entry_count + entries_added;
let temp_path = self.path.with_extension("7z.tmp");
let temp_file = File::create(&temp_path).map_err(Error::Io)?;
let temp_writer = BufWriter::new(temp_file);
let result = {
let original_file = File::open(&self.path).map_err(Error::Io)?;
let original_reader = BufReader::new(original_file);
let mut original_archive = Archive::open(original_reader)?;
let mut writer = Writer::create(temp_writer)?.options(self.options.clone());
let entries: Vec<_> = original_archive.entries().to_vec();
for (idx, entry) in entries.iter().enumerate() {
if entry.is_directory {
writer.add_directory(entry.path.clone(), EntryMeta::default())?;
} else {
let data = original_archive.extract_entry_to_vec_by_index(idx)?;
writer.add_bytes(entry.path.clone(), &data)?;
}
}
for pending in self.new_entries {
if pending.is_directory {
writer.add_directory(pending.path, EntryMeta::default())?;
} else {
writer.add_bytes(pending.path, &pending.data)?;
}
}
writer.finish()?
};
std::fs::rename(&temp_path, &self.path).map_err(Error::Io)?;
Ok(AppendResult {
entries_added,
total_entries,
total_bytes: result.total_size,
})
}
}
#[cfg(all(test, feature = "lzma"))]
mod tests {
use super::*;
use tempfile::TempDir;
fn create_test_archive(dir: &TempDir) -> std::path::PathBuf {
let archive_path = dir.path().join("test.7z");
let file = File::create(&archive_path).unwrap();
let buf_writer = BufWriter::new(file);
let mut writer = Writer::create(buf_writer).unwrap();
writer
.add_bytes(ArchivePath::new("file1.txt").unwrap(), b"content1")
.unwrap();
writer
.add_bytes(ArchivePath::new("file2.txt").unwrap(), b"content2")
.unwrap();
let _ = writer.finish().unwrap();
archive_path
}
#[test]
fn test_appender_open() {
let dir = TempDir::new().unwrap();
let archive_path = create_test_archive(&dir);
let appender = ArchiveAppender::open(&archive_path).unwrap();
assert_eq!(appender.existing_entry_count(), 2);
assert_eq!(appender.pending_entry_count(), 0);
}
#[test]
fn test_appender_add_bytes() {
let dir = TempDir::new().unwrap();
let archive_path = create_test_archive(&dir);
let mut appender = ArchiveAppender::open(&archive_path).unwrap();
appender
.add_bytes(ArchivePath::new("new.txt").unwrap(), b"new content")
.unwrap();
assert_eq!(appender.pending_entry_count(), 1);
}
#[test]
fn test_appender_duplicate_path_error() {
let dir = TempDir::new().unwrap();
let archive_path = create_test_archive(&dir);
let mut appender = ArchiveAppender::open(&archive_path).unwrap();
let result = appender.add_bytes(ArchivePath::new("file1.txt").unwrap(), b"data");
assert!(result.is_err());
}
#[test]
fn test_appender_finish() {
let dir = TempDir::new().unwrap();
let archive_path = create_test_archive(&dir);
let mut appender = ArchiveAppender::open(&archive_path).unwrap();
appender
.add_bytes(ArchivePath::new("new.txt").unwrap(), b"new content")
.unwrap();
let result = appender.finish().unwrap();
assert_eq!(result.entries_added, 1);
assert_eq!(result.total_entries, 3);
let file = File::open(&archive_path).unwrap();
let reader = BufReader::new(file);
let archive = Archive::open(reader).unwrap();
assert_eq!(archive.entries().len(), 3);
}
#[test]
fn test_appender_finish_empty() {
let dir = TempDir::new().unwrap();
let archive_path = create_test_archive(&dir);
let appender = ArchiveAppender::open(&archive_path).unwrap();
let result = appender.finish().unwrap();
assert_eq!(result.entries_added, 0);
assert_eq!(result.total_entries, 2);
}
#[test]
fn test_appender_path_exists() {
let dir = TempDir::new().unwrap();
let archive_path = create_test_archive(&dir);
let mut appender = ArchiveAppender::open(&archive_path).unwrap();
assert!(appender.path_exists(&ArchivePath::new("file1.txt").unwrap()));
assert!(!appender.path_exists(&ArchivePath::new("new.txt").unwrap()));
appender
.add_bytes(ArchivePath::new("new.txt").unwrap(), b"content")
.unwrap();
assert!(appender.path_exists(&ArchivePath::new("new.txt").unwrap()));
}
#[test]
fn test_appender_add_directory() {
let dir = TempDir::new().unwrap();
let archive_path = create_test_archive(&dir);
let mut appender = ArchiveAppender::open(&archive_path).unwrap();
appender
.add_directory(ArchivePath::new("new_dir").unwrap())
.unwrap();
let result = appender.finish().unwrap();
assert_eq!(result.entries_added, 1);
assert_eq!(result.total_entries, 3);
}
}