perf: scale fixes for 1M+ photo libraries
Indexes: share_targets.target_id, duplicate_groups.status, GIN on stacks members + duplicate candidates JSONB, composite (owner_user_id, created_at DESC) on assets. N+1 elimination: batch metadata loading via find_by_assets(ids) using WHERE asset_id = ANY($1), used in timeline + sidecar export. Visibility: cache find_targets_for_user per request via OnceCell, extract filter_visible helper to reduce duplication. Streaming: FileStoragePort.open_file() returns (DataStream, u64), LocalFileStorage uses ReaderStream instead of loading full file. serve_file/serve_derivative use Body::from_stream(). Unbounded queries: sidecar full_export/import batched in 500-row chunks instead of u32::MAX. find_unresolved paginated with limit/offset. list_duplicates API accepts pagination params.
This commit is contained in:
@@ -203,12 +203,12 @@ pub async fn serve_file(
|
||||
Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(header::CONTENT_TYPE, &result.mime_type)
|
||||
.header(header::CONTENT_LENGTH, result.data.len())
|
||||
.header(header::CONTENT_LENGTH, result.size)
|
||||
.header(
|
||||
header::CONTENT_DISPOSITION,
|
||||
format!("inline; filename=\"{}\"", result.filename),
|
||||
)
|
||||
.body(Body::from(result.data))
|
||||
.body(Body::from_stream(result.stream))
|
||||
.map_err(|e| AppError::from(domain::errors::DomainError::Internal(e.to_string())))
|
||||
}
|
||||
|
||||
@@ -256,9 +256,9 @@ pub async fn serve_derivative(
|
||||
Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(header::CONTENT_TYPE, &result.mime_type)
|
||||
.header(header::CONTENT_LENGTH, result.data.len())
|
||||
.header(header::CONTENT_LENGTH, result.size)
|
||||
.header(header::CACHE_CONTROL, "public, max-age=31536000, immutable")
|
||||
.body(Body::from(result.data))
|
||||
.body(Body::from_stream(result.stream))
|
||||
.map_err(|e| AppError::from(DomainError::Internal(e.to_string())))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +1,35 @@
|
||||
use crate::{errors::AppError, extractors::JwtClaims, state::AppState};
|
||||
use crate::{
|
||||
constants::{DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE},
|
||||
errors::AppError,
|
||||
extractors::JwtClaims,
|
||||
state::AppState,
|
||||
};
|
||||
use api_types::{requests::ResolveDuplicateRequest, responses::DuplicateGroupResponse};
|
||||
use application::catalog::{ListDuplicatesQuery, ResolveDuplicateCommand};
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, State},
|
||||
extract::{Path, Query, State},
|
||||
http::StatusCode,
|
||||
};
|
||||
use domain::value_objects::SystemId;
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct ListDuplicatesParams {
|
||||
pub limit: Option<u32>,
|
||||
pub offset: Option<u32>,
|
||||
}
|
||||
|
||||
pub async fn list_duplicates(
|
||||
State(state): State<AppState>,
|
||||
claims: JwtClaims,
|
||||
Query(params): Query<ListDuplicatesParams>,
|
||||
) -> Result<Json<Vec<DuplicateGroupResponse>>, AppError> {
|
||||
super::require_admin(&claims)?;
|
||||
let groups = state
|
||||
.catalog
|
||||
.list_duplicates
|
||||
.execute(ListDuplicatesQuery)
|
||||
.await?;
|
||||
let query = ListDuplicatesQuery {
|
||||
limit: params.limit.unwrap_or(DEFAULT_PAGE_SIZE).min(MAX_PAGE_SIZE),
|
||||
offset: params.offset.unwrap_or(0),
|
||||
};
|
||||
let groups = state.catalog.list_duplicates.execute(query).await?;
|
||||
let resp = groups
|
||||
.iter()
|
||||
.map(DuplicateGroupResponse::from_domain)
|
||||
|
||||
Reference in New Issue
Block a user