- shift-click selects ranges in the gallery; selection bar gains
'Delete N' with a single confirm (POST /api/photos/delete)
- PATCH /api/shares/{id}: allow_download and expiry editable in place,
token and client feedback preserved
- Gallery: re-attach ResizeObserver via callback ref — after a filter
with zero matches the gallery stayed blank at width 0
This commit is contained in:
+5
-1
@@ -78,9 +78,13 @@ pub fn router(state: &AppState) -> Router<AppState> {
|
||||
)
|
||||
.route("/api/albums/{id}/zip", post(zip::album_zip))
|
||||
.route("/api/albums/{id}/photos/by-hash/{sha256}", get(photos::by_hash))
|
||||
.route("/api/photos/delete", post(photos::delete_many))
|
||||
.route("/api/photos/{id}", delete(photos::delete))
|
||||
.route("/api/photos/{id}/reprocess", post(photos::reprocess))
|
||||
.route("/api/shares/{id}", delete(shares::delete))
|
||||
.route(
|
||||
"/api/shares/{id}",
|
||||
delete(shares::delete).patch(shares::update),
|
||||
)
|
||||
.route("/api/shares/{id}/reset-lock", post(shares::reset_lock))
|
||||
.route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
|
||||
@@ -211,6 +211,35 @@ pub async fn delete(
|
||||
Ok(Json(serde_json::json!({ "ok": true })))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct DeleteManyBody {
|
||||
ids: Vec<Uuid>,
|
||||
}
|
||||
|
||||
/// Bulk delete: photos vanish and their S3 cleanup jobs are enqueued in one
|
||||
/// statement, same as album deletion.
|
||||
pub async fn delete_many(
|
||||
State(state): State<AppState>,
|
||||
Json(body): Json<DeleteManyBody>,
|
||||
) -> ApiResult<Json<serde_json::Value>> {
|
||||
if body.ids.is_empty() {
|
||||
return Err(ApiError::bad_request("ids must not be empty"));
|
||||
}
|
||||
let mut tx = state.db.begin().await?;
|
||||
let jobs = sqlx::query(
|
||||
"with deleted as (delete from photos where id = any($1) returning id)
|
||||
insert into jobs (kind, payload)
|
||||
select $2, jsonb_build_object('prefix', 'photos/' || id || '/')
|
||||
from deleted",
|
||||
)
|
||||
.bind(&body.ids)
|
||||
.bind(JobKind::DeleteS3Prefix.as_str())
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
Ok(Json(serde_json::json!({ "ok": true, "deleted": jobs.rows_affected() })))
|
||||
}
|
||||
|
||||
pub async fn reprocess(
|
||||
State(state): State<AppState>,
|
||||
Path(photo_id): Path<Uuid>,
|
||||
|
||||
@@ -138,6 +138,49 @@ pub async fn create(
|
||||
Ok(Json(share_json(&state, &row)))
|
||||
}
|
||||
|
||||
/// Distinguishes an absent JSON field (keep current value) from an explicit
|
||||
/// null (clear the expiry): absent → None, present → Some(inner).
|
||||
fn double_option<'de, D>(de: D) -> Result<Option<Option<DateTime<Utc>>>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
serde::Deserialize::deserialize(de).map(Some)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UpdateShare {
|
||||
allow_download: Option<bool>,
|
||||
#[serde(default, deserialize_with = "double_option")]
|
||||
expires_at: Option<Option<DateTime<Utc>>>,
|
||||
}
|
||||
|
||||
pub async fn update(
|
||||
State(state): State<AppState>,
|
||||
Path(share_id): Path<Uuid>,
|
||||
Json(body): Json<UpdateShare>,
|
||||
) -> ApiResult<Json<serde_json::Value>> {
|
||||
let updated = sqlx::query(
|
||||
"update shares set
|
||||
allow_download = coalesce($2, allow_download),
|
||||
expires_at = case when $3 then $4 else expires_at end
|
||||
where id = $1",
|
||||
)
|
||||
.bind(share_id)
|
||||
.bind(body.allow_download)
|
||||
.bind(body.expires_at.is_some())
|
||||
.bind(body.expires_at.flatten())
|
||||
.execute(&state.db)
|
||||
.await?;
|
||||
if updated.rows_affected() == 0 {
|
||||
return Err(ApiError::not_found());
|
||||
}
|
||||
let row: ShareAdminRow = sqlx::query_as(&format!("{SHARE_SELECT} where s.id = $1"))
|
||||
.bind(share_id)
|
||||
.fetch_one(&state.db)
|
||||
.await?;
|
||||
Ok(Json(share_json(&state, &row)))
|
||||
}
|
||||
|
||||
/// Clear a share's password-lockout state (e.g. after a client fat-fingered
|
||||
/// their way into the 15-minute lock).
|
||||
pub async fn reset_lock(
|
||||
|
||||
Reference in New Issue
Block a user