6 Commits
v0.1 ... v0.2

Author SHA1 Message Date
df0b260c7b v0.2 2025-10-17 13:48:34 +03:00
c7fff59f6c cleanup 2025-10-17 13:16:55 +03:00
0f645d0689 readme 2025-10-17 13:13:18 +03:00
2bbcca5253 readme 2025-10-17 13:11:06 +03:00
e5f238a126 messages 2025-10-17 13:05:11 +03:00
ac8b4d6f81 patch hash check 2025-10-17 12:53:20 +03:00
5 changed files with 157 additions and 37 deletions

View File

@@ -1,5 +1,9 @@
build:
all: build-linux build-win
build-linux:
cargo build --release --target x86_64-unknown-linux-gnu --package zsdiff_all --bin zspatch
cargo build --release --target x86_64-unknown-linux-gnu --package zsdiff_all --bin zsdiff
#cargo build --release --target x86_64-apple-darwin --package zdiff_all --bin zpatch
#cargo build --release --target x86_64-pc-windows-gnu --package zdiff_all --bin zpatch
build-win:
cargo build --release --target x86_64-pc-windows-gnu --package zsdiff_all --bin zspatch
cargo build --release --target x86_64-pc-windows-gnu --package zsdiff_all --bin zsdiff

30
README.md Normal file
View File

