feat(folders): add curser and the normalized way to get foler's item list. add reverse order
This commit is contained in:
@@ -10,10 +10,16 @@ use std::hash::{Hash, Hasher};
|
||||
use std::sync::Arc;
|
||||
use tokio_util::io::ReaderStream;
|
||||
|
||||
use crate::application::dtos::display_helpers::{
|
||||
category_for, format_file_size, icon_class_for, icon_special_class_for,
|
||||
};
|
||||
use crate::application::dtos::file_dto::FileDto;
|
||||
use crate::application::dtos::folder_dto::{
|
||||
CreateFolderDto, FolderDto, MoveFolderDto, RenameFolderDto,
|
||||
CreateFolderDto, FolderDto, FolderResourceItemDto, FolderResourcesDto, FolderResourcesQuery,
|
||||
ListResourcesOptions, MoveFolderDto, RenameFolderDto,
|
||||
};
|
||||
use crate::application::dtos::folder_listing_dto::FolderListingDto;
|
||||
use crate::application::dtos::grant_dto::{ResourceContentDto, ResourceTypeDto};
|
||||
use crate::application::dtos::pagination::PaginationRequestDto;
|
||||
use crate::application::ports::file_ports::FileRetrievalUseCase;
|
||||
use crate::application::ports::folder_ports::FolderUseCase;
|
||||
@@ -466,6 +472,7 @@ pub async fn list_root_folders(
|
||||
FolderHandler::list_root_folders_impl(state, auth_user).await
|
||||
}
|
||||
|
||||
#[deprecated = "Use /api/folders/{id}/resources instead"]
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/folders/{id}/contents",
|
||||
@@ -477,6 +484,7 @@ pub async fn list_root_folders(
|
||||
security(("bearerAuth" = [])),
|
||||
tag = "folders"
|
||||
)]
|
||||
#[allow(deprecated)]
|
||||
pub async fn list_folder_contents(
|
||||
state: State<AppState>,
|
||||
auth_user: AuthUser,
|
||||
@@ -503,6 +511,7 @@ pub async fn list_root_folders_paginated(
|
||||
FolderHandler::list_root_folders_paginated_impl(state, auth_user, pagination).await
|
||||
}
|
||||
|
||||
#[deprecated = "Use /api/folders/{id}/resources instead"]
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/folders/{id}/contents/paginated",
|
||||
@@ -517,6 +526,7 @@ pub async fn list_root_folders_paginated(
|
||||
security(("bearerAuth" = [])),
|
||||
tag = "folders"
|
||||
)]
|
||||
#[allow(deprecated)]
|
||||
pub async fn list_folder_contents_paginated(
|
||||
state: State<AppState>,
|
||||
auth_user: AuthUser,
|
||||
@@ -526,6 +536,7 @@ pub async fn list_folder_contents_paginated(
|
||||
FolderHandler::list_folder_contents_paginated_impl(state, auth_user, path, pagination).await
|
||||
}
|
||||
|
||||
#[deprecated = "Use /api/folders/{id}/resources instead"]
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/folders/{id}/listing",
|
||||
@@ -538,6 +549,7 @@ pub async fn list_folder_contents_paginated(
|
||||
security(("bearerAuth" = [])),
|
||||
tag = "folders"
|
||||
)]
|
||||
#[allow(deprecated)]
|
||||
pub async fn list_folder_listing(
|
||||
state: State<Arc<GlobalAppState>>,
|
||||
auth_user: AuthUser,
|
||||
@@ -628,3 +640,105 @@ pub async fn download_folder_zip(
|
||||
) -> impl IntoResponse {
|
||||
FolderHandler::download_folder_zip_impl(state, auth_user, path, query).await
|
||||
}
|
||||
|
||||
// ── GET /api/folders/{id}/resources ─────────────────────────────────────────
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/folders/{id}/resources",
|
||||
params(
|
||||
("id" = String, Path, description = "Folder ID"),
|
||||
FolderResourcesQuery,
|
||||
),
|
||||
responses(
|
||||
(status = 200,
|
||||
description = "Cursor-paginated files and folders inside the requested folder. \
|
||||
Items arrive in `order_by` order (folders first when order_by=name). \
|
||||
`next_cursor` is absent on the last page.",
|
||||
body = FolderResourcesDto),
|
||||
(status = 404, description = "Folder not found or access denied"),
|
||||
),
|
||||
tag = "folders"
|
||||
)]
|
||||
pub async fn list_folder_resources(
|
||||
State(service): State<AppState>,
|
||||
auth_user: AuthUser,
|
||||
Path(id): Path<String>,
|
||||
Query(q): Query<FolderResourcesQuery>,
|
||||
) -> impl IntoResponse {
|
||||
let order_by = q.order_by.clone().unwrap_or_else(|| "name".to_owned());
|
||||
let kinds = q.resource_kinds();
|
||||
let opts = ListResourcesOptions {
|
||||
limit: q.limit_clamped(),
|
||||
cursor: q.decode_cursor(),
|
||||
order_by: &order_by,
|
||||
kinds: kinds.as_deref(),
|
||||
reverse: q.reverse,
|
||||
};
|
||||
|
||||
match service
|
||||
.list_resources_paged_with_perms(&id, auth_user.id, opts)
|
||||
.await
|
||||
{
|
||||
Ok((rows, next_cursor)) => {
|
||||
let items: Vec<FolderResourceItemDto> = rows
|
||||
.into_iter()
|
||||
.map(|row| {
|
||||
if row.resource_type == "folder" {
|
||||
let dto = FolderDto {
|
||||
id: row.id.to_string(),
|
||||
name: row.name.clone(),
|
||||
path: String::new(), // cleared — share recipients must not see hierarchy
|
||||
parent_id: row.parent_id.map(|u| u.to_string()),
|
||||
owner_id: Some(row.owner_id.to_string()),
|
||||
created_at: row.created_at.timestamp() as u64,
|
||||
modified_at: row.modified_at.timestamp() as u64,
|
||||
is_root: false,
|
||||
icon_class: Arc::from("fas fa-folder"),
|
||||
icon_special_class: Arc::from("folder-icon"),
|
||||
category: Arc::from("Folder"),
|
||||
};
|
||||
FolderResourceItemDto {
|
||||
resource_type: ResourceTypeDto::Folder,
|
||||
resource: ResourceContentDto::Folder(dto),
|
||||
}
|
||||
} else {
|
||||
let mime = row
|
||||
.mime_type
|
||||
.as_deref()
|
||||
.unwrap_or("application/octet-stream");
|
||||
let size_bytes = row.size.max(0) as u64;
|
||||
let dto = FileDto {
|
||||
id: row.id.to_string(),
|
||||
name: row.name.clone(),
|
||||
path: String::new(),
|
||||
size: size_bytes,
|
||||
mime_type: Arc::from(mime),
|
||||
folder_id: row.parent_id.map(|u| u.to_string()),
|
||||
created_at: row.created_at.timestamp() as u64,
|
||||
modified_at: row.modified_at.timestamp() as u64,
|
||||
icon_class: Arc::from(icon_class_for(&row.name, mime)),
|
||||
icon_special_class: Arc::from(icon_special_class_for(&row.name, mime)),
|
||||
category: Arc::from(category_for(&row.name, mime)),
|
||||
size_formatted: format_file_size(size_bytes),
|
||||
owner_id: Some(row.owner_id.to_string()),
|
||||
sort_date: None,
|
||||
etag: String::new(),
|
||||
};
|
||||
FolderResourceItemDto {
|
||||
resource_type: ResourceTypeDto::File,
|
||||
resource: ResourceContentDto::File(dto),
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(FolderResourcesDto::with_cursor(items, next_cursor)),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
Err(e) => AppError::from(e).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,16 +341,18 @@ pub async fn list_shared_with_me(
|
||||
.into_response();
|
||||
}
|
||||
|
||||
// Decode cursor — discard it when the sort dimension changed to avoid
|
||||
// keyset confusion across sort modes.
|
||||
let reverse = q.reverse;
|
||||
|
||||
// Decode cursor — discard it when the sort dimension or direction changed
|
||||
// to avoid keyset confusion across sort modes.
|
||||
let cursor = q
|
||||
.decode_cursor::<GrantCursor>()
|
||||
.filter(|c| c.sort_by == sort_by);
|
||||
.filter(|c| c.sort_by == sort_by && c.reverse == reverse);
|
||||
|
||||
// Fetch paged summaries from the ACL engine.
|
||||
let (summaries, next_cursor) = match state
|
||||
.authorization
|
||||
.list_incoming_resources_paged(subject, &kinds, limit, cursor, sort_by)
|
||||
.list_incoming_resources_paged(subject, &kinds, limit, cursor, sort_by, reverse)
|
||||
.await
|
||||
{
|
||||
Ok(r) => r,
|
||||
|
||||
@@ -58,9 +58,10 @@ use crate::interfaces::api::handlers::file_handler::{
|
||||
delete_file, download_file, get_file_metadata, get_thumbnail, list_files_query,
|
||||
move_file_simple, rename_file, upload_file_with_thumbnails, upload_thumbnail,
|
||||
};
|
||||
#[allow(deprecated)]
|
||||
use crate::interfaces::api::handlers::folder_handler::{
|
||||
create_folder, delete_folder_with_trash, download_folder_zip, get_folder, list_folder_contents,
|
||||
list_folder_contents_paginated, list_folder_listing, list_root_folders,
|
||||
list_folder_contents_paginated, list_folder_listing, list_folder_resources, list_root_folders,
|
||||
list_root_folders_paginated, move_folder, rename_folder,
|
||||
};
|
||||
use crate::interfaces::api::handlers::i18n_handler::{
|
||||
@@ -155,6 +156,9 @@ pub fn create_public_api_routes(app_state: &Arc<AppState>) -> Router<Arc<AppStat
|
||||
/// These routes require authentication when auth is enabled.
|
||||
/// Receives the fully-assembled `AppState` and extracts all needed services
|
||||
/// from it, avoiding a long parameter list.
|
||||
// Legacy folder endpoints (contents, listing) are kept for backward-compat;
|
||||
// they are marked #[deprecated] so the OpenAPI spec shows them as deprecated.
|
||||
#[allow(deprecated)]
|
||||
pub fn create_api_routes(app_state: &Arc<AppState>) -> Router<Arc<AppState>> {
|
||||
// Extract services from the pre-built AppState
|
||||
let folder_service = app_state.applications.folder_service_concrete.clone();
|
||||
@@ -195,6 +199,7 @@ pub fn create_api_routes(app_state: &Arc<AppState>) -> Router<Arc<AppState>> {
|
||||
"/{id}/contents/paginated",
|
||||
get(list_folder_contents_paginated),
|
||||
)
|
||||
.route("/{id}/resources", get(list_folder_resources))
|
||||
.route("/{id}/rename", put(rename_folder))
|
||||
.route("/{id}/move", put(move_folder))
|
||||
.with_state(folder_service.clone());
|
||||
|
||||
Reference in New Issue
Block a user