8981c1dfb9
An audit (backend /api routes vs SvelteKit frontend usage, adversarially
verified across the whole repo) found these 5 routes have ZERO callers — no
frontend, no test, no protocol layer, no internal caller — and are superseded:
- GET /api/folders/paginated no-op duplicate of GET /api/folders
(discards the page arg); superseded by
the cursor-paginated /{id}/resources.
- POST /api/dedup/upload superseded by /api/files/upload, which
does the identical CDC dedup ingest.
- POST /api/people/{id}/hide the hide-person toggle was never built
into the UI (is_hidden never read).
- GET /api/admin/settings/general never called anywhere.
- GET /api/admin/settings/registration only the PUT is used; the GET had no
caller (PUT kept).
Removes each route, its handler + _impl, the now-orphaned DedupUploadResponse
DTO + its two serialization tests, the set_hidden service method (only caller
was hide_person), and the OpenAPI path/schema registrations.
Also cleans 4 stale markers: two #[allow(deprecated)] that no longer suppress
anything (zero #[deprecated] remain), the "Legacy folder endpoints (contents,
listing)" comment (both already removed), and a "Re-export AppError for backward
compatibility" comment describing a re-export that doesn't exist.
Net -323 lines. The 31 other unused-by-frontend routes (device-code auth,
CardDAV contact-groups, people/photos & music WIP, dedup/admin debug, i18n,
openapi.json) are intentional surface and were left untouched.
cargo clippy --all-features --all-targets -D warnings: clean. cargo test: 446 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
154 lines
4.6 KiB
Rust
154 lines
4.6 KiB
Rust
//! HTTP handlers for the People (faces) feature.
|
|
//!
|
|
//! Every route is mounted only when `OXICLOUD_ENABLE_FACES` is on (the service
|
|
//! is present in `AppState`); each handler is also defensive. All work is
|
|
//! strictly caller-scoped by `PeopleService` (the repository filters by user).
|
|
|
|
use std::sync::Arc;
|
|
|
|
use axum::{
|
|
Json,
|
|
extract::{Path, State},
|
|
http::StatusCode,
|
|
response::{IntoResponse, Response},
|
|
};
|
|
use serde::Deserialize;
|
|
use uuid::Uuid;
|
|
|
|
use crate::common::di::AppState;
|
|
use crate::interfaces::errors::AppError;
|
|
use crate::interfaces::middleware::auth::AuthUser;
|
|
|
|
fn disabled() -> Response {
|
|
(
|
|
StatusCode::NOT_FOUND,
|
|
Json(serde_json::json!({ "error": "People feature is disabled" })),
|
|
)
|
|
.into_response()
|
|
}
|
|
|
|
fn bad_id() -> Response {
|
|
(
|
|
StatusCode::BAD_REQUEST,
|
|
Json(serde_json::json!({ "error": "invalid id" })),
|
|
)
|
|
.into_response()
|
|
}
|
|
|
|
/// GET /api/people — identity clusters for the caller.
|
|
pub async fn list_people(State(state): State<Arc<AppState>>, auth_user: AuthUser) -> Response {
|
|
let Some(svc) = state.people_service.as_ref() else {
|
|
return disabled();
|
|
};
|
|
match svc.list_people(auth_user.id).await {
|
|
Ok(people) => Json(people).into_response(),
|
|
Err(e) => AppError::from(e).into_response(),
|
|
}
|
|
}
|
|
|
|
/// GET /api/people/{id}/photos — file ids of a person's photos.
|
|
pub async fn person_photos(
|
|
State(state): State<Arc<AppState>>,
|
|
auth_user: AuthUser,
|
|
Path(id): Path<String>,
|
|
) -> Response {
|
|
let Some(svc) = state.people_service.as_ref() else {
|
|
return disabled();
|
|
};
|
|
let Ok(person_id) = Uuid::parse_str(&id) else {
|
|
return bad_id();
|
|
};
|
|
match svc.person_photos(auth_user.id, person_id).await {
|
|
Ok(files) => Json(files).into_response(),
|
|
Err(e) => AppError::from(e).into_response(),
|
|
}
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
pub struct RenameBody {
|
|
pub name: Option<String>,
|
|
}
|
|
|
|
/// PATCH /api/people/{id} — name (or clear the name of) a person.
|
|
pub async fn rename_person(
|
|
State(state): State<Arc<AppState>>,
|
|
auth_user: AuthUser,
|
|
Path(id): Path<String>,
|
|
Json(body): Json<RenameBody>,
|
|
) -> Response {
|
|
let Some(svc) = state.people_service.as_ref() else {
|
|
return disabled();
|
|
};
|
|
let Ok(person_id) = Uuid::parse_str(&id) else {
|
|
return bad_id();
|
|
};
|
|
match svc.rename_person(auth_user.id, person_id, body.name).await {
|
|
Ok(()) => StatusCode::NO_CONTENT.into_response(),
|
|
Err(e) => AppError::from(e).into_response(),
|
|
}
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
pub struct MergeBody {
|
|
pub into: String,
|
|
pub from: String,
|
|
}
|
|
|
|
/// POST /api/people/merge — merge `from` into `into`.
|
|
pub async fn merge_people(
|
|
State(state): State<Arc<AppState>>,
|
|
auth_user: AuthUser,
|
|
Json(body): Json<MergeBody>,
|
|
) -> Response {
|
|
let Some(svc) = state.people_service.as_ref() else {
|
|
return disabled();
|
|
};
|
|
let (Ok(into), Ok(from)) = (Uuid::parse_str(&body.into), Uuid::parse_str(&body.from)) else {
|
|
return bad_id();
|
|
};
|
|
match svc.merge(auth_user.id, into, from).await {
|
|
Ok(()) => StatusCode::NO_CONTENT.into_response(),
|
|
Err(e) => AppError::from(e).into_response(),
|
|
}
|
|
}
|
|
|
|
/// POST /api/people/recluster — re-run identity clustering for the caller.
|
|
pub async fn recluster(State(state): State<Arc<AppState>>, auth_user: AuthUser) -> Response {
|
|
let Some(svc) = state.people_service.as_ref() else {
|
|
return disabled();
|
|
};
|
|
match svc.recluster(auth_user.id).await {
|
|
Ok(n) => Json(serde_json::json!({ "persons_created": n })).into_response(),
|
|
Err(e) => AppError::from(e).into_response(),
|
|
}
|
|
}
|
|
|
|
/// DELETE /api/people/data — erase all of the caller's face data.
|
|
pub async fn delete_all(State(state): State<Arc<AppState>>, auth_user: AuthUser) -> Response {
|
|
let Some(svc) = state.people_service.as_ref() else {
|
|
return disabled();
|
|
};
|
|
match svc.delete_all(auth_user.id).await {
|
|
Ok(()) => StatusCode::NO_CONTENT.into_response(),
|
|
Err(e) => AppError::from(e).into_response(),
|
|
}
|
|
}
|
|
|
|
/// GET /api/people/faces/{file_id} — face boxes within a photo (lightbox tags).
|
|
pub async fn faces_for_file(
|
|
State(state): State<Arc<AppState>>,
|
|
auth_user: AuthUser,
|
|
Path(file_id): Path<String>,
|
|
) -> Response {
|
|
let Some(svc) = state.people_service.as_ref() else {
|
|
return disabled();
|
|
};
|
|
let Ok(fid) = Uuid::parse_str(&file_id) else {
|
|
return bad_id();
|
|
};
|
|
match svc.faces_for_file(auth_user.id, fid).await {
|
|
Ok(boxes) => Json(boxes).into_response(),
|
|
Err(e) => AppError::from(e).into_response(),
|
|
}
|
|
}
|