@@ -0,0 +1,30 @@
# ZsDiff
A partial update program. Use [Zstandrard](https://github.com/facebook/zstd).
`zsdiff` products a [filename].zdiff file and [filename].zdiff.md5. It compares ALL file hashes from old with new.
```
Usage: zsdiff [OPTIONS] --filename <FILENAME> --old <OLD> --new <NEW>
Options:
-f, --filename <FILENAME>
-c, --compress-level <COMPRESS_LEVEL> [default: 11]
-o, --old <OLD>
-n, --new <NEW>
-h, --help Print help
```
`zspatch` extract files from [filename].zdiff to [dest-dir]. If some file was deleted between old and new, `zspatch`
also delete it from [dest-dir].
Flag --hash-check compare computed hash of [filename].zdiff and hash from [filename].zdiff.md5
```
Usage: zspatch [OPTIONS] --filename <FILENAME> --dest-dir <DEST_DIR>
Options:
-f, --filename <FILENAME>
-d, --dest-dir <DEST_DIR>
-h, --hash-check
--help Print help
```

View File

@@ -1,3 +1,4 @@
use md5;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::{fs, io};
@@ -9,7 +10,7 @@ pub struct Zsdiff {
}
impl Zsdiff {
pub async fn from_vec(_data: Vec<Vec<u8>>) -> Result<Self, std::io::Error> {
pub async fn from_vec(_data: Vec<Vec<u8>>) -> Result<Self, io::Error> {
let mut content = HashMap::new();
for part in _data {
let filename_size = u32::from_be_bytes(part[0..4].try_into().unwrap()) as usize;
@@ -54,9 +55,10 @@ pub struct Metadata {
pub(crate) diff_files: Vec<String>,
pub hashes: HashMap<String, String>,
pub remove_files: Vec<String>,
pub remove_folders: Vec<String>,
}
pub async fn get_hash(data: &Vec<u8>) -> String {
pub async fn get_hash(data: Vec<u8>) -> String {
let hash = md5::compute(&data[..]);
format!("{:x}", hash)
}

View File

@@ -2,14 +2,17 @@ mod utils;
use clap::Parser;
use std::collections::HashMap;
use std::{fs, io};
use std::io::Write;
use std::{fs, io, time};
use utils::{Metadata, Zsdiff, get_hash};
use walkdir::WalkDir;
#[derive(Debug)]
struct FileInfo {
path: String,
relative_path: String, // Without dir prefix
hash: String,
is_dir: bool,
}
async fn walk_dir(dir: String) -> HashMap<String, FileInfo> {
@@ -17,33 +20,66 @@ async fn walk_dir(dir: String) -> HashMap<String, FileInfo> {
for e in WalkDir::new(&dir) {
let e = e.unwrap();
let path = e.path();
if path.is_dir() {
println!(
"Path: {}, {}",
path.display(),
path.display().to_string().eq(&dir)
);
if path.display().to_string().eq(&dir) {
continue;
}
let content = fs::read(path).unwrap();
let hash = get_hash(&content).await;
let path_str = path.display().to_string();
let file_info = FileInfo {
relative_path: path_str[dir.len() + 1..].to_string(),
path: path_str,
hash: hash.clone(),
};
hash_list.entry(hash).or_insert(file_info);
if path.is_dir() {
let path_str = path.display().to_string();
let hash = get_hash(path_str.clone().into_bytes()).await;
let file_info = FileInfo {
relative_path: path_str[dir.len() + 1..].to_string(),
path: path_str,
hash: hash.clone(),
is_dir: true,
};
hash_list.entry(hash).or_insert(file_info);
} else {
let content = fs::read(path).unwrap();
let hash = get_hash(content).await;
let path_str = path.display().to_string();
let file_info = FileInfo {
relative_path: path_str[dir.len() + 1..].to_string(),
path: path_str,
hash: hash.clone(),
is_dir: false,
};
hash_list.entry(hash).or_insert(file_info);
}
}
println!("{:?}", hash_list);
hash_list
}
async fn compare_hashes(old: HashMap<String, FileInfo>, new: HashMap<String, FileInfo>) -> Zsdiff {
let mut diff_files: HashMap<String, Vec<u8>> = HashMap::new();
let mut remove_files: Vec<String> = vec![];
let mut remove_folders: Vec<String> = vec![];
let mut hashes: HashMap<String, String> = HashMap::new();
for (_, info) in &old {
remove_files.push(info.relative_path.clone());
if info.is_dir {
remove_folders.push(info.relative_path.clone());
} else {
remove_files.push(info.relative_path.clone());
}
}
for (new_hash, new_fileinfo) in &new {
let old_fileinfo = old.get(new_hash);
remove_files.retain(|filename| !filename.eq(&new_fileinfo.relative_path));
if new_fileinfo.is_dir {
remove_folders.retain(|filename| !filename.eq(&new_fileinfo.relative_path));
} else {
remove_files.retain(|filename| !filename.eq(&new_fileinfo.relative_path));
}
if new_fileinfo.is_dir {
println!("{}", new_fileinfo.is_dir);
continue;
}
if old_fileinfo.is_none() {
let path = new_fileinfo.relative_path.clone();
@@ -61,6 +97,7 @@ async fn compare_hashes(old: HashMap<String, FileInfo>, new: HashMap<String, Fil
diff_files: diff_files.keys().cloned().collect(),
hashes,
remove_files,
remove_folders,
},
}
}
@@ -76,16 +113,29 @@ pub async fn zsdiff(
let new_hashes = walk_dir(new).await;
let compare_hashes = compare_hashes(old_hashes, new_hashes).await;
let parts = compare_hashes.to_vec().await;
let file = fs::File::create(output_filename)?;
utils::compress_parts(parts, &file, level).await;
// let mut buf = Vec::new();
// file.read(&mut buf)?;
// let output_hash = get_hash(&buf).await;
// println!("{}", output_hash);
let mut size_before = 0;
for p in &parts {
size_before += p.len();
}
let now = time::Instant::now();
utils::compress_parts(parts, &fs::File::create(output_filename)?, level).await;
let output_data = fs::read(output_filename)?;
let size_after = output_data.len();
let output_hash = get_hash(output_data).await;
fs::File::create(format!("{}.md5", output_filename))?.write_all(output_hash.as_bytes())?;
let elapsed = now.elapsed();
println!("Zsdiff hash: {}", output_hash);
println!("Size before: {:.1?}KB", size_before / 1024);
println!("Size after: {:.1?}KB", size_after / 1024);
println!(
"Compress ratio: {:.2?}%",
size_after as f64 / size_before as f64 * 100.0
);
print!("Time elapsed: {:.2?}", elapsed);
Ok(())
}
#[derive(Parser, Debug)]
#[derive(Parser)]
struct Args {
#[arg(short, long)]
filename: String,
@@ -100,6 +150,5 @@ struct Args {
#[tokio::main]
async fn main() -> io::Result<()> {
let args = Args::parse();
zsdiff(args.filename, args.old, args.new, args.compress_level).await?;
Ok(())
zsdiff(args.filename, args.old, args.new, args.compress_level).await
}

View File

@@ -4,7 +4,7 @@ use clap::Parser;
use std::fs::read;
use std::io::Write;
use std::path::Path;
use std::{fs, io};
use std::{fs, io, time};
use utils::Zsdiff;
async fn create_tmp_dir(dir_name: String) -> Result<String, io::Error> {
@@ -14,7 +14,13 @@ async fn create_tmp_dir(dir_name: String) -> Result<String, io::Error> {
Ok(name)
}
async fn extract_files(zsdiff: &Zsdiff, filename: &String) -> Result<String, io::Error> {
async fn load_file(filename: String) -> Result<Zsdiff, io::Error> {
let filename = &format!("{}.zdiff", filename);
let parts = utils::decompress_parts(read(filename)?).await?;
Ok(Zsdiff::from_vec(parts).await?)
}
async fn extract_files(zsdiff: &Zsdiff, filename: String) -> Result<String, io::Error> {
let tmp_dir_name = create_tmp_dir(filename.to_string()).await?;
let path = Path::new(&tmp_dir_name);
fs::remove_dir_all(path).ok();
@@ -26,11 +32,21 @@ async fn extract_files(zsdiff: &Zsdiff, filename: &String) -> Result<String, io:
Ok(tmp_dir_name)
}
async fn zpatch(filename: String, dest_dir: String) -> Result<(), io::Error> {
let filename = &format!("{}.zdiff", filename);
let parts = utils::decompress_parts(read(filename)?).await?;
let diff = Zsdiff::from_vec(parts).await?;
async fn check_hash(filename: String) -> Result<(), io::Error> {
let file_data = read(format!("{}.zdiff", filename))?;
let hash_file = read(format!("{}.zdiff.md5", filename))?;
let hash = utils::get_hash(file_data).await;
if !String::from_utf8(hash_file).unwrap().eq(&hash) {
return Err(io::Error::new(io::ErrorKind::Other, "Hash mismatch"));
}
println!("Zsdiff hash: {}", hash);
Ok(())
}
async fn zspatch(filename: String, dest_dir: String) -> Result<(), io::Error> {
let diff = load_file(filename.clone()).await?;
let tmp_dir_name = extract_files(&diff, filename).await?;
let now = time::Instant::now();
for name in diff.content.keys().collect::<Vec<&String>>() {
let from_path = Path::new(&tmp_dir_name).join(name);
let to_path = Path::new(&dest_dir).join(name);
@@ -43,28 +59,47 @@ async fn zpatch(filename: String, dest_dir: String) -> Result<(), io::Error> {
fs::remove_file(path).ok();
}
for folder in diff.metadata.remove_folders {
let path = Path::new(&dest_dir).join(folder);
fs::remove_dir_all(path).ok();
}
for (k, hash) in diff.metadata.hashes {
let path = Path::new(&dest_dir).join(k);
let content = read(path)?;
let fs_hash = utils::get_hash(&content).await;
let fs_hash = utils::get_hash(content).await;
if !fs_hash.eq(&hash) {
Err(io::Error::new(io::ErrorKind::Other, "Hash mismatch"))?
}
}
fs::remove_dir_all(tmp_dir_name).ok();
println!("Patching done!");
println!("Elapsed time: {:.2?}", now.elapsed());
Ok(())
}
#[derive(Parser, Debug)]
#[derive(Parser)]
struct Args {
#[arg(short, long)]
filename: String,
#[arg(short, long)]
dest_dir: String,
#[arg(short, long)]
metadata: bool,
#[arg(short, long)]
hash_check: bool,
}
#[tokio::main]
async fn main() -> io::Result<()> {
let args = Args::parse();
zpatch(args.filename, args.dest_dir).await?;
Ok(())
if args.hash_check {
check_hash(args.filename.clone()).await?;
}
if args.metadata {
let diff = load_file(args.filename).await?;
println!("{}", serde_json::to_string(&diff.metadata)?);
return Ok(());
}
zspatch(args.filename, args.dest_dir).await
}