fix: preserve archive restores after pruning

This commit is contained in:
2026-05-08 16:24:45 +00:00
parent 904318531a
commit 56a5fa27f4
5 changed files with 1945 additions and 4100 deletions
+10 -10
View File
@@ -7,7 +7,7 @@
"typings": "dist_ts/index.d.ts",
"type": "module",
"scripts": {
"test": "(tstest test/ --verbose --timeout 60)",
"test": "(pnpm run build && tstest test/ --verbose --timeout 60)",
"build": "(tsrust && tsbuild tsfolders --allowimplicitany)"
},
"repository": {
@@ -21,17 +21,17 @@
},
"homepage": "https://code.foss.global/serve.zone/containerarchive",
"dependencies": {
"@push.rocks/lik": "^6.0.0",
"@push.rocks/smartpromise": "^4.0.0",
"@push.rocks/smartrust": "^1.3.2",
"@push.rocks/smartrx": "^3.0.0"
"@push.rocks/lik": "^6.4.1",
"@push.rocks/smartpromise": "^4.2.4",
"@push.rocks/smartrust": "^1.4.0",
"@push.rocks/smartrx": "^3.0.10"
},
"devDependencies": {
"@git.zone/tsbuild": "^2.0.0",
"@git.zone/tsrun": "^1.0.0",
"@git.zone/tstest": "^1.0.0",
"@git.zone/tsrust": "^1.3.0",
"@types/node": "^22.0.0"
"@git.zone/tsbuild": "^4.4.0",
"@git.zone/tsrun": "^2.0.3",
"@git.zone/tsrust": "^1.3.3",
"@git.zone/tstest": "^3.6.3",
"@types/node": "^25.6.1"
},
"files": [
"ts/**/*",
+1903 -4086
View File
File diff suppressed because it is too large Load Diff
+16 -4
View File
@@ -4,7 +4,7 @@
/// deletes expired snapshots, and removes pack files where ALL chunks
/// are unreferenced (whole-pack GC only).
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
use std::path::Path;
use serde::{Deserialize, Serialize};
@@ -233,6 +233,7 @@ async fn rewrite_partial_packs(
// Read referenced chunks and write them to a new pack
let mut new_pack_writer = PackWriter::new(repo.config.pack_target_size);
let mut rewritten_offsets: HashMap<String, u64> = HashMap::new();
for entry in &entries {
let hash_hex = hasher::hash_to_hex(&entry.content_hash);
@@ -245,6 +246,10 @@ async fn rewrite_partial_packs(
&pack_path, entry.offset, entry.compressed_size,
).await?;
let new_offset = new_pack_writer.entries().iter()
.map(|existing| existing.compressed_size as u64)
.sum::<u64>();
new_pack_writer.add_chunk(
entry.content_hash,
&chunk_data,
@@ -252,6 +257,7 @@ async fn rewrite_partial_packs(
entry.nonce,
entry.flags,
);
rewritten_offsets.insert(hash_hex, new_offset);
}
// Finalize the new pack
@@ -267,9 +273,15 @@ async fn rewrite_partial_packs(
} else {
None
};
let rewritten_offset = *rewritten_offsets.get(&hash_hex).ok_or_else(|| {
ArchiveError::Corruption(format!(
"Missing rewritten offset for chunk {} in pack {}",
hash_hex, pack_id,
))
})?;
repo.index.add_entry(hash_hex, IndexEntry {
pack_id: new_pack_info.pack_id.clone(),
offset: entry.offset, // Note: offset in the new pack may differ
offset: rewritten_offset,
compressed_size: entry.compressed_size,
plaintext_size: entry.plaintext_size,
nonce,
@@ -280,9 +292,9 @@ async fn rewrite_partial_packs(
}
// Delete old pack + idx
let old_size = tokio::fs::metadata(&pack_path).await
let _old_size = tokio::fs::metadata(&pack_path).await
.map(|m| m.len()).unwrap_or(0);
let old_idx_size = tokio::fs::metadata(&idx_path).await
let _old_idx_size = tokio::fs::metadata(&idx_path).await
.map(|m| m.len()).unwrap_or(0);
let _ = tokio::fs::remove_file(&pack_path).await;
+13
View File
@@ -185,6 +185,19 @@ tap.test('should prune with keepLast=1', async () => {
// Verify only 1 snapshot remains
const snapshots = await repo.listSnapshots();
expect(snapshots.length).toEqual(1);
// The remaining snapshot must still restore after partial-pack GC rewrites chunks.
const restoreStream = await repo.restore(snapshots[0].id, { item: 'config.tar' });
const chunks: Buffer[] = [];
await new Promise<void>((resolve, reject) => {
restoreStream.on('data', (chunk: Buffer) => chunks.push(chunk));
restoreStream.on('end', resolve);
restoreStream.on('error', reject);
});
const restored = Buffer.concat(chunks);
const expected = Buffer.alloc(32 * 1024, 'item-two-data');
expect(restored.equals(expected)).toBeTrue();
});
// ==================== Close ====================
+3
View File
@@ -5,6 +5,9 @@
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"types": [
"node"
],
"esModuleInterop": true,
"verbatimModuleSyntax": true
},