feat(sync): add rsync-like sync command with dry-run and bump version to v0.5.0
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
|
||||
use sanctum::crypto::{
|
||||
derive_kek, generate_dek, generate_salt, wrap_dek, KdfParams, CHUNK_SIZE, FORMAT_VERSION,
|
||||
};
|
||||
use sanctum::storage::Database;
|
||||
use sanctum::sync::{run_sync, SyncDirection, SyncOptions};
|
||||
|
||||
fn create_test_container(path: &Path) -> (Database, [u8; 32]) {
|
||||
if path.exists() {
|
||||
let _ = fs::remove_file(path);
|
||||
}
|
||||
let db = Database::open(path).expect("Open database");
|
||||
let salt = generate_salt();
|
||||
let kdf_params = KdfParams {
|
||||
memory_cost: 1024,
|
||||
time_cost: 1,
|
||||
parallelism: 1,
|
||||
};
|
||||
let kek = derive_kek("sync_password", &salt, &kdf_params).unwrap();
|
||||
let dek = generate_dek();
|
||||
let (wrapped_dek, header_nonce, header_tag) = wrap_dek(&kek, &dek).unwrap();
|
||||
db.init_schema(&salt, &kdf_params, &wrapped_dek, &header_nonce, &header_tag).unwrap();
|
||||
(db, *dek)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sync_push_and_pull_basic() {
|
||||
let temp_root = std::env::temp_dir().join(format!("sanctum_sync_test_{}", rand::random::<u64>()));
|
||||
let container_path = temp_root.join("test.sanctum");
|
||||
let source_dir = temp_root.join("source");
|
||||
let target_dir = temp_root.join("restored");
|
||||
|
||||
fs::create_dir_all(&source_dir).unwrap();
|
||||
fs::create_dir_all(source_dir.join("sub")).unwrap();
|
||||
|
||||
// Testdateien anlegen
|
||||
fs::write(source_dir.join("file1.txt"), b"Hello Sanctum Sync!").unwrap();
|
||||
fs::write(source_dir.join("sub").join("file2.bin"), vec![0x42u8; 100_000]).unwrap();
|
||||
|
||||
let (db, dek) = create_test_container(&container_path);
|
||||
|
||||
// 1. Push
|
||||
let mut opts = SyncOptions::default();
|
||||
opts.direction = SyncDirection::Push;
|
||||
opts.quiet = true;
|
||||
|
||||
let stats = run_sync(
|
||||
&db,
|
||||
0,
|
||||
&dek,
|
||||
FORMAT_VERSION,
|
||||
source_dir.to_str().unwrap(),
|
||||
"/Backup",
|
||||
&opts,
|
||||
).expect("Sync push");
|
||||
|
||||
assert_eq!(stats.files_scanned, 2);
|
||||
assert_eq!(stats.files_transferred, 2);
|
||||
assert_eq!(stats.files_skipped, 0);
|
||||
|
||||
// 2. Erneuter Push (Fast check / Delta): Muss 0 übertragen, 2 überspringen
|
||||
let stats_delta = run_sync(
|
||||
&db,
|
||||
0,
|
||||
&dek,
|
||||
FORMAT_VERSION,
|
||||
source_dir.to_str().unwrap(),
|
||||
"/Backup",
|
||||
&opts,
|
||||
).expect("Sync push delta");
|
||||
|
||||
assert_eq!(stats_delta.files_transferred, 0);
|
||||
assert_eq!(stats_delta.files_skipped, 2);
|
||||
|
||||
// 3. Dry-Run mit neuer Datei
|
||||
fs::write(source_dir.join("new_file.txt"), b"Brand new file").unwrap();
|
||||
let mut dry_opts = opts.clone();
|
||||
dry_opts.dry_run = true;
|
||||
|
||||
let stats_dry = run_sync(
|
||||
&db,
|
||||
0,
|
||||
&dek,
|
||||
FORMAT_VERSION,
|
||||
source_dir.to_str().unwrap(),
|
||||
"/Backup",
|
||||
&dry_opts,
|
||||
).expect("Sync push dry-run");
|
||||
|
||||
assert_eq!(stats_dry.files_transferred, 1);
|
||||
assert_eq!(stats_dry.files_skipped, 2);
|
||||
|
||||
// Verifizieren, dass new_file.txt im Tresor tatsächlich NICHT existiert
|
||||
let node = db.resolve_path_in_vault("/Backup/new_file.txt", 0, &dek).unwrap();
|
||||
assert!(node.is_none());
|
||||
|
||||
// 4. Pull
|
||||
let mut pull_opts = SyncOptions::default();
|
||||
pull_opts.direction = SyncDirection::Pull;
|
||||
pull_opts.quiet = true;
|
||||
|
||||
let stats_pull = run_sync(
|
||||
&db,
|
||||
0,
|
||||
&dek,
|
||||
FORMAT_VERSION,
|
||||
"/Backup",
|
||||
target_dir.to_str().unwrap(),
|
||||
&pull_opts,
|
||||
).expect("Sync pull");
|
||||
|
||||
assert_eq!(stats_pull.files_transferred, 2);
|
||||
|
||||
// Inhalt vergleichen
|
||||
let c1 = fs::read(target_dir.join("file1.txt")).unwrap();
|
||||
assert_eq!(c1, b"Hello Sanctum Sync!");
|
||||
let c2 = fs::read(target_dir.join("sub").join("file2.bin")).unwrap();
|
||||
assert_eq!(c2, vec![0x42u8; 100_000]);
|
||||
|
||||
// Aufräumen
|
||||
let _ = fs::remove_dir_all(&temp_root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sync_multi_megabyte_large_file() {
|
||||
let temp_root = std::env::temp_dir().join(format!("sanctum_sync_large_{}", rand::random::<u64>()));
|
||||
let container_path = temp_root.join("test_large.sanctum");
|
||||
let source_dir = temp_root.join("source");
|
||||
let target_dir = temp_root.join("restored");
|
||||
|
||||
fs::create_dir_all(&source_dir).unwrap();
|
||||
|
||||
// 3.5 MB große Datei erzeugen (geht über 4 Chunks: 0, 1, 2, 3)
|
||||
let large_file_path = source_dir.join("large_payload.bin");
|
||||
let mut large_file = File::create(&large_file_path).unwrap();
|
||||
let chunk_sample = vec![0xA5u8; CHUNK_SIZE];
|
||||
for _ in 0..3 {
|
||||
large_file.write_all(&chunk_sample).unwrap();
|
||||
}
|
||||
large_file.write_all(&vec![0x5Au8; 512 * 1024]).unwrap(); // 3.5 MB
|
||||
large_file.flush().unwrap();
|
||||
drop(large_file);
|
||||
|
||||
let (db, dek) = create_test_container(&container_path);
|
||||
|
||||
// Push
|
||||
let mut opts = SyncOptions::default();
|
||||
opts.direction = SyncDirection::Push;
|
||||
opts.quiet = true;
|
||||
|
||||
let stats = run_sync(
|
||||
&db,
|
||||
0,
|
||||
&dek,
|
||||
FORMAT_VERSION,
|
||||
source_dir.to_str().unwrap(),
|
||||
"/LargeTest",
|
||||
&opts,
|
||||
).expect("Sync push large");
|
||||
|
||||
assert_eq!(stats.files_transferred, 1);
|
||||
assert_eq!(stats.bytes_transferred, 3 * (CHUNK_SIZE as u64) + 512 * 1024);
|
||||
|
||||
// Pull
|
||||
let mut pull_opts = SyncOptions::default();
|
||||
pull_opts.direction = SyncDirection::Pull;
|
||||
pull_opts.quiet = true;
|
||||
|
||||
run_sync(
|
||||
&db,
|
||||
0,
|
||||
&dek,
|
||||
FORMAT_VERSION,
|
||||
"/LargeTest",
|
||||
target_dir.to_str().unwrap(),
|
||||
&pull_opts,
|
||||
).expect("Sync pull large");
|
||||
|
||||
let restored = fs::read(target_dir.join("large_payload.bin")).unwrap();
|
||||
let original = fs::read(&large_file_path).unwrap();
|
||||
assert_eq!(restored.len(), original.len());
|
||||
assert_eq!(restored, original);
|
||||
|
||||
let _ = fs::remove_dir_all(&temp_root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sync_delete_and_exclude_flags() {
|
||||
let temp_root = std::env::temp_dir().join(format!("sanctum_sync_flags_{}", rand::random::<u64>()));
|
||||
let container_path = temp_root.join("test_flags.sanctum");
|
||||
let source_dir = temp_root.join("source");
|
||||
|
||||
fs::create_dir_all(&source_dir).unwrap();
|
||||
fs::write(source_dir.join("keep.txt"), b"Keep this").unwrap();
|
||||
fs::write(source_dir.join("remove_me.txt"), b"Will be deleted later").unwrap();
|
||||
fs::write(source_dir.join("ignore.tmp"), b"Temporary file").unwrap();
|
||||
|
||||
let (db, dek) = create_test_container(&container_path);
|
||||
|
||||
let mut opts = SyncOptions::default();
|
||||
opts.direction = SyncDirection::Push;
|
||||
opts.exclude_patterns = vec!["*.tmp".to_string()];
|
||||
opts.quiet = true;
|
||||
|
||||
// 1. Initialer Push mit Exclude
|
||||
let stats = run_sync(
|
||||
&db,
|
||||
0,
|
||||
&dek,
|
||||
FORMAT_VERSION,
|
||||
source_dir.to_str().unwrap(),
|
||||
"/Files",
|
||||
&opts,
|
||||
).unwrap();
|
||||
|
||||
assert_eq!(stats.files_transferred, 2); // keep.txt und remove_me.txt
|
||||
assert!(db.resolve_path_in_vault("/Files/ignore.tmp", 0, &dek).unwrap().is_none());
|
||||
|
||||
// 2. Lokale Datei löschen und Sync mit --delete ausführen
|
||||
fs::remove_file(source_dir.join("remove_me.txt")).unwrap();
|
||||
opts.delete = true;
|
||||
|
||||
let stats_del = run_sync(
|
||||
&db,
|
||||
0,
|
||||
&dek,
|
||||
FORMAT_VERSION,
|
||||
source_dir.to_str().unwrap(),
|
||||
"/Files",
|
||||
&opts,
|
||||
).unwrap();
|
||||
|
||||
assert_eq!(stats_del.files_deleted, 1);
|
||||
assert!(db.resolve_path_in_vault("/Files/remove_me.txt", 0, &dek).unwrap().is_none());
|
||||
assert!(db.resolve_path_in_vault("/Files/keep.txt", 0, &dek).unwrap().is_some());
|
||||
|
||||
let _ = fs::remove_dir_all(&temp_root);
|
||||
}
|
||||
Reference in New Issue
Block a user