refactor(file|folder): separate etag and blob_hash
This commit is contained in:
@@ -62,14 +62,32 @@ pub struct FileDto {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub sort_date: Option<u64>,
|
||||
|
||||
/// Content-addressable ETag (= blob_hash). Changes on every content write.
|
||||
/// Used for WebDAV/Nextcloud ETag headers. Omitted from REST API JSON.
|
||||
#[serde(skip)]
|
||||
/// Raw BLAKE3 content hash. Populated from `File::content_hash()`.
|
||||
/// Exposed in REST JSON so API consumers can use it for
|
||||
/// content-addressable URLs, dedup verification, and integrity
|
||||
/// audits. Distinct from `etag` (which is an HTTP-only cache
|
||||
/// token whose formula may grow to include `modified_at` etc.).
|
||||
pub content_hash: String,
|
||||
|
||||
/// Opaque HTTP ETag. Populated from `File::etag()`. Used by
|
||||
/// WebDAV/NextCloud handlers when emitting `ETag` headers and
|
||||
/// also exposed in REST JSON so frontends can pass it back
|
||||
/// through `If-Match` / `If-None-Match` on download / mutation
|
||||
/// endpoints without a separate HEAD round-trip.
|
||||
pub etag: String,
|
||||
}
|
||||
|
||||
impl From<File> for FileDto {
|
||||
fn from(file: File) -> Self {
|
||||
// Compute the HTTP ETag BEFORE consuming the entity — once
|
||||
// `File::etag()` folds modified_at (and possibly more) into
|
||||
// the formula, this becomes more than a string clone and
|
||||
// must run against a live entity, not against
|
||||
// already-extracted parts. content_hash is just the blob
|
||||
// hash; etag is the cache token derived from it.
|
||||
let etag = file.etag().to_string();
|
||||
let content_hash = file.content_hash().to_string();
|
||||
|
||||
// Consume the entity by moving all fields — zero heap allocations
|
||||
// for id, name, path, folder_id, owner_id (previously 5× .to_string()).
|
||||
let parts = file.into_parts();
|
||||
@@ -95,7 +113,8 @@ impl From<File> for FileDto {
|
||||
size_formatted,
|
||||
owner_id: parts.owner_id.map(|u| u.to_string()),
|
||||
sort_date: None,
|
||||
etag: parts.etag,
|
||||
content_hash,
|
||||
etag,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -150,6 +169,7 @@ impl FileDto {
|
||||
category: Arc::from("Document"),
|
||||
size_formatted: "0 Bytes".to_string(),
|
||||
owner_id: None,
|
||||
content_hash: String::new(),
|
||||
etag: String::new(),
|
||||
sort_date: None,
|
||||
}
|
||||
|
||||
@@ -73,11 +73,19 @@ pub struct FolderDto {
|
||||
/// Human-readable category (always "Folder")
|
||||
#[schema(value_type = String)]
|
||||
pub category: Arc<str>,
|
||||
|
||||
/// Opaque ETag for HTTP responses. Populated from `Folder::etag()`
|
||||
/// at conversion time so every WebDAV / NextCloud handler emits
|
||||
/// the same value, and exposed in REST JSON so the frontend can
|
||||
/// pass it back through `If-Match` on rename / move endpoints
|
||||
/// without a separate HEAD round-trip.
|
||||
pub etag: String,
|
||||
}
|
||||
|
||||
impl From<Folder> for FolderDto {
|
||||
fn from(folder: Folder) -> Self {
|
||||
let is_root = folder.parent_id().is_none();
|
||||
let etag = folder.etag().to_string();
|
||||
|
||||
Self {
|
||||
id: folder.id().to_string(),
|
||||
@@ -91,6 +99,7 @@ impl From<Folder> for FolderDto {
|
||||
icon_class: Arc::from("fas fa-folder"),
|
||||
icon_special_class: Arc::from("folder-icon"),
|
||||
category: Arc::from("Folder"),
|
||||
etag,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,6 +150,7 @@ impl FolderDto {
|
||||
icon_class: Arc::from("fas fa-folder"),
|
||||
icon_special_class: Arc::from("folder-icon"),
|
||||
category: Arc::from("Folder"),
|
||||
etag: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user