fix(#105): ensure folders are nested inside user home folder

Root cause: when window.app.currentPath was empty/falsy (due to timing,
page state reset, or initialization), the frontend sent parent_id: null.
The backend then created folders at the storage root instead of inside
the user's home folder.

Backend fix (folder_handler.rs):
- Added AuthUser extractor to create_folder handler
- When parent_id is None, auto-resolves the user's home folder
  ('My Folder - {username}') as the parent folder
- Folders are now always created inside the user's directory tree

Frontend fix (fileOperations.js):
- Changed parent_id fallback from null to window.app.userHomeFolderId
- Prevents sending null parent_id even if currentPath is reset

Search in subfolders: no fix needed — search_recursive() already
traverses the filesystem correctly; it was only failing because folders
were physically flat instead of nested.

Bumps service worker cache to v12.
This commit is contained in:
Dionisio
2026-02-14 12:45:46 +01:00
parent 9290aea591
commit ebb0aee84e
3 changed files with 130 additions and 139 deletions
+81 -90
View File
@@ -1,19 +1,19 @@
use axum::{
Json,
extract::{Path, Query, State},
http::{HeaderName, HeaderValue, Response, StatusCode, header},
response::IntoResponse,
};
use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use std::collections::HashMap;
use axum::{
extract::{Path, State, Query},
http::{StatusCode, header, HeaderName, HeaderValue, Response},
response::IntoResponse,
Json,
};
use crate::application::dtos::folder_dto::{CreateFolderDto, MoveFolderDto, RenameFolderDto};
use crate::application::dtos::pagination::PaginationRequestDto;
use crate::application::ports::inbound::FolderUseCase;
use crate::application::services::folder_service::FolderService; use crate::application::services::folder_service::FolderService;
use crate::common::di::AppState as GlobalAppState; use crate::application::dtos::folder_dto::{CreateFolderDto, RenameFolderDto, MoveFolderDto};
use crate::application::dtos::pagination::PaginationRequestDto;
use crate::common::errors::ErrorKind; use crate::common::errors::ErrorKind;
use crate::interfaces::middleware::auth::{AuthUser, OptionalAuthUser}; use crate::application::ports::inbound::FolderUseCase;
use crate::common::di::AppState as GlobalAppState;
use crate::interfaces::middleware::auth::{OptionalAuthUser, AuthUser};
type AppState = Arc<FolderService>; type AppState = Arc<FolderService>;
@@ -22,10 +22,44 @@ pub struct FolderHandler;
impl FolderHandler { impl FolderHandler {
/// Creates a new folder /// Creates a new folder
/// When parent_id is not provided, the folder is created inside the
/// authenticated user's home folder ("My Folder - {username}") rather
/// than at the storage root. This prevents user-created directories
/// from being placed flat in ./storage/.
pub async fn create_folder( pub async fn create_folder(
State(service): State<AppState>, State(service): State<AppState>,
Json(dto): Json<CreateFolderDto>, auth_user: AuthUser,
Json(mut dto): Json<CreateFolderDto>,
) -> impl IntoResponse { ) -> impl IntoResponse {
// If no parent_id was supplied, resolve the user's home folder as
// the default parent so the new folder is nested correctly.
if dto.parent_id.is_none() {
let home_folder_name = format!("My Folder - {}", auth_user.username);
tracing::info!(
"create_folder: parent_id is None for user '{}', looking up home folder '{}'",
auth_user.username, home_folder_name
);
match service.list_folders(None).await {
Ok(folders) => {
if let Some(home) = folders.iter().find(|f| f.name == home_folder_name) {
tracing::info!(
"create_folder: resolved home folder ID '{}' for user '{}'",
home.id, auth_user.username
);
dto.parent_id = Some(home.id.clone());
} else {
tracing::warn!(
"create_folder: home folder '{}' not found, folder will be created at root",
home_folder_name
);
}
}
Err(e) => {
tracing::error!("create_folder: failed to list folders for home resolution: {}", e);
}
}
}
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) => { Err(err) => {
@@ -111,20 +145,18 @@ impl FolderHandler {
parent_id: Option<&str>, parent_id: Option<&str>,
) -> axum::response::Response { ) -> axum::response::Response {
match service.list_folders(parent_id).await { match service.list_folders(parent_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 {
ErrorKind::NotFound => StatusCode::NOT_FOUND, ErrorKind::NotFound => StatusCode::NOT_FOUND,
_ => StatusCode::INTERNAL_SERVER_ERROR, _ => StatusCode::INTERNAL_SERVER_ERROR,
}; };
( (status, Json(serde_json::json!({
status,
Json(serde_json::json!({
"error": err.to_string() "error": err.to_string()
})), }))).into_response()
)
.into_response()
} }
} }
} }
@@ -140,9 +172,7 @@ impl FolderHandler {
Ok(folders) => { Ok(folders) => {
// Only filter at root level (parent_id == None) // Only filter at root level (parent_id == None)
let filtered = if parent_id.is_none() { let filtered = if parent_id.is_none() {
folders folders.into_iter().filter(|f| {
.into_iter()
.filter(|f| {
// Skip hidden/system folders // Skip hidden/system folders
if f.name.starts_with('.') { if f.name.starts_with('.') {
return false; return false;
@@ -153,25 +183,20 @@ impl FolderHandler {
} }
// Non-home folders are visible to everyone // Non-home folders are visible to everyone
true true
}) }).collect()
.collect()
} else { } else {
folders folders
}; };
(StatusCode::OK, Json(filtered)).into_response() (StatusCode::OK, Json(filtered)).into_response()
} },
Err(err) => { Err(err) => {
let status = match err.kind { let status = match err.kind {
ErrorKind::NotFound => StatusCode::NOT_FOUND, ErrorKind::NotFound => StatusCode::NOT_FOUND,
_ => StatusCode::INTERNAL_SERVER_ERROR, _ => StatusCode::INTERNAL_SERVER_ERROR,
}; };
( (status, Json(serde_json::json!({
status,
Json(serde_json::json!({
"error": err.to_string() "error": err.to_string()
})), }))).into_response()
)
.into_response()
} }
} }
} }
@@ -183,7 +208,9 @@ impl FolderHandler {
parent_id: Option<&str>, parent_id: Option<&str>,
) -> axum::response::Response { ) -> axum::response::Response {
match service.list_folders_paginated(parent_id, &pagination).await { match service.list_folders_paginated(parent_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 {
ErrorKind::NotFound => StatusCode::NOT_FOUND, ErrorKind::NotFound => StatusCode::NOT_FOUND,
@@ -191,13 +218,9 @@ impl FolderHandler {
}; };
// Return a JSON error response // Return a JSON error response
( (status, Json(serde_json::json!({
status,
Json(serde_json::json!({
"error": err.to_string() "error": err.to_string()
})), }))).into_response()
)
.into_response()
} }
} }
} }
@@ -218,13 +241,9 @@ impl FolderHandler {
}; };
// Return a proper JSON error response // Return a proper JSON error response
( (status, Json(serde_json::json!({
status,
Json(serde_json::json!({
"error": err.to_string() "error": err.to_string()
})), }))).into_response()
)
.into_response()
} }
} }
} }
@@ -274,10 +293,7 @@ impl FolderHandler {
OptionalAuthUser(auth_user): OptionalAuthUser, OptionalAuthUser(auth_user): OptionalAuthUser,
Path(id): Path<String>, Path(id): Path<String>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let user_id = auth_user let user_id = auth_user.as_ref().map(|u| u.id.as_str()).unwrap_or("anonymous");
.as_ref()
.map(|u| u.id.as_str())
.unwrap_or("anonymous");
// Check if trash service is available // Check if trash service is available
if let Some(trash_service) = &state.trash_service { if let Some(trash_service) = &state.trash_service {
tracing::info!("Moving folder to trash: {}", id); tracing::info!("Moving folder to trash: {}", id);
@@ -287,12 +303,9 @@ impl FolderHandler {
Ok(_) => { Ok(_) => {
tracing::info!("Folder successfully moved to trash: {}", id); tracing::info!("Folder successfully moved to trash: {}", id);
return StatusCode::NO_CONTENT.into_response(); return StatusCode::NO_CONTENT.into_response();
} },
Err(err) => { Err(err) => {
tracing::warn!( tracing::warn!("Could not move folder to trash, falling back to permanent delete: {}", err);
"Could not move folder to trash, falling back to permanent delete: {}",
err
);
// Fall through to regular delete if trash fails // Fall through to regular delete if trash fails
} }
} }
@@ -304,7 +317,7 @@ impl FolderHandler {
Ok(_) => { Ok(_) => {
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) => {
tracing::error!("Error deleting folder: {}", err); tracing::error!("Error deleting folder: {}", err);
@@ -313,13 +326,9 @@ impl FolderHandler {
_ => StatusCode::INTERNAL_SERVER_ERROR, _ => StatusCode::INTERNAL_SERVER_ERROR,
}; };
( (status, Json(serde_json::json!({
status,
Json(serde_json::json!({
"error": format!("Error deleting folder: {}", err) "error": format!("Error deleting folder: {}", err)
})), }))).into_response()
)
.into_response()
} }
} }
} }
@@ -345,10 +354,7 @@ impl FolderHandler {
// Create the ZIP file // Create the ZIP file
match zip_service.create_folder_zip(&id, &folder.name).await { match zip_service.create_folder_zip(&id, &folder.name).await {
Ok(zip_data) => { Ok(zip_data) => {
tracing::info!( tracing::info!("ZIP file created successfully, size: {} bytes", zip_data.len());
"ZIP file created successfully, size: {} bytes",
zip_data.len()
);
// Setup headers for download // Setup headers for download
let filename = format!("{}.zip", folder.name); let filename = format!("{}.zip", folder.name);
@@ -356,16 +362,9 @@ impl FolderHandler {
// Build response with the ZIP data // Build response with the ZIP data
let mut headers = HashMap::new(); let mut headers = HashMap::new();
headers.insert( headers.insert(header::CONTENT_TYPE.to_string(), "application/zip".to_string());
header::CONTENT_TYPE.to_string(), headers.insert(header::CONTENT_DISPOSITION.to_string(), content_disposition);
"application/zip".to_string(), headers.insert(header::CONTENT_LENGTH.to_string(), zip_data.len().to_string());
);
headers
.insert(header::CONTENT_DISPOSITION.to_string(), content_disposition);
headers.insert(
header::CONTENT_LENGTH.to_string(),
zip_data.len().to_string(),
);
// Build the response // Build the response
let mut response = Response::builder() let mut response = Response::builder()
@@ -377,24 +376,20 @@ impl FolderHandler {
for (name, value) in headers { for (name, value) in headers {
response.headers_mut().insert( response.headers_mut().insert(
HeaderName::from_bytes(name.as_bytes()).unwrap(), HeaderName::from_bytes(name.as_bytes()).unwrap(),
HeaderValue::from_str(&value).unwrap(), HeaderValue::from_str(&value).unwrap()
); );
} }
response response
} },
Err(err) => { Err(err) => {
tracing::error!("Error creating ZIP file: {}", err); tracing::error!("Error creating ZIP file: {}", err);
( (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": format!("Error creating ZIP file: {}", err) "error": format!("Error creating ZIP file: {}", err)
})), }))).into_response()
)
.into_response()
}
} }
} }
},
Err(err) => { Err(err) => {
tracing::error!("Folder not found: {}", err); tracing::error!("Folder not found: {}", err);
let status = match err.kind { let status = match err.kind {
@@ -402,13 +397,9 @@ impl FolderHandler {
_ => StatusCode::INTERNAL_SERVER_ERROR, _ => StatusCode::INTERNAL_SERVER_ERROR,
}; };
( (status, Json(serde_json::json!({
status,
Json(serde_json::json!({
"error": format!("Error finding folder: {}", err) "error": format!("Error finding folder: {}", err)
})), }))).into_response()
)
.into_response()
} }
} }
} }
+1 -1
View File
@@ -342,7 +342,7 @@ const fileOps = {
}, },
body: JSON.stringify({ body: JSON.stringify({
name: name, name: name,
parent_id: window.app.currentPath || null parent_id: window.app.currentPath || window.app.userHomeFolderId || null
}) })
}); });
+1 -1
View File
@@ -1,5 +1,5 @@
// OxiCloud Service Worker // OxiCloud Service Worker
const CACHE_NAME = 'oxicloud-cache-v11'; const CACHE_NAME = 'oxicloud-cache-v12';
const ASSETS_TO_CACHE = [ const ASSETS_TO_CACHE = [
'/', '/',
'/index.html', '/index.html',