feat(fileDto, folderDto): add is_favorite + is_shared
- provide is_shared and is_favorite information in DTO, information propagated as badge/buttons per items
regarding performances I try to be minimalis on SQL to prevent any perf regression
- remove old set of sharedids and favoriteids (was not functionnal anymore)
- fix date picker (no past selection) in grant
- fix contextmenu close on /files section
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
//! Shared enrichment helpers that populate the `is_favorite` and
|
||||
//! `is_shared` wire-contract flags on `FileDto` / `FolderDto` before
|
||||
//! Json emission.
|
||||
//!
|
||||
//! The two functions live here so single-item handlers across
|
||||
//! `folder_handler`, `file_handler`, `delta_upload_handler`,
|
||||
//! `photos_handler`, etc. all go through the same path — one place
|
||||
//! to change if the enrichment strategy ever moves (e.g. batch
|
||||
//! lookups, background prefetch).
|
||||
//!
|
||||
//! Every handler that returns a `FileDto` or `FolderDto` to the SPA
|
||||
//! MUST call one of these helpers. Handlers that emit only to
|
||||
//! WebDAV / NextCloud DAV surfaces (which drop these fields via the
|
||||
//! XML property serializer) can skip enrichment — the default `false`
|
||||
//! is never observable on those wires.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::application::dtos::file_dto::FileDto;
|
||||
use crate::application::dtos::folder_dto::FolderDto;
|
||||
use crate::common::di::AppState as GlobalAppState;
|
||||
|
||||
/// Populate the `is_favorite` + `is_shared` flags on a `FolderDto`.
|
||||
///
|
||||
/// Silently leaves the flags at their default `false` when the
|
||||
/// favorites service isn't wired (feature-off) or when the resource
|
||||
/// id doesn't parse as a UUID — the DTO stays valid on the wire and
|
||||
/// the misleading-`false` window closes as soon as the next listing
|
||||
/// refetch runs.
|
||||
pub async fn enrich_folder_flags(
|
||||
state: &Arc<GlobalAppState>,
|
||||
dto: &mut FolderDto,
|
||||
caller_id: Uuid,
|
||||
) {
|
||||
let Some(favs) = state.favorites_service.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let Ok(resource_id) = Uuid::parse_str(&dto.id) else {
|
||||
return;
|
||||
};
|
||||
if let Ok((fav, shr)) = favs.caller_flags(caller_id, "folder", resource_id).await {
|
||||
dto.is_favorite = fav;
|
||||
dto.is_shared = shr;
|
||||
}
|
||||
}
|
||||
|
||||
/// File counterpart of [`enrich_folder_flags`] — see that doc.
|
||||
pub async fn enrich_file_flags(state: &Arc<GlobalAppState>, dto: &mut FileDto, caller_id: Uuid) {
|
||||
let Some(favs) = state.favorites_service.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let Ok(resource_id) = Uuid::parse_str(&dto.id) else {
|
||||
return;
|
||||
};
|
||||
if let Ok((fav, shr)) = favs.caller_flags(caller_id, "file", resource_id).await {
|
||||
dto.is_favorite = fav;
|
||||
dto.is_shared = shr;
|
||||
}
|
||||
}
|
||||
|
||||
/// Batch variant: enrich every `FileDto` in a slice with per-item
|
||||
/// `caller_flags`. Runs the lookups sequentially — for the bulk
|
||||
/// endpoints (`get_files_by_ids`, `photos_handler`) this is one
|
||||
/// round trip per item; if that becomes hot on a large fetch, the
|
||||
/// callsite can be replaced with a single SQL query returning the
|
||||
/// pairs. Kept simple for now; the DTO is `&mut`, no clones.
|
||||
pub async fn enrich_file_flags_batch(
|
||||
state: &Arc<GlobalAppState>,
|
||||
dtos: &mut [FileDto],
|
||||
caller_id: Uuid,
|
||||
) {
|
||||
for dto in dtos.iter_mut() {
|
||||
enrich_file_flags(state, dto, caller_id).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Batch variant for folders — mirror of [`enrich_file_flags_batch`].
|
||||
pub async fn enrich_folder_flags_batch(
|
||||
state: &Arc<GlobalAppState>,
|
||||
dtos: &mut [FolderDto],
|
||||
caller_id: Uuid,
|
||||
) {
|
||||
for dto in dtos.iter_mut() {
|
||||
enrich_folder_flags(state, dto, caller_id).await;
|
||||
}
|
||||
}
|
||||
@@ -235,12 +235,18 @@ pub async fn delta_commit(
|
||||
.await
|
||||
.map_err(AppError::from)?;
|
||||
Ok(match outcome {
|
||||
DeltaCommitOutcome::Done { file, created } => {
|
||||
DeltaCommitOutcome::Done { mut file, created } => {
|
||||
let status = if created {
|
||||
StatusCode::CREATED
|
||||
} else {
|
||||
StatusCode::OK
|
||||
};
|
||||
crate::interfaces::api::handlers::caller_flags::enrich_file_flags(
|
||||
&state,
|
||||
&mut file,
|
||||
auth_user.id,
|
||||
)
|
||||
.await;
|
||||
(status, Json(file)).into_response()
|
||||
}
|
||||
DeltaCommitOutcome::StillMissing(still_missing) => (
|
||||
|
||||
@@ -219,6 +219,11 @@ pub async fn list_favorites_resources(
|
||||
category: intern_display("Folder"),
|
||||
created_by: row.created_by,
|
||||
updated_by: row.updated_by,
|
||||
// Every row on this listing is a favorite by
|
||||
// construction; the listing repo returns
|
||||
// `TRUE AS is_favorite` unconditionally.
|
||||
is_favorite: row.is_favorite,
|
||||
is_shared: row.is_shared,
|
||||
};
|
||||
FavoritesResourceItemDto {
|
||||
resource_type: ResourceTypeDto::Folder,
|
||||
@@ -267,6 +272,8 @@ pub async fn list_favorites_resources(
|
||||
etag,
|
||||
created_by: row.created_by,
|
||||
updated_by: row.updated_by,
|
||||
is_favorite: row.is_favorite,
|
||||
is_shared: row.is_shared,
|
||||
};
|
||||
FavoritesResourceItemDto {
|
||||
resource_type: ResourceTypeDto::File,
|
||||
|
||||
@@ -64,7 +64,15 @@ impl FileHandler {
|
||||
multipart: Multipart,
|
||||
) -> impl IntoResponse {
|
||||
match Self::upload_file_inner(&state, &auth_user, multipart).await {
|
||||
Ok((file, _blob_hash)) => Self::created_json_response(&file).into_response(),
|
||||
Ok((mut file, _blob_hash)) => {
|
||||
crate::interfaces::api::handlers::caller_flags::enrich_file_flags(
|
||||
&state,
|
||||
&mut file,
|
||||
auth_user.id,
|
||||
)
|
||||
.await;
|
||||
Self::created_json_response(&file).into_response()
|
||||
}
|
||||
Err(response) => response.into_response(),
|
||||
}
|
||||
}
|
||||
@@ -115,7 +123,15 @@ impl FileHandler {
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(file) => Self::created_json_response(&file).into_response(),
|
||||
Ok(mut file) => {
|
||||
crate::interfaces::api::handlers::caller_flags::enrich_file_flags(
|
||||
&state,
|
||||
&mut file,
|
||||
auth_user.id,
|
||||
)
|
||||
.await;
|
||||
Self::created_json_response(&file).into_response()
|
||||
}
|
||||
Err(err) => {
|
||||
// Anti-enumeration shape: every "caller cannot reach this
|
||||
// hash" outcome collapses into the same 404 with an
|
||||
@@ -890,11 +906,16 @@ impl FileHandler {
|
||||
auth_user: AuthUser,
|
||||
multipart: Multipart,
|
||||
) -> impl IntoResponse {
|
||||
let (file, _) = match Self::upload_file_inner(&state, &auth_user, multipart).await {
|
||||
let (mut file, _) = match Self::upload_file_inner(&state, &auth_user, multipart).await {
|
||||
Ok(pair) => pair,
|
||||
Err(response) => return response.into_response(),
|
||||
};
|
||||
|
||||
crate::interfaces::api::handlers::caller_flags::enrich_file_flags(
|
||||
&state,
|
||||
&mut file,
|
||||
auth_user.id,
|
||||
)
|
||||
.await;
|
||||
Self::created_json_response(&file).into_response()
|
||||
}
|
||||
|
||||
@@ -1026,7 +1047,15 @@ impl FileHandler {
|
||||
.move_file_with_perms(&id, auth_user.id, payload.folder_id)
|
||||
.await
|
||||
{
|
||||
Ok(file) => (StatusCode::OK, Json(file)).into_response(),
|
||||
Ok(mut file) => {
|
||||
crate::interfaces::api::handlers::caller_flags::enrich_file_flags(
|
||||
&state,
|
||||
&mut file,
|
||||
auth_user.id,
|
||||
)
|
||||
.await;
|
||||
(StatusCode::OK, Json(file)).into_response()
|
||||
}
|
||||
Err(err) => AppError::from(err).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ use crate::interfaces::middleware::auth::AuthUser;
|
||||
|
||||
type AppState = Arc<FolderService>;
|
||||
|
||||
use crate::interfaces::api::handlers::caller_flags::enrich_folder_flags;
|
||||
|
||||
/// Handler for folder-related API endpoints
|
||||
pub struct FolderHandler;
|
||||
|
||||
@@ -40,10 +42,11 @@ impl FolderHandler {
|
||||
/// When parent_id is not provided, the folder is created inside the
|
||||
/// authenticated user's home folder rather than at the storage root.
|
||||
pub(super) async fn create_folder_impl(
|
||||
State(service): State<AppState>,
|
||||
State(state): State<Arc<GlobalAppState>>,
|
||||
auth_user: AuthUser,
|
||||
Json(mut dto): Json<CreateFolderDto>,
|
||||
) -> impl IntoResponse {
|
||||
let service = &state.applications.folder_service_concrete;
|
||||
// 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() {
|
||||
@@ -77,7 +80,10 @@ impl FolderHandler {
|
||||
}
|
||||
|
||||
match service.create_folder_with_perms(dto, auth_user.id).await {
|
||||
Ok(folder) => (StatusCode::CREATED, Json(folder)).into_response(),
|
||||
Ok(mut folder) => {
|
||||
enrich_folder_flags(&state, &mut folder, auth_user.id).await;
|
||||
(StatusCode::CREATED, Json(folder)).into_response()
|
||||
}
|
||||
Err(err) => AppError::from(err).into_response(),
|
||||
}
|
||||
}
|
||||
@@ -85,12 +91,16 @@ impl FolderHandler {
|
||||
/// Gets a folder by ID.
|
||||
/// Validates that the authenticated user owns the folder.
|
||||
pub(super) async fn get_folder_impl(
|
||||
State(service): State<AppState>,
|
||||
State(state): State<Arc<GlobalAppState>>,
|
||||
auth_user: AuthUser,
|
||||
Path(id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
let service = &state.applications.folder_service_concrete;
|
||||
match service.get_folder_with_perms(&id, auth_user.id).await {
|
||||
Ok(folder) => (StatusCode::OK, Json(folder)).into_response(),
|
||||
Ok(mut folder) => {
|
||||
enrich_folder_flags(&state, &mut folder, auth_user.id).await;
|
||||
(StatusCode::OK, Json(folder)).into_response()
|
||||
}
|
||||
Err(err) => AppError::from(err).into_response(),
|
||||
}
|
||||
}
|
||||
@@ -126,29 +136,37 @@ impl FolderHandler {
|
||||
|
||||
/// Renames a folder (ownership enforced).
|
||||
pub(super) async fn rename_folder_impl(
|
||||
State(service): State<AppState>,
|
||||
State(state): State<Arc<GlobalAppState>>,
|
||||
auth_user: AuthUser,
|
||||
Path(id): Path<String>,
|
||||
Json(dto): Json<RenameFolderDto>,
|
||||
) -> impl IntoResponse {
|
||||
let service = &state.applications.folder_service_concrete;
|
||||
match service
|
||||
.rename_folder_with_perms(&id, dto, auth_user.id)
|
||||
.await
|
||||
{
|
||||
Ok(folder) => (StatusCode::OK, Json(folder)).into_response(),
|
||||
Ok(mut folder) => {
|
||||
enrich_folder_flags(&state, &mut folder, auth_user.id).await;
|
||||
(StatusCode::OK, Json(folder)).into_response()
|
||||
}
|
||||
Err(err) => AppError::from(err).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves a folder to a new parent (ownership enforced).
|
||||
pub(super) async fn move_folder_impl(
|
||||
State(service): State<AppState>,
|
||||
State(state): State<Arc<GlobalAppState>>,
|
||||
auth_user: AuthUser,
|
||||
Path(id): Path<String>,
|
||||
Json(dto): Json<MoveFolderDto>,
|
||||
) -> impl IntoResponse {
|
||||
let service = &state.applications.folder_service_concrete;
|
||||
match service.move_folder_with_perms(&id, dto, auth_user.id).await {
|
||||
Ok(folder) => (StatusCode::OK, Json(folder)).into_response(),
|
||||
Ok(mut folder) => {
|
||||
enrich_folder_flags(&state, &mut folder, auth_user.id).await;
|
||||
(StatusCode::OK, Json(folder)).into_response()
|
||||
}
|
||||
Err(err) => AppError::from(err).into_response(),
|
||||
}
|
||||
}
|
||||
@@ -300,7 +318,7 @@ impl FolderHandler {
|
||||
tag = "folders"
|
||||
)]
|
||||
pub async fn create_folder(
|
||||
state: State<AppState>,
|
||||
state: State<Arc<GlobalAppState>>,
|
||||
auth_user: AuthUser,
|
||||
json: Json<CreateFolderDto>,
|
||||
) -> impl IntoResponse {
|
||||
@@ -319,7 +337,7 @@ pub async fn create_folder(
|
||||
tag = "folders"
|
||||
)]
|
||||
pub async fn get_folder(
|
||||
state: State<AppState>,
|
||||
state: State<Arc<GlobalAppState>>,
|
||||
auth_user: AuthUser,
|
||||
path: Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
@@ -355,7 +373,7 @@ pub async fn list_root_folders(
|
||||
tag = "folders"
|
||||
)]
|
||||
pub async fn rename_folder(
|
||||
state: State<AppState>,
|
||||
state: State<Arc<GlobalAppState>>,
|
||||
auth_user: AuthUser,
|
||||
path: Path<String>,
|
||||
json: Json<RenameFolderDto>,
|
||||
@@ -376,7 +394,7 @@ pub async fn rename_folder(
|
||||
tag = "folders"
|
||||
)]
|
||||
pub async fn move_folder(
|
||||
state: State<AppState>,
|
||||
state: State<Arc<GlobalAppState>>,
|
||||
auth_user: AuthUser,
|
||||
path: Path<String>,
|
||||
json: Json<MoveFolderDto>,
|
||||
@@ -490,6 +508,8 @@ pub async fn list_folder_resources(
|
||||
category: intern_display("Folder"),
|
||||
created_by: row.created_by,
|
||||
updated_by: row.updated_by,
|
||||
is_favorite: row.is_favorite,
|
||||
is_shared: row.is_shared,
|
||||
};
|
||||
FolderResourceItemDto {
|
||||
resource_type: ResourceTypeDto::Folder,
|
||||
@@ -541,6 +561,8 @@ pub async fn list_folder_resources(
|
||||
etag,
|
||||
created_by: row.created_by,
|
||||
updated_by: row.updated_by,
|
||||
is_favorite: row.is_favorite,
|
||||
is_shared: row.is_shared,
|
||||
};
|
||||
FolderResourceItemDto {
|
||||
resource_type: ResourceTypeDto::File,
|
||||
|
||||
@@ -933,19 +933,28 @@ pub async fn list_shared_with_me(
|
||||
// looking each resolved resource up by id.
|
||||
let mut items: Vec<SharedWithMeItemDto> = Vec::with_capacity(summaries.len());
|
||||
|
||||
// Enrich caller flags on every returned resource DTO. Incoming
|
||||
// grants pages are typically small (10-50 items), so N sequential
|
||||
// helper calls is acceptable; the folded-into-SQL treatment
|
||||
// photos got is overkill here. Follow-up path if this grows
|
||||
// hot: same LATERAL EXISTS shape in `get_files_by_ids` /
|
||||
// `get_folders_by_ids`.
|
||||
for summary in &summaries {
|
||||
let rid = summary.resource_id.to_string();
|
||||
match summary.resource_type {
|
||||
ResourceKind::File => match file_map.get(&rid) {
|
||||
Some(file_dto) => {
|
||||
let mut dto = file_dto.clone().without_hierarchy_info();
|
||||
crate::interfaces::api::handlers::caller_flags::enrich_file_flags(
|
||||
&state, &mut dto, caller_id,
|
||||
)
|
||||
.await;
|
||||
items.push(SharedWithMeItemDto {
|
||||
resource_type: ResourceTypeDto::File,
|
||||
permissions: summary.permissions.iter().map(|p| (*p).into()).collect(),
|
||||
granted_at: summary.granted_at,
|
||||
granted_by: summary.granted_by,
|
||||
resource: ResourceContentDto::File(
|
||||
file_dto.clone().without_hierarchy_info(),
|
||||
),
|
||||
resource: ResourceContentDto::File(dto),
|
||||
});
|
||||
}
|
||||
None => warn!(
|
||||
@@ -955,14 +964,17 @@ pub async fn list_shared_with_me(
|
||||
},
|
||||
ResourceKind::Folder => match folder_map.get(&rid) {
|
||||
Some(folder_dto) => {
|
||||
let mut dto = folder_dto.clone().without_hierarchy_info();
|
||||
crate::interfaces::api::handlers::caller_flags::enrich_folder_flags(
|
||||
&state, &mut dto, caller_id,
|
||||
)
|
||||
.await;
|
||||
items.push(SharedWithMeItemDto {
|
||||
resource_type: ResourceTypeDto::Folder,
|
||||
permissions: summary.permissions.iter().map(|p| (*p).into()).collect(),
|
||||
granted_at: summary.granted_at,
|
||||
granted_by: summary.granted_by,
|
||||
resource: ResourceContentDto::Folder(
|
||||
folder_dto.clone().without_hierarchy_info(),
|
||||
),
|
||||
resource: ResourceContentDto::Folder(dto),
|
||||
});
|
||||
}
|
||||
None => warn!(
|
||||
@@ -1209,10 +1221,18 @@ pub async fn list_my_shares(
|
||||
// Caller is the granter — they had share-access to the
|
||||
// resource, so the containing hierarchy is already known
|
||||
// to them. Keep `path` (unlike list_shared_with_me).
|
||||
let mut dto = file_dto.clone();
|
||||
// is_shared: TRUE by construction on this feed.
|
||||
// is_favorite: real EXISTS via the shared helper.
|
||||
crate::interfaces::api::handlers::caller_flags::enrich_file_flags(
|
||||
&state, &mut dto, caller_id,
|
||||
)
|
||||
.await;
|
||||
dto.is_shared = true;
|
||||
items.push(OutgoingResourceItemDto {
|
||||
resource_type: ResourceTypeDto::File,
|
||||
first_shared_at: summary.first_shared_at,
|
||||
resource: ResourceContentDto::File(file_dto.clone()),
|
||||
resource: ResourceContentDto::File(dto),
|
||||
grants,
|
||||
});
|
||||
}
|
||||
@@ -1223,10 +1243,16 @@ pub async fn list_my_shares(
|
||||
},
|
||||
ResourceKind::Folder => match folder_map.get(&rid) {
|
||||
Some(folder_dto) => {
|
||||
let mut dto = folder_dto.clone();
|
||||
crate::interfaces::api::handlers::caller_flags::enrich_folder_flags(
|
||||
&state, &mut dto, caller_id,
|
||||
)
|
||||
.await;
|
||||
dto.is_shared = true;
|
||||
items.push(OutgoingResourceItemDto {
|
||||
resource_type: ResourceTypeDto::Folder,
|
||||
first_shared_at: summary.first_shared_at,
|
||||
resource: ResourceContentDto::Folder(folder_dto.clone()),
|
||||
resource: ResourceContentDto::Folder(dto),
|
||||
grants,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ pub mod app_password_handler;
|
||||
pub mod auth_handler;
|
||||
pub mod batch_handler;
|
||||
pub mod caldav_handler;
|
||||
pub mod caller_flags;
|
||||
pub mod carddav_handler;
|
||||
pub mod chunked_upload_handler;
|
||||
pub mod contacts_handler;
|
||||
|
||||
@@ -75,7 +75,7 @@ pub async fn list_photos(
|
||||
.list_media_files(caller_id, params.before, limit)
|
||||
.await
|
||||
{
|
||||
Ok((files, sort_dates, dims)) => {
|
||||
Ok((files, sort_dates, dims, flags)) => {
|
||||
// Lightweight revalidation ETag: page identity (cursor + limit) plus a
|
||||
// freshness signal (max modified_at + row count over the page),
|
||||
// mirroring the file-list endpoint. With `Cache-Control: no-cache` the
|
||||
@@ -106,14 +106,22 @@ pub async fn list_photos(
|
||||
|
||||
info!("Photos: returned {} media files for user", count);
|
||||
|
||||
// Convert to DTOs with sort_date + pixel dimensions populated.
|
||||
// Convert to DTOs with sort_date + pixel dimensions + inline
|
||||
// caller flags populated. `list_media_files` computes
|
||||
// `is_favorite` / `is_shared` via two per-row `EXISTS`
|
||||
// columns in its SELECT — the same pattern the four
|
||||
// `list_resources_paged` repos use — so this stays a
|
||||
// single round trip regardless of page size.
|
||||
let dtos: Vec<PhotoDto> = files
|
||||
.into_iter()
|
||||
.zip(sort_dates.iter())
|
||||
.zip(dims.iter())
|
||||
.map(|((file, &sd), &(w, h))| {
|
||||
.zip(flags.iter())
|
||||
.map(|(((file, &sd), &(w, h)), &(is_fav, is_shr))| {
|
||||
let mut dto = FileDto::from(file);
|
||||
dto.sort_date = Some(sd as u64);
|
||||
dto.is_favorite = is_fav;
|
||||
dto.is_shared = is_shr;
|
||||
PhotoDto {
|
||||
file: dto,
|
||||
width: w.map(|v| v.max(0) as u32),
|
||||
|
||||
@@ -239,6 +239,8 @@ pub async fn list_recent_resources(
|
||||
category: intern_display("Folder"),
|
||||
created_by: row.created_by,
|
||||
updated_by: row.updated_by,
|
||||
is_favorite: row.is_favorite,
|
||||
is_shared: row.is_shared,
|
||||
};
|
||||
RecentResourceItemDto {
|
||||
resource_type: ResourceTypeDto::Folder,
|
||||
@@ -285,6 +287,8 @@ pub async fn list_recent_resources(
|
||||
etag,
|
||||
created_by: row.created_by,
|
||||
updated_by: row.updated_by,
|
||||
is_favorite: row.is_favorite,
|
||||
is_shared: row.is_shared,
|
||||
};
|
||||
RecentResourceItemDto {
|
||||
resource_type: ResourceTypeDto::File,
|
||||
|
||||
@@ -560,6 +560,9 @@ async fn handle_propfind(
|
||||
category: intern_display("Folder"),
|
||||
created_by: None,
|
||||
updated_by: None,
|
||||
// Synthetic root, not on the SPA path — safe default.
|
||||
is_favorite: false,
|
||||
is_shared: false,
|
||||
};
|
||||
// Skip the 2-query quota resolution when the request's prop list
|
||||
// never mentions quota (benches/QUOTA-PATH.md).
|
||||
|
||||
@@ -206,15 +206,23 @@ pub fn create_api_routes(app_state: &Arc<AppState>) -> Router<Arc<AppState>> {
|
||||
batch_service: batch_service.clone(),
|
||||
};
|
||||
|
||||
// Create the basic folders router with service operations
|
||||
// Basic folder listing (no caller-flag enrichment needed — flags
|
||||
// come from the per-row SQL EXISTS in the listing repo).
|
||||
let folders_basic_router = Router::new()
|
||||
.route("/", post(create_folder))
|
||||
.route("/", get(list_root_folders))
|
||||
.route("/{id}", get(get_folder))
|
||||
.route("/{id}/resources", get(list_folder_resources))
|
||||
.with_state(folder_service.clone());
|
||||
|
||||
// Single-item CRUD emits a FolderDto to the SPA and MUST carry
|
||||
// authoritative `is_favorite` / `is_shared` flags. The impls call
|
||||
// the `caller_flags` enrichment helper via
|
||||
// `state.favorites_service`, so the full `AppState` is required.
|
||||
let folders_crud_router = Router::new()
|
||||
.route("/", post(create_folder))
|
||||
.route("/{id}", get(get_folder))
|
||||
.route("/{id}/rename", put(rename_folder))
|
||||
.route("/{id}/move", put(move_folder))
|
||||
.with_state(folder_service.clone());
|
||||
.with_state(app_state.clone());
|
||||
|
||||
// Special route for ZIP download that requires AppState instead of just FolderService
|
||||
let folder_zip_router = Router::new()
|
||||
@@ -226,6 +234,7 @@ pub fn create_api_routes(app_state: &Arc<AppState>) -> Router<Arc<AppState>> {
|
||||
|
||||
// Merge the routers
|
||||
let folders_router = folders_basic_router
|
||||
.merge(folders_crud_router)
|
||||
.merge(folders_ops_router)
|
||||
.merge(folder_zip_router);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user