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:
Jan Wiebe
2026-02-21 13:33:18 +01:00
parent d325d2317d
commit 807370e194
23 changed files with 902 additions and 731 deletions
+94 -56
View File
@@ -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]
+7 -4
View File
@@ -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();
+3 -2
View File
@@ -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,
-1
View File
@@ -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,
+7 -4
View File
@@ -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();
+1 -5
View File
@@ -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>;
} }
+17 -4
View File
@@ -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))
+11 -13
View File
@@ -702,22 +702,20 @@ 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); continue;
continue;
}
if let Err(e) = zip.write_all(&content) {
info!("Could not write zip entry for {}: {}", file_dto.name, e);
}
} }
Err(e) => { if let Err(e) = zip.write_all(&content) {
info!("Could not read file content {}: {}", file_id, e); info!("Could not write zip entry for {}: {}", file_dto.name, e);
} }
} }
} Err(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;
+26 -12
View File
@@ -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));
} }
+54 -54
View File
@@ -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,67 +289,67 @@ 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()
.map(FileDto::from)
.filter(|file| passes_file_filter(file, &criteria))
.collect();
let mut all_files = filtered_files;
let mut all_folders: Vec<FolderDto> = Vec::new();
// If recursive, process subfolders in parallel
if criteria.recursive {
let folders = folder_repo
.list_folders(current_folder_id.as_deref())
.await?;
let folder_dtos: Vec<FolderDto> = folders
.into_iter() .into_iter()
.map(FolderDto::from) .map(FileDto::from)
.filter(|f| passes_folder_filter(f, &criteria)) .filter(|file| passes_file_filter(file, &criteria))
.collect(); .collect();
all_folders.extend(folder_dtos.iter().cloned()); let mut all_files = filtered_files;
let mut all_folders: Vec<FolderDto> = Vec::new();
// Spawn parallel tasks for each subfolder // If recursive, process subfolders in parallel
let mut handles = Vec::with_capacity(folder_dtos.len()); if criteria.recursive {
for subfolder in &folder_dtos { let folders = folder_repo
let fr = file_repo.clone(); .list_folders(current_folder_id.as_deref())
let fdr = folder_repo.clone(); .await?;
let crit = criteria.clone();
let folder_id = subfolder.id.clone();
handles.push(tokio::spawn(async move { let folder_dtos: Vec<FolderDto> = folders
Self::search_parallel(fr, fdr, Some(folder_id), crit).await .into_iter()
})); .map(FolderDto::from)
} .filter(|f| passes_folder_filter(f, &criteria))
.collect();
// Collect results from all parallel tasks all_folders.extend(folder_dtos.iter().cloned());
for handle in handles {
match handle.await { // Spawn parallel tasks for each subfolder
Ok(Ok((sub_files, sub_folders))) => { let mut handles = Vec::with_capacity(folder_dtos.len());
all_files.extend(sub_files); for subfolder in &folder_dtos {
all_folders.extend(sub_folders); let fr = file_repo.clone();
} let fdr = folder_repo.clone();
Ok(Err(e)) => { let crit = criteria.clone();
tracing::warn!("Parallel search subtask error: {}", e); let folder_id = subfolder.id.clone();
}
Err(e) => { handles.push(tokio::spawn(async move {
tracing::warn!("Parallel search task join error: {}", e); Self::search_parallel(fr, fdr, Some(folder_id), crit).await
}));
}
// Collect results from all parallel tasks
for handle in handles {
match handle.await {
Ok(Ok((sub_files, sub_folders))) => {
all_files.extend(sub_files);
all_folders.extend(sub_folders);
}
Ok(Err(e)) => {
tracing::warn!("Parallel search subtask error: {}", e);
}
Err(e) => {
tracing::warn!("Parallel search task join error: {}", e);
}
} }
} }
} }
}
Ok((all_files, all_folders)) Ok((all_files, all_folders))
}) // end Box::pin }) // end Box::pin
} }
@@ -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
View File
@@ -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())
} }
} }
+9 -1
View File
@@ -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)
+1 -5
View File
@@ -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,23 +60,26 @@ impl FavoritesRepositoryPort for FavoritesPgRepository {
let favorites = rows let favorites = rows
.iter() .iter()
.map(|row| FavoriteItemDto { .map(|row| {
id: row.get("id"), FavoriteItemDto {
user_id: row.get("user_id"), id: row.get("id"),
item_id: row.get("item_id"), user_id: row.get("user_id"),
item_type: row.get("item_type"), item_id: row.get("item_id"),
created_at: row.get("created_at"), item_type: row.get("item_type"),
item_name: row.try_get("item_name").ok(), created_at: row.get("created_at"),
item_size: row.try_get("item_size").ok(), item_name: row.try_get("item_name").ok(),
item_mime_type: row.try_get("item_mime_type").ok(), item_size: row.try_get("item_size").ok(),
parent_id: row.try_get("parent_id").ok(), item_mime_type: row.try_get("item_mime_type").ok(),
modified_at: row.try_get("modified_at").ok(), parent_id: row.try_get("parent_id").ok(),
// Temporary defaults; with_display_fields() computes the real values modified_at: row.try_get("modified_at").ok(),
icon_class: String::new(), // Temporary defaults; with_display_fields() computes the real values
icon_special_class: String::new(), icon_class: String::new(),
category: String::new(), icon_special_class: String::new(),
size_formatted: String::new(), category: String::new(),
}.with_display_fields()) size_formatted: String::new(),
}
.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,10 +140,18 @@ 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,
sqlx::query_as( String,
r#" Option<String>,
Option<String>,
i64,
String,
i64,
i64,
)> = if let Some(fid) = folder_id {
sqlx::query_as(
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,
EXTRACT(EPOCH FROM fi.created_at)::bigint, EXTRACT(EPOCH FROM fi.created_at)::bigint,
@@ -153,13 +161,13 @@ impl FileReadPort for FileBlobReadRepository {
WHERE fi.folder_id = $1::uuid AND NOT fi.is_trashed WHERE fi.folder_id = $1::uuid AND NOT fi.is_trashed
ORDER BY fi.name ORDER BY fi.name
"#, "#,
) )
.bind(fid) .bind(fid)
.fetch_all(self.pool.as_ref()) .fetch_all(self.pool.as_ref())
.await .await
} else { } else {
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,
fi.size, fi.mime_type, fi.size, fi.mime_type,
EXTRACT(EPOCH FROM fi.created_at)::bigint, EXTRACT(EPOCH FROM fi.created_at)::bigint,
@@ -169,11 +177,11 @@ impl FileReadPort for FileBlobReadRepository {
WHERE fi.folder_id IS NULL AND NOT fi.is_trashed WHERE fi.folder_id IS NULL AND NOT fi.is_trashed
ORDER BY fi.name ORDER BY fi.name
"#, "#,
) )
.fetch_all(self.pool.as_ref()) .fetch_all(self.pool.as_ref())
.await .await
} }
.map_err(|e| DomainError::internal_error("FileBlobRead", format!("list: {e}")))?; .map_err(|e| DomainError::internal_error("FileBlobRead", format!("list: {e}")))?;
rows.into_iter() rows.into_iter()
.map(|(id, name, fid, fpath, size, mime, ca, ma)| { .map(|(id, name, fid, fpath, size, mime, ca, ma)| {
@@ -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,16 +55,18 @@ 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(
DomainError::internal_error("FileBlobWrite", format!("folder path: {e}")) "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))
} }
None => Ok(None), None => Ok(None),
@@ -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,9 +175,10 @@ 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)> =
sqlx::query_as( if let Some(pid) = parent_id {
r#" sqlx::query_as(
r#"
SELECT id::text, name, path, parent_id::text, user_id, SELECT id::text, name, path, parent_id::text, user_id,
EXTRACT(EPOCH FROM created_at)::bigint, EXTRACT(EPOCH FROM created_at)::bigint,
EXTRACT(EPOCH FROM updated_at)::bigint EXTRACT(EPOCH FROM updated_at)::bigint
@@ -185,13 +186,13 @@ impl FolderRepository for FolderDbRepository {
WHERE parent_id = $1::uuid AND NOT is_trashed WHERE parent_id = $1::uuid AND NOT is_trashed
ORDER BY name ORDER BY name
"#, "#,
) )
.bind(pid) .bind(pid)
.fetch_all(self.pool()) .fetch_all(self.pool())
.await .await
} else { } else {
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,
EXTRACT(EPOCH FROM created_at)::bigint, EXTRACT(EPOCH FROM created_at)::bigint,
EXTRACT(EPOCH FROM updated_at)::bigint EXTRACT(EPOCH FROM updated_at)::bigint
@@ -199,11 +200,11 @@ impl FolderRepository for FolderDbRepository {
WHERE parent_id IS NULL AND NOT is_trashed WHERE parent_id IS NULL AND NOT is_trashed
ORDER BY name ORDER BY name
"#, "#,
) )
.fetch_all(self.pool()) .fetch_all(self.pool())
.await .await
} }
.map_err(|e| DomainError::internal_error("FolderDb", format!("list: {e}")))?; .map_err(|e| DomainError::internal_error("FolderDb", format!("list: {e}")))?;
rows.into_iter() rows.into_iter()
.map(|(id, name, path, pid, uid, ca, ma)| { .map(|(id, name, path, pid, uid, ca, ma)| {
@@ -217,9 +218,10 @@ 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)> =
sqlx::query_as( if let Some(pid) = parent_id {
r#" sqlx::query_as(
r#"
SELECT id::text, name, path, parent_id::text, user_id, SELECT id::text, name, path, parent_id::text, user_id,
EXTRACT(EPOCH FROM created_at)::bigint, EXTRACT(EPOCH FROM created_at)::bigint,
EXTRACT(EPOCH FROM updated_at)::bigint EXTRACT(EPOCH FROM updated_at)::bigint
@@ -227,14 +229,14 @@ impl FolderRepository for FolderDbRepository {
WHERE parent_id = $1::uuid AND user_id = $2 AND NOT is_trashed WHERE parent_id = $1::uuid AND user_id = $2 AND NOT is_trashed
ORDER BY name ORDER BY name
"#, "#,
) )
.bind(pid) .bind(pid)
.bind(owner_id) .bind(owner_id)
.fetch_all(self.pool()) .fetch_all(self.pool())
.await .await
} else { } else {
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,
EXTRACT(EPOCH FROM created_at)::bigint, EXTRACT(EPOCH FROM created_at)::bigint,
EXTRACT(EPOCH FROM updated_at)::bigint EXTRACT(EPOCH FROM updated_at)::bigint
@@ -242,12 +244,12 @@ impl FolderRepository for FolderDbRepository {
WHERE parent_id IS NULL AND user_id = $1 AND NOT is_trashed WHERE parent_id IS NULL AND user_id = $1 AND NOT is_trashed
ORDER BY name ORDER BY name
"#, "#,
) )
.bind(owner_id) .bind(owner_id)
.fetch_all(self.pool()) .fetch_all(self.pool())
.await .await
} }
.map_err(|e| DomainError::internal_error("FolderDb", format!("list_by_owner: {e}")))?; .map_err(|e| DomainError::internal_error("FolderDb", format!("list_by_owner: {e}")))?;
rows.into_iter() rows.into_iter()
.map(|(id, name, path, pid, uid, ca, ma)| { .map(|(id, name, path, pid, uid, ca, ma)| {
@@ -284,9 +286,10 @@ 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)> =
sqlx::query_as( if let Some(pid) = parent_id {
r#" sqlx::query_as(
r#"
SELECT id::text, name, path, parent_id::text, user_id, SELECT id::text, name, path, parent_id::text, user_id,
EXTRACT(EPOCH FROM created_at)::bigint, EXTRACT(EPOCH FROM created_at)::bigint,
EXTRACT(EPOCH FROM updated_at)::bigint EXTRACT(EPOCH FROM updated_at)::bigint
@@ -295,15 +298,15 @@ impl FolderRepository for FolderDbRepository {
ORDER BY name ORDER BY name
LIMIT $2 OFFSET $3 LIMIT $2 OFFSET $3
"#, "#,
) )
.bind(pid) .bind(pid)
.bind(limit as i64) .bind(limit as i64)
.bind(offset as i64) .bind(offset as i64)
.fetch_all(self.pool()) .fetch_all(self.pool())
.await .await
} else { } else {
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,
EXTRACT(EPOCH FROM created_at)::bigint, EXTRACT(EPOCH FROM created_at)::bigint,
EXTRACT(EPOCH FROM updated_at)::bigint EXTRACT(EPOCH FROM updated_at)::bigint
@@ -312,13 +315,13 @@ impl FolderRepository for FolderDbRepository {
ORDER BY name ORDER BY name
LIMIT $1 OFFSET $2 LIMIT $1 OFFSET $2
"#, "#,
) )
.bind(limit as i64) .bind(limit as i64)
.bind(offset as i64) .bind(offset as i64)
.fetch_all(self.pool()) .fetch_all(self.pool())
.await .await
} }
.map_err(|e| DomainError::internal_error("FolderDb", format!("paginate: {e}")))?; .map_err(|e| DomainError::internal_error("FolderDb", format!("paginate: {e}")))?;
let folders: Result<Vec<Folder>, DomainError> = rows let folders: Result<Vec<Folder>, DomainError> = rows
.into_iter() .into_iter()
@@ -360,9 +363,10 @@ 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)> =
sqlx::query_as( if let Some(pid) = parent_id {
r#" sqlx::query_as(
r#"
SELECT id::text, name, path, parent_id::text, user_id, SELECT id::text, name, path, parent_id::text, user_id,
EXTRACT(EPOCH FROM created_at)::bigint, EXTRACT(EPOCH FROM created_at)::bigint,
EXTRACT(EPOCH FROM updated_at)::bigint EXTRACT(EPOCH FROM updated_at)::bigint
@@ -371,16 +375,16 @@ impl FolderRepository for FolderDbRepository {
ORDER BY name ORDER BY name
LIMIT $3 OFFSET $4 LIMIT $3 OFFSET $4
"#, "#,
) )
.bind(pid) .bind(pid)
.bind(owner_id) .bind(owner_id)
.bind(limit as i64) .bind(limit as i64)
.bind(offset as i64) .bind(offset as i64)
.fetch_all(self.pool()) .fetch_all(self.pool())
.await .await
} else { } else {
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,
EXTRACT(EPOCH FROM created_at)::bigint, EXTRACT(EPOCH FROM created_at)::bigint,
EXTRACT(EPOCH FROM updated_at)::bigint EXTRACT(EPOCH FROM updated_at)::bigint
@@ -389,14 +393,16 @@ impl FolderRepository for FolderDbRepository {
ORDER BY name ORDER BY name
LIMIT $2 OFFSET $3 LIMIT $2 OFFSET $3
"#, "#,
) )
.bind(owner_id) .bind(owner_id)
.bind(limit as i64) .bind(limit as i64)
.bind(offset as i64) .bind(offset as i64)
.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,14 +509,13 @@ 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 .map_err(|e| DomainError::internal_error("FolderDb", format!("get_path: {e}")))?
.map_err(|e| DomainError::internal_error("FolderDb", format!("get_path: {e}")))? .ok_or_else(|| DomainError::not_found("Folder", id))?;
.ok_or_else(|| DomainError::not_found("Folder", id))?;
Ok(StoragePath::from_string(&path)) Ok(StoragePath::from_string(&path))
} }
@@ -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,22 +61,25 @@ impl RecentItemsRepositoryPort for RecentItemsPgRepository {
let items = rows let items = rows
.iter() .iter()
.map(|row| RecentItemDto { .map(|row| {
id: row.get("id"), RecentItemDto {
user_id: row.get("user_id"), id: row.get("id"),
item_id: row.get("item_id"), user_id: row.get("user_id"),
item_type: row.get("item_type"), item_id: row.get("item_id"),
accessed_at: row.get("accessed_at"), item_type: row.get("item_type"),
item_name: row.try_get("item_name").ok(), accessed_at: row.get("accessed_at"),
item_size: row.try_get("item_size").ok(), item_name: row.try_get("item_name").ok(),
item_mime_type: row.try_get("item_mime_type").ok(), item_size: row.try_get("item_size").ok(),
parent_id: row.try_get("parent_id").ok(), item_mime_type: row.try_get("item_mime_type").ok(),
// Temporary defaults; with_display_fields() computes the real values parent_id: row.try_get("parent_id").ok(),
icon_class: String::new(), // Temporary defaults; with_display_fields() computes the real values
icon_special_class: String::new(), icon_class: String::new(),
category: String::new(), icon_special_class: String::new(),
size_formatted: String::new(), category: String::new(),
}.with_display_fields()) size_formatted: String::new(),
}
.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};
+16 -5
View File
@@ -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,
+10 -2
View File
@@ -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)))?;
} }