fix(vfs): V-01 — copy dispatches to CarrierFs in carrier mode

This commit is contained in:
2026-09-19 00:16:04 +02:00
parent 533764487a
commit 151fdba09d
3 changed files with 335 additions and 0 deletions
+128
View File
@@ -747,6 +747,134 @@ impl DavFileSystem for CarrierFs {
Ok(()) Ok(())
}) })
} }
fn copy<'a>(&'a self, from: &'a DavPath, to: &'a DavPath) -> FsFuture<'a, ()> {
Box::pin(async move {
self.touch();
let from_str = Self::path_to_str(from);
let to_str = Self::path_to_str(to);
if from_str == to_str {
return Ok(());
}
let mut inner = self.inner.lock().unwrap();
let node = inner.resolve_path(&from_str).ok_or(FsError::NotFound)?;
if node.is_dir {
return Err(FsError::NotImplemented);
}
let (to_parent_path, to_name) = inner.split_parent_and_name(&to_str);
if inner.anti_leak && is_leak_file(to_name) {
return Err(FsError::Forbidden);
}
let to_parent = inner
.resolve_path(to_parent_path)
.ok_or(FsError::NotFound)?;
if !to_parent.is_dir {
return Err(FsError::Forbidden);
}
// Falls Zieldatei existiert und ein Verzeichnis ist: Fehler
if let Some(dest) = inner.resolve_path(&to_str) {
if dest.is_dir {
return Err(FsError::Forbidden);
}
for b in dest.blocks {
let _ = shred_carrier_block(
&inner.db,
inner.carrier_node_id,
b,
&inner.dek_outer,
inner.format_version,
);
inner.manifest.free_blocks.push(b);
}
inner.manifest.inodes.remove(&dest.id);
}
if inner.manifest.free_blocks.len() < node.blocks.len() {
return Err(FsError::InsufficientStorage);
}
let mut dest_blocks = Vec::with_capacity(node.blocks.len());
for &src_block_idx in &node.blocks {
let dest_block_idx = match inner.allocate_block() {
Ok(b) => b,
Err(e) => {
for b in dest_blocks {
let _ = inner.free_block(b);
}
return Err(e);
}
};
let data = match read_carrier_block(
&inner.db,
inner.carrier_node_id,
src_block_idx,
&inner.dek_outer,
&inner.dek_inner,
inner.format_version,
) {
Ok(d) => d,
Err(e) => {
error!("Fehler beim Lesen des Quell-Blocks während copy: {e}");
let _ = inner.free_block(dest_block_idx);
for b in dest_blocks {
let _ = inner.free_block(b);
}
return Err(FsError::GeneralFailure);
}
};
if let Err(e) = write_carrier_block(
&inner.db,
inner.carrier_node_id,
dest_block_idx,
&inner.dek_outer,
&inner.dek_inner,
&data,
inner.format_version,
) {
error!("Fehler beim Schreiben des Ziel-Blocks während copy: {e}");
let _ = inner.free_block(dest_block_idx);
for b in dest_blocks {
let _ = inner.free_block(b);
}
return Err(FsError::GeneralFailure);
}
dest_blocks.push(dest_block_idx);
}
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let new_id = inner.manifest.next_inode_id;
inner.manifest.next_inode_id += 1;
let new_inode = CarrierInode {
id: new_id,
parent_id: Some(to_parent.id),
name: to_name.to_string(),
is_dir: false,
size: node.size,
created_at: now,
modified_at: now,
blocks: dest_blocks,
};
inner.manifest.inodes.insert(new_id, new_inode);
inner.save_manifest().map_err(|_| FsError::GeneralFailure)?;
Ok(())
})
}
} }
/// Datei-Handle für Dateien innerhalb des Carrier-Dateisystems mit Streaming und Chunk-Pufferung. /// Datei-Handle für Dateien innerhalb des Carrier-Dateisystems mit Streaming und Chunk-Pufferung.
+8
View File
@@ -952,11 +952,19 @@ impl DavFileSystem for SanctumFs {
} }
fn copy<'a>(&'a self, from: &'a DavPath, to: &'a DavPath) -> FsFuture<'a, ()> { fn copy<'a>(&'a self, from: &'a DavPath, to: &'a DavPath) -> FsFuture<'a, ()> {
if let Some(ref cfs) = self.carrier_fs {
return cfs.copy(from, to);
}
Box::pin(async move { Box::pin(async move {
self.touch(); self.touch();
let from_str = Self::path_to_str(from); let from_str = Self::path_to_str(from);
let to_str = Self::path_to_str(to); let to_str = Self::path_to_str(to);
if from_str == to_str {
return Ok(());
}
let node = self.resolve_path(&from_str)?.ok_or(FsError::NotFound)?; let node = self.resolve_path(&from_str)?.ok_or(FsError::NotFound)?;
if node.is_dir { if node.is_dir {
+199
View File
@@ -575,3 +575,202 @@ async fn test_carrier_file_drop_and_append_mode() {
let _ = std::fs::remove_file(&path); let _ = std::fs::remove_file(&path);
} }
#[tokio::test]
async fn test_v01_carrier_fs_copy_delegation_and_functionality() {
let path = temp_db_path("v01_copy");
let carrier_size_bytes = 10 * 1024 * 1024; // 10 MB
let carrier_name = "carrier_for_copy.bin";
let pass_decoy = "DecoyPass2026!";
let pass_hidden = "HiddenPass2026!";
let kdf_params = KdfParams {
memory_cost: MIN_MEMORY_COST_KIB,
time_cost: MIN_TIME_COST,
parallelism: 1,
};
let salt_0 = generate_salt();
let kek_0 = derive_kek(pass_decoy, &salt_0, &kdf_params).unwrap();
let dek_0 = generate_dek();
let salt_1 = generate_salt();
let kek_1 = derive_kek(pass_hidden, &salt_1, &kdf_params).unwrap();
let dek_1 = generate_dek();
let carrier_node_id = 3i64;
let (wrapped_0, nonce_0, tag_0) = wrap_slot0_payload(&kek_0, &dek_0, carrier_node_id).unwrap();
let (wrapped_1, nonce_1, tag_1) =
wrap_slot1_payload(&kek_1, &dek_1, &dek_0, carrier_node_id).unwrap();
let db = Database::open(&path).expect("Open database");
db.init_schema_with_carrier(
&salt_0,
&kdf_params,
&wrapped_0,
&nonce_0,
&tag_0,
Some((
carrier_name,
carrier_size_bytes,
&salt_1,
&kdf_params,
&wrapped_1,
&nonce_1,
&tag_1,
&dek_0,
&dek_1,
)),
)
.expect("Init carrier schema");
db.checkpoint().unwrap();
let meta = db.read_meta().unwrap();
let auth_hidden = meta.authenticate(pass_hidden).expect("Auth hidden");
let hidden_fs = SanctumFs::with_carrier(
db.clone(),
auth_hidden.dek().clone(),
auth_hidden.carrier_dek(),
auth_hidden.carrier_node_id(),
auth_hidden.version(),
true,
1,
);
// Ordner anlegen
let folder = DavPath::new("/work").unwrap();
hidden_fs.create_dir(&folder).await.expect("Create folder");
// Datei anlegen und Inhalt schreiben
let src_path = DavPath::new("/work/original.txt").unwrap();
let test_data = b"V01 CarrierFs Copy Functionality Test Payload 2026";
let mut file = hidden_fs
.open(
&src_path,
OpenOptions {
create: true,
write: true,
..Default::default()
},
)
.await
.expect("Create source file");
file.write_bytes(Bytes::from_static(test_data))
.await
.expect("Write source data");
file.flush().await.expect("Flush source file");
drop(file);
// V-01: copy von /work/original.txt nach /work/copy.txt aufrufen
let dest_path = DavPath::new("/work/copy.txt").unwrap();
hidden_fs
.copy(&src_path, &dest_path)
.await
.expect("V-01: Copy must succeed on CarrierFs");
// Metadaten der Kopie prüfen
let copy_meta = hidden_fs
.metadata(&dest_path)
.await
.expect("Metadata of copied file");
assert_eq!(copy_meta.len(), test_data.len() as u64);
assert!(!copy_meta.is_dir());
// Inhalt der Kopie lesen und verifizieren
let mut read_copy = hidden_fs
.open(
&dest_path,
OpenOptions {
read: true,
..Default::default()
},
)
.await
.expect("Open copied file");
let copy_bytes = read_copy.read_bytes(1024).await.expect("Read copy bytes");
assert_eq!(&copy_bytes[..], test_data);
drop(read_copy);
// Quelldatei löschen: Die Kopie muss unabhängig erhalten bleiben!
hidden_fs
.remove_file(&src_path)
.await
.expect("Delete source file");
assert!(hidden_fs.metadata(&src_path).await.is_err());
let mut read_copy_again = hidden_fs
.open(
&dest_path,
OpenOptions {
read: true,
..Default::default()
},
)
.await
.expect("Open copied file after source delete");
let copy_bytes_2 = read_copy_again
.read_bytes(1024)
.await
.expect("Read copy bytes after source delete");
assert_eq!(&copy_bytes_2[..], test_data);
drop(read_copy_again);
// Overwrite-Copy testen
let src2_path = DavPath::new("/work/source2.txt").unwrap();
let test_data_2 = b"New Overwrite Payload Content";
let mut file2 = hidden_fs
.open(
&src2_path,
OpenOptions {
create: true,
write: true,
..Default::default()
},
)
.await
.expect("Create source2");
file2
.write_bytes(Bytes::from_static(test_data_2))
.await
.expect("Write source2");
file2.flush().await.expect("Flush source2");
drop(file2);
hidden_fs
.copy(&src2_path, &dest_path)
.await
.expect("Overwrite copy must succeed");
let mut read_overwritten = hidden_fs
.open(
&dest_path,
OpenOptions {
read: true,
..Default::default()
},
)
.await
.expect("Open overwritten copy");
let overwritten_bytes = read_overwritten
.read_bytes(1024)
.await
.expect("Read overwritten bytes");
assert_eq!(&overwritten_bytes[..], test_data_2);
drop(read_overwritten);
// Checkpoint & Verify
db.checkpoint().unwrap();
let report = verify_container(&path, Some(&dek_0), true).expect("Verify with DEK_0");
assert!(
report.is_healthy(),
"Container muss integer und gesund bleiben: {:?}",
report.errors
);
let _ = std::fs::remove_file(&path);
}