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:
@@ -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,
|
"error": err.to_string()
|
||||||
Json(serde_json::json!({
|
}))).into_response()
|
||||||
"error": err.to_string()
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.into_response()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -140,38 +172,31 @@ 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()
|
// Skip hidden/system folders
|
||||||
.filter(|f| {
|
if f.name.starts_with('.') {
|
||||||
// Skip hidden/system folders
|
return false;
|
||||||
if f.name.starts_with('.') {
|
}
|
||||||
return false;
|
// If it's a user home folder, only show if it belongs to this user
|
||||||
}
|
if Self::is_user_home_folder(&f.name) {
|
||||||
// If it's a user home folder, only show if it belongs to this user
|
return Self::folder_belongs_to_user(&f.name, &auth_user.username);
|
||||||
if Self::is_user_home_folder(&f.name) {
|
}
|
||||||
return Self::folder_belongs_to_user(&f.name, &auth_user.username);
|
// Non-home folders are visible to everyone
|
||||||
}
|
true
|
||||||
// Non-home folders are visible to everyone
|
}).collect()
|
||||||
true
|
|
||||||
})
|
|
||||||
.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,
|
"error": err.to_string()
|
||||||
Json(serde_json::json!({
|
}))).into_response()
|
||||||
"error": err.to_string()
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.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,
|
"error": err.to_string()
|
||||||
Json(serde_json::json!({
|
}))).into_response()
|
||||||
"error": err.to_string()
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.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,
|
"error": err.to_string()
|
||||||
Json(serde_json::json!({
|
}))).into_response()
|
||||||
"error": err.to_string()
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.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,
|
"error": format!("Error deleting folder: {}", err)
|
||||||
Json(serde_json::json!({
|
}))).into_response()
|
||||||
"error": format!("Error deleting folder: {}", err)
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.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,
|
"error": format!("Error creating ZIP file: {}", err)
|
||||||
Json(serde_json::json!({
|
}))).into_response()
|
||||||
"error": format!("Error creating ZIP file: {}", err)
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.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,
|
"error": format!("Error finding folder: {}", err)
|
||||||
Json(serde_json::json!({
|
}))).into_response()
|
||||||
"error": format!("Error finding folder: {}", err)
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.into_response()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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',
|
||||||
|
|||||||
Reference in New Issue
Block a user