use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use utoipa::{IntoParams, ToSchema}; use uuid::Uuid; use super::cursor::{CursorListResponse, CursorQuery, PageCursor}; use super::grant_dto::{ResourceContentDto, ResourceTypeDto}; use crate::domain::services::authorization::ResourceKind; /// DTO representing an item in the trash #[derive(Debug, Serialize, Deserialize, ToSchema)] pub struct TrashedItemDto { pub id: String, pub original_id: String, pub item_type: String, // "file" o "folder" pub name: String, pub original_path: String, pub trashed_at: DateTime, pub days_until_deletion: i64, /// Human-readable category (e.g., "Image", "Folder", "Document") pub category: String, /// FontAwesome icon class for the file type pub icon_class: String, /// Special CSS class for icon styling (e.g., "image-icon", "pdf-icon") pub icon_special_class: String, } /// Request to move an item to trash #[derive(Debug, Deserialize, ToSchema)] pub struct MoveToTrashRequest { pub item_id: String, pub item_type: String, // "file" o "folder" } /// Request to restore an item from trash #[derive(Debug, Deserialize, ToSchema)] pub struct RestoreFromTrashRequest { pub trash_id: String, } /// Request to permanently delete an item from trash #[derive(Debug, Deserialize, ToSchema)] pub struct DeletePermanentlyRequest { pub trash_id: String, } // ════════════════════════════════════════════════════════════════════════════ // Cursor-paginated trash resources (GET /api/trash/resources) // ════════════════════════════════════════════════════════════════════════════ /// Raw row returned by the UNION-ALL query over `storage.files`/`storage.folders` /// where `is_trashed = TRUE`. Never serialised directly. pub struct TrashResourceRow { pub resource_type: String, // "file" | "folder" pub resource_id: Uuid, pub name: String, pub parent_id: Option, pub mime_type: Option, /// `-1` for folders (sentinel), actual byte-count for files. pub size: i64, pub resource_created_at: DateTime, pub modified_at: DateTime, /// Drive the trashed item belongs to. Surfaced verbatim on the wire /// (`TrashResourceItemDto.drive_id`) so the `/trash` UI can group by /// drive without an extra lookup per row. D2b: filtering by drive is /// done in SQL via `WHERE drive_id = ANY($accessible_drive_ids)`. pub drive_id: Uuid, /// Raw BLAKE3 content hash. `Some(_)` for file rows, `None` for /// folder rows. Feeds `File::compute_etag` so the trash listing's /// `etag` matches what GET/HEAD/PROPFIND would return for the /// same file (restorable trash items are conditional-request /// targets too). pub blob_hash: Option, pub trashed_at: DateTime, pub deletion_date: DateTime, /// Original location path (for folders: `path`; for files: `parent.path || '/' || name`). pub path: Option, // Pre-computed sort fields for cursor construction. pub sort_str: Option, pub sort_int: Option, pub sort_ts: Option>, } /// Opaque keyset-pagination cursor for `GET /api/trash/resources`. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TrashCursor { /// Sort dimension active when this cursor was produced. /// Values: `"deletion_date"` (default), `"trashed_at"`, `"name"`, `"type"`, `"size"`. #[serde(default = "TrashCursor::default_order")] pub order_by: String, /// UUID of the last item on the previous page (tie-breaker). pub resource_id: Uuid, /// `LOWER(name)` for `name`/`type` sorts. #[serde(skip_serializing_if = "Option::is_none")] pub sort_str: Option, /// Multipurpose integer: `folder_first` for `name`, `type_order` for `type`, /// size in bytes for `size`. #[serde(skip_serializing_if = "Option::is_none")] pub sort_int: Option, /// Timestamp for `deletion_date` and `trashed_at` sorts. #[serde(skip_serializing_if = "Option::is_none")] pub sort_ts: Option>, /// Whether the result set was reversed — must match on every page. #[serde(default)] pub reverse: bool, } impl TrashCursor { fn default_order() -> String { "deletion_date".to_owned() } } impl PageCursor for TrashCursor {} /// Query parameters for `GET /api/trash/resources`. #[derive(Debug, Deserialize, IntoParams)] pub struct TrashResourcesQuery { /// Maximum items per page (1–200, default 50). #[serde(default = "CursorQuery::default_limit")] pub limit: u32, /// Opaque cursor from a previous response. Omit to start from the first page. pub cursor: Option, /// Sort / group-by dimension. Supported: `"deletion_date"` (default — soonest /// expiry first), `"trashed_at"` (most recently trashed first), `"name"`, /// `"type"`, `"size"`. pub order_by: Option, /// Comma-separated resource types to include, e.g. `"file,folder"`. /// Omit to include both. pub resource_types: Option, /// Reverse the sort order. Default `false`. #[serde(default)] pub reverse: bool, } impl TrashResourcesQuery { pub fn limit_clamped(&self) -> usize { self.limit.clamp(1, 200) as usize } pub fn decode_cursor(&self) -> Option { self.cursor.as_deref().and_then(TrashCursor::decode) } /// Returns `None` when `resource_types` is absent (= include all). pub fn resource_kinds(&self) -> Option> { self.resource_types.as_deref().map(|s| { s.split(',') .filter_map(|t| ResourceKind::parse(t.trim())) .collect() }) } } /// One item in a `GET /api/trash/resources` page. /// /// `deletion_date` is the real timestamp at which the item will be permanently /// deleted (= `trashed_at + retention_days`). The client derives "days until /// deletion" itself from this + the current clock — the wire format does not /// duplicate that derivation. `resource.path` carries the original location /// (soft-delete preserves the row's `path` column). #[derive(Debug, Serialize, ToSchema)] pub struct TrashResourceItemDto { pub resource_type: ResourceTypeDto, /// When the user moved the item to trash. pub trashed_at: DateTime, /// When the item will be permanently deleted by the retention sweeper. pub deletion_date: DateTime, /// The drive the trashed item belongs to. Enables client-side /// group-by-drive in the `/trash` UI (D2b spec — see /// `project_trash_groupbys_d2b` memory). The drive's display name /// resolves through the `/api/drives` listing the client already /// holds in `drives.svelte` — no extra round-trip needed. pub drive_id: Uuid, /// Full resource details — shape determined by `resource_type`. pub resource: ResourceContentDto, } /// Response envelope for `GET /api/trash/resources`. pub type TrashResourcesDto = CursorListResponse;