Initial commit: Sanctum encrypted single-file container for Windows
This commit is contained in:
+709
@@ -0,0 +1,709 @@
|
||||
use std::fmt::Debug;
|
||||
use std::io::SeekFrom;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use bytes::{Buf, Bytes, BytesMut};
|
||||
use dav_server::{
|
||||
davpath::DavPath,
|
||||
fs::{
|
||||
DavDirEntry, DavFile, DavFileSystem, DavMetaData, FsError, FsFuture, FsResult, FsStream,
|
||||
OpenOptions, ReadDirMeta,
|
||||
},
|
||||
};
|
||||
use futures_util::stream;
|
||||
use tracing::{debug, error, warn};
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use crate::crypto::{decrypt_chunk, encrypt_chunk, CHUNK_SIZE};
|
||||
use crate::storage::{Database, NodeRecord};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Metadaten
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SanctumMetaData {
|
||||
pub is_dir: bool,
|
||||
pub size: u64,
|
||||
pub modified_at: SystemTime,
|
||||
pub created_at: SystemTime,
|
||||
}
|
||||
|
||||
impl DavMetaData for SanctumMetaData {
|
||||
fn len(&self) -> u64 {
|
||||
self.size
|
||||
}
|
||||
|
||||
fn modified(&self) -> FsResult<SystemTime> {
|
||||
Ok(self.modified_at)
|
||||
}
|
||||
|
||||
fn is_dir(&self) -> bool {
|
||||
self.is_dir
|
||||
}
|
||||
|
||||
fn created(&self) -> FsResult<SystemTime> {
|
||||
Ok(self.created_at)
|
||||
}
|
||||
|
||||
fn is_file(&self) -> bool {
|
||||
!self.is_dir
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Verzeichniseintrag
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SanctumDirEntry {
|
||||
pub name: String,
|
||||
pub meta: SanctumMetaData,
|
||||
}
|
||||
|
||||
impl DavDirEntry for SanctumDirEntry {
|
||||
fn name(&self) -> Vec<u8> {
|
||||
self.name.as_bytes().to_vec()
|
||||
}
|
||||
|
||||
fn metadata(&self) -> FsFuture<'_, Box<dyn DavMetaData>> {
|
||||
let meta = self.meta.clone();
|
||||
Box::pin(async move { Ok(Box::new(meta) as Box<dyn DavMetaData>) })
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Datei-Handle mit Streaming & Chunk-Pufferung
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub struct SanctumFile {
|
||||
node_id: i64,
|
||||
file_size: u64,
|
||||
cursor: u64,
|
||||
db: Database,
|
||||
dek: Arc<Zeroizing<[u8; 32]>>,
|
||||
meta: SanctumMetaData,
|
||||
// (chunk_index, decrypted_payload, is_dirty)
|
||||
cached_chunk: Option<(u32, Vec<u8>, bool)>,
|
||||
}
|
||||
|
||||
impl Debug for SanctumFile {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("SanctumFile")
|
||||
.field("node_id", &self.node_id)
|
||||
.field("file_size", &self.file_size)
|
||||
.field("cursor", &self.cursor)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl SanctumFile {
|
||||
pub fn new(
|
||||
node: NodeRecord,
|
||||
db: Database,
|
||||
dek: Arc<Zeroizing<[u8; 32]>>,
|
||||
) -> Self {
|
||||
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),
|
||||
};
|
||||
|
||||
Self {
|
||||
node_id: node.id,
|
||||
file_size: node.size,
|
||||
cursor: 0,
|
||||
db,
|
||||
dek,
|
||||
meta,
|
||||
cached_chunk: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Schreibt den aktuell im RAM gehaltenen Chunk verschlüsselt in die SQLite-Datenbank zurück.
|
||||
fn flush_cached_chunk(&mut self) -> Result<(), FsError> {
|
||||
if let Some((idx, ref data, true)) = self.cached_chunk {
|
||||
let (ciphertext, nonce, tag) =
|
||||
encrypt_chunk(&self.dek, self.node_id, idx, data).map_err(|e| {
|
||||
error!("Verschlüsselungsfehler beim Chunk-Flush: {e}");
|
||||
FsError::GeneralFailure
|
||||
})?;
|
||||
|
||||
self.db
|
||||
.write_chunk(self.node_id, idx, &nonce, &tag, &ciphertext)
|
||||
.map_err(|e| {
|
||||
error!("DB-Fehler beim Schreiben des Chunks: {e}");
|
||||
FsError::GeneralFailure
|
||||
})?;
|
||||
|
||||
if let Some((_, _, ref mut dirty)) = self.cached_chunk {
|
||||
*dirty = false;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stellt sicher, dass der angeforderte Chunk im Cache geladen und entschlüsselt ist.
|
||||
fn ensure_chunk_loaded(&mut self, chunk_index: u32) -> Result<&mut Vec<u8>, FsError> {
|
||||
let is_current = match &self.cached_chunk {
|
||||
Some((idx, _, _)) => *idx == chunk_index,
|
||||
None => false,
|
||||
};
|
||||
|
||||
if !is_current {
|
||||
self.flush_cached_chunk()?;
|
||||
|
||||
let payload = match self.db.read_chunk(self.node_id, chunk_index).map_err(|e| {
|
||||
error!("Fehler beim Lesen des Chunks #{chunk_index}: {e}");
|
||||
FsError::GeneralFailure
|
||||
})? {
|
||||
Some(record) => decrypt_chunk(
|
||||
&self.dek,
|
||||
self.node_id,
|
||||
chunk_index,
|
||||
&record.ciphertext,
|
||||
&record.nonce,
|
||||
&record.tag,
|
||||
)
|
||||
.map_err(|e| {
|
||||
error!("AEAD-Entschlüsselungsfehler bei Chunk #{chunk_index}: {e}");
|
||||
FsError::GeneralFailure
|
||||
})?,
|
||||
None => Vec::new(),
|
||||
};
|
||||
|
||||
self.cached_chunk = Some((chunk_index, payload, false));
|
||||
}
|
||||
|
||||
match &mut self.cached_chunk {
|
||||
Some((_, ref mut data, _)) => Ok(data),
|
||||
None => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SanctumFile {
|
||||
fn drop(&mut self) {
|
||||
if let Err(e) = self.flush_cached_chunk() {
|
||||
warn!("Fehler beim automatischen Flush im SanctumFile::drop: {:?}", e);
|
||||
}
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|d| d.as_secs())
|
||||
.unwrap_or(0);
|
||||
let _ = self.db.update_node_size_and_time(self.node_id, self.file_size, now);
|
||||
}
|
||||
}
|
||||
|
||||
impl DavFile for SanctumFile {
|
||||
fn metadata(&mut self) -> FsFuture<'_, Box<dyn DavMetaData>> {
|
||||
self.meta.size = self.file_size;
|
||||
let meta = self.meta.clone();
|
||||
Box::pin(async move { Ok(Box::new(meta) as Box<dyn DavMetaData>) })
|
||||
}
|
||||
|
||||
fn read_bytes(&mut self, mut count: usize) -> FsFuture<'_, Bytes> {
|
||||
Box::pin(async move {
|
||||
if self.cursor >= self.file_size || count == 0 {
|
||||
return Ok(Bytes::new());
|
||||
}
|
||||
|
||||
let remaining_file = (self.file_size - self.cursor) as usize;
|
||||
if count > remaining_file {
|
||||
count = remaining_file;
|
||||
}
|
||||
|
||||
let mut result = BytesMut::with_capacity(count);
|
||||
|
||||
while count > 0 && self.cursor < self.file_size {
|
||||
let chunk_idx = (self.cursor / CHUNK_SIZE as u64) as u32;
|
||||
let offset_in_chunk = (self.cursor % CHUNK_SIZE as u64) as usize;
|
||||
let bytes_in_chunk_left = CHUNK_SIZE - offset_in_chunk;
|
||||
|
||||
let to_read = count
|
||||
.min(bytes_in_chunk_left)
|
||||
.min((self.file_size - self.cursor) as usize);
|
||||
|
||||
let chunk_data = self.ensure_chunk_loaded(chunk_idx)?;
|
||||
if offset_in_chunk >= chunk_data.len() {
|
||||
break;
|
||||
}
|
||||
|
||||
let available = (chunk_data.len() - offset_in_chunk).min(to_read);
|
||||
result.extend_from_slice(&chunk_data[offset_in_chunk..offset_in_chunk + available]);
|
||||
|
||||
self.cursor += available as u64;
|
||||
count -= available;
|
||||
|
||||
if available < to_read {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result.freeze())
|
||||
})
|
||||
}
|
||||
|
||||
fn write_bytes(&mut self, buf: Bytes) -> FsFuture<'_, ()> {
|
||||
Box::pin(async move {
|
||||
let mut src = &buf[..];
|
||||
|
||||
while !src.is_empty() {
|
||||
let chunk_idx = (self.cursor / CHUNK_SIZE as u64) as u32;
|
||||
let offset_in_chunk = (self.cursor % CHUNK_SIZE as u64) as usize;
|
||||
let space_in_chunk = CHUNK_SIZE - offset_in_chunk;
|
||||
let to_write = src.len().min(space_in_chunk);
|
||||
|
||||
let chunk_data = self.ensure_chunk_loaded(chunk_idx)?;
|
||||
|
||||
if chunk_data.len() < offset_in_chunk {
|
||||
chunk_data.resize(offset_in_chunk, 0);
|
||||
}
|
||||
if chunk_data.len() < offset_in_chunk + to_write {
|
||||
chunk_data.resize(offset_in_chunk + to_write, 0);
|
||||
}
|
||||
|
||||
chunk_data[offset_in_chunk..offset_in_chunk + to_write]
|
||||
.copy_from_slice(&src[..to_write]);
|
||||
|
||||
if let Some((_, _, ref mut dirty)) = self.cached_chunk {
|
||||
*dirty = true;
|
||||
}
|
||||
|
||||
self.cursor += to_write as u64;
|
||||
if self.cursor > self.file_size {
|
||||
self.file_size = self.cursor;
|
||||
}
|
||||
|
||||
// Wenn der Chunk exakt 1 MB erreicht hat, sofort flushen, um RAM zu schonen
|
||||
if self.cached_chunk.as_ref().map(|(_, d, _)| d.len() >= CHUNK_SIZE).unwrap_or(false) {
|
||||
self.flush_cached_chunk()?;
|
||||
}
|
||||
|
||||
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> {
|
||||
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<'_, ()> {
|
||||
Box::pin(async move {
|
||||
self.flush_cached_chunk()?;
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|d| d.as_secs())
|
||||
.unwrap_or(0);
|
||||
self.db
|
||||
.update_node_size_and_time(self.node_id, self.file_size, now)
|
||||
.map_err(|e| {
|
||||
error!("Fehler beim Aktualisieren der Knotengröße: {e}");
|
||||
FsError::GeneralFailure
|
||||
})?;
|
||||
self.meta.size = self.file_size;
|
||||
self.meta.modified_at = UNIX_EPOCH + Duration::from_secs(now);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DavFileSystem Implementierung
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SanctumFs {
|
||||
db: Database,
|
||||
dek: Arc<Zeroizing<[u8; 32]>>,
|
||||
}
|
||||
|
||||
impl SanctumFs {
|
||||
pub fn new(db: Database, dek: Zeroizing<[u8; 32]>) -> Self {
|
||||
Self {
|
||||
db,
|
||||
dek: Arc::new(dek),
|
||||
}
|
||||
}
|
||||
|
||||
fn path_to_str(path: &DavPath) -> String {
|
||||
String::from_utf8_lossy(path.as_bytes()).to_string()
|
||||
}
|
||||
|
||||
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 DavFileSystem for SanctumFs {
|
||||
fn open<'a>(
|
||||
&'a self,
|
||||
path: &'a DavPath,
|
||||
options: OpenOptions,
|
||||
) -> FsFuture<'a, Box<dyn DavFile>> {
|
||||
Box::pin(async move {
|
||||
let path_str = Self::path_to_str(path);
|
||||
debug!("VFS open aufgerufen: path='{}', options={:?}", path_str, options);
|
||||
|
||||
let existing_node = self
|
||||
.db
|
||||
.resolve_path(&path_str)
|
||||
.map_err(|_| FsError::GeneralFailure)?;
|
||||
|
||||
let node = match existing_node {
|
||||
Some(n) => {
|
||||
if n.is_dir && (options.write || options.append) {
|
||||
return Err(FsError::Forbidden);
|
||||
}
|
||||
if options.create_new {
|
||||
return Err(FsError::Exists);
|
||||
}
|
||||
|
||||
if options.truncate {
|
||||
self.db
|
||||
.truncate_chunks_after(n.id, 0)
|
||||
.map_err(|_| FsError::GeneralFailure)?;
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|d| d.as_secs())
|
||||
.unwrap_or(0);
|
||||
self.db
|
||||
.update_node_size_and_time(n.id, 0, now)
|
||||
.map_err(|_| FsError::GeneralFailure)?;
|
||||
|
||||
NodeRecord {
|
||||
size: 0,
|
||||
modified_at: now,
|
||||
..n
|
||||
}
|
||||
} else {
|
||||
n
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if options.create || options.create_new {
|
||||
let (parent_path, file_name) = self.split_parent_and_name(&path_str);
|
||||
let parent = self
|
||||
.db
|
||||
.resolve_path(parent_path)
|
||||
.map_err(|_| FsError::GeneralFailure)?
|
||||
.ok_or(FsError::NotFound)?;
|
||||
|
||||
if !parent.is_dir {
|
||||
return Err(FsError::Forbidden);
|
||||
}
|
||||
|
||||
self.db
|
||||
.create_node(parent.id, file_name, false)
|
||||
.map_err(|e| {
|
||||
error!("Fehler beim Erstellen der Datei: {e}");
|
||||
FsError::GeneralFailure
|
||||
})?
|
||||
} else {
|
||||
return Err(FsError::NotFound);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let file = SanctumFile::new(node, self.db.clone(), self.dek.clone());
|
||||
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 node = self
|
||||
.db
|
||||
.resolve_path(&path_str)
|
||||
.map_err(|_| FsError::GeneralFailure)?
|
||||
.ok_or(FsError::NotFound)?;
|
||||
|
||||
if !node.is_dir {
|
||||
return Err(FsError::Forbidden);
|
||||
}
|
||||
|
||||
let children = self
|
||||
.db
|
||||
.list_children(node.id)
|
||||
.map_err(|_| FsError::GeneralFailure)?;
|
||||
|
||||
let entries: Vec<Result<Box<dyn DavDirEntry>, FsError>> = children
|
||||
.into_iter()
|
||||
.map(|child| {
|
||||
Ok(Box::new(SanctumDirEntry {
|
||||
name: child.name,
|
||||
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 node = self
|
||||
.db
|
||||
.resolve_path(&path_str)
|
||||
.map_err(|_| FsError::GeneralFailure)?
|
||||
.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 create_dir<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, ()> {
|
||||
Box::pin(async move {
|
||||
let path_str = Self::path_to_str(path);
|
||||
if self
|
||||
.db
|
||||
.resolve_path(&path_str)
|
||||
.map_err(|_| FsError::GeneralFailure)?
|
||||
.is_some()
|
||||
{
|
||||
return Err(FsError::Exists);
|
||||
}
|
||||
|
||||
let (parent_path, dir_name) = self.split_parent_and_name(&path_str);
|
||||
let parent = self
|
||||
.db
|
||||
.resolve_path(parent_path)
|
||||
.map_err(|_| FsError::GeneralFailure)?
|
||||
.ok_or(FsError::NotFound)?;
|
||||
|
||||
if !parent.is_dir {
|
||||
return Err(FsError::Forbidden);
|
||||
}
|
||||
|
||||
self.db
|
||||
.create_node(parent.id, dir_name, true)
|
||||
.map_err(|_| FsError::GeneralFailure)?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn remove_dir<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, ()> {
|
||||
Box::pin(async move {
|
||||
let path_str = Self::path_to_str(path);
|
||||
let node = self
|
||||
.db
|
||||
.resolve_path(&path_str)
|
||||
.map_err(|_| FsError::GeneralFailure)?
|
||||
.ok_or(FsError::NotFound)?;
|
||||
|
||||
if !node.is_dir {
|
||||
return Err(FsError::Forbidden);
|
||||
}
|
||||
if node.id == 1 {
|
||||
// Root-Verzeichnis darf nicht gelöscht werden
|
||||
return Err(FsError::Forbidden);
|
||||
}
|
||||
|
||||
self.db
|
||||
.delete_node(node.id)
|
||||
.map_err(|_| FsError::GeneralFailure)?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn remove_file<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, ()> {
|
||||
Box::pin(async move {
|
||||
let path_str = Self::path_to_str(path);
|
||||
let node = self
|
||||
.db
|
||||
.resolve_path(&path_str)
|
||||
.map_err(|_| FsError::GeneralFailure)?
|
||||
.ok_or(FsError::NotFound)?;
|
||||
|
||||
if node.is_dir {
|
||||
return Err(FsError::Forbidden);
|
||||
}
|
||||
|
||||
self.db
|
||||
.delete_node(node.id)
|
||||
.map_err(|_| FsError::GeneralFailure)?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn rename<'a>(
|
||||
&'a self,
|
||||
from: &'a DavPath,
|
||||
to: &'a DavPath,
|
||||
) -> FsFuture<'a, ()> {
|
||||
Box::pin(async move {
|
||||
let from_str = Self::path_to_str(from);
|
||||
let to_str = Self::path_to_str(to);
|
||||
|
||||
let node = self
|
||||
.db
|
||||
.resolve_path(&from_str)
|
||||
.map_err(|_| FsError::GeneralFailure)?
|
||||
.ok_or(FsError::NotFound)?;
|
||||
|
||||
let (to_parent_path, to_name) = self.split_parent_and_name(&to_str);
|
||||
let to_parent = self
|
||||
.db
|
||||
.resolve_path(to_parent_path)
|
||||
.map_err(|_| FsError::GeneralFailure)?
|
||||
.ok_or(FsError::NotFound)?;
|
||||
|
||||
if !to_parent.is_dir {
|
||||
return Err(FsError::Forbidden);
|
||||
}
|
||||
|
||||
// Falls Zieldatei bereits existiert und Datei ist: überschreiben / löschen
|
||||
if let Some(dest) = self
|
||||
.db
|
||||
.resolve_path(&to_str)
|
||||
.map_err(|_| FsError::GeneralFailure)?
|
||||
{
|
||||
if dest.is_dir {
|
||||
return Err(FsError::Forbidden);
|
||||
}
|
||||
self.db
|
||||
.delete_node(dest.id)
|
||||
.map_err(|_| FsError::GeneralFailure)?;
|
||||
}
|
||||
|
||||
self.db
|
||||
.rename_node(node.id, to_parent.id, to_name)
|
||||
.map_err(|_| FsError::GeneralFailure)?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn copy<'a>(
|
||||
&'a self,
|
||||
from: &'a DavPath,
|
||||
to: &'a DavPath,
|
||||
) -> FsFuture<'a, ()> {
|
||||
Box::pin(async move {
|
||||
let from_str = Self::path_to_str(from);
|
||||
let to_str = Self::path_to_str(to);
|
||||
|
||||
let node = self
|
||||
.db
|
||||
.resolve_path(&from_str)
|
||||
.map_err(|_| FsError::GeneralFailure)?
|
||||
.ok_or(FsError::NotFound)?;
|
||||
|
||||
if node.is_dir {
|
||||
return Err(FsError::NotImplemented);
|
||||
}
|
||||
|
||||
let (to_parent_path, to_name) = self.split_parent_and_name(&to_str);
|
||||
let to_parent = self
|
||||
.db
|
||||
.resolve_path(to_parent_path)
|
||||
.map_err(|_| FsError::GeneralFailure)?
|
||||
.ok_or(FsError::NotFound)?;
|
||||
|
||||
let dest_node = self
|
||||
.db
|
||||
.create_node(to_parent.id, to_name, false)
|
||||
.map_err(|_| FsError::GeneralFailure)?;
|
||||
|
||||
// Kopiere alle Chunks und re-verschlüssele mit neuer node_id (wegen AAD-Bindung!)
|
||||
let total_chunks = if node.size == 0 {
|
||||
0
|
||||
} else {
|
||||
((node.size - 1) / CHUNK_SIZE as u64 + 1) as u32
|
||||
};
|
||||
|
||||
for idx in 0..total_chunks {
|
||||
if let Some(record) = self
|
||||
.db
|
||||
.read_chunk(node.id, idx)
|
||||
.map_err(|_| FsError::GeneralFailure)?
|
||||
{
|
||||
let plaintext = decrypt_chunk(
|
||||
&self.dek,
|
||||
node.id,
|
||||
idx,
|
||||
&record.ciphertext,
|
||||
&record.nonce,
|
||||
&record.tag,
|
||||
)
|
||||
.map_err(|_| FsError::GeneralFailure)?;
|
||||
|
||||
let (new_ct, new_nonce, new_tag) =
|
||||
encrypt_chunk(&self.dek, dest_node.id, idx, &plaintext)
|
||||
.map_err(|_| FsError::GeneralFailure)?;
|
||||
|
||||
self.db
|
||||
.write_chunk(dest_node.id, idx, &new_nonce, &new_tag, &new_ct)
|
||||
.map_err(|_| FsError::GeneralFailure)?;
|
||||
}
|
||||
}
|
||||
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|d| d.as_secs())
|
||||
.unwrap_or(0);
|
||||
self.db
|
||||
.update_node_size_and_time(dest_node.id, node.size, now)
|
||||
.map_err(|_| FsError::GeneralFailure)?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn get_quota(&self) -> FsFuture<'_, (u64, Option<u64>)> {
|
||||
Box::pin(async move {
|
||||
// Virtueller Speicherplatz für Explorer: 1 TB
|
||||
let total_capacity: u64 = 1024 * 1024 * 1024 * 1024;
|
||||
Ok((0, Some(total_capacity)))
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user