feat: initial implementation of content-addressed incremental backup engine

Rust-centric architecture with TypeScript facade following smartproxy/smartstorage pattern.
Core engine in Rust (FastCDC chunking, SHA-256, gzip, AES-256-GCM + Argon2id, binary pack files,
global index, snapshots, locking, verification, pruning, repair). TypeScript provides npm interface
via @push.rocks/smartrust RustBridge IPC with Unix socket streaming for ingest/restore.
All 14 integration tests pass.
This commit is contained in:
2026-03-21 23:30:17 +00:00
commit a5849791d2
34 changed files with 15506 additions and 0 deletions

131
rust/src/restore.rs Normal file
View File

@@ -0,0 +1,131 @@
/// Restore pipeline: reads a snapshot manifest, looks up chunks in the global
/// index, reads from pack files, decrypts, decompresses, and writes to a Unix socket.
use tokio::io::AsyncWriteExt;
use tokio::net::UnixStream;
use crate::compression;
use crate::encryption;
use crate::error::ArchiveError;
use crate::hasher;
use crate::pack_reader;
use crate::repository::Repository;
use crate::snapshot;
/// Restore a snapshot (or a specific item) to a Unix socket.
pub async fn restore(
repo: &Repository,
snapshot_id: &str,
socket_path: &str,
item_name: Option<&str>,
) -> Result<(), ArchiveError> {
// Load snapshot manifest
let snap = snapshot::load_snapshot(&repo.path, snapshot_id).await?;
// Determine which items to restore
let items_to_restore: Vec<&snapshot::SnapshotItem> = if let Some(name) = item_name {
snap.items.iter()
.filter(|i| i.name == name)
.collect()
} else {
snap.items.iter().collect()
};
if items_to_restore.is_empty() {
return Err(ArchiveError::NotFound(format!(
"No items found in snapshot {}{}",
snapshot_id,
item_name.map(|n| format!(" with name '{}'", n)).unwrap_or_default()
)));
}
// Connect to the Unix socket where TypeScript will read the restored data
let mut stream = UnixStream::connect(socket_path).await
.map_err(|e| ArchiveError::Io(e))?;
tracing::info!("Connected to restore socket: {}", socket_path);
let mut restored_bytes: u64 = 0;
let mut chunks_read: u64 = 0;
for item in items_to_restore {
for hash_hex in &item.chunks {
// Look up chunk in global index
let index_entry = repo.index.get(hash_hex)
.ok_or_else(|| ArchiveError::NotFound(format!(
"Chunk {} not found in index", hash_hex
)))?;
// Determine pack file path
let shard = &index_entry.pack_id[..2];
let pack_path = std::path::Path::new(&repo.path)
.join("packs")
.join("data")
.join(shard)
.join(format!("{}.pack", index_entry.pack_id));
// Read chunk data from pack
let stored_data = pack_reader::read_chunk(
&pack_path,
index_entry.offset,
index_entry.compressed_size,
).await?;
// Decrypt if encrypted
let compressed = if let Some(ref key) = repo.master_key {
// We need the nonce. Read it from the IDX file.
let idx_path = std::path::Path::new(&repo.path)
.join("packs")
.join("data")
.join(shard)
.join(format!("{}.idx", index_entry.pack_id));
let entries = pack_reader::load_idx(&idx_path).await?;
let hash_bytes = hasher::hex_to_hash(hash_hex)
.map_err(|_| ArchiveError::Corruption(format!("Invalid hash: {}", hash_hex)))?;
let idx_entry = pack_reader::find_in_idx(&entries, &hash_bytes)
.ok_or_else(|| ArchiveError::NotFound(format!(
"Chunk {} not found in pack index {}", hash_hex, index_entry.pack_id
)))?;
encryption::decrypt_chunk(&stored_data, key, &idx_entry.nonce)?
} else {
stored_data
};
// Decompress
let plaintext = compression::decompress(&compressed)?;
// Verify hash
let actual_hash = hasher::hash_chunk(&plaintext);
let expected_hash = hasher::hex_to_hash(hash_hex)
.map_err(|_| ArchiveError::Corruption(format!("Invalid hash: {}", hash_hex)))?;
if actual_hash != expected_hash {
return Err(ArchiveError::Corruption(format!(
"Hash mismatch for chunk {}: expected {}, got {}",
hash_hex,
hash_hex,
hasher::hash_to_hex(&actual_hash)
)));
}
// Write to output socket
stream.write_all(&plaintext).await?;
restored_bytes += plaintext.len() as u64;
chunks_read += 1;
}
}
// Close the write side
stream.shutdown().await?;
tracing::info!(
"Restore complete: {} bytes, {} chunks from snapshot {}",
restored_bytes, chunks_read, snapshot_id
);
Ok(())
}