1266 lines
41 KiB
Rust
1266 lines
41 KiB
Rust
use std::collections::HashMap;
|
|
use std::fmt::Debug;
|
|
use std::io::SeekFrom;
|
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
use std::sync::{Arc, Mutex};
|
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
|
|
|
use anyhow::{bail, Result};
|
|
use bytes::{Buf, Bytes, BytesMut};
|
|
use dav_server::{
|
|
davpath::DavPath,
|
|
fs::{
|
|
DavDirEntry, DavFile, DavFileSystem, DavMetaData, FsError, FsFuture, FsStream, OpenOptions,
|
|
ReadDirMeta,
|
|
},
|
|
};
|
|
use futures_util::stream;
|
|
use rand::rngs::OsRng;
|
|
use rand::RngCore;
|
|
use serde::{Deserialize, Serialize};
|
|
use tracing::{error, warn};
|
|
use zeroize::{Zeroize, Zeroizing};
|
|
|
|
use crate::crypto::{decrypt_chunk, encrypt_chunk, CHUNK_SIZE};
|
|
use crate::storage::Database;
|
|
use crate::vfs::{is_leak_file_with_custom, SanctumDirEntry, SanctumMetaData};
|
|
|
|
pub const CARRIER_MAGIC: &[u8; 8] = b"SANCTCAR";
|
|
pub const CARRIER_VERSION: u32 = 1;
|
|
|
|
/// Manifest für das steganografische Dateisystem innerhalb des Alibi-Carriers (Block 0).
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct CarrierManifest {
|
|
pub magic: [u8; 8],
|
|
pub version: u32,
|
|
pub total_blocks: u32,
|
|
pub free_blocks: Vec<u32>,
|
|
pub next_inode_id: i64,
|
|
pub inodes: HashMap<i64, CarrierInode>,
|
|
}
|
|
|
|
impl CarrierManifest {
|
|
pub fn new(total_blocks: u32) -> Self {
|
|
let mut inodes = HashMap::new();
|
|
let now = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.map(|d| d.as_secs())
|
|
.unwrap_or(0);
|
|
|
|
// Inode 1 ist das Wurzelverzeichnis '/'
|
|
inodes.insert(
|
|
1,
|
|
CarrierInode {
|
|
id: 1,
|
|
parent_id: None,
|
|
name: String::new(),
|
|
is_dir: true,
|
|
size: 0,
|
|
created_at: now,
|
|
modified_at: now,
|
|
blocks: Vec::new(),
|
|
},
|
|
);
|
|
|
|
// Block 0 ist für das Manifest reserviert. Nutzblöcke sind 1..total_blocks-1.
|
|
let free_blocks = (1..total_blocks).collect();
|
|
|
|
Self {
|
|
magic: *CARRIER_MAGIC,
|
|
version: CARRIER_VERSION,
|
|
total_blocks,
|
|
free_blocks,
|
|
next_inode_id: 2,
|
|
inodes,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Inode-Eintrag für Dateien und Verzeichnisse im Carrier-Dateisystem.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct CarrierInode {
|
|
pub id: i64,
|
|
pub parent_id: Option<i64>,
|
|
pub name: String,
|
|
pub is_dir: bool,
|
|
pub size: u64,
|
|
pub created_at: u64,
|
|
pub modified_at: u64,
|
|
pub blocks: Vec<u32>, // Indizes der 1-MB-Blöcke innerhalb der Trägerdatei (1..total_blocks-1)
|
|
}
|
|
|
|
pub const CARRIER_BLOCK_PAYLOAD_SIZE: usize = CHUNK_SIZE - 64;
|
|
|
|
/// Liest einen Carrier-Block mit Zwei-Schichten-AEAD:
|
|
/// 1. Äußere Entschlüsselung mit dek_outer (DEK_0)
|
|
/// 2. Innere Entschlüsselung mit dek_inner (DEK_1)
|
|
pub fn read_carrier_block(
|
|
db: &Database,
|
|
carrier_node_id: i64,
|
|
block_idx: u32,
|
|
dek_outer: &[u8; 32],
|
|
dek_inner: &[u8; 32],
|
|
format_version: u32,
|
|
) -> Result<Vec<u8>> {
|
|
let chunk_rec = db
|
|
.read_chunk(carrier_node_id, block_idx)?
|
|
.ok_or_else(|| anyhow::anyhow!("Carrier-Chunk #{} nicht gefunden", block_idx))?;
|
|
|
|
// 1. Äußere Schicht entschlüsseln (mit dek_outer = DEK_0)
|
|
let outer_decrypted = decrypt_chunk(
|
|
dek_outer,
|
|
carrier_node_id,
|
|
block_idx,
|
|
&chunk_rec.ciphertext,
|
|
&chunk_rec.nonce,
|
|
&chunk_rec.tag,
|
|
format_version,
|
|
chunk_rec.generation,
|
|
)?;
|
|
|
|
// Das outer_decrypted enthält: inner_nonce (12B) || inner_tag (16B) || inner_ct_len (4B LE) || inner_ct || CSPRNG-Padding
|
|
if outer_decrypted.len() < 32 {
|
|
bail!(
|
|
"Carrier-Block #{}: Äußere Nutzdaten zu kurz (< 32 Bytes)",
|
|
block_idx
|
|
);
|
|
}
|
|
|
|
let mut inner_nonce = [0u8; 12];
|
|
inner_nonce.copy_from_slice(&outer_decrypted[0..12]);
|
|
let mut inner_tag = [0u8; 16];
|
|
inner_tag.copy_from_slice(&outer_decrypted[12..28]);
|
|
let inner_ct_len = u32::from_le_bytes(outer_decrypted[28..32].try_into().unwrap()) as usize;
|
|
if 32 + inner_ct_len > outer_decrypted.len() {
|
|
bail!(
|
|
"Carrier-Block #{}: Ungültige innere Ciphertext-Länge {} (verfügbar: {})",
|
|
block_idx,
|
|
inner_ct_len,
|
|
outer_decrypted.len() - 32
|
|
);
|
|
}
|
|
let inner_ciphertext = &outer_decrypted[32..32 + inner_ct_len];
|
|
|
|
// 2. Innere Schicht entschlüsseln (mit dek_inner = DEK_1)
|
|
let inner_plaintext = decrypt_chunk(
|
|
dek_inner,
|
|
carrier_node_id,
|
|
block_idx,
|
|
inner_ciphertext,
|
|
&inner_nonce,
|
|
&inner_tag,
|
|
format_version,
|
|
chunk_rec.generation,
|
|
)?;
|
|
|
|
Ok(inner_plaintext)
|
|
}
|
|
|
|
/// Schreibt einen Carrier-Block mit Zwei-Schichten-AEAD:
|
|
/// 1. Innere Verschlüsselung mit dek_inner (DEK_1)
|
|
/// 2. Auffüllen auf exakt CHUNK_SIZE mit CSPRNG-Zufallsdaten
|
|
/// 3. Äußere Verschlüsselung mit dek_outer (DEK_0)
|
|
pub fn write_carrier_block(
|
|
db: &Database,
|
|
carrier_node_id: i64,
|
|
block_idx: u32,
|
|
dek_outer: &[u8; 32],
|
|
dek_inner: &[u8; 32],
|
|
plaintext: &[u8],
|
|
format_version: u32,
|
|
) -> Result<()> {
|
|
let gen = 0u64;
|
|
|
|
// 1. Innere Schicht verschlüsseln (mit dek_inner = DEK_1)
|
|
let (inner_ct, inner_nonce, inner_tag) = encrypt_chunk(
|
|
dek_inner,
|
|
carrier_node_id,
|
|
block_idx,
|
|
plaintext,
|
|
format_version,
|
|
gen,
|
|
)?;
|
|
|
|
let inner_ct_len = inner_ct.len() as u32;
|
|
|
|
// 2. Äußere Nutzlast vorbereiten: Exakt CHUNK_SIZE (1 MB)
|
|
// Format: inner_nonce (12B) || inner_tag (16B) || inner_ct_len (4B LE) || inner_ct || CSPRNG-Padding
|
|
let mut outer_plaintext = vec![0u8; CHUNK_SIZE];
|
|
|
|
outer_plaintext[0..12].copy_from_slice(&inner_nonce);
|
|
outer_plaintext[12..28].copy_from_slice(&inner_tag);
|
|
outer_plaintext[28..32].copy_from_slice(&inner_ct_len.to_le_bytes());
|
|
let ct_end = 32 + inner_ct.len();
|
|
if ct_end > CHUNK_SIZE {
|
|
bail!(
|
|
"Carrier-Block #{}: Payload überschreitet Blockgröße ({} > {})",
|
|
block_idx,
|
|
ct_end,
|
|
CHUNK_SIZE
|
|
);
|
|
}
|
|
outer_plaintext[32..ct_end].copy_from_slice(&inner_ct);
|
|
|
|
// Nur den ungenutzten Slack-Space mit kryptografischem Zufall auffüllen (~30x schneller als 1MB OsRng)
|
|
if ct_end < CHUNK_SIZE {
|
|
OsRng.fill_bytes(&mut outer_plaintext[ct_end..]);
|
|
}
|
|
|
|
// 3. Äußere Schicht verschlüsseln (mit dek_outer = DEK_0)
|
|
let (outer_ct, outer_nonce, outer_tag) = encrypt_chunk(
|
|
dek_outer,
|
|
carrier_node_id,
|
|
block_idx,
|
|
&outer_plaintext,
|
|
format_version,
|
|
gen,
|
|
)?;
|
|
|
|
// 4. In SQLite schreiben (in-place Überschreiben des bestehenden Chunks)
|
|
db.write_chunk(
|
|
carrier_node_id,
|
|
block_idx,
|
|
gen,
|
|
&outer_nonce,
|
|
&outer_tag,
|
|
&outer_ct,
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Überschreibt einen freigegebenen Carrier-Block mit frischem kryptografischen Rauschen
|
|
/// (unter dek_outer verschlüsselt), sodass er weiterhin als 100% gültiger DEK_0-Chunk authentifiziert.
|
|
pub fn shred_carrier_block(
|
|
db: &Database,
|
|
carrier_node_id: i64,
|
|
block_idx: u32,
|
|
dek_outer: &[u8; 32],
|
|
format_version: u32,
|
|
) -> Result<()> {
|
|
let mut noise = vec![0u8; CHUNK_SIZE];
|
|
OsRng.fill_bytes(&mut noise);
|
|
|
|
let gen = 0u64;
|
|
let (outer_ct, outer_nonce, outer_tag) = encrypt_chunk(
|
|
dek_outer,
|
|
carrier_node_id,
|
|
block_idx,
|
|
&noise,
|
|
format_version,
|
|
gen,
|
|
)?;
|
|
|
|
db.write_chunk(
|
|
carrier_node_id,
|
|
block_idx,
|
|
gen,
|
|
&outer_nonce,
|
|
&outer_tag,
|
|
&outer_ct,
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Interner Zustand des Carrier-Dateisystems.
|
|
pub struct CarrierFsInner {
|
|
pub db: Database,
|
|
pub carrier_node_id: i64,
|
|
pub dek_outer: Arc<Zeroizing<[u8; 32]>>,
|
|
pub dek_inner: Arc<Zeroizing<[u8; 32]>>,
|
|
pub format_version: u32,
|
|
pub anti_leak: bool,
|
|
pub custom_leak_rules: Arc<Vec<String>>,
|
|
pub leak_counter: Arc<AtomicU64>,
|
|
pub manifest: CarrierManifest,
|
|
pub last_activity: Arc<AtomicU64>,
|
|
}
|
|
|
|
impl CarrierFsInner {
|
|
pub fn is_leak(&self, name: &str) -> bool {
|
|
self.anti_leak && is_leak_file_with_custom(name, &self.custom_leak_rules)
|
|
}
|
|
pub fn save_manifest(&mut self) -> Result<()> {
|
|
let manifest_bytes = serde_json::to_vec(&self.manifest)?;
|
|
if manifest_bytes.len() > (crate::crypto::CHUNK_SIZE - 64) {
|
|
bail!(
|
|
"Carrier-Manifest überschreitet die maximale Blockgröße ({} > {} Bytes).",
|
|
manifest_bytes.len(),
|
|
crate::crypto::CHUNK_SIZE - 64
|
|
);
|
|
}
|
|
if manifest_bytes.len() > 800_000 {
|
|
warn!(
|
|
"Carrier-Manifest erreicht 80% der Blockkapazität ({}/{} Bytes)",
|
|
manifest_bytes.len(),
|
|
crate::crypto::CHUNK_SIZE - 64
|
|
);
|
|
}
|
|
write_carrier_block(
|
|
&self.db,
|
|
self.carrier_node_id,
|
|
0,
|
|
&self.dek_outer,
|
|
&self.dek_inner,
|
|
&manifest_bytes,
|
|
self.format_version,
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn allocate_block(&mut self) -> Result<u32, FsError> {
|
|
self.manifest
|
|
.free_blocks
|
|
.pop()
|
|
.ok_or(FsError::InsufficientStorage)
|
|
}
|
|
|
|
pub fn free_block(&mut self, block_idx: u32) -> Result<(), FsError> {
|
|
let _ = shred_carrier_block(
|
|
&self.db,
|
|
self.carrier_node_id,
|
|
block_idx,
|
|
&self.dek_outer,
|
|
self.format_version,
|
|
);
|
|
self.manifest.free_blocks.push(block_idx);
|
|
Ok(())
|
|
}
|
|
|
|
pub fn resolve_path(&self, raw_path: &str) -> Option<CarrierInode> {
|
|
let trimmed = raw_path.trim_matches('/');
|
|
if trimmed.is_empty() {
|
|
return self.manifest.inodes.get(&1).cloned();
|
|
}
|
|
|
|
let segments: Vec<&str> = trimmed.split('/').filter(|s| !s.is_empty()).collect();
|
|
let mut current_id = 1i64;
|
|
|
|
for (idx, segment) in segments.iter().enumerate() {
|
|
let child = self.manifest.inodes.values().find(|inode| {
|
|
inode.parent_id == Some(current_id) && inode.name.as_str() == *segment
|
|
})?;
|
|
|
|
if idx + 1 < segments.len() && !child.is_dir {
|
|
return None;
|
|
}
|
|
current_id = child.id;
|
|
}
|
|
|
|
self.manifest.inodes.get(¤t_id).cloned()
|
|
}
|
|
|
|
pub fn split_parent_and_name<'a>(&self, path: &'a str) -> (&'a str, &'a str) {
|
|
let trimmed = path.trim_matches('/');
|
|
match trimmed.rfind('/') {
|
|
Some(pos) => (&trimmed[..pos], &trimmed[pos + 1..]),
|
|
None => ("", trimmed),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Drop for CarrierFsInner {
|
|
fn drop(&mut self) {
|
|
crate::windows::unlock_memory(self.dek_outer.as_ptr(), 32);
|
|
crate::windows::unlock_memory(self.dek_inner.as_ptr(), 32);
|
|
}
|
|
}
|
|
|
|
/// WebDAV-Filesystem-Treiber für den steganografischen Alibi-Carrier (Hidden Vault).
|
|
#[derive(Clone)]
|
|
pub struct CarrierFs {
|
|
inner: Arc<Mutex<CarrierFsInner>>,
|
|
}
|
|
|
|
impl CarrierFs {
|
|
/// Lädt ein bestehendes Carrier-Dateisystem mit benutzerdefinierten Anti-Leak-Regeln (V-04).
|
|
pub fn load_with_leak_rules(
|
|
db: Database,
|
|
carrier_node_id: i64,
|
|
dek_outer: Arc<Zeroizing<[u8; 32]>>,
|
|
dek_inner: Arc<Zeroizing<[u8; 32]>>,
|
|
format_version: u32,
|
|
anti_leak: bool,
|
|
custom_leak_rules: Vec<String>,
|
|
) -> Result<Self> {
|
|
let manifest_bytes = read_carrier_block(
|
|
&db,
|
|
carrier_node_id,
|
|
0,
|
|
&dek_outer,
|
|
&dek_inner,
|
|
format_version,
|
|
)?;
|
|
|
|
let manifest: CarrierManifest = serde_json::from_slice(&manifest_bytes)?;
|
|
if manifest.magic != *CARRIER_MAGIC {
|
|
bail!("Ungültige Carrier-Magic-Bytes in Block 0");
|
|
}
|
|
|
|
let now = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.map(|d| d.as_secs())
|
|
.unwrap_or(0);
|
|
|
|
// Forensischer RAM-Paging-Schutz via VirtualLock
|
|
crate::windows::lock_memory(dek_outer.as_ptr(), 32);
|
|
crate::windows::lock_memory(dek_inner.as_ptr(), 32);
|
|
|
|
let inner = CarrierFsInner {
|
|
db,
|
|
carrier_node_id,
|
|
dek_outer,
|
|
dek_inner,
|
|
format_version,
|
|
anti_leak,
|
|
custom_leak_rules: Arc::new(custom_leak_rules),
|
|
leak_counter: Arc::new(AtomicU64::new(0)),
|
|
manifest,
|
|
last_activity: Arc::new(AtomicU64::new(now)),
|
|
};
|
|
|
|
Ok(Self {
|
|
inner: Arc::new(Mutex::new(inner)),
|
|
})
|
|
}
|
|
|
|
/// Lädt ein bestehendes Carrier-Dateisystem aus Block 0 der Trägerdatei.
|
|
pub fn load(
|
|
db: Database,
|
|
carrier_node_id: i64,
|
|
dek_outer: Arc<Zeroizing<[u8; 32]>>,
|
|
dek_inner: Arc<Zeroizing<[u8; 32]>>,
|
|
format_version: u32,
|
|
anti_leak: bool,
|
|
) -> Result<Self> {
|
|
Self::load_with_leak_rules(
|
|
db,
|
|
carrier_node_id,
|
|
dek_outer,
|
|
dek_inner,
|
|
format_version,
|
|
anti_leak,
|
|
Vec::new(),
|
|
)
|
|
}
|
|
|
|
pub fn leak_counter(&self) -> Arc<AtomicU64> {
|
|
let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
|
|
inner.leak_counter.clone()
|
|
}
|
|
|
|
pub fn last_activity(&self) -> Arc<AtomicU64> {
|
|
let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
|
|
inner.last_activity.clone()
|
|
}
|
|
|
|
pub fn touch(&self) {
|
|
let now = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.map(|d| d.as_secs())
|
|
.unwrap_or(0);
|
|
let inner = self.inner.lock().unwrap();
|
|
inner.last_activity.store(now, Ordering::Relaxed);
|
|
}
|
|
|
|
fn path_to_str(path: &DavPath) -> String {
|
|
String::from_utf8_lossy(path.as_bytes()).to_string()
|
|
}
|
|
}
|
|
|
|
impl DavFileSystem for CarrierFs {
|
|
fn open<'a>(
|
|
&'a self,
|
|
path: &'a DavPath,
|
|
options: OpenOptions,
|
|
) -> FsFuture<'a, Box<dyn DavFile>> {
|
|
Box::pin(async move {
|
|
self.touch();
|
|
let path_str = Self::path_to_str(path);
|
|
|
|
let mut inner = self.inner.lock().unwrap();
|
|
let (parent_path, file_name) = inner.split_parent_and_name(&path_str);
|
|
|
|
if inner.is_leak(file_name) {
|
|
if options.create
|
|
|| options.create_new
|
|
|| options.write
|
|
|| options.append
|
|
|| options.truncate
|
|
{
|
|
inner.leak_counter.fetch_add(1, Ordering::Relaxed);
|
|
return Err(FsError::Forbidden);
|
|
}
|
|
}
|
|
|
|
let existing_node = inner.resolve_path(&path_str);
|
|
|
|
let inode = match existing_node {
|
|
Some(mut n) => {
|
|
if n.is_dir && (options.write || options.append) {
|
|
return Err(FsError::Forbidden);
|
|
}
|
|
if options.create_new {
|
|
return Err(FsError::Exists);
|
|
}
|
|
|
|
if options.truncate {
|
|
for b in n.blocks.drain(..) {
|
|
let _ = shred_carrier_block(
|
|
&inner.db,
|
|
inner.carrier_node_id,
|
|
b,
|
|
&inner.dek_outer,
|
|
inner.format_version,
|
|
);
|
|
inner.manifest.free_blocks.push(b);
|
|
}
|
|
let now = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.map(|d| d.as_secs())
|
|
.unwrap_or(0);
|
|
n.size = 0;
|
|
n.modified_at = now;
|
|
inner.manifest.inodes.insert(n.id, n.clone());
|
|
let _ = inner.save_manifest();
|
|
}
|
|
n
|
|
}
|
|
None => {
|
|
if options.create || options.create_new {
|
|
let parent = inner.resolve_path(parent_path).ok_or(FsError::NotFound)?;
|
|
if !parent.is_dir {
|
|
return Err(FsError::Forbidden);
|
|
}
|
|
|
|
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(parent.id),
|
|
name: file_name.to_string(),
|
|
is_dir: false,
|
|
size: 0,
|
|
created_at: now,
|
|
modified_at: now,
|
|
blocks: Vec::new(),
|
|
};
|
|
|
|
inner.manifest.inodes.insert(new_id, new_inode.clone());
|
|
let _ = inner.save_manifest();
|
|
new_inode
|
|
} else {
|
|
return Err(FsError::NotFound);
|
|
}
|
|
}
|
|
};
|
|
|
|
drop(inner);
|
|
|
|
let file = CarrierFile::new(inode, self.inner.clone(), options.append);
|
|
Ok(Box::new(file) as Box<dyn DavFile>)
|
|
})
|
|
}
|
|
|
|
fn read_dir<'a>(
|
|
&'a self,
|
|
path: &'a DavPath,
|
|
_meta: ReadDirMeta,
|
|
) -> FsFuture<'a, FsStream<Box<dyn DavDirEntry>>> {
|
|
Box::pin(async move {
|
|
let path_str = Self::path_to_str(path);
|
|
let inner = self.inner.lock().unwrap();
|
|
|
|
let node = inner.resolve_path(&path_str).ok_or(FsError::NotFound)?;
|
|
if !node.is_dir {
|
|
return Err(FsError::Forbidden);
|
|
}
|
|
|
|
let entries: Vec<Result<Box<dyn DavDirEntry>, FsError>> = inner
|
|
.manifest
|
|
.inodes
|
|
.values()
|
|
.filter(|child| child.parent_id == Some(node.id))
|
|
.filter(|child| {
|
|
if inner.is_leak(&child.name) {
|
|
inner.leak_counter.fetch_add(1, Ordering::Relaxed);
|
|
false
|
|
} else {
|
|
true
|
|
}
|
|
})
|
|
.map(|child| {
|
|
Ok(Box::new(SanctumDirEntry {
|
|
name: child.name.clone(),
|
|
meta: SanctumMetaData {
|
|
is_dir: child.is_dir,
|
|
size: child.size,
|
|
created_at: UNIX_EPOCH + Duration::from_secs(child.created_at),
|
|
modified_at: UNIX_EPOCH + Duration::from_secs(child.modified_at),
|
|
},
|
|
}) as Box<dyn DavDirEntry>)
|
|
})
|
|
.collect();
|
|
|
|
Ok(Box::pin(stream::iter(entries)) as FsStream<Box<dyn DavDirEntry>>)
|
|
})
|
|
}
|
|
|
|
fn metadata<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, Box<dyn DavMetaData>> {
|
|
Box::pin(async move {
|
|
let path_str = Self::path_to_str(path);
|
|
|
|
let inner = self.inner.lock().unwrap();
|
|
let node = inner.resolve_path(&path_str).ok_or(FsError::NotFound)?;
|
|
|
|
let meta = SanctumMetaData {
|
|
is_dir: node.is_dir,
|
|
size: node.size,
|
|
created_at: UNIX_EPOCH + Duration::from_secs(node.created_at),
|
|
modified_at: UNIX_EPOCH + Duration::from_secs(node.modified_at),
|
|
};
|
|
|
|
Ok(Box::new(meta) as Box<dyn DavMetaData>)
|
|
})
|
|
}
|
|
|
|
fn symlink_metadata<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, Box<dyn DavMetaData>> {
|
|
self.metadata(path)
|
|
}
|
|
|
|
fn create_dir<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, ()> {
|
|
Box::pin(async move {
|
|
self.touch();
|
|
let path_str = Self::path_to_str(path);
|
|
let mut inner = self.inner.lock().unwrap();
|
|
let (parent_path, dir_name) = inner.split_parent_and_name(&path_str);
|
|
|
|
if inner.is_leak(dir_name) {
|
|
inner.leak_counter.fetch_add(1, Ordering::Relaxed);
|
|
return Err(FsError::Forbidden);
|
|
}
|
|
|
|
if inner.resolve_path(&path_str).is_some() {
|
|
return Err(FsError::Exists);
|
|
}
|
|
|
|
let parent = inner.resolve_path(parent_path).ok_or(FsError::NotFound)?;
|
|
if !parent.is_dir {
|
|
return Err(FsError::Forbidden);
|
|
}
|
|
|
|
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_dir = CarrierInode {
|
|
id: new_id,
|
|
parent_id: Some(parent.id),
|
|
name: dir_name.to_string(),
|
|
is_dir: true,
|
|
size: 0,
|
|
created_at: now,
|
|
modified_at: now,
|
|
blocks: Vec::new(),
|
|
};
|
|
|
|
inner.manifest.inodes.insert(new_id, new_dir);
|
|
inner.save_manifest().map_err(|_| FsError::GeneralFailure)?;
|
|
|
|
Ok(())
|
|
})
|
|
}
|
|
|
|
fn remove_dir<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, ()> {
|
|
Box::pin(async move {
|
|
self.touch();
|
|
let path_str = Self::path_to_str(path);
|
|
let mut inner = self.inner.lock().unwrap();
|
|
|
|
let node = inner.resolve_path(&path_str).ok_or(FsError::NotFound)?;
|
|
if !node.is_dir {
|
|
return Err(FsError::Forbidden);
|
|
}
|
|
if node.id == 1 {
|
|
// Wurzelknoten darf nicht gelöscht werden
|
|
return Err(FsError::Forbidden);
|
|
}
|
|
|
|
// Prüfe, ob das Verzeichnis leer ist
|
|
let has_children = inner
|
|
.manifest
|
|
.inodes
|
|
.values()
|
|
.any(|child| child.parent_id == Some(node.id));
|
|
|
|
if has_children {
|
|
return Err(FsError::Forbidden);
|
|
}
|
|
|
|
inner.manifest.inodes.remove(&node.id);
|
|
inner.save_manifest().map_err(|_| FsError::GeneralFailure)?;
|
|
|
|
Ok(())
|
|
})
|
|
}
|
|
|
|
fn remove_file<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, ()> {
|
|
Box::pin(async move {
|
|
self.touch();
|
|
let path_str = Self::path_to_str(path);
|
|
let mut inner = self.inner.lock().unwrap();
|
|
|
|
let node = inner.resolve_path(&path_str).ok_or(FsError::NotFound)?;
|
|
if node.is_dir {
|
|
return Err(FsError::Forbidden);
|
|
}
|
|
|
|
for b in node.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(&node.id);
|
|
inner.save_manifest().map_err(|_| FsError::GeneralFailure)?;
|
|
|
|
Ok(())
|
|
})
|
|
}
|
|
|
|
fn rename<'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);
|
|
|
|
let mut inner = self.inner.lock().unwrap();
|
|
let node = inner.resolve_path(&from_str).ok_or(FsError::NotFound)?;
|
|
|
|
let (to_parent_path, to_name) = inner.split_parent_and_name(&to_str);
|
|
if inner.is_leak(to_name) {
|
|
inner.leak_counter.fetch_add(1, Ordering::Relaxed);
|
|
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 kein Verzeichnis ist: überschreiben
|
|
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);
|
|
}
|
|
|
|
let now = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.map(|d| d.as_secs())
|
|
.unwrap_or(0);
|
|
|
|
if let Some(inode) = inner.manifest.inodes.get_mut(&node.id) {
|
|
inode.parent_id = Some(to_parent.id);
|
|
inode.name = to_name.to_string();
|
|
inode.modified_at = now;
|
|
}
|
|
|
|
inner.save_manifest().map_err(|_| FsError::GeneralFailure)?;
|
|
|
|
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.is_leak(to_name) {
|
|
inner.leak_counter.fetch_add(1, Ordering::Relaxed);
|
|
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(())
|
|
})
|
|
}
|
|
|
|
fn get_quota(&self) -> FsFuture<'_, (u64, Option<u64>)> {
|
|
Box::pin(async move {
|
|
self.touch();
|
|
let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
|
|
let total_capacity = inner.manifest.total_blocks as u64 * crate::crypto::CHUNK_SIZE as u64;
|
|
let used_bytes = inner
|
|
.manifest
|
|
.inodes
|
|
.values()
|
|
.filter(|i| !i.is_dir)
|
|
.map(|i| i.size)
|
|
.sum::<u64>();
|
|
Ok((used_bytes, Some(total_capacity)))
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Datei-Handle für Dateien innerhalb des Carrier-Dateisystems mit Streaming und Chunk-Pufferung.
|
|
pub struct CarrierFile {
|
|
inode_id: i64,
|
|
file_size: u64,
|
|
cursor: u64,
|
|
blocks: Vec<u32>,
|
|
inner_fs: Arc<Mutex<CarrierFsInner>>,
|
|
meta: SanctumMetaData,
|
|
// (block_index_in_file, decrypted_payload, is_dirty)
|
|
cached_block: Option<(usize, Vec<u8>, bool)>,
|
|
}
|
|
|
|
impl Debug for CarrierFile {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.debug_struct("CarrierFile")
|
|
.field("inode_id", &self.inode_id)
|
|
.field("file_size", &self.file_size)
|
|
.field("cursor", &self.cursor)
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
impl CarrierFile {
|
|
pub fn new(inode: CarrierInode, inner_fs: Arc<Mutex<CarrierFsInner>>, append: bool) -> Self {
|
|
let meta = SanctumMetaData {
|
|
is_dir: inode.is_dir,
|
|
size: inode.size,
|
|
created_at: UNIX_EPOCH + Duration::from_secs(inode.created_at),
|
|
modified_at: UNIX_EPOCH + Duration::from_secs(inode.modified_at),
|
|
};
|
|
|
|
let cursor = if append { inode.size } else { 0 };
|
|
|
|
Self {
|
|
inode_id: inode.id,
|
|
file_size: inode.size,
|
|
cursor,
|
|
blocks: inode.blocks,
|
|
inner_fs,
|
|
meta,
|
|
cached_block: None,
|
|
}
|
|
}
|
|
|
|
fn touch(&self) {
|
|
let now = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.map(|d| d.as_secs())
|
|
.unwrap_or(0);
|
|
let inner = self.inner_fs.lock().unwrap();
|
|
inner.last_activity.store(now, Ordering::Relaxed);
|
|
}
|
|
|
|
fn flush_cached_block(&mut self) -> Result<(), FsError> {
|
|
if let Some((block_idx_in_file, ref data, true)) = self.cached_block {
|
|
let mut inner = self.inner_fs.lock().unwrap();
|
|
|
|
// Allokiere Blöcke bis zum aktuellen Index falls nötig
|
|
while self.blocks.len() <= block_idx_in_file {
|
|
let new_block = inner.allocate_block()?;
|
|
self.blocks.push(new_block);
|
|
}
|
|
|
|
let carrier_block_idx = self.blocks[block_idx_in_file];
|
|
write_carrier_block(
|
|
&inner.db,
|
|
inner.carrier_node_id,
|
|
carrier_block_idx,
|
|
&inner.dek_outer,
|
|
&inner.dek_inner,
|
|
data,
|
|
inner.format_version,
|
|
)
|
|
.map_err(|e| {
|
|
error!("Fehler beim Schreiben des Carrier-Blocks: {e}");
|
|
FsError::GeneralFailure
|
|
})?;
|
|
|
|
if let Some((_, _, ref mut dirty)) = self.cached_block {
|
|
*dirty = false;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn ensure_block_loaded(&mut self, block_idx_in_file: usize) -> Result<&mut Vec<u8>, FsError> {
|
|
if let Some((cached_idx, _, _)) = self.cached_block {
|
|
if cached_idx == block_idx_in_file {
|
|
return Ok(&mut self.cached_block.as_mut().unwrap().1);
|
|
}
|
|
}
|
|
|
|
self.flush_cached_block()?;
|
|
if let Some((_, ref mut data, _)) = self.cached_block {
|
|
data.zeroize();
|
|
}
|
|
|
|
let data = if block_idx_in_file < self.blocks.len() {
|
|
let carrier_block_idx = self.blocks[block_idx_in_file];
|
|
let inner = self.inner_fs.lock().unwrap();
|
|
match read_carrier_block(
|
|
&inner.db,
|
|
inner.carrier_node_id,
|
|
carrier_block_idx,
|
|
&inner.dek_outer,
|
|
&inner.dek_inner,
|
|
inner.format_version,
|
|
) {
|
|
Ok(bytes) => bytes,
|
|
Err(e) => {
|
|
error!("Fehler beim Lesen des Carrier-Blocks: {e}");
|
|
return Err(FsError::GeneralFailure);
|
|
}
|
|
}
|
|
} else {
|
|
Vec::new()
|
|
};
|
|
|
|
self.cached_block = Some((block_idx_in_file, data, false));
|
|
Ok(&mut self.cached_block.as_mut().unwrap().1)
|
|
}
|
|
}
|
|
|
|
impl DavFile for CarrierFile {
|
|
fn metadata(&mut self) -> FsFuture<'_, Box<dyn DavMetaData>> {
|
|
let meta = self.meta.clone();
|
|
Box::pin(async move { Ok(Box::new(meta) as Box<dyn DavMetaData>) })
|
|
}
|
|
|
|
fn read_bytes(&mut self, count: usize) -> FsFuture<'_, Bytes> {
|
|
self.touch();
|
|
Box::pin(async move {
|
|
if self.cursor >= self.file_size {
|
|
return Ok(Bytes::new());
|
|
}
|
|
|
|
let available = (self.file_size - self.cursor) as usize;
|
|
let to_read = count.min(available);
|
|
let mut result = BytesMut::with_capacity(to_read);
|
|
let mut remaining = to_read;
|
|
|
|
while remaining > 0 {
|
|
let block_idx = (self.cursor / CARRIER_BLOCK_PAYLOAD_SIZE as u64) as usize;
|
|
let offset_in_block = (self.cursor % CARRIER_BLOCK_PAYLOAD_SIZE as u64) as usize;
|
|
let space_in_block = CARRIER_BLOCK_PAYLOAD_SIZE - offset_in_block;
|
|
let bytes_from_block = remaining.min(space_in_block);
|
|
|
|
let block_data = self.ensure_block_loaded(block_idx)?;
|
|
|
|
if offset_in_block < block_data.len() {
|
|
let end = (offset_in_block + bytes_from_block).min(block_data.len());
|
|
result.extend_from_slice(&block_data[offset_in_block..end]);
|
|
let actually_read = end - offset_in_block;
|
|
self.cursor += actually_read as u64;
|
|
remaining -= actually_read;
|
|
if actually_read < bytes_from_block {
|
|
break;
|
|
}
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
Ok(result.freeze())
|
|
})
|
|
}
|
|
|
|
fn write_bytes(&mut self, buf: Bytes) -> FsFuture<'_, ()> {
|
|
self.touch();
|
|
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[..];
|
|
|
|
while !src.is_empty() {
|
|
let block_idx = (self.cursor / CARRIER_BLOCK_PAYLOAD_SIZE as u64) as usize;
|
|
let offset_in_block = (self.cursor % CARRIER_BLOCK_PAYLOAD_SIZE as u64) as usize;
|
|
let space_in_block = CARRIER_BLOCK_PAYLOAD_SIZE - offset_in_block;
|
|
let to_write = src.len().min(space_in_block);
|
|
|
|
let block_data = self.ensure_block_loaded(block_idx)?;
|
|
|
|
if block_data.len() < offset_in_block {
|
|
block_data.resize(offset_in_block, 0);
|
|
}
|
|
if block_data.len() < offset_in_block + to_write {
|
|
block_data.resize(offset_in_block + to_write, 0);
|
|
}
|
|
|
|
block_data[offset_in_block..offset_in_block + to_write]
|
|
.copy_from_slice(&src[..to_write]);
|
|
|
|
if let Some((_, _, ref mut dirty)) = self.cached_block {
|
|
*dirty = true;
|
|
}
|
|
|
|
self.cursor += to_write as u64;
|
|
if self.cursor > self.file_size {
|
|
self.file_size = self.cursor;
|
|
}
|
|
|
|
if self
|
|
.cached_block
|
|
.as_ref()
|
|
.map(|(_, d, _)| d.len() >= CARRIER_BLOCK_PAYLOAD_SIZE)
|
|
.unwrap_or(false)
|
|
{
|
|
self.flush_cached_block()?;
|
|
}
|
|
|
|
src = &src[to_write..];
|
|
}
|
|
|
|
Ok(())
|
|
})
|
|
}
|
|
|
|
fn write_buf(&mut self, mut buf: Box<dyn Buf + Send>) -> FsFuture<'_, ()> {
|
|
let bytes = buf.copy_to_bytes(buf.remaining());
|
|
self.write_bytes(bytes)
|
|
}
|
|
|
|
fn seek(&mut self, pos: SeekFrom) -> FsFuture<'_, u64> {
|
|
self.touch();
|
|
Box::pin(async move {
|
|
let new_cursor = match pos {
|
|
SeekFrom::Start(offset) => offset as i64,
|
|
SeekFrom::End(offset) => self.file_size as i64 + offset,
|
|
SeekFrom::Current(offset) => self.cursor as i64 + offset,
|
|
};
|
|
|
|
if new_cursor < 0 {
|
|
return Err(FsError::GeneralFailure);
|
|
}
|
|
|
|
self.cursor = new_cursor as u64;
|
|
Ok(self.cursor)
|
|
})
|
|
}
|
|
|
|
fn flush(&mut self) -> FsFuture<'_, ()> {
|
|
self.touch();
|
|
Box::pin(async move {
|
|
self.flush_cached_block()?;
|
|
let now = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.map(|d| d.as_secs())
|
|
.unwrap_or(0);
|
|
|
|
let mut inner = self.inner_fs.lock().unwrap();
|
|
if let Some(inode) = inner.manifest.inodes.get_mut(&self.inode_id) {
|
|
inode.size = self.file_size;
|
|
inode.blocks = self.blocks.clone();
|
|
inode.modified_at = now;
|
|
}
|
|
|
|
inner.save_manifest().map_err(|_| FsError::GeneralFailure)?;
|
|
|
|
self.meta.size = self.file_size;
|
|
self.meta.modified_at = UNIX_EPOCH + Duration::from_secs(now);
|
|
Ok(())
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Drop for CarrierFile {
|
|
fn drop(&mut self) {
|
|
if let Err(e) = self.flush_cached_block() {
|
|
tracing::warn!(
|
|
"Fehler beim automatischen Flush im CarrierFile::drop: {:?}",
|
|
e
|
|
);
|
|
}
|
|
if let Some((_, ref mut data, _)) = self.cached_block {
|
|
data.zeroize();
|
|
}
|
|
let now = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.map(|d| d.as_secs())
|
|
.unwrap_or(0);
|
|
if let Ok(mut inner) = self.inner_fs.lock() {
|
|
if let Some(inode) = inner.manifest.inodes.get_mut(&self.inode_id) {
|
|
inode.size = self.file_size;
|
|
inode.blocks = self.blocks.clone();
|
|
inode.modified_at = now;
|
|
}
|
|
let _ = inner.save_manifest();
|
|
}
|
|
}
|
|
}
|