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>;
/// 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.
///
+1 -5
View File
@@ -16,11 +16,7 @@ pub trait ShareUseCase: Send + Sync + 'static {
) -> Result<ShareDto, DomainError>;
/// Get a shared link by its ID (ownership-verified)
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>;
/// 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>;
@@ -384,10 +384,7 @@ impl AdminSettingsService {
/// initialized (the caller "won" the race), or `Ok(false)` if another
/// request already did it. This eliminates the race-condition window
/// between `is_system_initialized()` and `mark_system_initialized()`.
pub async fn try_claim_initialization(
&self,
admin_user_id: &str,
) -> Result<bool, DomainError> {
pub async fn try_claim_initialization(&self, admin_user_id: &str) -> Result<bool, DomainError> {
self.settings_repo
.try_claim_initialization(admin_user_id)
.await
+8 -2
View File
@@ -682,7 +682,11 @@ impl BatchOperationService {
// ── Add folders as sub-trees (bulk subtree queries, not N+1) ─────
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) => {
if let Err(e) = self
.add_folder_subtree_to_zip(&mut zip, folder_id, &root_folder, caller_id)
@@ -928,7 +932,9 @@ impl BatchOperationService {
async move {
// 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);
return (id, Err(e));
}
@@ -228,7 +228,10 @@ impl FileRetrievalUseCase for FileRetrievalService {
folder_id: Option<&str>,
owner_id: &str,
) -> 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())
}
+5 -1
View File
@@ -32,7 +32,11 @@ impl FolderService {
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())
}
+16 -13
View File
@@ -20,7 +20,10 @@ use crate::{
storage_ports::FileReadPort,
},
},
common::{config::AppConfig, errors::{DomainError, ErrorKind}},
common::{
config::AppConfig,
errors::{DomainError, ErrorKind},
},
domain::entities::share::{Share, ShareItemType, SharePermissions},
};
@@ -147,11 +150,7 @@ impl ShareService {
/// but belongs to a different user — this prevents share-ID enumeration
/// attacks where an attacker probes IDs and uses 403-vs-404 to learn
/// which ones are valid.
async fn fetch_owned_share(
&self,
id: &str,
requester_id: &str,
) -> Result<Share, DomainError> {
async fn fetch_owned_share(&self, id: &str, requester_id: &str) -> Result<Share, DomainError> {
let share = self
.share_repository
.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()))
}
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> {
// SECURITY: ownership-verified lookup — returns 404 if the share
// doesn't exist OR belongs to another user.
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()))
}
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
.share_repository
.find_share_by_id_for_user(id, requester_id)
@@ -634,7 +633,11 @@ mod tests {
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
.delete_share_for_user(id, requester_id)
.await
+5 -1
View File
@@ -186,7 +186,11 @@ impl TrashUseCase for TrashService {
// Returns NotFound if the file does not exist OR belongs to
// another user, preventing cross-user trash operations.
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) => {
debug!("File found: {} ({})", file.name(), item_id);
file
+5 -1
View File
@@ -359,7 +359,11 @@ impl FolderUseCase for StubFolderUseCase {
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())
}
+10 -4
View File
@@ -292,8 +292,10 @@ impl User {
));
}
// Disallow leading/trailing dots or hyphens
if username.starts_with('.') || username.starts_with('-')
|| username.ends_with('.') || username.ends_with('-')
if username.starts_with('.')
|| username.starts_with('-')
|| username.ends_with('.')
|| username.ends_with('-')
{
return Err(UserError::InvalidUsername(
"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<()> {
let parts: Vec<&str> = email.splitn(2, '@').collect();
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]);
if local.is_empty() || domain.is_empty() {
@@ -324,7 +328,9 @@ impl User {
));
}
// 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)) {
return Err(UserError::ValidationError(
"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
/// get-then-set pattern for repositories that don't support a native
/// atomic upsert.
async fn try_claim_initialization(
&self,
admin_user_id: &str,
) -> Result<bool, DomainError> {
async fn try_claim_initialization(&self, admin_user_id: &str) -> Result<bool, DomainError> {
// Default: non-atomic fallback (overridden by PG implementation)
match self.get("system_initialized").await? {
Some(v) if v == "true" => Ok(false),
@@ -325,9 +325,7 @@ impl FileReadPort for FileBlobReadRepository {
.fetch_all(self.pool.as_ref())
.await
}
.map_err(|e| {
DomainError::internal_error("FileBlobRead", format!("list_for_owner: {e}"))
})?;
.map_err(|e| DomainError::internal_error("FileBlobRead", format!("list_for_owner: {e}")))?;
rows.into_iter()
.map(|(id, name, fid, fpath, size, mime, ca, ma, uid)| {
@@ -868,7 +868,9 @@ impl FolderRepository for FolderDbRepository {
user_id: &str,
) -> Result<Vec<Folder>, DomainError> {
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),
};
+4 -1
View File
@@ -50,6 +50,9 @@ pub use user_pg_repository::UserPgRepository;
/// `%` is a wildcard in LIKE patterns.
#[inline]
pub fn like_escape(raw: &str) -> String {
let escaped = raw.replace('\\', "\\\\").replace('%', "\\%").replace('_', "\\_");
let escaped = raw
.replace('\\', "\\\\")
.replace('%', "\\%")
.replace('_', "\\_");
format!("%{escaped}%")
}
@@ -102,10 +102,7 @@ impl SettingsRepository for SettingsPgRepository {
///
/// Only the first caller that inserts the row gets `rows_affected == 1`;
/// concurrent callers see 0 rows affected and receive `false`.
async fn try_claim_initialization(
&self,
admin_user_id: &str,
) -> Result<bool, DomainError> {
async fn try_claim_initialization(&self, admin_user_id: &str) -> Result<bool, DomainError> {
let result = sqlx::query(
"INSERT INTO auth.admin_settings (key, value, category, is_secret, updated_by, updated_at)
VALUES ('system_initialized', 'true', 'system', false, $1, NOW())
@@ -202,10 +202,7 @@ impl ShareStoragePort for SharePgRepository {
.await
.map_err(|e| {
tracing::error!("Database error deleting share for user: {}", e);
DomainError::internal_error(
"Share",
format!("Failed to delete share: {e}"),
)
DomainError::internal_error("Share", format!("Failed to delete share: {e}"))
})?;
if result.rows_affected() == 0 {
@@ -242,10 +239,7 @@ impl ShareStoragePort for SharePgRepository {
.await
.map_err(|e| {
tracing::error!("Database error finding shares by item for user: {}", e);
DomainError::internal_error(
"Share",
format!("Failed to find shares by item: {e}"),
)
DomainError::internal_error("Share", format!("Failed to find shares by item: {e}"))
})?;
rows.iter().map(Self::row_to_entity).collect()
@@ -371,11 +371,7 @@ impl ChunkedUploadService {
/// Verify that the given session belongs to the given user.
/// Returns 404 (not 403) to avoid revealing the existence of other users' sessions.
fn verify_session_owner(
&self,
upload_id: &str,
user_id: &str,
) -> Result<(), String> {
fn verify_session_owner(&self, upload_id: &str, user_id: &str) -> Result<(), String> {
let session = self
.sessions
.get(upload_id)
@@ -592,7 +588,11 @@ impl ChunkedUploadService {
}
/// 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)?;
let session = self
@@ -808,7 +808,14 @@ impl ChunkedUploadPort for ChunkedUploadService {
total_size: u64,
chunk_size: Option<usize>,
) -> Result<CreateUploadResponseDto, DomainError> {
self.create_session_inner(user_id.to_owned(), filename, folder_id, content_type, total_size, chunk_size)
self.create_session_inner(
user_id.to_owned(),
filename,
folder_id,
content_type,
total_size,
chunk_size,
)
.await
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ChunkedUpload", e))
}
@@ -826,7 +833,11 @@ impl ChunkedUploadPort for ChunkedUploadService {
.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)
.await
.map_err(|e| DomainError::new(ErrorKind::NotFound, "ChunkedUpload", e))
@@ -1097,14 +1108,19 @@ mod tests {
assert_eq!(r1.bytes_received, 1024);
// 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_eq!(status.completed_chunks, 2);
assert!(status.pending_chunks.is_empty());
// 4. Complete (assemble)
let (path, filename, _folder, _ct, size, hash) =
service.complete_upload_inner(&id, "test-user").await.expect("complete");
let (path, filename, _folder, _ct, size, hash) = service
.complete_upload_inner(&id, "test-user")
.await
.expect("complete");
assert_eq!(filename, "test.txt");
assert_eq!(size, 1024);
assert!(!hash.is_empty());
@@ -1116,7 +1132,10 @@ mod tests {
assert_eq!(&content[512..], &[b'B'; 512]);
// 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);
let _ = fs::remove_dir_all(&base).await;
@@ -1,8 +1,8 @@
use serde_json::Value;
use std::collections::HashMap;
use std::path::PathBuf;
use tokio::sync::RwLock;
use tokio::fs;
use tokio::sync::RwLock;
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.
use serde::Deserialize;
use tokio::sync::RwLock;
use std::time::{Duration, Instant};
use tokio::sync::RwLock;
use crate::application::ports::auth_ports::{OidcIdClaims, OidcServicePort, OidcTokenSet};
use crate::common::config::OidcConfig;
@@ -215,7 +215,9 @@ impl PathResolverService {
.bind(user_id)
.fetch_one(self.pool.as_ref())
.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)
}
@@ -8,10 +8,7 @@ fn tiny_png() -> Vec<u8> {
let mut img = image::RgbaImage::new(1, 1);
img.put_pixel(0, 0, image::Rgba([255, 0, 0, 255]));
let mut buf = Vec::new();
img.write_to(
&mut std::io::Cursor::new(&mut buf),
image::ImageFormat::Png,
)
img.write_to(&mut std::io::Cursor::new(&mut buf), image::ImageFormat::Png)
.expect("encode test PNG");
buf
}
+3 -1
View File
@@ -342,7 +342,9 @@ async fn logout(
.ok()
.map(|dto| dto.refresh_token)
.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_application_service
+40 -10
View File
@@ -164,7 +164,10 @@ pub async fn move_files_batch(
.await
.map_err(|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
@@ -217,7 +220,10 @@ pub async fn copy_files_batch(
.await
.map_err(|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
@@ -270,7 +276,10 @@ pub async fn delete_files_batch(
.await
.map_err(|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
@@ -331,7 +340,10 @@ pub async fn delete_folders_batch(
.await
.map_err(|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
@@ -399,7 +411,10 @@ pub async fn create_folders_batch(
.await
.map_err(|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
@@ -452,7 +467,10 @@ pub async fn get_files_batch(
.await
.map_err(|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
@@ -505,7 +523,10 @@ pub async fn get_folders_batch(
.await
.map_err(|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
@@ -690,7 +711,10 @@ pub async fn move_folders_batch(
.await
.map_err(|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();
@@ -727,7 +751,10 @@ pub async fn download_batch(
if combined_size > MAX_BATCH_SIZE {
return Err((
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
.map_err(|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
@@ -190,7 +190,13 @@ impl ChunkedUploadHandler {
});
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
{
Ok(response) => {
@@ -213,7 +219,7 @@ impl ChunkedUploadHandler {
.unwrap()
.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)
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,
Err(e) => {
return AppError::from(e).into_response();
@@ -289,7 +298,9 @@ impl ChunkedUploadHandler {
{
Ok(file) => {
// 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!(
"✅ CHUNKED UPLOAD COMPLETE: {} (ID: {}, {} bytes)",
@@ -311,8 +322,7 @@ impl ChunkedUploadHandler {
}
Err(e) => {
tracing::error!("Failed to create file from assembled upload: {:?}", e);
AppError::internal_error(format!("Failed to create file: {}", e))
.into_response()
AppError::internal_error(format!("Failed to create file: {}", e)).into_response()
}
}
}
@@ -327,10 +337,14 @@ impl ChunkedUploadHandler {
) -> impl IntoResponse {
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(),
Err(e) => AppError::internal_error(format!("Failed to cancel upload: {}", e))
.into_response(),
Err(e) => {
AppError::internal_error(format!("Failed to cancel upload: {}", e)).into_response()
}
}
}
}
+2 -6
View File
@@ -231,9 +231,7 @@ impl DedupHandler {
return Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(
r#"{"error": "Upload failed"}"#,
))
.body(Body::from(r#"{"error": "Upload failed"}"#))
.unwrap()
.into_response();
}
@@ -408,9 +406,7 @@ impl DedupHandler {
return Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(
r#"{"error": "Verification failed"}"#,
))
.body(Body::from(r#"{"error": "Verification failed"}"#))
.unwrap()
.into_response();
}
+20 -19
View File
@@ -106,7 +106,11 @@ impl FileHandler {
if let Some(ref fid) = folder_id {
use crate::application::ports::inbound::FolderUseCase;
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!(
"⛔ UPLOAD REJECTED (IDOR): user='{}' attempted upload to folder '{}' owned by another user",
auth_user.username,
@@ -323,7 +327,12 @@ impl FileHandler {
}
// 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,
Err(err) => {
return (
@@ -353,10 +362,8 @@ impl FileHandler {
.unwrap()
.into_response()
}
Err(err) => {
AppError::internal_error(format!("Thumbnail generation failed: {}", err))
.into_response()
}
Err(err) => AppError::internal_error(format!("Thumbnail generation failed: {}", err))
.into_response(),
}
}
@@ -529,9 +536,7 @@ impl FileHandler {
.unwrap()
.into_response(),
},
Err(err) => {
AppError::from(err).into_response()
}
Err(err) => AppError::from(err).into_response(),
}
}
@@ -581,9 +586,7 @@ impl FileHandler {
.insert(header::ETAG, header::HeaderValue::from_str(&etag).unwrap());
resp
}
Err(err) => {
AppError::from(err).into_response()
}
Err(err) => AppError::from(err).into_response(),
}
}
@@ -664,7 +667,7 @@ impl FileHandler {
match result {
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;
match mgmt.rename_file_owned(&id, &auth_user.id, &new_name).await {
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
{
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;
match mgmt.move_file_owned(&id, &auth_user.id, folder_id).await {
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();
format!(
"{disposition}; filename=\"{ascii_safe}\"; filename*=UTF-8''{encoded}"
)
format!("{disposition}; filename=\"{ascii_safe}\"; filename*=UTF-8''{encoded}")
}
/// 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) ──
if let Some(ref parent_id) = dto.parent_id {
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!(
"create_folder: user '{}' attempted to create folder in parent '{}' owned by another user",
auth_user.username,
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 {
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()
}
Err(err) => AppError::from(err).into_response()
Err(err) => AppError::from(err).into_response(),
}
}
@@ -154,7 +159,7 @@ impl FolderHandler {
.await
{
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
{
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());
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 {
match service.rename_folder(&id, dto, &auth_user.id).await {
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 {
match service.move_folder(&id, dto, &auth_user.id).await {
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 {
match service.delete_folder(&id, &auth_user.id).await {
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);
StatusCode::NO_CONTENT.into_response()
}
Err(err) => {
AppError::from(err).into_response()
}
Err(err) => AppError::from(err).into_response(),
}
}
+8 -16
View File
@@ -40,12 +40,9 @@ pub async fn create_shared_link(
auth_user: AuthUser,
Json(dto): Json<CreateShareDto>,
) -> impl IntoResponse {
match share_use_case
.create_shared_link(&auth_user.id, dto)
.await
{
match share_use_case.create_shared_link(&auth_user.id, dto).await {
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 {
match share_use_case.get_shared_link(&id, &auth_user.id).await {
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
{
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,
Path(id): Path<String>,
) -> impl IntoResponse {
match share_use_case
.delete_shared_link(&id, &auth_user.id)
.await
{
match share_use_case.delete_shared_link(&id, &auth_user.id).await {
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();
}
if err.message.contains("expired") {
return AppError::new(StatusCode::GONE, err.message, "Expired")
.into_response();
return AppError::new(StatusCode::GONE, err.message, "Expired").into_response();
}
}
AppError::from(err).into_response()
@@ -183,8 +176,7 @@ pub async fn verify_shared_item_password(
Err(err) => {
if err.kind == ErrorKind::AccessDenied {
if err.message.contains("expired") {
return AppError::new(StatusCode::GONE, err.message, "Expired")
.into_response();
return AppError::new(StatusCode::GONE, err.message, "Expired").into_response();
}
if err.message.contains("password") {
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)
if let Some(resolver) = &state.path_resolver {
match resolver
.resolve_path_for_user(&source_path, &user.id)
.await
{
match resolver.resolve_path_for_user(&source_path, &user.id).await {
Ok(ResolvedResource::Folder(folder)) => {
let dest_folder_name = destination_path
.split('/')
@@ -1183,7 +1180,11 @@ async fn handle_move(
match folder_service.get_folder_by_path(dest_parent_path).await {
Ok(parent) => {
// 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)
}
Err(_) => None,
@@ -1228,7 +1229,10 @@ async fn handle_move(
if source_parent_path != dest_parent_path {
// 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)?;
}
file_management_service
@@ -1327,7 +1331,9 @@ async fn handle_move(
if source_parent_path != dest_parent_path {
// 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)?;
}
file_management_service
@@ -1436,10 +1442,7 @@ async fn handle_copy(
// Resolve source: single-query when PathResolver is available (user-scoped)
if let Some(resolver) = &state.path_resolver {
match resolver
.resolve_path_for_user(&source_path, &user.id)
.await
{
match resolver.resolve_path_for_user(&source_path, &user.id).await {
Ok(ResolvedResource::Folder(folder)) => {
let recursive = depth != "0";