fix(vfs): V-06 — sparse writes zero-fill gaps between previous end of file and seek offset
This commit is contained in:
@@ -1057,6 +1057,34 @@ impl DavFile for CarrierFile {
|
|||||||
fn write_bytes(&mut self, buf: Bytes) -> FsFuture<'_, ()> {
|
fn write_bytes(&mut self, buf: Bytes) -> FsFuture<'_, ()> {
|
||||||
self.touch();
|
self.touch();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
|
// V-06: Sparse Writes — Lücke zwischen bisherigem Dateiende und Cursor mit Nullen füllen
|
||||||
|
if self.cursor > self.file_size {
|
||||||
|
let target = self.cursor;
|
||||||
|
while self.file_size < target {
|
||||||
|
let block_idx = (self.file_size / CARRIER_BLOCK_PAYLOAD_SIZE as u64) as usize;
|
||||||
|
let offset_in_block = (self.file_size % CARRIER_BLOCK_PAYLOAD_SIZE as u64) as usize;
|
||||||
|
let space_in_block = CARRIER_BLOCK_PAYLOAD_SIZE - offset_in_block;
|
||||||
|
let to_pad = ((target - self.file_size) as usize).min(space_in_block);
|
||||||
|
|
||||||
|
let block_data = self.ensure_block_loaded(block_idx)?;
|
||||||
|
if block_data.len() < offset_in_block + to_pad {
|
||||||
|
block_data.resize(offset_in_block + to_pad, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut is_full = false;
|
||||||
|
if let Some((_, ref d, ref mut dirty)) = self.cached_block {
|
||||||
|
*dirty = true;
|
||||||
|
is_full = d.len() >= CARRIER_BLOCK_PAYLOAD_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.file_size += to_pad as u64;
|
||||||
|
|
||||||
|
if is_full {
|
||||||
|
self.flush_cached_block()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut src = &buf[..];
|
let mut src = &buf[..];
|
||||||
|
|
||||||
while !src.is_empty() {
|
while !src.is_empty() {
|
||||||
|
|||||||
+80
@@ -396,6 +396,34 @@ impl DavFile for SanctumFile {
|
|||||||
fn write_bytes(&mut self, buf: Bytes) -> FsFuture<'_, ()> {
|
fn write_bytes(&mut self, buf: Bytes) -> FsFuture<'_, ()> {
|
||||||
self.touch();
|
self.touch();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
|
// V-06: Sparse Writes — Lücke zwischen bisherigem Dateiende und Cursor mit Nullen füllen
|
||||||
|
if self.cursor > self.file_size {
|
||||||
|
let target = self.cursor;
|
||||||
|
while self.file_size < target {
|
||||||
|
let chunk_idx = (self.file_size / CHUNK_SIZE as u64) as u32;
|
||||||
|
let offset_in_chunk = (self.file_size % CHUNK_SIZE as u64) as usize;
|
||||||
|
let space_in_chunk = CHUNK_SIZE - offset_in_chunk;
|
||||||
|
let to_pad = ((target - self.file_size) as usize).min(space_in_chunk);
|
||||||
|
|
||||||
|
let chunk_data = self.ensure_chunk_loaded(chunk_idx)?;
|
||||||
|
if chunk_data.len() < offset_in_chunk + to_pad {
|
||||||
|
chunk_data.resize(offset_in_chunk + to_pad, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut is_full = false;
|
||||||
|
if let Some((_, ref d, ref mut dirty)) = self.cached_chunk {
|
||||||
|
*dirty = true;
|
||||||
|
is_full = d.len() >= CHUNK_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.file_size += to_pad as u64;
|
||||||
|
|
||||||
|
if is_full {
|
||||||
|
self.flush_cached_chunk_and_size()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut src = &buf[..];
|
let mut src = &buf[..];
|
||||||
|
|
||||||
while !src.is_empty() {
|
while !src.is_empty() {
|
||||||
@@ -1488,4 +1516,56 @@ mod tests {
|
|||||||
let read_bytes = read_file.read_bytes(test_data.len()).await.unwrap();
|
let read_bytes = read_file.read_bytes(test_data.len()).await.unwrap();
|
||||||
assert_eq!(&read_bytes[..], test_data);
|
assert_eq!(&read_bytes[..], test_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_v06_sparse_write_zero_fills_gap() {
|
||||||
|
use std::io::SeekFrom;
|
||||||
|
|
||||||
|
let (fs, _dir) = create_test_fs(true);
|
||||||
|
let path = DavPath::new("/sparse_test.bin").unwrap();
|
||||||
|
|
||||||
|
let mut opts = OpenOptions::default();
|
||||||
|
opts.write = true;
|
||||||
|
opts.create_new = true;
|
||||||
|
let mut file = fs.open(&path, opts).await.unwrap();
|
||||||
|
|
||||||
|
// 1. Schreibe 5 Bytes am Anfang (Chunk 0)
|
||||||
|
file.write_bytes(Bytes::copy_from_slice(b"START"))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// 2. Springe weit über das Dateiende hinaus in Chunk 2 (2,5 MB)
|
||||||
|
let seek_pos = 2 * CHUNK_SIZE as u64 + 500;
|
||||||
|
file.seek(SeekFrom::Start(seek_pos)).await.unwrap();
|
||||||
|
|
||||||
|
// 3. Schreibe 3 Bytes an Position seek_pos
|
||||||
|
file.write_bytes(Bytes::copy_from_slice(b"END"))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
file.flush().await.unwrap();
|
||||||
|
drop(file);
|
||||||
|
|
||||||
|
// 4. Verifiziere Dateigröße
|
||||||
|
let node = fs.resolve_path("/sparse_test.bin").unwrap().unwrap();
|
||||||
|
assert_eq!(node.size, seek_pos + 3);
|
||||||
|
|
||||||
|
// 5. Lese Datei vollständig ein und prüfe Nullen in der Lücke
|
||||||
|
let mut read_opts = OpenOptions::default();
|
||||||
|
read_opts.read = true;
|
||||||
|
let mut read_file = fs.open(&path, read_opts).await.unwrap();
|
||||||
|
|
||||||
|
// Anfang lesen
|
||||||
|
let start_bytes = read_file.read_bytes(5).await.unwrap();
|
||||||
|
assert_eq!(&start_bytes[..], b"START");
|
||||||
|
|
||||||
|
// Lücke in Chunk 0 und 1 muss mit Nullen gefüllt sein
|
||||||
|
let zero_gap = read_file.read_bytes(1000).await.unwrap();
|
||||||
|
assert_eq!(zero_gap.len(), 1000);
|
||||||
|
assert!(zero_gap.iter().all(|&b| b == 0));
|
||||||
|
|
||||||
|
// Springe zum Ende und lies die geschriebenen Daten
|
||||||
|
read_file.seek(SeekFrom::Start(seek_pos)).await.unwrap();
|
||||||
|
let end_bytes = read_file.read_bytes(3).await.unwrap();
|
||||||
|
assert_eq!(&end_bytes[..], b"END");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user