style: apply rustfmt formatting to fix CI

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
zjean
2026-03-05 21:28:51 +01:00
parent aa666f5bbb
commit 190527edfb
29 changed files with 242 additions and 165 deletions
@@ -80,7 +80,11 @@ pub trait ChunkedUploadPort: Send + Sync + 'static {
) -> Result<ChunkUploadResponseDto, DomainError>; ) -> Result<ChunkUploadResponseDto, DomainError>;
/// Get the current status of an upload session. /// Get the current status of an upload session.
async fn get_status(&self, upload_id: &str, user_id: &str) -> Result<UploadStatusResponseDto, DomainError>; async fn get_status(
&self,
upload_id: &str,
user_id: &str,
) -> Result<UploadStatusResponseDto, DomainError>;
/// Assemble all chunks into the final file. /// Assemble all chunks into the final file.
/// ///
+1 -5
View File
@@ -16,11 +16,7 @@ pub trait ShareUseCase: Send + Sync + 'static {
) -> Result<ShareDto, DomainError>; ) -> Result<ShareDto, DomainError>;
/// Get a shared link by its ID (ownership-verified) /// Get a shared link by its ID (ownership-verified)
async fn get_shared_link( async fn get_shared_link(&self, id: &str, requester_id: &str) -> Result<ShareDto, DomainError>;
&self,
id: &str,
requester_id: &str,
) -> Result<ShareDto, DomainError>;
/// Get a shared link by its token (for access by non-users) /// Get a shared link by its token (for access by non-users)
async fn get_shared_link_by_token(&self, token: &str) -> Result<ShareDto, DomainError>; async fn get_shared_link_by_token(&self, token: &str) -> Result<ShareDto, DomainError>;
@@ -384,10 +384,7 @@ impl AdminSettingsService {
/// initialized (the caller "won" the race), or `Ok(false)` if another /// initialized (the caller "won" the race), or `Ok(false)` if another
/// request already did it. This eliminates the race-condition window /// request already did it. This eliminates the race-condition window
/// between `is_system_initialized()` and `mark_system_initialized()`. /// between `is_system_initialized()` and `mark_system_initialized()`.
pub async fn try_claim_initialization( pub async fn try_claim_initialization(&self, admin_user_id: &str) -> Result<bool, DomainError> {
&self,
admin_user_id: &str,
) -> Result<bool, DomainError> {
self.settings_repo self.settings_repo
.try_claim_initialization(admin_user_id) .try_claim_initialization(admin_user_id)
.await .await
+8 -2
View File
@@ -682,7 +682,11 @@ impl BatchOperationService {
// ── Add folders as sub-trees (bulk subtree queries, not N+1) ───── // ── Add folders as sub-trees (bulk subtree queries, not N+1) ─────
for folder_id in &folder_ids { for folder_id in &folder_ids {
match self.folder_service.get_folder_owned(folder_id, caller_id).await { match self
.folder_service
.get_folder_owned(folder_id, caller_id)
.await
{
Ok(root_folder) => { Ok(root_folder) => {
if let Err(e) = self if let Err(e) = self
.add_folder_subtree_to_zip(&mut zip, folder_id, &root_folder, caller_id) .add_folder_subtree_to_zip(&mut zip, folder_id, &root_folder, caller_id)
@@ -928,7 +932,9 @@ impl BatchOperationService {
async move { async move {
// If a parent is specified, verify the caller owns it // If a parent is specified, verify the caller owns it
if let Some(ref pid) = parent_id && let Err(e) = folder_service.get_folder_owned(pid, &caller).await { if let Some(ref pid) = parent_id
&& let Err(e) = folder_service.get_folder_owned(pid, &caller).await
{
let id = format!("{}:{}", name, pid); let id = format!("{}:{}", name, pid);
return (id, Err(e)); return (id, Err(e));
} }
@@ -228,7 +228,10 @@ impl FileRetrievalUseCase for FileRetrievalService {
folder_id: Option<&str>, folder_id: Option<&str>,
owner_id: &str, owner_id: &str,
) -> Result<Vec<FileDto>, DomainError> { ) -> Result<Vec<FileDto>, DomainError> {
let files = self.file_read.list_files_for_owner(folder_id, owner_id).await?; let files = self
.file_read
.list_files_for_owner(folder_id, owner_id)
.await?;
Ok(files.into_iter().map(FileDto::from).collect()) Ok(files.into_iter().map(FileDto::from).collect())
} }
+5 -1
View File
@@ -32,7 +32,11 @@ impl FolderService {
Ok(FolderDto::empty()) Ok(FolderDto::empty())
} }
async fn get_folder_owned(&self, _id: &str, _caller_id: &str) -> Result<FolderDto, DomainError> { async fn get_folder_owned(
&self,
_id: &str,
_caller_id: &str,
) -> Result<FolderDto, DomainError> {
Ok(FolderDto::empty()) Ok(FolderDto::empty())
} }
+16 -13
View File
@@ -20,7 +20,10 @@ use crate::{
storage_ports::FileReadPort, storage_ports::FileReadPort,
}, },
}, },
common::{config::AppConfig, errors::{DomainError, ErrorKind}}, common::{
config::AppConfig,
errors::{DomainError, ErrorKind},
},
domain::entities::share::{Share, ShareItemType, SharePermissions}, domain::entities::share::{Share, ShareItemType, SharePermissions},
}; };
@@ -147,11 +150,7 @@ impl ShareService {
/// but belongs to a different user — this prevents share-ID enumeration /// but belongs to a different user — this prevents share-ID enumeration
/// attacks where an attacker probes IDs and uses 403-vs-404 to learn /// attacks where an attacker probes IDs and uses 403-vs-404 to learn
/// which ones are valid. /// which ones are valid.
async fn fetch_owned_share( async fn fetch_owned_share(&self, id: &str, requester_id: &str) -> Result<Share, DomainError> {
&self,
id: &str,
requester_id: &str,
) -> Result<Share, DomainError> {
let share = self let share = self
.share_repository .share_repository
.find_share_by_id_for_user(id, requester_id) .find_share_by_id_for_user(id, requester_id)
@@ -206,11 +205,7 @@ impl ShareUseCase for ShareService {
Ok(ShareDto::from_entity(&saved_share, &self.config.base_url())) Ok(ShareDto::from_entity(&saved_share, &self.config.base_url()))
} }
async fn get_shared_link( async fn get_shared_link(&self, id: &str, requester_id: &str) -> Result<ShareDto, DomainError> {
&self,
id: &str,
requester_id: &str,
) -> Result<ShareDto, DomainError> {
// SECURITY: ownership-verified lookup — returns 404 if the share // SECURITY: ownership-verified lookup — returns 404 if the share
// doesn't exist OR belongs to another user. // doesn't exist OR belongs to another user.
let share = self.fetch_owned_share(id, requester_id).await?; let share = self.fetch_owned_share(id, requester_id).await?;
@@ -553,7 +548,11 @@ mod tests {
Ok(ShareDto::from_entity(&saved_share, &self.config.base_url())) Ok(ShareDto::from_entity(&saved_share, &self.config.base_url()))
} }
async fn get_shared_link(&self, id: &str, requester_id: &str) -> Result<ShareDto, DomainError> { async fn get_shared_link(
&self,
id: &str,
requester_id: &str,
) -> Result<ShareDto, DomainError> {
let share = self let share = self
.share_repository .share_repository
.find_share_by_id_for_user(id, requester_id) .find_share_by_id_for_user(id, requester_id)
@@ -634,7 +633,11 @@ mod tests {
Ok(ShareDto::from_entity(&updated, &self.config.base_url())) Ok(ShareDto::from_entity(&updated, &self.config.base_url()))
} }
async fn delete_shared_link(&self, id: &str, requester_id: &str) -> Result<(), DomainError> { async fn delete_shared_link(
&self,
id: &str,
requester_id: &str,
) -> Result<(), DomainError> {
self.share_repository self.share_repository
.delete_share_for_user(id, requester_id) .delete_share_for_user(id, requester_id)
.await .await
+5 -1
View File
@@ -186,7 +186,11 @@ impl TrashUseCase for TrashService {
// Returns NotFound if the file does not exist OR belongs to // Returns NotFound if the file does not exist OR belongs to
// another user, preventing cross-user trash operations. // another user, preventing cross-user trash operations.
debug!("Getting file data (owner-scoped): {}", item_id); debug!("Getting file data (owner-scoped): {}", item_id);
let file = match self.file_read_port.get_file_for_owner(item_id, user_id).await { let file = match self
.file_read_port
.get_file_for_owner(item_id, user_id)
.await
{
Ok(file) => { Ok(file) => {
debug!("File found: {} ({})", file.name(), item_id); debug!("File found: {} ({})", file.name(), item_id);
file file
+5 -1
View File
@@ -359,7 +359,11 @@ impl FolderUseCase for StubFolderUseCase {
Ok(FolderDto::default()) Ok(FolderDto::default())
} }
async fn get_folder_owned(&self, _id: &str, _caller_id: &str) -> Result<FolderDto, DomainError> { async fn get_folder_owned(
&self,
_id: &str,
_caller_id: &str,
) -> Result<FolderDto, DomainError> {
Ok(FolderDto::default()) Ok(FolderDto::default())
} }
+10 -4
View File
@@ -292,8 +292,10 @@ impl User {
)); ));
} }
// Disallow leading/trailing dots or hyphens // Disallow leading/trailing dots or hyphens
if username.starts_with('.') || username.starts_with('-') if username.starts_with('.')
|| username.ends_with('.') || username.ends_with('-') || username.starts_with('-')
|| username.ends_with('.')
|| username.ends_with('-')
{ {
return Err(UserError::InvalidUsername( return Err(UserError::InvalidUsername(
"Username must not start or end with a dot or hyphen".to_string(), "Username must not start or end with a dot or hyphen".to_string(),
@@ -310,7 +312,9 @@ impl User {
fn validate_email(email: &str) -> UserResult<()> { fn validate_email(email: &str) -> UserResult<()> {
let parts: Vec<&str> = email.splitn(2, '@').collect(); let parts: Vec<&str> = email.splitn(2, '@').collect();
if parts.len() != 2 { if parts.len() != 2 {
return Err(UserError::ValidationError("Invalid email: missing @".to_string())); return Err(UserError::ValidationError(
"Invalid email: missing @".to_string(),
));
} }
let (local, domain) = (parts[0], parts[1]); let (local, domain) = (parts[0], parts[1]);
if local.is_empty() || domain.is_empty() { if local.is_empty() || domain.is_empty() {
@@ -324,7 +328,9 @@ impl User {
)); ));
} }
// Reject characters commonly used in XSS / header injection // Reject characters commonly used in XSS / header injection
let forbidden = ['<', '>', '"', '\'', '\\', ' ', '\t', '\n', '\r', '(', ')', ',', ';']; let forbidden = [
'<', '>', '"', '\'', '\\', ' ', '\t', '\n', '\r', '(', ')', ',', ';',
];
if email.chars().any(|c| forbidden.contains(&c)) { if email.chars().any(|c| forbidden.contains(&c)) {
return Err(UserError::ValidationError( return Err(UserError::ValidationError(
"Invalid email: contains forbidden characters".to_string(), "Invalid email: contains forbidden characters".to_string(),
@@ -34,10 +34,7 @@ pub trait SettingsRepository: Send + Sync + 'static {
/// The default implementation falls back to the non-atomic /// The default implementation falls back to the non-atomic
/// get-then-set pattern for repositories that don't support a native /// get-then-set pattern for repositories that don't support a native
/// atomic upsert. /// atomic upsert.
async fn try_claim_initialization( async fn try_claim_initialization(&self, admin_user_id: &str) -> Result<bool, DomainError> {
&self,
admin_user_id: &str,
) -> Result<bool, DomainError> {
// Default: non-atomic fallback (overridden by PG implementation) // Default: non-atomic fallback (overridden by PG implementation)
match self.get("system_initialized").await? { match self.get("system_initialized").await? {
Some(v) if v == "true" => Ok(false), Some(v) if v == "true" => Ok(false),
@@ -325,9 +325,7 @@ impl FileReadPort for FileBlobReadRepository {
.fetch_all(self.pool.as_ref()) .fetch_all(self.pool.as_ref())
.await .await
} }
.map_err(|e| { .map_err(|e| DomainError::internal_error("FileBlobRead", format!("list_for_owner: {e}")))?;
DomainError::internal_error("FileBlobRead", format!("list_for_owner: {e}"))
})?;
rows.into_iter() rows.into_iter()
.map(|(id, name, fid, fpath, size, mime, ca, ma, uid)| { .map(|(id, name, fid, fpath, size, mime, ca, ma, uid)| {
@@ -868,7 +868,9 @@ impl FolderRepository for FolderDbRepository {
user_id: &str, user_id: &str,
) -> Result<Vec<Folder>, DomainError> { ) -> Result<Vec<Folder>, DomainError> {
let (where_extra, name_pattern) = match name_contains { let (where_extra, name_pattern) = match name_contains {
Some(name) if name.len() >= 3 => (" AND fo.name ILIKE $3", Some(super::like_escape(name))), Some(name) if name.len() >= 3 => {
(" AND fo.name ILIKE $3", Some(super::like_escape(name)))
}
_ => ("", None), _ => ("", None),
}; };
+4 -1
View File
@@ -50,6 +50,9 @@ pub use user_pg_repository::UserPgRepository;
/// `%` is a wildcard in LIKE patterns. /// `%` is a wildcard in LIKE patterns.
#[inline] #[inline]
pub fn like_escape(raw: &str) -> String { pub fn like_escape(raw: &str) -> String {
let escaped = raw.replace('\\', "\\\\").replace('%', "\\%").replace('_', "\\_"); let escaped = raw
.replace('\\', "\\\\")
.replace('%', "\\%")
.replace('_', "\\_");
format!("%{escaped}%") format!("%{escaped}%")
} }
@@ -102,10 +102,7 @@ impl SettingsRepository for SettingsPgRepository {
/// ///
/// Only the first caller that inserts the row gets `rows_affected == 1`; /// Only the first caller that inserts the row gets `rows_affected == 1`;
/// concurrent callers see 0 rows affected and receive `false`. /// concurrent callers see 0 rows affected and receive `false`.
async fn try_claim_initialization( async fn try_claim_initialization(&self, admin_user_id: &str) -> Result<bool, DomainError> {
&self,
admin_user_id: &str,
) -> Result<bool, DomainError> {
let result = sqlx::query( let result = sqlx::query(
"INSERT INTO auth.admin_settings (key, value, category, is_secret, updated_by, updated_at) "INSERT INTO auth.admin_settings (key, value, category, is_secret, updated_by, updated_at)
VALUES ('system_initialized', 'true', 'system', false, $1, NOW()) VALUES ('system_initialized', 'true', 'system', false, $1, NOW())
@@ -202,10 +202,7 @@ impl ShareStoragePort for SharePgRepository {
.await .await
.map_err(|e| { .map_err(|e| {
tracing::error!("Database error deleting share for user: {}", e); tracing::error!("Database error deleting share for user: {}", e);
DomainError::internal_error( DomainError::internal_error("Share", format!("Failed to delete share: {e}"))
"Share",
format!("Failed to delete share: {e}"),
)
})?; })?;
if result.rows_affected() == 0 { if result.rows_affected() == 0 {
@@ -242,10 +239,7 @@ impl ShareStoragePort for SharePgRepository {
.await .await
.map_err(|e| { .map_err(|e| {
tracing::error!("Database error finding shares by item for user: {}", e); tracing::error!("Database error finding shares by item for user: {}", e);
DomainError::internal_error( DomainError::internal_error("Share", format!("Failed to find shares by item: {e}"))
"Share",
format!("Failed to find shares by item: {e}"),
)
})?; })?;
rows.iter().map(Self::row_to_entity).collect() rows.iter().map(Self::row_to_entity).collect()
@@ -371,11 +371,7 @@ impl ChunkedUploadService {
/// Verify that the given session belongs to the given user. /// Verify that the given session belongs to the given user.
/// Returns 404 (not 403) to avoid revealing the existence of other users' sessions. /// Returns 404 (not 403) to avoid revealing the existence of other users' sessions.
fn verify_session_owner( fn verify_session_owner(&self, upload_id: &str, user_id: &str) -> Result<(), String> {
&self,
upload_id: &str,
user_id: &str,
) -> Result<(), String> {
let session = self let session = self
.sessions .sessions
.get(upload_id) .get(upload_id)
@@ -592,7 +588,11 @@ impl ChunkedUploadService {
} }
/// Get upload status /// Get upload status
async fn get_status_inner(&self, upload_id: &str, user_id: &str) -> Result<UploadStatusResponseDto, String> { async fn get_status_inner(
&self,
upload_id: &str,
user_id: &str,
) -> Result<UploadStatusResponseDto, String> {
self.verify_session_owner(upload_id, user_id)?; self.verify_session_owner(upload_id, user_id)?;
let session = self let session = self
@@ -808,9 +808,16 @@ impl ChunkedUploadPort for ChunkedUploadService {
total_size: u64, total_size: u64,
chunk_size: Option<usize>, chunk_size: Option<usize>,
) -> Result<CreateUploadResponseDto, DomainError> { ) -> Result<CreateUploadResponseDto, DomainError> {
self.create_session_inner(user_id.to_owned(), filename, folder_id, content_type, total_size, chunk_size) self.create_session_inner(
.await user_id.to_owned(),
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ChunkedUpload", e)) filename,
folder_id,
content_type,
total_size,
chunk_size,
)
.await
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ChunkedUpload", e))
} }
async fn upload_chunk( async fn upload_chunk(
@@ -826,7 +833,11 @@ impl ChunkedUploadPort for ChunkedUploadService {
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ChunkedUpload", e)) .map_err(|e| DomainError::new(ErrorKind::InternalError, "ChunkedUpload", e))
} }
async fn get_status(&self, upload_id: &str, user_id: &str) -> Result<UploadStatusResponseDto, DomainError> { async fn get_status(
&self,
upload_id: &str,
user_id: &str,
) -> Result<UploadStatusResponseDto, DomainError> {
self.get_status_inner(upload_id, user_id) self.get_status_inner(upload_id, user_id)
.await .await
.map_err(|e| DomainError::new(ErrorKind::NotFound, "ChunkedUpload", e)) .map_err(|e| DomainError::new(ErrorKind::NotFound, "ChunkedUpload", e))
@@ -1097,14 +1108,19 @@ mod tests {
assert_eq!(r1.bytes_received, 1024); assert_eq!(r1.bytes_received, 1024);
// 3. Status check // 3. Status check
let status = service.get_status_inner(&id, "test-user").await.expect("status"); let status = service
.get_status_inner(&id, "test-user")
.await
.expect("status");
assert!(status.is_complete); assert!(status.is_complete);
assert_eq!(status.completed_chunks, 2); assert_eq!(status.completed_chunks, 2);
assert!(status.pending_chunks.is_empty()); assert!(status.pending_chunks.is_empty());
// 4. Complete (assemble) // 4. Complete (assemble)
let (path, filename, _folder, _ct, size, hash) = let (path, filename, _folder, _ct, size, hash) = service
service.complete_upload_inner(&id, "test-user").await.expect("complete"); .complete_upload_inner(&id, "test-user")
.await
.expect("complete");
assert_eq!(filename, "test.txt"); assert_eq!(filename, "test.txt");
assert_eq!(size, 1024); assert_eq!(size, 1024);
assert!(!hash.is_empty()); assert!(!hash.is_empty());
@@ -1116,7 +1132,10 @@ mod tests {
assert_eq!(&content[512..], &[b'B'; 512]); assert_eq!(&content[512..], &[b'B'; 512]);
// 6. Finalize // 6. Finalize
service.finalize_upload_inner(&id, "test-user").await.expect("finalize"); service
.finalize_upload_inner(&id, "test-user")
.await
.expect("finalize");
assert_eq!(service.active_sessions().await, 0); assert_eq!(service.active_sessions().await, 0);
let _ = fs::remove_dir_all(&base).await; let _ = fs::remove_dir_all(&base).await;
@@ -1,8 +1,8 @@
use serde_json::Value; use serde_json::Value;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::PathBuf; use std::path::PathBuf;
use tokio::sync::RwLock;
use tokio::fs; use tokio::fs;
use tokio::sync::RwLock;
use crate::domain::services::i18n_service::{I18nError, I18nResult, I18nService, Locale}; use crate::domain::services::i18n_service::{I18nError, I18nResult, I18nService, Locale};
+1 -1
View File
@@ -6,8 +6,8 @@
//! Compatible with Authentik, Keycloak, and any standard OIDC provider. //! Compatible with Authentik, Keycloak, and any standard OIDC provider.
use serde::Deserialize; use serde::Deserialize;
use tokio::sync::RwLock;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use tokio::sync::RwLock;
use crate::application::ports::auth_ports::{OidcIdClaims, OidcServicePort, OidcTokenSet}; use crate::application::ports::auth_ports::{OidcIdClaims, OidcServicePort, OidcTokenSet};
use crate::common::config::OidcConfig; use crate::common::config::OidcConfig;
@@ -119,10 +119,10 @@ impl PathResolverService {
LIMIT 1 LIMIT 1
"#, "#,
) )
.bind(path) // $1 .bind(path) // $1
.bind(filename) // $2 .bind(filename) // $2
.bind(&folder_path) // $3 .bind(&folder_path) // $3
.bind(user_id) // $4 .bind(user_id) // $4
.fetch_optional(self.pool.as_ref()) .fetch_optional(self.pool.as_ref())
.await .await
.map_err(|e| DomainError::internal_error("PathResolver", format!("resolve_for_user: {e}")))? .map_err(|e| DomainError::internal_error("PathResolver", format!("resolve_for_user: {e}")))?
@@ -215,7 +215,9 @@ impl PathResolverService {
.bind(user_id) .bind(user_id)
.fetch_one(self.pool.as_ref()) .fetch_one(self.pool.as_ref())
.await .await
.map_err(|e| DomainError::internal_error("PathResolver", format!("exists_for_user: {e}")))?; .map_err(|e| {
DomainError::internal_error("PathResolver", format!("exists_for_user: {e}"))
})?;
Ok(exists) Ok(exists)
} }
@@ -8,11 +8,8 @@ fn tiny_png() -> Vec<u8> {
let mut img = image::RgbaImage::new(1, 1); let mut img = image::RgbaImage::new(1, 1);
img.put_pixel(0, 0, image::Rgba([255, 0, 0, 255])); img.put_pixel(0, 0, image::Rgba([255, 0, 0, 255]));
let mut buf = Vec::new(); let mut buf = Vec::new();
img.write_to( img.write_to(&mut std::io::Cursor::new(&mut buf), image::ImageFormat::Png)
&mut std::io::Cursor::new(&mut buf), .expect("encode test PNG");
image::ImageFormat::Png,
)
.expect("encode test PNG");
buf buf
} }
+3 -1
View File
@@ -342,7 +342,9 @@ async fn logout(
.ok() .ok()
.map(|dto| dto.refresh_token) .map(|dto| dto.refresh_token)
.or_else(|| cookie_auth::extract_cookie_value(&headers, cookie_auth::REFRESH_COOKIE)) .or_else(|| cookie_auth::extract_cookie_value(&headers, cookie_auth::REFRESH_COOKIE))
.ok_or_else(|| AppError::unauthorized("Refresh token required for logout (JSON body or cookie)"))?; .ok_or_else(|| {
AppError::unauthorized("Refresh token required for logout (JSON body or cookie)")
})?;
auth_service auth_service
.auth_application_service .auth_application_service
+40 -10
View File
@@ -164,7 +164,10 @@ pub async fn move_files_batch(
.await .await
.map_err(|e| { .map_err(|e| {
tracing::error!("Batch move_files failed: {}", e); tracing::error!("Batch move_files failed: {}", e);
(StatusCode::INTERNAL_SERVER_ERROR, "Batch operation failed".to_string()) (
StatusCode::INTERNAL_SERVER_ERROR,
"Batch operation failed".to_string(),
)
})?; })?;
// Convert result to DTO // Convert result to DTO
@@ -217,7 +220,10 @@ pub async fn copy_files_batch(
.await .await
.map_err(|e| { .map_err(|e| {
tracing::error!("Batch copy_files failed: {}", e); tracing::error!("Batch copy_files failed: {}", e);
(StatusCode::INTERNAL_SERVER_ERROR, "Batch operation failed".to_string()) (
StatusCode::INTERNAL_SERVER_ERROR,
"Batch operation failed".to_string(),
)
})?; })?;
// Convert result to DTO // Convert result to DTO
@@ -270,7 +276,10 @@ pub async fn delete_files_batch(
.await .await
.map_err(|e| { .map_err(|e| {
tracing::error!("Batch delete_files failed: {}", e); tracing::error!("Batch delete_files failed: {}", e);
(StatusCode::INTERNAL_SERVER_ERROR, "Batch operation failed".to_string()) (
StatusCode::INTERNAL_SERVER_ERROR,
"Batch operation failed".to_string(),
)
})?; })?;
// Create custom response for string IDs // Create custom response for string IDs
@@ -331,7 +340,10 @@ pub async fn delete_folders_batch(
.await .await
.map_err(|e| { .map_err(|e| {
tracing::error!("Batch delete_folders failed: {}", e); tracing::error!("Batch delete_folders failed: {}", e);
(StatusCode::INTERNAL_SERVER_ERROR, "Batch operation failed".to_string()) (
StatusCode::INTERNAL_SERVER_ERROR,
"Batch operation failed".to_string(),
)
})?; })?;
// Create custom response for string IDs // Create custom response for string IDs
@@ -399,7 +411,10 @@ pub async fn create_folders_batch(
.await .await
.map_err(|e| { .map_err(|e| {
tracing::error!("Batch create_folders failed: {}", e); tracing::error!("Batch create_folders failed: {}", e);
(StatusCode::INTERNAL_SERVER_ERROR, "Batch operation failed".to_string()) (
StatusCode::INTERNAL_SERVER_ERROR,
"Batch operation failed".to_string(),
)
})?; })?;
// Convert result to DTO // Convert result to DTO
@@ -452,7 +467,10 @@ pub async fn get_files_batch(
.await .await
.map_err(|e| { .map_err(|e| {
tracing::error!("Batch get_files failed: {}", e); tracing::error!("Batch get_files failed: {}", e);
(StatusCode::INTERNAL_SERVER_ERROR, "Batch operation failed".to_string()) (
StatusCode::INTERNAL_SERVER_ERROR,
"Batch operation failed".to_string(),
)
})?; })?;
// Convert result to DTO // Convert result to DTO
@@ -505,7 +523,10 @@ pub async fn get_folders_batch(
.await .await
.map_err(|e| { .map_err(|e| {
tracing::error!("Batch get_folders failed: {}", e); tracing::error!("Batch get_folders failed: {}", e);
(StatusCode::INTERNAL_SERVER_ERROR, "Batch operation failed".to_string()) (
StatusCode::INTERNAL_SERVER_ERROR,
"Batch operation failed".to_string(),
)
})?; })?;
// Convert result to DTO // Convert result to DTO
@@ -690,7 +711,10 @@ pub async fn move_folders_batch(
.await .await
.map_err(|e| { .map_err(|e| {
tracing::error!("Batch move_folders failed: {}", e); tracing::error!("Batch move_folders failed: {}", e);
(StatusCode::INTERNAL_SERVER_ERROR, "Batch operation failed".to_string()) (
StatusCode::INTERNAL_SERVER_ERROR,
"Batch operation failed".to_string(),
)
})?; })?;
let response: BatchOperationResponse<FolderDto> = result.into(); let response: BatchOperationResponse<FolderDto> = result.into();
@@ -727,7 +751,10 @@ pub async fn download_batch(
if combined_size > MAX_BATCH_SIZE { if combined_size > MAX_BATCH_SIZE {
return Err(( return Err((
StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST,
format!("Batch size {} exceeds maximum of {}", combined_size, MAX_BATCH_SIZE), format!(
"Batch size {} exceeds maximum of {}",
combined_size, MAX_BATCH_SIZE
),
)); ));
} }
@@ -737,7 +764,10 @@ pub async fn download_batch(
.await .await
.map_err(|e| { .map_err(|e| {
tracing::error!("Batch download ZIP failed: {}", e); tracing::error!("Batch download ZIP failed: {}", e);
(StatusCode::INTERNAL_SERVER_ERROR, "Batch download failed".to_string()) (
StatusCode::INTERNAL_SERVER_ERROR,
"Batch download failed".to_string(),
)
})?; })?;
// Read file size for Content-Length before splitting ownership // Read file size for Content-Length before splitting ownership
@@ -190,7 +190,13 @@ impl ChunkedUploadHandler {
}); });
match chunked_service match chunked_service
.upload_chunk(&upload_id, &auth_user.id, params.chunk_index, body, checksum) .upload_chunk(
&upload_id,
&auth_user.id,
params.chunk_index,
body,
checksum,
)
.await .await
{ {
Ok(response) => { Ok(response) => {
@@ -213,7 +219,7 @@ impl ChunkedUploadHandler {
.unwrap() .unwrap()
.into_response() .into_response()
} }
Err(e) => AppError::from(e).into_response() Err(e) => AppError::from(e).into_response(),
} }
} }
@@ -261,7 +267,10 @@ impl ChunkedUploadHandler {
// Assemble chunks (hash-on-write: SHA-256 computed during assembly) // Assemble chunks (hash-on-write: SHA-256 computed during assembly)
let (assembled_path, filename, folder_id, content_type, total_size, hash) = let (assembled_path, filename, folder_id, content_type, total_size, hash) =
match chunked_service.complete_upload(&upload_id, &auth_user.id).await { match chunked_service
.complete_upload(&upload_id, &auth_user.id)
.await
{
Ok(result) => result, Ok(result) => result,
Err(e) => { Err(e) => {
return AppError::from(e).into_response(); return AppError::from(e).into_response();
@@ -289,7 +298,9 @@ impl ChunkedUploadHandler {
{ {
Ok(file) => { Ok(file) => {
// Cleanup session // Cleanup session
let _ = chunked_service.finalize_upload(&upload_id, &auth_user.id).await; let _ = chunked_service
.finalize_upload(&upload_id, &auth_user.id)
.await;
tracing::info!( tracing::info!(
"✅ CHUNKED UPLOAD COMPLETE: {} (ID: {}, {} bytes)", "✅ CHUNKED UPLOAD COMPLETE: {} (ID: {}, {} bytes)",
@@ -311,8 +322,7 @@ impl ChunkedUploadHandler {
} }
Err(e) => { Err(e) => {
tracing::error!("Failed to create file from assembled upload: {:?}", e); tracing::error!("Failed to create file from assembled upload: {:?}", e);
AppError::internal_error(format!("Failed to create file: {}", e)) AppError::internal_error(format!("Failed to create file: {}", e)).into_response()
.into_response()
} }
} }
} }
@@ -327,10 +337,14 @@ impl ChunkedUploadHandler {
) -> impl IntoResponse { ) -> impl IntoResponse {
let chunked_service = &state.core.chunked_upload_service; let chunked_service = &state.core.chunked_upload_service;
match chunked_service.cancel_upload(&upload_id, &auth_user.id).await { match chunked_service
.cancel_upload(&upload_id, &auth_user.id)
.await
{
Ok(_) => StatusCode::NO_CONTENT.into_response(), Ok(_) => StatusCode::NO_CONTENT.into_response(),
Err(e) => AppError::internal_error(format!("Failed to cancel upload: {}", e)) Err(e) => {
.into_response(), AppError::internal_error(format!("Failed to cancel upload: {}", e)).into_response()
}
} }
} }
} }
+2 -6
View File
@@ -231,9 +231,7 @@ impl DedupHandler {
return Response::builder() return Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR) .status(StatusCode::INTERNAL_SERVER_ERROR)
.header(header::CONTENT_TYPE, "application/json") .header(header::CONTENT_TYPE, "application/json")
.body(Body::from( .body(Body::from(r#"{"error": "Upload failed"}"#))
r#"{"error": "Upload failed"}"#,
))
.unwrap() .unwrap()
.into_response(); .into_response();
} }
@@ -408,9 +406,7 @@ impl DedupHandler {
return Response::builder() return Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR) .status(StatusCode::INTERNAL_SERVER_ERROR)
.header(header::CONTENT_TYPE, "application/json") .header(header::CONTENT_TYPE, "application/json")
.body(Body::from( .body(Body::from(r#"{"error": "Verification failed"}"#))
r#"{"error": "Verification failed"}"#,
))
.unwrap() .unwrap()
.into_response(); .into_response();
} }
+20 -19
View File
@@ -106,7 +106,11 @@ impl FileHandler {
if let Some(ref fid) = folder_id { if let Some(ref fid) = folder_id {
use crate::application::ports::inbound::FolderUseCase; use crate::application::ports::inbound::FolderUseCase;
let folder_service = &state.applications.folder_service; let folder_service = &state.applications.folder_service;
if folder_service.get_folder_owned(fid, &auth_user.id).await.is_err() { if folder_service
.get_folder_owned(fid, &auth_user.id)
.await
.is_err()
{
tracing::warn!( tracing::warn!(
"⛔ UPLOAD REJECTED (IDOR): user='{}' attempted upload to folder '{}' owned by another user", "⛔ UPLOAD REJECTED (IDOR): user='{}' attempted upload to folder '{}' owned by another user",
auth_user.username, auth_user.username,
@@ -323,7 +327,12 @@ impl FileHandler {
} }
// Resolve the actual blob path on disk (not the logical file path). // Resolve the actual blob path on disk (not the logical file path).
let blob_hash = match state.repositories.file_read_repository.get_blob_hash(&id).await { let blob_hash = match state
.repositories
.file_read_repository
.get_blob_hash(&id)
.await
{
Ok(h) => h, Ok(h) => h,
Err(err) => { Err(err) => {
return ( return (
@@ -353,10 +362,8 @@ impl FileHandler {
.unwrap() .unwrap()
.into_response() .into_response()
} }
Err(err) => { Err(err) => AppError::internal_error(format!("Thumbnail generation failed: {}", err))
AppError::internal_error(format!("Thumbnail generation failed: {}", err)) .into_response(),
.into_response()
}
} }
} }
@@ -529,9 +536,7 @@ impl FileHandler {
.unwrap() .unwrap()
.into_response(), .into_response(),
}, },
Err(err) => { Err(err) => AppError::from(err).into_response(),
AppError::from(err).into_response()
}
} }
} }
@@ -581,9 +586,7 @@ impl FileHandler {
.insert(header::ETAG, header::HeaderValue::from_str(&etag).unwrap()); .insert(header::ETAG, header::HeaderValue::from_str(&etag).unwrap());
resp resp
} }
Err(err) => { Err(err) => AppError::from(err).into_response(),
AppError::from(err).into_response()
}
} }
} }
@@ -664,7 +667,7 @@ impl FileHandler {
match result { match result {
Ok(_) => StatusCode::NO_CONTENT.into_response(), Ok(_) => StatusCode::NO_CONTENT.into_response(),
Err(err) => AppError::from(err).into_response() Err(err) => AppError::from(err).into_response(),
} }
} }
@@ -696,7 +699,7 @@ impl FileHandler {
let mgmt = &state.applications.file_management_service; let mgmt = &state.applications.file_management_service;
match mgmt.rename_file_owned(&id, &auth_user.id, &new_name).await { match mgmt.rename_file_owned(&id, &auth_user.id, &new_name).await {
Ok(file_dto) => (StatusCode::OK, Json(file_dto)).into_response(), Ok(file_dto) => (StatusCode::OK, Json(file_dto)).into_response(),
Err(err) => AppError::from(err).into_response() Err(err) => AppError::from(err).into_response(),
} }
} }
@@ -716,7 +719,7 @@ impl FileHandler {
.await .await
{ {
Ok(file) => (StatusCode::OK, Json(file)).into_response(), Ok(file) => (StatusCode::OK, Json(file)).into_response(),
Err(err) => AppError::from(err).into_response() Err(err) => AppError::from(err).into_response(),
} }
} }
@@ -735,7 +738,7 @@ impl FileHandler {
let mgmt = &state.applications.file_management_service; let mgmt = &state.applications.file_management_service;
match mgmt.move_file_owned(&id, &auth_user.id, folder_id).await { match mgmt.move_file_owned(&id, &auth_user.id, folder_id).await {
Ok(file_dto) => (StatusCode::OK, Json(file_dto)).into_response(), Ok(file_dto) => (StatusCode::OK, Json(file_dto)).into_response(),
Err(err) => AppError::from(err).into_response() Err(err) => AppError::from(err).into_response(),
} }
} }
@@ -794,9 +797,7 @@ impl FileHandler {
}) })
.collect(); .collect();
format!( format!("{disposition}; filename=\"{ascii_safe}\"; filename*=UTF-8''{encoded}")
"{disposition}; filename=\"{ascii_safe}\"; filename*=UTF-8''{encoded}"
)
} }
/// Build a 201 Created JSON response. /// Build a 201 Created JSON response.
+16 -13
View File
@@ -70,19 +70,24 @@ impl FolderHandler {
// ── SECURITY: Verify parent folder ownership (IDOR V-04 fix) ── // ── SECURITY: Verify parent folder ownership (IDOR V-04 fix) ──
if let Some(ref parent_id) = dto.parent_id { if let Some(ref parent_id) = dto.parent_id {
use crate::application::ports::inbound::FolderUseCase; use crate::application::ports::inbound::FolderUseCase;
if service.get_folder_owned(parent_id, &auth_user.id).await.is_err() { if service
.get_folder_owned(parent_id, &auth_user.id)
.await
.is_err()
{
tracing::warn!( tracing::warn!(
"create_folder: user '{}' attempted to create folder in parent '{}' owned by another user", "create_folder: user '{}' attempted to create folder in parent '{}' owned by another user",
auth_user.username, auth_user.username,
parent_id, parent_id,
); );
return AppError::not_found(format!("Parent folder not found: {}", parent_id)).into_response(); return AppError::not_found(format!("Parent folder not found: {}", parent_id))
.into_response();
} }
} }
match service.create_folder(dto).await { match service.create_folder(dto).await {
Ok(folder) => (StatusCode::CREATED, Json(folder)).into_response(), Ok(folder) => (StatusCode::CREATED, Json(folder)).into_response(),
Err(err) => AppError::from(err).into_response() Err(err) => AppError::from(err).into_response(),
} }
} }
@@ -109,7 +114,7 @@ impl FolderHandler {
} }
(StatusCode::OK, Json(folder)).into_response() (StatusCode::OK, Json(folder)).into_response()
} }
Err(err) => AppError::from(err).into_response() Err(err) => AppError::from(err).into_response(),
} }
} }
@@ -154,7 +159,7 @@ impl FolderHandler {
.await .await
{ {
Ok(paginated_result) => (StatusCode::OK, Json(paginated_result)).into_response(), Ok(paginated_result) => (StatusCode::OK, Json(paginated_result)).into_response(),
Err(err) => AppError::from(err).into_response() Err(err) => AppError::from(err).into_response(),
} }
} }
@@ -171,7 +176,7 @@ impl FolderHandler {
.await .await
{ {
Ok(folders) => (StatusCode::OK, Json(folders)).into_response(), Ok(folders) => (StatusCode::OK, Json(folders)).into_response(),
Err(err) => AppError::from(err).into_response() Err(err) => AppError::from(err).into_response(),
} }
} }
@@ -237,7 +242,7 @@ impl FolderHandler {
.insert(header::ETAG, header::HeaderValue::from_str(&etag).unwrap()); .insert(header::ETAG, header::HeaderValue::from_str(&etag).unwrap());
resp resp
} }
(Err(err), _) | (_, Err(err)) => AppError::from(err).into_response() (Err(err), _) | (_, Err(err)) => AppError::from(err).into_response(),
} }
} }
@@ -250,7 +255,7 @@ impl FolderHandler {
) -> impl IntoResponse { ) -> impl IntoResponse {
match service.rename_folder(&id, dto, &auth_user.id).await { match service.rename_folder(&id, dto, &auth_user.id).await {
Ok(folder) => (StatusCode::OK, Json(folder)).into_response(), Ok(folder) => (StatusCode::OK, Json(folder)).into_response(),
Err(err) => AppError::from(err).into_response() Err(err) => AppError::from(err).into_response(),
} }
} }
@@ -263,7 +268,7 @@ impl FolderHandler {
) -> impl IntoResponse { ) -> impl IntoResponse {
match service.move_folder(&id, dto, &auth_user.id).await { match service.move_folder(&id, dto, &auth_user.id).await {
Ok(folder) => (StatusCode::OK, Json(folder)).into_response(), Ok(folder) => (StatusCode::OK, Json(folder)).into_response(),
Err(err) => AppError::from(err).into_response() Err(err) => AppError::from(err).into_response(),
} }
} }
@@ -275,7 +280,7 @@ impl FolderHandler {
) -> impl IntoResponse { ) -> impl IntoResponse {
match service.delete_folder(&id, &auth_user.id).await { match service.delete_folder(&id, &auth_user.id).await {
Ok(_) => StatusCode::NO_CONTENT.into_response(), Ok(_) => StatusCode::NO_CONTENT.into_response(),
Err(err) => AppError::from(err).into_response() Err(err) => AppError::from(err).into_response(),
} }
} }
@@ -313,9 +318,7 @@ impl FolderHandler {
tracing::info!("Folder permanently deleted: {}", id); tracing::info!("Folder permanently deleted: {}", id);
StatusCode::NO_CONTENT.into_response() StatusCode::NO_CONTENT.into_response()
} }
Err(err) => { Err(err) => AppError::from(err).into_response(),
AppError::from(err).into_response()
}
} }
} }
+8 -16
View File
@@ -40,12 +40,9 @@ pub async fn create_shared_link(
auth_user: AuthUser, auth_user: AuthUser,
Json(dto): Json<CreateShareDto>, Json(dto): Json<CreateShareDto>,
) -> impl IntoResponse { ) -> impl IntoResponse {
match share_use_case match share_use_case.create_shared_link(&auth_user.id, dto).await {
.create_shared_link(&auth_user.id, dto)
.await
{
Ok(share) => (StatusCode::CREATED, Json(share)).into_response(), Ok(share) => (StatusCode::CREATED, Json(share)).into_response(),
Err(err) => AppError::from(err).into_response() Err(err) => AppError::from(err).into_response(),
} }
} }
@@ -57,7 +54,7 @@ pub async fn get_shared_link(
) -> impl IntoResponse { ) -> impl IntoResponse {
match share_use_case.get_shared_link(&id, &auth_user.id).await { match share_use_case.get_shared_link(&id, &auth_user.id).await {
Ok(share) => (StatusCode::OK, Json(share)).into_response(), Ok(share) => (StatusCode::OK, Json(share)).into_response(),
Err(err) => AppError::from(err).into_response() Err(err) => AppError::from(err).into_response(),
} }
} }
@@ -116,7 +113,7 @@ pub async fn update_shared_link(
.await .await
{ {
Ok(share) => (StatusCode::OK, Json(share)).into_response(), Ok(share) => (StatusCode::OK, Json(share)).into_response(),
Err(err) => AppError::from(err).into_response() Err(err) => AppError::from(err).into_response(),
} }
} }
@@ -126,12 +123,9 @@ pub async fn delete_shared_link(
auth_user: AuthUser, auth_user: AuthUser,
Path(id): Path<String>, Path(id): Path<String>,
) -> impl IntoResponse { ) -> impl IntoResponse {
match share_use_case match share_use_case.delete_shared_link(&id, &auth_user.id).await {
.delete_shared_link(&id, &auth_user.id)
.await
{
Ok(_) => StatusCode::NO_CONTENT.into_response(), Ok(_) => StatusCode::NO_CONTENT.into_response(),
Err(err) => AppError::from(err).into_response() Err(err) => AppError::from(err).into_response(),
} }
} }
@@ -160,8 +154,7 @@ pub async fn access_shared_item(
.into_response(); .into_response();
} }
if err.message.contains("expired") { if err.message.contains("expired") {
return AppError::new(StatusCode::GONE, err.message, "Expired") return AppError::new(StatusCode::GONE, err.message, "Expired").into_response();
.into_response();
} }
} }
AppError::from(err).into_response() AppError::from(err).into_response()
@@ -183,8 +176,7 @@ pub async fn verify_shared_item_password(
Err(err) => { Err(err) => {
if err.kind == ErrorKind::AccessDenied { if err.kind == ErrorKind::AccessDenied {
if err.message.contains("expired") { if err.message.contains("expired") {
return AppError::new(StatusCode::GONE, err.message, "Expired") return AppError::new(StatusCode::GONE, err.message, "Expired").into_response();
.into_response();
} }
if err.message.contains("password") { if err.message.contains("password") {
return AppError::unauthorized("Invalid password").into_response(); return AppError::unauthorized("Invalid password").into_response();
+14 -11
View File
@@ -1161,10 +1161,7 @@ async fn handle_move(
// Resolve source: single-query when PathResolver is available (user-scoped) // Resolve source: single-query when PathResolver is available (user-scoped)
if let Some(resolver) = &state.path_resolver { if let Some(resolver) = &state.path_resolver {
match resolver match resolver.resolve_path_for_user(&source_path, &user.id).await {
.resolve_path_for_user(&source_path, &user.id)
.await
{
Ok(ResolvedResource::Folder(folder)) => { Ok(ResolvedResource::Folder(folder)) => {
let dest_folder_name = destination_path let dest_folder_name = destination_path
.split('/') .split('/')
@@ -1183,7 +1180,11 @@ async fn handle_move(
match folder_service.get_folder_by_path(dest_parent_path).await { match folder_service.get_folder_by_path(dest_parent_path).await {
Ok(parent) => { Ok(parent) => {
// SECURITY: verify destination parent belongs to caller (V-08) // SECURITY: verify destination parent belongs to caller (V-08)
assert_owner(parent.owner_id.as_deref(), &user.id, dest_parent_path)?; assert_owner(
parent.owner_id.as_deref(),
&user.id,
dest_parent_path,
)?;
Some(parent.id) Some(parent.id)
} }
Err(_) => None, Err(_) => None,
@@ -1228,7 +1229,10 @@ async fn handle_move(
if source_parent_path != dest_parent_path { if source_parent_path != dest_parent_path {
// SECURITY: verify destination parent belongs to caller (V-08) // SECURITY: verify destination parent belongs to caller (V-08)
if !dest_parent_path.is_empty() && let Ok(parent) = folder_service.get_folder_by_path(dest_parent_path).await { if !dest_parent_path.is_empty()
&& let Ok(parent) =
folder_service.get_folder_by_path(dest_parent_path).await
{
assert_owner(parent.owner_id.as_deref(), &user.id, dest_parent_path)?; assert_owner(parent.owner_id.as_deref(), &user.id, dest_parent_path)?;
} }
file_management_service file_management_service
@@ -1327,7 +1331,9 @@ async fn handle_move(
if source_parent_path != dest_parent_path { if source_parent_path != dest_parent_path {
// SECURITY: verify destination parent belongs to caller (V-08) // SECURITY: verify destination parent belongs to caller (V-08)
if !dest_parent_path.is_empty() && let Ok(parent) = folder_service.get_folder_by_path(dest_parent_path).await { if !dest_parent_path.is_empty()
&& let Ok(parent) = folder_service.get_folder_by_path(dest_parent_path).await
{
assert_owner(parent.owner_id.as_deref(), &user.id, dest_parent_path)?; assert_owner(parent.owner_id.as_deref(), &user.id, dest_parent_path)?;
} }
file_management_service file_management_service
@@ -1436,10 +1442,7 @@ async fn handle_copy(
// Resolve source: single-query when PathResolver is available (user-scoped) // Resolve source: single-query when PathResolver is available (user-scoped)
if let Some(resolver) = &state.path_resolver { if let Some(resolver) = &state.path_resolver {
match resolver match resolver.resolve_path_for_user(&source_path, &user.id).await {
.resolve_path_for_user(&source_path, &user.id)
.await
{
Ok(ResolvedResource::Folder(folder)) => { Ok(ResolvedResource::Folder(folder)) => {
let recursive = depth != "0"; let recursive = depth != "0";