use std::path::Path as FsPath; use uuid::Uuid; use crate::error::{AppError, AppResult}; use crate::models::{Document, DocumentAsset, DocumentAssetObject, DocumentVersion}; use crate::state::AppState; use crate::utils::time::to_iso; use super::{ DocumentAssetDetailResponse, DocumentAssetObjectResponse, DocumentAssetResponse, DocumentVersionResponse, }; pub fn build_download_path( state: &AppState, document: &Document, user_id: Uuid, ) -> AppResult { state .jwt .generate_download_token(document.id, user_id, document.tenant_id) .map(|token| format!("/download/{token}")) .map_err(|err| AppError::internal(format!("failed to generate download token: {err}"))) } pub fn to_version_response( version: DocumentVersion, include_operations_summary: bool, ) -> DocumentVersionResponse { DocumentVersionResponse { id: version.id, version_number: version.version_number, s3_key: version.s3_key, size_bytes: version.size_bytes, checksum: version.checksum, created_at: to_iso(version.created_at), metadata: version.metadata, operations_summary: if include_operations_summary { Some(version.operations_summary) } else { None }, } } pub fn to_asset_summary(asset: DocumentAsset) -> DocumentAssetResponse { DocumentAssetResponse { id: asset.id, asset_type: asset.asset_type, mime_type: asset.mime_type, metadata: asset.metadata, cardinality: asset.cardinality, } } pub fn to_asset_detail_response( asset: DocumentAsset, objects: Vec, ) -> DocumentAssetDetailResponse { DocumentAssetDetailResponse { id: asset.id, asset_type: asset.asset_type, mime_type: asset.mime_type, metadata: asset.metadata, created_at: to_iso(asset.created_at), cardinality: asset.cardinality, objects, } } pub fn to_asset_object_response( object: DocumentAssetObject, url: Option, expires_at: Option, ) -> DocumentAssetObjectResponse { DocumentAssetObjectResponse { id: object.id, ordinal: object.ordinal, metadata: object.metadata, url, expires_at, } } pub fn derive_document_title(original: &str) -> String { let trimmed = original.trim(); if trimmed.is_empty() { return "Document".to_string(); } let stem = FsPath::new(trimmed) .file_stem() .and_then(|s| s.to_str()) .map(|s| s.trim()) .filter(|s| !s.is_empty()) .map(|s| s.to_string()); stem.unwrap_or_else(|| trimmed.to_string()) } pub fn filename_with_retained_extension(title: &str, current_filename: &str) -> String { let extension = FsPath::new(current_filename) .extension() .and_then(|ext| ext.to_str()); if let Some(ext) = extension { if title .rsplit_once('.') .map(|(_, existing_ext)| existing_ext.eq_ignore_ascii_case(ext)) .unwrap_or(false) { title.to_string() } else { format!("{title}.{ext}") } } else { title.to_string() } }