style: apply cargo fmt formatting to existing codebase
Run `cargo fmt` across all Rust source files to enforce consistent formatting (import ordering, line wrapping, match arm braces). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -113,18 +113,21 @@ pub fn icon_class_for(name: &str, mime: &str) -> &'static str {
|
|||||||
"doc" | "docx" | "odt" | "rtf" => "fas fa-file-word",
|
"doc" | "docx" | "odt" | "rtf" => "fas fa-file-word",
|
||||||
"xls" | "xlsx" | "ods" | "csv" => "fas fa-file-excel",
|
"xls" | "xlsx" | "ods" | "csv" => "fas fa-file-excel",
|
||||||
"ppt" | "pptx" | "odp" | "key" => "fas fa-file-powerpoint",
|
"ppt" | "pptx" | "odp" | "key" => "fas fa-file-powerpoint",
|
||||||
"jpg" | "jpeg" | "png" | "gif" | "bmp" | "svg" | "webp" | "ico" | "tiff" | "tif" | "heic" | "heif" | "avif" => "fas fa-file-image",
|
"jpg" | "jpeg" | "png" | "gif" | "bmp" | "svg" | "webp" | "ico" | "tiff" | "tif"
|
||||||
|
| "heic" | "heif" | "avif" => "fas fa-file-image",
|
||||||
"mp4" | "avi" | "mkv" | "mov" | "wmv" | "flv" | "webm" | "m4v" => "fas fa-file-video",
|
"mp4" | "avi" | "mkv" | "mov" | "wmv" | "flv" | "webm" | "m4v" => "fas fa-file-video",
|
||||||
"mp3" | "wav" | "ogg" | "flac" | "aac" | "wma" | "m4a" | "opus" => "fas fa-file-audio",
|
"mp3" | "wav" | "ogg" | "flac" | "aac" | "wma" | "m4a" | "opus" => "fas fa-file-audio",
|
||||||
"zip" | "rar" | "7z" | "tar" | "gz" | "bz2" | "xz" | "zst" | "lz4" => "fas fa-file-archive",
|
"zip" | "rar" | "7z" | "tar" | "gz" | "bz2" | "xz" | "zst" | "lz4" => {
|
||||||
"exe" | "msi" | "dmg" | "deb" | "rpm" | "appimage" | "pkg" | "snap" | "flatpak" => "fas fa-hdd",
|
"fas fa-file-archive"
|
||||||
"js" | "jsx" | "mjs" | "cjs" | "ts" | "tsx"
|
}
|
||||||
| "py" | "pyw" | "rs" | "go" | "java" | "kt" | "kts" | "scala"
|
"exe" | "msi" | "dmg" | "deb" | "rpm" | "appimage" | "pkg" | "snap" | "flatpak" => {
|
||||||
| "c" | "h" | "cpp" | "hpp" | "cc" | "cxx" | "cs"
|
"fas fa-hdd"
|
||||||
| "rb" | "php" | "swift" | "r" | "lua" | "pl" | "pm"
|
}
|
||||||
| "html" | "htm" | "css" | "scss" | "sass" | "less"
|
"js" | "jsx" | "mjs" | "cjs" | "ts" | "tsx" | "py" | "pyw" | "rs" | "go" | "java"
|
||||||
| "json" | "xml" | "yaml" | "yml" | "toml" | "ini" | "cfg" | "conf"
|
| "kt" | "kts" | "scala" | "c" | "h" | "cpp" | "hpp" | "cc" | "cxx" | "cs" | "rb"
|
||||||
| "sql" | "graphql" | "proto" | "vue" | "svelte" => "fas fa-file-code",
|
| "php" | "swift" | "r" | "lua" | "pl" | "pm" | "html" | "htm" | "css" | "scss"
|
||||||
|
| "sass" | "less" | "json" | "xml" | "yaml" | "yml" | "toml" | "ini" | "cfg"
|
||||||
|
| "conf" | "sql" | "graphql" | "proto" | "vue" | "svelte" => "fas fa-file-code",
|
||||||
"sh" | "bash" | "zsh" | "fish" | "ps1" | "bat" | "cmd" => "fas fa-terminal",
|
"sh" | "bash" | "zsh" | "fish" | "ps1" | "bat" | "cmd" => "fas fa-terminal",
|
||||||
"md" | "markdown" | "rst" | "txt" => "fas fa-file-alt",
|
"md" | "markdown" | "rst" | "txt" => "fas fa-file-alt",
|
||||||
_ => "fas fa-file",
|
_ => "fas fa-file",
|
||||||
@@ -173,7 +176,7 @@ pub fn icon_special_class_for(name: &str, mime: &str) -> &'static str {
|
|||||||
"application/x-yaml" | "application/toml" => return "code-icon config-icon",
|
"application/x-yaml" | "application/toml" => return "code-icon config-icon",
|
||||||
"application/x-httpd-php" => return "code-icon php-icon",
|
"application/x-httpd-php" => return "code-icon php-icon",
|
||||||
"application/x-sh" | "application/x-shellscript" | "application/x-csh" => {
|
"application/x-sh" | "application/x-shellscript" | "application/x-csh" => {
|
||||||
return "script-icon"
|
return "script-icon";
|
||||||
}
|
}
|
||||||
"application/x-apple-diskimage"
|
"application/x-apple-diskimage"
|
||||||
| "application/x-ms-dos-executable"
|
| "application/x-ms-dos-executable"
|
||||||
@@ -227,11 +230,14 @@ pub fn icon_special_class_for(name: &str, mime: &str) -> &'static str {
|
|||||||
"doc" | "docx" | "odt" | "rtf" => "doc-icon",
|
"doc" | "docx" | "odt" | "rtf" => "doc-icon",
|
||||||
"xls" | "xlsx" | "ods" | "csv" => "spreadsheet-icon",
|
"xls" | "xlsx" | "ods" | "csv" => "spreadsheet-icon",
|
||||||
"ppt" | "pptx" | "odp" | "key" => "presentation-icon",
|
"ppt" | "pptx" | "odp" | "key" => "presentation-icon",
|
||||||
"jpg" | "jpeg" | "png" | "gif" | "bmp" | "svg" | "webp" | "ico" | "tiff" | "tif" | "heic" | "heif" | "avif" => "image-icon",
|
"jpg" | "jpeg" | "png" | "gif" | "bmp" | "svg" | "webp" | "ico" | "tiff" | "tif"
|
||||||
|
| "heic" | "heif" | "avif" => "image-icon",
|
||||||
"mp4" | "avi" | "mkv" | "mov" | "wmv" | "flv" | "webm" | "m4v" => "video-icon",
|
"mp4" | "avi" | "mkv" | "mov" | "wmv" | "flv" | "webm" | "m4v" => "video-icon",
|
||||||
"mp3" | "wav" | "ogg" | "flac" | "aac" | "wma" | "m4a" | "opus" => "audio-icon",
|
"mp3" | "wav" | "ogg" | "flac" | "aac" | "wma" | "m4a" | "opus" => "audio-icon",
|
||||||
"zip" | "rar" | "7z" | "tar" | "gz" | "bz2" | "xz" | "zst" | "lz4" => "archive-icon",
|
"zip" | "rar" | "7z" | "tar" | "gz" | "bz2" | "xz" | "zst" | "lz4" => "archive-icon",
|
||||||
"exe" | "msi" | "dmg" | "deb" | "rpm" | "appimage" | "pkg" | "snap" | "flatpak" => "installer-icon",
|
"exe" | "msi" | "dmg" | "deb" | "rpm" | "appimage" | "pkg" | "snap" | "flatpak" => {
|
||||||
|
"installer-icon"
|
||||||
|
}
|
||||||
"py" | "pyw" => "code-icon py-icon",
|
"py" | "pyw" => "code-icon py-icon",
|
||||||
"rs" => "code-icon rust-icon",
|
"rs" => "code-icon rust-icon",
|
||||||
"go" => "code-icon go-icon",
|
"go" => "code-icon go-icon",
|
||||||
@@ -305,7 +311,8 @@ pub fn category_for(name: &str, mime: &str) -> &'static str {
|
|||||||
return "Video";
|
return "Video";
|
||||||
} else if mime.starts_with("audio/") {
|
} else if mime.starts_with("audio/") {
|
||||||
return "Audio";
|
return "Audio";
|
||||||
} else if mime.starts_with("text/x-") || mime.contains("script") || mime.contains("javascript") {
|
} else if mime.starts_with("text/x-") || mime.contains("script") || mime.contains("javascript")
|
||||||
|
{
|
||||||
return "Code";
|
return "Code";
|
||||||
} else if mime.starts_with("text/markdown") {
|
} else if mime.starts_with("text/markdown") {
|
||||||
return "Markdown";
|
return "Markdown";
|
||||||
@@ -322,15 +329,16 @@ pub fn category_for(name: &str, mime: &str) -> &'static str {
|
|||||||
"doc" | "docx" | "odt" | "rtf" | "txt" => "Document",
|
"doc" | "docx" | "odt" | "rtf" | "txt" => "Document",
|
||||||
"xls" | "xlsx" | "ods" | "csv" => "Spreadsheet",
|
"xls" | "xlsx" | "ods" | "csv" => "Spreadsheet",
|
||||||
"ppt" | "pptx" | "odp" | "key" => "Presentation",
|
"ppt" | "pptx" | "odp" | "key" => "Presentation",
|
||||||
"jpg" | "jpeg" | "png" | "gif" | "bmp" | "svg" | "webp" | "ico" | "tiff" | "heic" | "avif" => "Image",
|
"jpg" | "jpeg" | "png" | "gif" | "bmp" | "svg" | "webp" | "ico" | "tiff" | "heic"
|
||||||
|
| "avif" => "Image",
|
||||||
"mp4" | "avi" | "mkv" | "mov" | "wmv" | "flv" | "webm" | "m4v" => "Video",
|
"mp4" | "avi" | "mkv" | "mov" | "wmv" | "flv" | "webm" | "m4v" => "Video",
|
||||||
"mp3" | "wav" | "ogg" | "flac" | "aac" | "wma" | "m4a" | "opus" => "Audio",
|
"mp3" | "wav" | "ogg" | "flac" | "aac" | "wma" | "m4a" | "opus" => "Audio",
|
||||||
"zip" | "rar" | "7z" | "tar" | "gz" | "bz2" | "xz" => "Archive",
|
"zip" | "rar" | "7z" | "tar" | "gz" | "bz2" | "xz" => "Archive",
|
||||||
"exe" | "msi" | "dmg" | "deb" | "rpm" | "appimage" => "Installer",
|
"exe" | "msi" | "dmg" | "deb" | "rpm" | "appimage" => "Installer",
|
||||||
"js" | "jsx" | "ts" | "tsx" | "py" | "rs" | "go" | "java" | "c" | "cpp" | "cs"
|
"js" | "jsx" | "ts" | "tsx" | "py" | "rs" | "go" | "java" | "c" | "cpp" | "cs"
|
||||||
| "rb" | "php" | "swift" | "kt" | "scala" | "r" | "lua" | "pl"
|
| "rb" | "php" | "swift" | "kt" | "scala" | "r" | "lua" | "pl" | "html" | "htm"
|
||||||
| "html" | "htm" | "css" | "scss" | "json" | "xml" | "yaml" | "yml"
|
| "css" | "scss" | "json" | "xml" | "yaml" | "yml" | "toml" | "sql" | "sh" | "bash"
|
||||||
| "toml" | "sql" | "sh" | "bash" | "bat" | "ps1" | "vue" | "svelte" => "Code",
|
| "bat" | "ps1" | "vue" | "svelte" => "Code",
|
||||||
"md" | "markdown" | "rst" => "Markdown",
|
"md" | "markdown" | "rst" => "Markdown",
|
||||||
_ => "Document",
|
_ => "Document",
|
||||||
};
|
};
|
||||||
@@ -360,23 +368,11 @@ pub fn format_file_size(bytes: u64) -> String {
|
|||||||
|
|
||||||
// Two decimal places, then strip trailing zeros (matches JS parseFloat behaviour)
|
// Two decimal places, then strip trailing zeros (matches JS parseFloat behaviour)
|
||||||
let formatted = format!("{:.2}", value);
|
let formatted = format!("{:.2}", value);
|
||||||
let formatted = formatted
|
let formatted = formatted.trim_end_matches('0').trim_end_matches('.');
|
||||||
.trim_end_matches('0')
|
|
||||||
.trim_end_matches('.');
|
|
||||||
|
|
||||||
format!("{} {}", formatted, SIZES[i])
|
format!("{} {}", formatted, SIZES[i])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Formats a byte count for quota display. When bytes is 0, returns "∞" (unlimited).
|
|
||||||
///
|
|
||||||
/// Matches the JavaScript `formatQuotaSize()` output.
|
|
||||||
pub fn format_quota_size(bytes: u64) -> String {
|
|
||||||
if bytes == 0 {
|
|
||||||
return "∞".to_string();
|
|
||||||
}
|
|
||||||
format_file_size(bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -392,27 +388,42 @@ mod tests {
|
|||||||
assert_eq!(format_file_size(1_073_741_824), "1 GB");
|
assert_eq!(format_file_size(1_073_741_824), "1 GB");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_format_quota_size() {
|
|
||||||
// Unlimited quota (0) should show infinity symbol
|
|
||||||
assert_eq!(format_quota_size(0), "∞");
|
|
||||||
// Non-zero values should format normally
|
|
||||||
assert_eq!(format_quota_size(500), "500 Bytes");
|
|
||||||
assert_eq!(format_quota_size(1_073_741_824), "1 GB");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_icon_class_for_with_extension_fallback() {
|
fn test_icon_class_for_with_extension_fallback() {
|
||||||
// Specific MIME types
|
// Specific MIME types
|
||||||
assert_eq!(icon_class_for("doc.pdf", "application/pdf"), "fas fa-file-pdf");
|
assert_eq!(
|
||||||
assert_eq!(icon_class_for("file.docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"), "fas fa-file-word");
|
icon_class_for("doc.pdf", "application/pdf"),
|
||||||
|
"fas fa-file-pdf"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
icon_class_for(
|
||||||
|
"file.docx",
|
||||||
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||||
|
),
|
||||||
|
"fas fa-file-word"
|
||||||
|
);
|
||||||
|
|
||||||
// Extension fallback when MIME is generic
|
// Extension fallback when MIME is generic
|
||||||
assert_eq!(icon_class_for("script.py", "application/octet-stream"), "fas fa-file-code");
|
assert_eq!(
|
||||||
assert_eq!(icon_class_for("app.dmg", "application/octet-stream"), "fas fa-hdd");
|
icon_class_for("script.py", "application/octet-stream"),
|
||||||
assert_eq!(icon_class_for("archive.zip", "application/octet-stream"), "fas fa-file-archive");
|
"fas fa-file-code"
|
||||||
assert_eq!(icon_class_for("data.xlsx", "application/octet-stream"), "fas fa-file-excel");
|
);
|
||||||
assert_eq!(icon_class_for("run.sh", "application/octet-stream"), "fas fa-terminal");
|
assert_eq!(
|
||||||
|
icon_class_for("app.dmg", "application/octet-stream"),
|
||||||
|
"fas fa-hdd"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
icon_class_for("archive.zip", "application/octet-stream"),
|
||||||
|
"fas fa-file-archive"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
icon_class_for("data.xlsx", "application/octet-stream"),
|
||||||
|
"fas fa-file-excel"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
icon_class_for("run.sh", "application/octet-stream"),
|
||||||
|
"fas fa-terminal"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -420,15 +431,36 @@ mod tests {
|
|||||||
// MIME-based
|
// MIME-based
|
||||||
assert_eq!(icon_special_class_for("", "image/png"), "image-icon");
|
assert_eq!(icon_special_class_for("", "image/png"), "image-icon");
|
||||||
assert_eq!(icon_special_class_for("", "application/pdf"), "pdf-icon");
|
assert_eq!(icon_special_class_for("", "application/pdf"), "pdf-icon");
|
||||||
assert_eq!(icon_special_class_for("", "application/json"), "code-icon json-icon");
|
assert_eq!(
|
||||||
|
icon_special_class_for("", "application/json"),
|
||||||
|
"code-icon json-icon"
|
||||||
|
);
|
||||||
|
|
||||||
// Extension-based fallback
|
// Extension-based fallback
|
||||||
assert_eq!(icon_special_class_for("main.py", "application/octet-stream"), "code-icon py-icon");
|
assert_eq!(
|
||||||
assert_eq!(icon_special_class_for("lib.rs", "application/octet-stream"), "code-icon rust-icon");
|
icon_special_class_for("main.py", "application/octet-stream"),
|
||||||
assert_eq!(icon_special_class_for("style.css", "application/octet-stream"), "code-icon css-icon");
|
"code-icon py-icon"
|
||||||
assert_eq!(icon_special_class_for("data.xlsx", "application/octet-stream"), "spreadsheet-icon");
|
);
|
||||||
assert_eq!(icon_special_class_for("backup.tar", "application/octet-stream"), "archive-icon");
|
assert_eq!(
|
||||||
assert_eq!(icon_special_class_for("setup.dmg", "application/octet-stream"), "installer-icon");
|
icon_special_class_for("lib.rs", "application/octet-stream"),
|
||||||
|
"code-icon rust-icon"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
icon_special_class_for("style.css", "application/octet-stream"),
|
||||||
|
"code-icon css-icon"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
icon_special_class_for("data.xlsx", "application/octet-stream"),
|
||||||
|
"spreadsheet-icon"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
icon_special_class_for("backup.tar", "application/octet-stream"),
|
||||||
|
"archive-icon"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
icon_special_class_for("setup.dmg", "application/octet-stream"),
|
||||||
|
"installer-icon"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -442,8 +474,14 @@ mod tests {
|
|||||||
|
|
||||||
// Extension-based fallback
|
// Extension-based fallback
|
||||||
assert_eq!(category_for("main.rs", "application/octet-stream"), "Code");
|
assert_eq!(category_for("main.rs", "application/octet-stream"), "Code");
|
||||||
assert_eq!(category_for("photo.jpg", "application/octet-stream"), "Image");
|
assert_eq!(
|
||||||
assert_eq!(category_for("notes.md", "application/octet-stream"), "Markdown");
|
category_for("photo.jpg", "application/octet-stream"),
|
||||||
|
"Image"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
category_for("notes.md", "application/octet-stream"),
|
||||||
|
"Markdown"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::display_helpers::{format_file_size, icon_class_for, icon_special_class_for, category_for};
|
use super::display_helpers::{
|
||||||
|
category_for, format_file_size, icon_class_for, icon_special_class_for,
|
||||||
|
};
|
||||||
|
|
||||||
/// DTO for favorites item, enriched with item metadata via SQL JOIN
|
/// DTO for favorites item, enriched with item metadata via SQL JOIN
|
||||||
/// so the frontend does not need N+1 requests to resolve names/sizes.
|
/// so the frontend does not need N+1 requests to resolve names/sizes.
|
||||||
@@ -23,7 +25,6 @@ pub struct FavoriteItemDto {
|
|||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
|
|
||||||
// ── Enriched metadata (resolved via JOIN) ──
|
// ── Enriched metadata (resolved via JOIN) ──
|
||||||
|
|
||||||
/// Display name of the file or folder
|
/// Display name of the file or folder
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub item_name: Option<String>,
|
pub item_name: Option<String>,
|
||||||
@@ -45,7 +46,6 @@ pub struct FavoriteItemDto {
|
|||||||
pub modified_at: Option<DateTime<Utc>>,
|
pub modified_at: Option<DateTime<Utc>>,
|
||||||
|
|
||||||
// ── Pre-computed display fields ──
|
// ── Pre-computed display fields ──
|
||||||
|
|
||||||
/// FontAwesome icon CSS class (e.g. "fas fa-file-image", "fas fa-folder")
|
/// FontAwesome icon CSS class (e.g. "fas fa-file-image", "fas fa-folder")
|
||||||
pub icon_class: String,
|
pub icon_class: String,
|
||||||
|
|
||||||
@@ -70,7 +70,10 @@ impl FavoriteItemDto {
|
|||||||
self.size_formatted = "--".to_string();
|
self.size_formatted = "--".to_string();
|
||||||
} else {
|
} else {
|
||||||
let name = self.item_name.as_deref().unwrap_or("");
|
let name = self.item_name.as_deref().unwrap_or("");
|
||||||
let mime = self.item_mime_type.as_deref().unwrap_or("application/octet-stream");
|
let mime = self
|
||||||
|
.item_mime_type
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("application/octet-stream");
|
||||||
self.icon_class = icon_class_for(name, mime).to_string();
|
self.icon_class = icon_class_for(name, mime).to_string();
|
||||||
self.icon_special_class = icon_special_class_for(name, mime).to_string();
|
self.icon_special_class = icon_special_class_for(name, mime).to_string();
|
||||||
self.category = category_for(name, mime).to_string();
|
self.category = category_for(name, mime).to_string();
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
use crate::domain::entities::file::File;
|
use crate::domain::entities::file::File;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::display_helpers::{format_file_size, icon_class_for, icon_special_class_for, category_for};
|
use super::display_helpers::{
|
||||||
|
category_for, format_file_size, icon_class_for, icon_special_class_for,
|
||||||
|
};
|
||||||
|
|
||||||
/// DTO for file responses
|
/// DTO for file responses
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -31,7 +33,6 @@ pub struct FileDto {
|
|||||||
pub modified_at: u64,
|
pub modified_at: u64,
|
||||||
|
|
||||||
// ── Pre-computed display fields ──
|
// ── Pre-computed display fields ──
|
||||||
|
|
||||||
/// FontAwesome icon CSS class (e.g. "fas fa-file-image")
|
/// FontAwesome icon CSS class (e.g. "fas fa-file-image")
|
||||||
pub icon_class: String,
|
pub icon_class: String,
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ pub struct FolderDto {
|
|||||||
pub is_root: bool,
|
pub is_root: bool,
|
||||||
|
|
||||||
// ── Pre-computed display fields ──
|
// ── Pre-computed display fields ──
|
||||||
|
|
||||||
/// FontAwesome icon CSS class (always "fas fa-folder")
|
/// FontAwesome icon CSS class (always "fas fa-folder")
|
||||||
pub icon_class: String,
|
pub icon_class: String,
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::display_helpers::{format_file_size, icon_class_for, icon_special_class_for, category_for};
|
use super::display_helpers::{
|
||||||
|
category_for, format_file_size, icon_class_for, icon_special_class_for,
|
||||||
|
};
|
||||||
|
|
||||||
/// DTO for recent items, enriched with item metadata via SQL JOIN
|
/// DTO for recent items, enriched with item metadata via SQL JOIN
|
||||||
/// so the frontend does not need N+1 requests to resolve names/sizes.
|
/// so the frontend does not need N+1 requests to resolve names/sizes.
|
||||||
@@ -23,7 +25,6 @@ pub struct RecentItemDto {
|
|||||||
pub accessed_at: DateTime<Utc>,
|
pub accessed_at: DateTime<Utc>,
|
||||||
|
|
||||||
// ── Enriched metadata (resolved via JOIN) ──
|
// ── Enriched metadata (resolved via JOIN) ──
|
||||||
|
|
||||||
/// Display name of the file or folder
|
/// Display name of the file or folder
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub item_name: Option<String>,
|
pub item_name: Option<String>,
|
||||||
@@ -41,7 +42,6 @@ pub struct RecentItemDto {
|
|||||||
pub parent_id: Option<String>,
|
pub parent_id: Option<String>,
|
||||||
|
|
||||||
// ── Pre-computed display fields ──
|
// ── Pre-computed display fields ──
|
||||||
|
|
||||||
/// FontAwesome icon CSS class (e.g. "fas fa-file-image", "fas fa-folder")
|
/// FontAwesome icon CSS class (e.g. "fas fa-file-image", "fas fa-folder")
|
||||||
pub icon_class: String,
|
pub icon_class: String,
|
||||||
|
|
||||||
@@ -66,7 +66,10 @@ impl RecentItemDto {
|
|||||||
self.size_formatted = "--".to_string();
|
self.size_formatted = "--".to_string();
|
||||||
} else {
|
} else {
|
||||||
let name = self.item_name.as_deref().unwrap_or("");
|
let name = self.item_name.as_deref().unwrap_or("");
|
||||||
let mime = self.item_mime_type.as_deref().unwrap_or("application/octet-stream");
|
let mime = self
|
||||||
|
.item_mime_type
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("application/octet-stream");
|
||||||
self.icon_class = icon_class_for(name, mime).to_string();
|
self.icon_class = icon_class_for(name, mime).to_string();
|
||||||
self.icon_special_class = icon_special_class_for(name, mime).to_string();
|
self.icon_special_class = icon_special_class_for(name, mime).to_string();
|
||||||
self.category = category_for(name, mime).to_string();
|
self.category = category_for(name, mime).to_string();
|
||||||
|
|||||||
@@ -56,9 +56,5 @@ pub trait FavoritesRepositoryPort: Send + Sync + 'static {
|
|||||||
|
|
||||||
/// Insert multiple items in a single transaction.
|
/// Insert multiple items in a single transaction.
|
||||||
/// Returns the number of rows actually inserted (ignoring duplicates).
|
/// Returns the number of rows actually inserted (ignoring duplicates).
|
||||||
async fn add_favorites_batch(
|
async fn add_favorites_batch(&self, user_id: &str, items: &[(String, String)]) -> Result<u64>;
|
||||||
&self,
|
|
||||||
user_id: &str,
|
|
||||||
items: &[(String, String)],
|
|
||||||
) -> Result<u64>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,17 +47,30 @@ pub trait FolderUseCase: Send + Sync + 'static {
|
|||||||
) -> Result<crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>, DomainError>;
|
) -> Result<crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>, DomainError>;
|
||||||
|
|
||||||
/// Renames a folder (ownership verified against caller_id)
|
/// Renames a folder (ownership verified against caller_id)
|
||||||
async fn rename_folder(&self, id: &str, dto: RenameFolderDto, caller_id: &str)
|
async fn rename_folder(
|
||||||
-> Result<FolderDto, DomainError>;
|
&self,
|
||||||
|
id: &str,
|
||||||
|
dto: RenameFolderDto,
|
||||||
|
caller_id: &str,
|
||||||
|
) -> Result<FolderDto, DomainError>;
|
||||||
|
|
||||||
/// Moves a folder to another parent (ownership verified against caller_id)
|
/// Moves a folder to another parent (ownership verified against caller_id)
|
||||||
async fn move_folder(&self, id: &str, dto: MoveFolderDto, caller_id: &str) -> Result<FolderDto, DomainError>;
|
async fn move_folder(
|
||||||
|
&self,
|
||||||
|
id: &str,
|
||||||
|
dto: MoveFolderDto,
|
||||||
|
caller_id: &str,
|
||||||
|
) -> Result<FolderDto, DomainError>;
|
||||||
|
|
||||||
/// Deletes a folder (ownership verified against caller_id)
|
/// Deletes a folder (ownership verified against caller_id)
|
||||||
async fn delete_folder(&self, id: &str, caller_id: &str) -> Result<(), DomainError>;
|
async fn delete_folder(&self, id: &str, caller_id: &str) -> Result<(), DomainError>;
|
||||||
|
|
||||||
/// Creates a root-level home folder for a user during registration.
|
/// Creates a root-level home folder for a user during registration.
|
||||||
async fn create_home_folder(&self, user_id: &str, name: String) -> Result<FolderDto, DomainError>;
|
async fn create_home_folder(
|
||||||
|
&self,
|
||||||
|
user_id: &str,
|
||||||
|
name: String,
|
||||||
|
) -> Result<FolderDto, DomainError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -317,7 +317,8 @@ impl AuthApplicationService {
|
|||||||
let created_user = self.user_storage.create_user(user).await?;
|
let created_user = self.user_storage.create_user(user).await?;
|
||||||
|
|
||||||
// Create personal folder for the user
|
// Create personal folder for the user
|
||||||
self.create_personal_folder(&dto.username, created_user.id()).await;
|
self.create_personal_folder(&dto.username, created_user.id())
|
||||||
|
.await;
|
||||||
|
|
||||||
tracing::info!("User registered: {}", created_user.id());
|
tracing::info!("User registered: {}", created_user.id());
|
||||||
Ok(UserDto::from(created_user))
|
Ok(UserDto::from(created_user))
|
||||||
@@ -659,7 +660,8 @@ impl AuthApplicationService {
|
|||||||
let created_user = self.user_storage.create_user(user).await?;
|
let created_user = self.user_storage.create_user(user).await?;
|
||||||
|
|
||||||
// 5. Create personal folder for the new admin
|
// 5. Create personal folder for the new admin
|
||||||
self.create_personal_folder(&dto.username, created_user.id()).await;
|
self.create_personal_folder(&dto.username, created_user.id())
|
||||||
|
.await;
|
||||||
|
|
||||||
tracing::info!("Custom admin created: {}", created_user.id());
|
tracing::info!("Custom admin created: {}", created_user.id());
|
||||||
Ok(UserDto::from(created_user))
|
Ok(UserDto::from(created_user))
|
||||||
@@ -765,7 +767,8 @@ impl AuthApplicationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create personal folder
|
// Create personal folder
|
||||||
self.create_personal_folder(&dto.username, created.id()).await;
|
self.create_personal_folder(&dto.username, created.id())
|
||||||
|
.await;
|
||||||
|
|
||||||
tracing::info!("Admin created user: {} ({})", dto.username, created.id());
|
tracing::info!("Admin created user: {} ({})", dto.username, created.id());
|
||||||
Ok(UserDto::from(created))
|
Ok(UserDto::from(created))
|
||||||
|
|||||||
@@ -702,8 +702,7 @@ impl BatchOperationService {
|
|||||||
// Add individual files at the root of the ZIP
|
// Add individual files at the root of the ZIP
|
||||||
for file_id in &file_ids {
|
for file_id in &file_ids {
|
||||||
match self.file_retrieval.get_file(file_id).await {
|
match self.file_retrieval.get_file(file_id).await {
|
||||||
Ok(file_dto) => {
|
Ok(file_dto) => match self.file_retrieval.get_file_content(file_id).await {
|
||||||
match self.file_retrieval.get_file_content(file_id).await {
|
|
||||||
Ok(content) => {
|
Ok(content) => {
|
||||||
if let Err(e) = zip.start_file(&file_dto.name, options) {
|
if let Err(e) = zip.start_file(&file_dto.name, options) {
|
||||||
info!("Could not start zip entry for {}: {}", file_dto.name, e);
|
info!("Could not start zip entry for {}: {}", file_dto.name, e);
|
||||||
@@ -716,8 +715,7 @@ impl BatchOperationService {
|
|||||||
Err(e) => {
|
Err(e) => {
|
||||||
info!("Could not read file content {}: {}", file_id, e);
|
info!("Could not read file content {}: {}", file_id, e);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
info!("Could not get file metadata {}: {}", file_id, e);
|
info!("Could not get file metadata {}: {}", file_id, e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
use crate::application::dtos::favorites_dto::{BatchFavoritesResult, BatchFavoritesStats, FavoriteItemDto};
|
use crate::application::dtos::favorites_dto::{
|
||||||
|
BatchFavoritesResult, BatchFavoritesStats, FavoriteItemDto,
|
||||||
|
};
|
||||||
use crate::application::ports::favorites_ports::{FavoritesRepositoryPort, FavoritesUseCase};
|
use crate::application::ports::favorites_ports::{FavoritesRepositoryPort, FavoritesUseCase};
|
||||||
use crate::common::errors::{DomainError, ErrorKind, Result};
|
use crate::common::errors::{DomainError, ErrorKind, Result};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|||||||
@@ -112,7 +112,11 @@ impl FolderService {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_home_folder(&self, _user_id: &str, _name: String) -> Result<FolderDto, DomainError> {
|
async fn create_home_folder(
|
||||||
|
&self,
|
||||||
|
_user_id: &str,
|
||||||
|
_name: String,
|
||||||
|
) -> Result<FolderDto, DomainError> {
|
||||||
Ok(FolderDto::empty())
|
Ok(FolderDto::empty())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -159,7 +163,11 @@ impl FolderUseCase for FolderService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a root-level home folder for a user during registration.
|
/// Creates a root-level home folder for a user during registration.
|
||||||
async fn create_home_folder(&self, user_id: &str, name: String) -> Result<FolderDto, DomainError> {
|
async fn create_home_folder(
|
||||||
|
&self,
|
||||||
|
user_id: &str,
|
||||||
|
name: String,
|
||||||
|
) -> Result<FolderDto, DomainError> {
|
||||||
let folder = self
|
let folder = self
|
||||||
.folder_storage
|
.folder_storage
|
||||||
.create_home_folder(user_id, name)
|
.create_home_folder(user_id, name)
|
||||||
@@ -256,12 +264,7 @@ impl FolderUseCase for FolderService {
|
|||||||
|
|
||||||
let (folders, total_items) = self
|
let (folders, total_items) = self
|
||||||
.folder_storage
|
.folder_storage
|
||||||
.list_folders_paginated(
|
.list_folders_paginated(parent_id, pagination.offset(), pagination.limit(), true)
|
||||||
parent_id,
|
|
||||||
pagination.offset(),
|
|
||||||
pagination.limit(),
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
DomainError::internal_error(
|
DomainError::internal_error(
|
||||||
@@ -354,7 +357,9 @@ impl FolderUseCase for FolderService {
|
|||||||
if existing_folder.owner_id() != Some(caller_id) {
|
if existing_folder.owner_id() != Some(caller_id) {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
"rename_folder: user '{}' attempted to rename folder '{}' owned by '{:?}'",
|
"rename_folder: user '{}' attempted to rename folder '{}' owned by '{:?}'",
|
||||||
caller_id, id, existing_folder.owner_id()
|
caller_id,
|
||||||
|
id,
|
||||||
|
existing_folder.owner_id()
|
||||||
);
|
);
|
||||||
return Err(DomainError::not_found("Folder", id));
|
return Err(DomainError::not_found("Folder", id));
|
||||||
}
|
}
|
||||||
@@ -412,7 +417,12 @@ impl FolderUseCase for FolderService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Moves a folder to a new parent after verifying ownership.
|
/// Moves a folder to a new parent after verifying ownership.
|
||||||
async fn move_folder(&self, id: &str, dto: MoveFolderDto, caller_id: &str) -> Result<FolderDto, DomainError> {
|
async fn move_folder(
|
||||||
|
&self,
|
||||||
|
id: &str,
|
||||||
|
dto: MoveFolderDto,
|
||||||
|
caller_id: &str,
|
||||||
|
) -> Result<FolderDto, DomainError> {
|
||||||
// Verify the source folder exists and belongs to the caller
|
// Verify the source folder exists and belongs to the caller
|
||||||
let source_folder = self.folder_storage.get_folder(id).await.map_err(|e| {
|
let source_folder = self.folder_storage.get_folder(id).await.map_err(|e| {
|
||||||
DomainError::internal_error(
|
DomainError::internal_error(
|
||||||
@@ -424,7 +434,9 @@ impl FolderUseCase for FolderService {
|
|||||||
if source_folder.owner_id() != Some(caller_id) {
|
if source_folder.owner_id() != Some(caller_id) {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
"move_folder: user '{}' attempted to move folder '{}' owned by '{:?}'",
|
"move_folder: user '{}' attempted to move folder '{}' owned by '{:?}'",
|
||||||
caller_id, id, source_folder.owner_id()
|
caller_id,
|
||||||
|
id,
|
||||||
|
source_folder.owner_id()
|
||||||
);
|
);
|
||||||
return Err(DomainError::not_found("Folder", id));
|
return Err(DomainError::not_found("Folder", id));
|
||||||
}
|
}
|
||||||
@@ -517,7 +529,9 @@ impl FolderUseCase for FolderService {
|
|||||||
if folder.owner_id() != Some(caller_id) {
|
if folder.owner_id() != Some(caller_id) {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
"delete_folder: user '{}' attempted to delete folder '{}' owned by '{:?}'",
|
"delete_folder: user '{}' attempted to delete folder '{}' owned by '{:?}'",
|
||||||
caller_id, id, folder.owner_id()
|
caller_id,
|
||||||
|
id,
|
||||||
|
folder.owner_id()
|
||||||
);
|
);
|
||||||
return Err(DomainError::not_found("Folder", id));
|
return Err(DomainError::not_found("Folder", id));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ use std::sync::Mutex;
|
|||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use tokio::time;
|
use tokio::time;
|
||||||
|
|
||||||
|
use crate::application::dtos::display_helpers::{
|
||||||
|
category_for, icon_class_for, icon_special_class_for,
|
||||||
|
};
|
||||||
use crate::application::dtos::file_dto::FileDto;
|
use crate::application::dtos::file_dto::FileDto;
|
||||||
use crate::application::dtos::folder_dto::FolderDto;
|
use crate::application::dtos::folder_dto::FolderDto;
|
||||||
use crate::application::dtos::display_helpers::{icon_class_for, icon_special_class_for, category_for};
|
|
||||||
use crate::application::dtos::search_dto::{
|
use crate::application::dtos::search_dto::{
|
||||||
SearchCriteriaDto, SearchFileResultDto, SearchFolderResultDto, SearchResultsDto,
|
SearchCriteriaDto, SearchFileResultDto, SearchFolderResultDto, SearchResultsDto,
|
||||||
SearchSuggestionItem, SearchSuggestionsDto,
|
SearchSuggestionItem, SearchSuggestionsDto,
|
||||||
@@ -287,12 +289,12 @@ impl SearchService {
|
|||||||
folder_repo: Arc<dyn FolderStoragePort>,
|
folder_repo: Arc<dyn FolderStoragePort>,
|
||||||
current_folder_id: Option<String>,
|
current_folder_id: Option<String>,
|
||||||
criteria: Arc<SearchCriteriaDto>,
|
criteria: Arc<SearchCriteriaDto>,
|
||||||
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(Vec<FileDto>, Vec<FolderDto>)>> + Send>> {
|
) -> std::pin::Pin<
|
||||||
|
Box<dyn std::future::Future<Output = Result<(Vec<FileDto>, Vec<FolderDto>)>> + Send>,
|
||||||
|
> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
// List files in the current folder
|
// List files in the current folder
|
||||||
let files = file_repo
|
let files = file_repo.list_files(current_folder_id.as_deref()).await?;
|
||||||
.list_files(current_folder_id.as_deref())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let filtered_files: Vec<FileDto> = files
|
let filtered_files: Vec<FileDto> = files
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -560,13 +562,11 @@ impl SearchUseCase for SearchService {
|
|||||||
match criteria.sort_by.as_str() {
|
match criteria.sort_by.as_str() {
|
||||||
"name" => {
|
"name" => {
|
||||||
enriched_files.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
|
enriched_files.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
|
||||||
enriched_folders
|
enriched_folders.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
|
||||||
.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
|
|
||||||
}
|
}
|
||||||
"name_desc" => {
|
"name_desc" => {
|
||||||
enriched_files.sort_by(|a, b| b.name.to_lowercase().cmp(&a.name.to_lowercase()));
|
enriched_files.sort_by(|a, b| b.name.to_lowercase().cmp(&a.name.to_lowercase()));
|
||||||
enriched_folders
|
enriched_folders.sort_by(|a, b| b.name.to_lowercase().cmp(&a.name.to_lowercase()));
|
||||||
.sort_by(|a, b| b.name.to_lowercase().cmp(&a.name.to_lowercase()));
|
|
||||||
}
|
}
|
||||||
"date" => {
|
"date" => {
|
||||||
enriched_files.sort_by(|a, b| a.modified_at.cmp(&b.modified_at));
|
enriched_files.sort_by(|a, b| a.modified_at.cmp(&b.modified_at));
|
||||||
|
|||||||
+14
-3
@@ -19,7 +19,9 @@ use crate::application::dtos::folder_dto::{
|
|||||||
CreateFolderDto, FolderDto, MoveFolderDto, RenameFolderDto,
|
CreateFolderDto, FolderDto, MoveFolderDto, RenameFolderDto,
|
||||||
};
|
};
|
||||||
use crate::application::dtos::pagination::{PaginatedResponseDto, PaginationRequestDto};
|
use crate::application::dtos::pagination::{PaginatedResponseDto, PaginationRequestDto};
|
||||||
use crate::application::dtos::search_dto::{SearchCriteriaDto, SearchResultsDto, SearchSuggestionsDto};
|
use crate::application::dtos::search_dto::{
|
||||||
|
SearchCriteriaDto, SearchResultsDto, SearchSuggestionsDto,
|
||||||
|
};
|
||||||
use crate::application::ports::compression_ports::{CompressionLevel, CompressionPort};
|
use crate::application::ports::compression_ports::{CompressionLevel, CompressionPort};
|
||||||
use crate::application::ports::file_ports::{
|
use crate::application::ports::file_ports::{
|
||||||
FileManagementUseCase, FileRetrievalUseCase, FileUploadUseCase, FileUseCaseFactory,
|
FileManagementUseCase, FileRetrievalUseCase, FileUploadUseCase, FileUseCaseFactory,
|
||||||
@@ -415,7 +417,12 @@ impl FolderUseCase for StubFolderUseCase {
|
|||||||
Ok(FolderDto::default())
|
Ok(FolderDto::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn move_folder(&self, _id: &str, _dto: MoveFolderDto, _caller_id: &str) -> Result<FolderDto, DomainError> {
|
async fn move_folder(
|
||||||
|
&self,
|
||||||
|
_id: &str,
|
||||||
|
_dto: MoveFolderDto,
|
||||||
|
_caller_id: &str,
|
||||||
|
) -> Result<FolderDto, DomainError> {
|
||||||
Ok(FolderDto::default())
|
Ok(FolderDto::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -423,7 +430,11 @@ impl FolderUseCase for StubFolderUseCase {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_home_folder(&self, _user_id: &str, _name: String) -> Result<FolderDto, DomainError> {
|
async fn create_home_folder(
|
||||||
|
&self,
|
||||||
|
_user_id: &str,
|
||||||
|
_name: String,
|
||||||
|
) -> Result<FolderDto, DomainError> {
|
||||||
Ok(FolderDto::default())
|
Ok(FolderDto::default())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,7 +102,15 @@ impl Folder {
|
|||||||
created_at: u64,
|
created_at: u64,
|
||||||
modified_at: u64,
|
modified_at: u64,
|
||||||
) -> FolderResult<Self> {
|
) -> FolderResult<Self> {
|
||||||
Self::with_timestamps_and_owner(id, name, storage_path, parent_id, None, created_at, modified_at)
|
Self::with_timestamps_and_owner(
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
storage_path,
|
||||||
|
parent_id,
|
||||||
|
None,
|
||||||
|
created_at,
|
||||||
|
modified_at,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a folder with specific timestamps and owner (for DB reconstruction)
|
/// Creates a folder with specific timestamps and owner (for DB reconstruction)
|
||||||
|
|||||||
@@ -102,9 +102,5 @@ pub trait FolderRepository: Send + Sync + 'static {
|
|||||||
|
|
||||||
/// Creates a root-level home folder for a user.
|
/// Creates a root-level home folder for a user.
|
||||||
/// This is used during user registration to create the user's personal folder.
|
/// This is used during user registration to create the user's personal folder.
|
||||||
async fn create_home_folder(
|
async fn create_home_folder(&self, user_id: &str, name: String) -> Result<Folder, DomainError>;
|
||||||
&self,
|
|
||||||
user_id: &str,
|
|
||||||
name: String,
|
|
||||||
) -> Result<Folder, DomainError>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,8 @@ impl FavoritesRepositoryPort for FavoritesPgRepository {
|
|||||||
|
|
||||||
let favorites = rows
|
let favorites = rows
|
||||||
.iter()
|
.iter()
|
||||||
.map(|row| FavoriteItemDto {
|
.map(|row| {
|
||||||
|
FavoriteItemDto {
|
||||||
id: row.get("id"),
|
id: row.get("id"),
|
||||||
user_id: row.get("user_id"),
|
user_id: row.get("user_id"),
|
||||||
item_id: row.get("item_id"),
|
item_id: row.get("item_id"),
|
||||||
@@ -76,7 +77,9 @@ impl FavoritesRepositoryPort for FavoritesPgRepository {
|
|||||||
icon_special_class: String::new(),
|
icon_special_class: String::new(),
|
||||||
category: String::new(),
|
category: String::new(),
|
||||||
size_formatted: String::new(),
|
size_formatted: String::new(),
|
||||||
}.with_display_fields())
|
}
|
||||||
|
.with_display_fields()
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Ok(favorites)
|
Ok(favorites)
|
||||||
@@ -163,11 +166,7 @@ impl FavoritesRepositoryPort for FavoritesPgRepository {
|
|||||||
Ok(row.try_get("is_favorite").unwrap_or(false))
|
Ok(row.try_get("is_favorite").unwrap_or(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn add_favorites_batch(
|
async fn add_favorites_batch(&self, user_id: &str, items: &[(String, String)]) -> Result<u64> {
|
||||||
&self,
|
|
||||||
user_id: &str,
|
|
||||||
items: &[(String, String)],
|
|
||||||
) -> Result<u64> {
|
|
||||||
if items.is_empty() {
|
if items.is_empty() {
|
||||||
return Ok(0);
|
return Ok(0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,8 +140,16 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn list_files(&self, folder_id: Option<&str>) -> Result<Vec<File>, DomainError> {
|
async fn list_files(&self, folder_id: Option<&str>) -> Result<Vec<File>, DomainError> {
|
||||||
let rows: Vec<(String, String, Option<String>, Option<String>, i64, String, i64, i64)> =
|
let rows: Vec<(
|
||||||
if let Some(fid) = folder_id {
|
String,
|
||||||
|
String,
|
||||||
|
Option<String>,
|
||||||
|
Option<String>,
|
||||||
|
i64,
|
||||||
|
String,
|
||||||
|
i64,
|
||||||
|
i64,
|
||||||
|
)> = if let Some(fid) = folder_id {
|
||||||
sqlx::query_as(
|
sqlx::query_as(
|
||||||
r#"
|
r#"
|
||||||
SELECT fi.id::text, fi.name, fi.folder_id::text, fo.path,
|
SELECT fi.id::text, fi.name, fi.folder_id::text, fo.path,
|
||||||
@@ -289,7 +297,19 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
|
|
||||||
let row = if folder_path.is_empty() {
|
let row = if folder_path.is_empty() {
|
||||||
// File at root level (no parent folder)
|
// File at root level (no parent folder)
|
||||||
sqlx::query_as::<_, (String, String, Option<String>, Option<String>, i64, String, i64, i64)>(
|
sqlx::query_as::<
|
||||||
|
_,
|
||||||
|
(
|
||||||
|
String,
|
||||||
|
String,
|
||||||
|
Option<String>,
|
||||||
|
Option<String>,
|
||||||
|
i64,
|
||||||
|
String,
|
||||||
|
i64,
|
||||||
|
i64,
|
||||||
|
),
|
||||||
|
>(
|
||||||
r#"
|
r#"
|
||||||
SELECT fi.id::text, fi.name, fi.folder_id::text, fo.path,
|
SELECT fi.id::text, fi.name, fi.folder_id::text, fo.path,
|
||||||
fi.size, fi.mime_type,
|
fi.size, fi.mime_type,
|
||||||
@@ -305,7 +325,19 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
.await
|
.await
|
||||||
} else {
|
} else {
|
||||||
// File inside a folder — look up by folder path + filename
|
// File inside a folder — look up by folder path + filename
|
||||||
sqlx::query_as::<_, (String, String, Option<String>, Option<String>, i64, String, i64, i64)>(
|
sqlx::query_as::<
|
||||||
|
_,
|
||||||
|
(
|
||||||
|
String,
|
||||||
|
String,
|
||||||
|
Option<String>,
|
||||||
|
Option<String>,
|
||||||
|
i64,
|
||||||
|
String,
|
||||||
|
i64,
|
||||||
|
i64,
|
||||||
|
),
|
||||||
|
>(
|
||||||
r#"
|
r#"
|
||||||
SELECT fi.id::text, fi.name, fi.folder_id::text, fo.path,
|
SELECT fi.id::text, fi.name, fi.folder_id::text, fo.path,
|
||||||
fi.size, fi.mime_type,
|
fi.size, fi.mime_type,
|
||||||
|
|||||||
@@ -55,14 +55,16 @@ impl FileBlobWriteRepository {
|
|||||||
) -> Result<Option<String>, DomainError> {
|
) -> Result<Option<String>, DomainError> {
|
||||||
match folder_id {
|
match folder_id {
|
||||||
Some(fid) => {
|
Some(fid) => {
|
||||||
let path: String = sqlx::query_scalar(
|
let path: String =
|
||||||
"SELECT path FROM storage.folders WHERE id = $1::uuid",
|
sqlx::query_scalar("SELECT path FROM storage.folders WHERE id = $1::uuid")
|
||||||
)
|
|
||||||
.bind(fid)
|
.bind(fid)
|
||||||
.fetch_optional(self.pool.as_ref())
|
.fetch_optional(self.pool.as_ref())
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
DomainError::internal_error("FileBlobWrite", format!("folder path: {e}"))
|
DomainError::internal_error(
|
||||||
|
"FileBlobWrite",
|
||||||
|
format!("folder path: {e}"),
|
||||||
|
)
|
||||||
})?
|
})?
|
||||||
.ok_or_else(|| DomainError::not_found("Folder", fid))?;
|
.ok_or_else(|| DomainError::not_found("Folder", fid))?;
|
||||||
Ok(Some(path))
|
Ok(Some(path))
|
||||||
@@ -179,7 +181,16 @@ impl FileWritePort for FileBlobWriteRepository {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let folder_path = self.lookup_folder_path(folder_id.as_deref()).await?;
|
let folder_path = self.lookup_folder_path(folder_id.as_deref()).await?;
|
||||||
Self::row_to_file(row.0, name, folder_id, folder_path, size, content_type, row.1, row.2)
|
Self::row_to_file(
|
||||||
|
row.0,
|
||||||
|
name,
|
||||||
|
folder_id,
|
||||||
|
folder_path,
|
||||||
|
size,
|
||||||
|
content_type,
|
||||||
|
row.1,
|
||||||
|
row.2,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn save_file_from_temp(
|
async fn save_file_from_temp(
|
||||||
|
|||||||
@@ -175,7 +175,8 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn list_folders(&self, parent_id: Option<&str>) -> Result<Vec<Folder>, DomainError> {
|
async fn list_folders(&self, parent_id: Option<&str>) -> Result<Vec<Folder>, DomainError> {
|
||||||
let rows: Vec<(String, String, String, Option<String>, String, i64, i64)> = if let Some(pid) = parent_id {
|
let rows: Vec<(String, String, String, Option<String>, String, i64, i64)> =
|
||||||
|
if let Some(pid) = parent_id {
|
||||||
sqlx::query_as(
|
sqlx::query_as(
|
||||||
r#"
|
r#"
|
||||||
SELECT id::text, name, path, parent_id::text, user_id,
|
SELECT id::text, name, path, parent_id::text, user_id,
|
||||||
@@ -217,7 +218,8 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
parent_id: Option<&str>,
|
parent_id: Option<&str>,
|
||||||
owner_id: &str,
|
owner_id: &str,
|
||||||
) -> Result<Vec<Folder>, DomainError> {
|
) -> Result<Vec<Folder>, DomainError> {
|
||||||
let rows: Vec<(String, String, String, Option<String>, String, i64, i64)> = if let Some(pid) = parent_id {
|
let rows: Vec<(String, String, String, Option<String>, String, i64, i64)> =
|
||||||
|
if let Some(pid) = parent_id {
|
||||||
sqlx::query_as(
|
sqlx::query_as(
|
||||||
r#"
|
r#"
|
||||||
SELECT id::text, name, path, parent_id::text, user_id,
|
SELECT id::text, name, path, parent_id::text, user_id,
|
||||||
@@ -284,7 +286,8 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let rows: Vec<(String, String, String, Option<String>, String, i64, i64)> = if let Some(pid) = parent_id {
|
let rows: Vec<(String, String, String, Option<String>, String, i64, i64)> =
|
||||||
|
if let Some(pid) = parent_id {
|
||||||
sqlx::query_as(
|
sqlx::query_as(
|
||||||
r#"
|
r#"
|
||||||
SELECT id::text, name, path, parent_id::text, user_id,
|
SELECT id::text, name, path, parent_id::text, user_id,
|
||||||
@@ -360,7 +363,8 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let rows: Vec<(String, String, String, Option<String>, String, i64, i64)> = if let Some(pid) = parent_id {
|
let rows: Vec<(String, String, String, Option<String>, String, i64, i64)> =
|
||||||
|
if let Some(pid) = parent_id {
|
||||||
sqlx::query_as(
|
sqlx::query_as(
|
||||||
r#"
|
r#"
|
||||||
SELECT id::text, name, path, parent_id::text, user_id,
|
SELECT id::text, name, path, parent_id::text, user_id,
|
||||||
@@ -396,7 +400,9 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
.fetch_all(self.pool())
|
.fetch_all(self.pool())
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
.map_err(|e| DomainError::internal_error("FolderDb", format!("paginate_by_owner: {e}")))?;
|
.map_err(|e| {
|
||||||
|
DomainError::internal_error("FolderDb", format!("paginate_by_owner: {e}"))
|
||||||
|
})?;
|
||||||
|
|
||||||
let folders: Result<Vec<Folder>, DomainError> = rows
|
let folders: Result<Vec<Folder>, DomainError> = rows
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -503,9 +509,8 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn get_folder_path(&self, id: &str) -> Result<StoragePath, DomainError> {
|
async fn get_folder_path(&self, id: &str) -> Result<StoragePath, DomainError> {
|
||||||
let path: String = sqlx::query_scalar(
|
let path: String =
|
||||||
"SELECT path FROM storage.folders WHERE id = $1::uuid",
|
sqlx::query_scalar("SELECT path FROM storage.folders WHERE id = $1::uuid")
|
||||||
)
|
|
||||||
.bind(id)
|
.bind(id)
|
||||||
.fetch_optional(self.pool())
|
.fetch_optional(self.pool())
|
||||||
.await
|
.await
|
||||||
@@ -615,11 +620,7 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_home_folder(
|
async fn create_home_folder(&self, user_id: &str, name: String) -> Result<Folder, DomainError> {
|
||||||
&self,
|
|
||||||
user_id: &str,
|
|
||||||
name: String,
|
|
||||||
) -> Result<Folder, DomainError> {
|
|
||||||
let row = sqlx::query_as::<_, (String, String, i64, i64)>(
|
let row = sqlx::query_as::<_, (String, String, i64, i64)>(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO storage.folders (name, parent_id, user_id)
|
INSERT INTO storage.folders (name, parent_id, user_id)
|
||||||
@@ -638,7 +639,15 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
.map_err(|e| DomainError::internal_error("FolderDb", format!("home folder: {e}")))?;
|
.map_err(|e| DomainError::internal_error("FolderDb", format!("home folder: {e}")))?;
|
||||||
|
|
||||||
match row {
|
match row {
|
||||||
Some((id, path, ca, ma)) => Self::row_to_folder(id, name.clone(), path, None, Some(user_id.to_string()), ca, ma),
|
Some((id, path, ca, ma)) => Self::row_to_folder(
|
||||||
|
id,
|
||||||
|
name.clone(),
|
||||||
|
path,
|
||||||
|
None,
|
||||||
|
Some(user_id.to_string()),
|
||||||
|
ca,
|
||||||
|
ma,
|
||||||
|
),
|
||||||
None => {
|
None => {
|
||||||
// Already exists — fetch it
|
// Already exists — fetch it
|
||||||
let existing = sqlx::query_as::<_, (String, String, i64, i64)>(
|
let existing = sqlx::query_as::<_, (String, String, i64, i64)>(
|
||||||
@@ -656,7 +665,15 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
.fetch_one(self.pool())
|
.fetch_one(self.pool())
|
||||||
.await
|
.await
|
||||||
.map_err(|e| DomainError::internal_error("FolderDb", format!("home fetch: {e}")))?;
|
.map_err(|e| DomainError::internal_error("FolderDb", format!("home fetch: {e}")))?;
|
||||||
Self::row_to_folder(existing.0, name, existing.1, None, Some(user_id.to_string()), existing.2, existing.3)
|
Self::row_to_folder(
|
||||||
|
existing.0,
|
||||||
|
name,
|
||||||
|
existing.1,
|
||||||
|
None,
|
||||||
|
Some(user_id.to_string()),
|
||||||
|
existing.2,
|
||||||
|
existing.3,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,8 @@ impl RecentItemsRepositoryPort for RecentItemsPgRepository {
|
|||||||
|
|
||||||
let items = rows
|
let items = rows
|
||||||
.iter()
|
.iter()
|
||||||
.map(|row| RecentItemDto {
|
.map(|row| {
|
||||||
|
RecentItemDto {
|
||||||
id: row.get("id"),
|
id: row.get("id"),
|
||||||
user_id: row.get("user_id"),
|
user_id: row.get("user_id"),
|
||||||
item_id: row.get("item_id"),
|
item_id: row.get("item_id"),
|
||||||
@@ -76,7 +77,9 @@ impl RecentItemsRepositoryPort for RecentItemsPgRepository {
|
|||||||
icon_special_class: String::new(),
|
icon_special_class: String::new(),
|
||||||
category: String::new(),
|
category: String::new(),
|
||||||
size_formatted: String::new(),
|
size_formatted: String::new(),
|
||||||
}.with_display_fields())
|
}
|
||||||
|
.with_display_fields()
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Ok(items)
|
Ok(items)
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
use axum::{Json, extract::{Path, State}, http::StatusCode, response::IntoResponse};
|
use axum::{
|
||||||
|
Json,
|
||||||
|
extract::{Path, State},
|
||||||
|
http::StatusCode,
|
||||||
|
response::IntoResponse,
|
||||||
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
|
|||||||
@@ -90,9 +90,12 @@ impl FolderHandler {
|
|||||||
if owner != &auth_user.id {
|
if owner != &auth_user.id {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
"get_folder: user '{}' attempted to access folder '{}' owned by '{}'",
|
"get_folder: user '{}' attempted to access folder '{}' owned by '{}'",
|
||||||
auth_user.id, id, owner
|
auth_user.id,
|
||||||
|
id,
|
||||||
|
owner
|
||||||
);
|
);
|
||||||
return (StatusCode::NOT_FOUND, "Folder not found".to_string()).into_response();
|
return (StatusCode::NOT_FOUND, "Folder not found".to_string())
|
||||||
|
.into_response();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(StatusCode::OK, Json(folder)).into_response()
|
(StatusCode::OK, Json(folder)).into_response()
|
||||||
@@ -144,7 +147,10 @@ impl FolderHandler {
|
|||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
pagination: Query<PaginationRequestDto>,
|
pagination: Query<PaginationRequestDto>,
|
||||||
) -> axum::response::Response {
|
) -> axum::response::Response {
|
||||||
match service.list_folders_for_owner_paginated(Some(&id), &auth_user.id, &pagination).await {
|
match service
|
||||||
|
.list_folders_for_owner_paginated(Some(&id), &auth_user.id, &pagination)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(paginated_result) => (StatusCode::OK, Json(paginated_result)).into_response(),
|
Ok(paginated_result) => (StatusCode::OK, Json(paginated_result)).into_response(),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let status = match err.kind {
|
let status = match err.kind {
|
||||||
@@ -168,7 +174,10 @@ impl FolderHandler {
|
|||||||
parent_id: Option<&str>,
|
parent_id: Option<&str>,
|
||||||
auth_user: &AuthUser,
|
auth_user: &AuthUser,
|
||||||
) -> axum::response::Response {
|
) -> axum::response::Response {
|
||||||
match service.list_folders_for_owner(parent_id, &auth_user.id).await {
|
match service
|
||||||
|
.list_folders_for_owner(parent_id, &auth_user.id)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(folders) => (StatusCode::OK, Json(folders)).into_response(),
|
Ok(folders) => (StatusCode::OK, Json(folders)).into_response(),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let status = match err.kind {
|
let status = match err.kind {
|
||||||
@@ -360,7 +369,9 @@ impl FolderHandler {
|
|||||||
if folder.owner_id.as_deref() != Some(&auth_user.id) {
|
if folder.owner_id.as_deref() != Some(&auth_user.id) {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
"download_folder_zip: user '{}' attempted to download folder '{}' owned by '{:?}'",
|
"download_folder_zip: user '{}' attempted to download folder '{}' owned by '{:?}'",
|
||||||
auth_user.id, id, folder.owner_id
|
auth_user.id,
|
||||||
|
id,
|
||||||
|
folder.owner_id
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
StatusCode::NOT_FOUND,
|
StatusCode::NOT_FOUND,
|
||||||
|
|||||||
@@ -765,7 +765,11 @@ async fn handle_move(
|
|||||||
};
|
};
|
||||||
|
|
||||||
folder_service
|
folder_service
|
||||||
.move_folder(&folder.id, move_dto, folder.owner_id.as_deref().unwrap_or("webdav"))
|
.move_folder(
|
||||||
|
&folder.id,
|
||||||
|
move_dto,
|
||||||
|
folder.owner_id.as_deref().unwrap_or("webdav"),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to move folder: {}", e)))?;
|
.map_err(|e| AppError::internal_error(format!("Failed to move folder: {}", e)))?;
|
||||||
|
|
||||||
@@ -775,7 +779,11 @@ async fn handle_move(
|
|||||||
};
|
};
|
||||||
|
|
||||||
folder_service
|
folder_service
|
||||||
.rename_folder(&folder.id, rename_dto, folder.owner_id.as_deref().unwrap_or("webdav"))
|
.rename_folder(
|
||||||
|
&folder.id,
|
||||||
|
rename_dto,
|
||||||
|
folder.owner_id.as_deref().unwrap_or("webdav"),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to rename folder: {}", e)))?;
|
.map_err(|e| AppError::internal_error(format!("Failed to rename folder: {}", e)))?;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user