Files
Oxicloud/src/application/dtos/file_dto.rs
T
2026-06-06 18:49:14 +02:00

183 lines
6.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use std::sync::Arc;
use crate::domain::entities::file::File;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use super::display_helpers::{
category_for, format_file_size, icon_class_for, icon_special_class_for,
};
/// DTO for file responses
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct FileDto {
/// File ID
pub id: String,
/// File name
pub name: String,
/// Path to the file (relative)
pub path: String,
/// Size in bytes
pub size: u64,
/// MIME type — `Arc<str>` because MIME values repeat across files
/// and DTOs are cloned on every request (clone is O(1) atomic increment).
#[schema(value_type = String)]
pub mime_type: Arc<str>,
/// Parent folder ID
pub folder_id: Option<String>,
/// Creation timestamp
pub created_at: u64,
/// Last modification timestamp
pub modified_at: u64,
// ── Pre-computed display fields (Arc<str>: values come from static tables) ──
/// FontAwesome icon CSS class (e.g. "fas fa-file-image")
#[schema(value_type = String)]
pub icon_class: Arc<str>,
/// Extra CSS class for icon styling (e.g. "image-icon", "" when default)
#[schema(value_type = String)]
pub icon_special_class: Arc<str>,
/// Human-readable file category (e.g. "Image", "Document")
#[schema(value_type = String)]
pub category: Arc<str>,
/// Human-readable formatted size (e.g. "3.27 MB")
pub size_formatted: String,
/// Owner user ID (omitted from JSON when None)
#[serde(skip_serializing_if = "Option::is_none")]
pub owner_id: Option<String>,
/// Sort date for Photos timeline — COALESCE(EXIF captured_at, created_at).
/// Only populated by the /api/photos endpoint.
#[serde(skip_serializing_if = "Option::is_none")]
pub sort_date: Option<u64>,
/// 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 —
// `File::etag()` derives from `blob_hash` + `modified_at`,
// so it must run against the live entity, not against
// already-extracted parts. `content_hash` is just the raw
// blob hash; `etag` is the cache token derived from it.
let etag = file.etag();
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();
let icon_class = Arc::from(icon_class_for(&parts.name, &parts.mime_type));
let icon_special_class = Arc::from(icon_special_class_for(&parts.name, &parts.mime_type));
let category = Arc::from(category_for(&parts.name, &parts.mime_type));
let size_formatted = format_file_size(parts.size);
let mime_type = Arc::from(parts.mime_type.as_str());
Self {
id: parts.id,
name: parts.name,
path: parts.path_string,
size: parts.size,
mime_type,
folder_id: parts.folder_id,
created_at: parts.created_at,
modified_at: parts.modified_at,
icon_class,
icon_special_class,
category,
size_formatted,
owner_id: parts.owner_id.map(|u| u.to_string()),
sort_date: None,
content_hash,
etag,
}
}
}
// To convert from FileDto to File for batch handlers
impl From<FileDto> for File {
fn from(dto: FileDto) -> Self {
// Display fields (icon_class, icon_special_class, category, size_formatted)
// are not part of the domain entity and are ignored.
File::from_dto(
dto.id,
dto.name,
dto.path,
dto.size,
dto.mime_type.to_string(),
dto.folder_id,
dto.created_at,
dto.modified_at,
)
}
}
impl FileDto {
/// Returns a copy of this DTO with the `path` field cleared.
///
/// Used when a file is returned to a share recipient: `path` reveals the
/// full folder hierarchy above the file which the recipient may not have
/// access to. `folder_id` and `owner_id` are intentionally kept — the
/// former is needed for sub-folder navigation (covered by the cascade
/// grant), and the latter is harmless metadata.
#[must_use]
pub fn without_hierarchy_info(self) -> Self {
Self {
path: String::new(),
..self
}
}
/// Creates an empty file DTO for stub implementations
pub fn empty() -> Self {
Self {
id: "stub-id".to_string(),
name: "stub-file".to_string(),
path: "/stub/path".to_string(),
size: 0,
mime_type: Arc::from("application/octet-stream"),
folder_id: None,
created_at: 0,
modified_at: 0,
icon_class: Arc::from("fas fa-file"),
icon_special_class: Arc::from(""),
category: Arc::from("Document"),
size_formatted: "0 Bytes".to_string(),
owner_id: None,
content_hash: String::new(),
etag: String::new(),
sort_date: None,
}
}
}
impl Default for FileDto {
fn default() -> Self {
Self::empty()
}
}