image_exif: manual date_taken override (set/clear endpoints)
Add `POST /image/exif/date` and `POST /image/exif/date/clear` so an operator can correct a row whose canonical-date waterfall landed on the wrong value (camera clock reset, fs_time fallback for a copied-from- backup file, etc). New `original_date_taken` / `original_date_taken_source` columns snapshot the prior value on first override so revert is lossless. The waterfall source set is now `'exif' | 'exiftool' | 'filename' | 'fs_time' | 'manual'`. The existing `idx_image_exif_date_backfill` partial index already filters to `date_taken IS NULL OR date_taken_source = 'fs_time'`, so manual rows are naturally excluded from the per-tick drain — no index change needed. `ExifMetadata` now exposes `date_taken_source` + originals so a UI can render "manually set; was X via filename". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE image_exif DROP COLUMN original_date_taken_source;
|
||||||
|
ALTER TABLE image_exif DROP COLUMN original_date_taken;
|
||||||
15
migrations/2026-05-06-000200_add_manual_date_override/up.sql
Normal file
15
migrations/2026-05-06-000200_add_manual_date_override/up.sql
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
-- Manual date_taken override: when an operator overrides a row's date via
|
||||||
|
-- POST /image/exif/date, the prior `(date_taken, date_taken_source)` is
|
||||||
|
-- snapshotted into these columns and the live columns hold the new value
|
||||||
|
-- with `date_taken_source = 'manual'`. POST /image/exif/date/clear restores
|
||||||
|
-- the pair and nulls the originals.
|
||||||
|
--
|
||||||
|
-- The waterfall source-name set is now:
|
||||||
|
-- 'exif' | 'exiftool' | 'filename' | 'fs_time' | 'manual'
|
||||||
|
--
|
||||||
|
-- The `idx_image_exif_date_backfill` partial index already filters to
|
||||||
|
-- `date_taken IS NULL OR date_taken_source = 'fs_time'`, so 'manual' rows
|
||||||
|
-- are naturally excluded from the per-tick backfill drain — no index
|
||||||
|
-- change needed.
|
||||||
|
ALTER TABLE image_exif ADD COLUMN original_date_taken BIGINT;
|
||||||
|
ALTER TABLE image_exif ADD COLUMN original_date_taken_source TEXT;
|
||||||
@@ -286,6 +286,16 @@ pub struct ExifMetadata {
|
|||||||
pub gps: Option<GpsCoordinates>,
|
pub gps: Option<GpsCoordinates>,
|
||||||
pub capture_settings: Option<CaptureSettings>,
|
pub capture_settings: Option<CaptureSettings>,
|
||||||
pub date_taken: Option<i64>,
|
pub date_taken: Option<i64>,
|
||||||
|
/// Which step of the canonical-date waterfall populated `date_taken`:
|
||||||
|
/// `"exif" | "exiftool" | "filename" | "fs_time" | "manual"`. NULL when
|
||||||
|
/// `date_taken` itself is NULL.
|
||||||
|
pub date_taken_source: Option<String>,
|
||||||
|
/// When `date_taken_source = "manual"`, the prior `date_taken` snapshot.
|
||||||
|
/// Used by the UI's revert affordance and to label "manually overridden;
|
||||||
|
/// originally X" in the details modal.
|
||||||
|
pub original_date_taken: Option<i64>,
|
||||||
|
/// When `date_taken_source = "manual"`, the prior source.
|
||||||
|
pub original_date_taken_source: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
@@ -370,6 +380,9 @@ impl From<ImageExif> for ExifMetadata {
|
|||||||
None
|
None
|
||||||
},
|
},
|
||||||
date_taken: exif.date_taken,
|
date_taken: exif.date_taken,
|
||||||
|
date_taken_source: exif.date_taken_source,
|
||||||
|
original_date_taken: exif.original_date_taken,
|
||||||
|
original_date_taken_source: exif.original_date_taken_source,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -439,6 +439,32 @@ pub trait ExifDao: Sync + Send {
|
|||||||
source: &str,
|
source: &str,
|
||||||
) -> Result<(), DbError>;
|
) -> Result<(), DbError>;
|
||||||
|
|
||||||
|
/// Operator-driven date_taken override (POST /image/exif/date). Snapshots
|
||||||
|
/// the prior `(date_taken, date_taken_source)` into the `original_*`
|
||||||
|
/// pair on first override, then writes the new value with
|
||||||
|
/// `date_taken_source = 'manual'`. Subsequent overrides keep the
|
||||||
|
/// original snapshot intact so a single revert restores the resolver
|
||||||
|
/// result, not whatever override was set just before. Returns the
|
||||||
|
/// post-update row.
|
||||||
|
fn set_manual_date_taken(
|
||||||
|
&mut self,
|
||||||
|
context: &opentelemetry::Context,
|
||||||
|
library_id: i32,
|
||||||
|
rel_path: &str,
|
||||||
|
date_taken: i64,
|
||||||
|
) -> Result<ImageExif, DbError>;
|
||||||
|
|
||||||
|
/// Revert a manual override (POST /image/exif/date/clear): restore
|
||||||
|
/// `date_taken` + `date_taken_source` from the `original_*` snapshot,
|
||||||
|
/// then null both originals. No-op (returns current row unchanged) when
|
||||||
|
/// no override is active.
|
||||||
|
fn clear_manual_date_taken(
|
||||||
|
&mut self,
|
||||||
|
context: &opentelemetry::Context,
|
||||||
|
library_id: i32,
|
||||||
|
rel_path: &str,
|
||||||
|
) -> Result<ImageExif, DbError>;
|
||||||
|
|
||||||
/// Single-query backend for `/memories`. Returns
|
/// Single-query backend for `/memories`. Returns
|
||||||
/// `(rel_path, date_taken, last_modified)` for rows in `library_id`
|
/// `(rel_path, date_taken, last_modified)` for rows in `library_id`
|
||||||
/// whose `date_taken` falls within `[now - years_back y, now]` and
|
/// whose `date_taken` falls within `[now - years_back y, now]` and
|
||||||
@@ -1185,6 +1211,108 @@ impl ExifDao for SqliteExifDao {
|
|||||||
.map_err(|_| DbError::new(DbErrorKind::UpdateError))
|
.map_err(|_| DbError::new(DbErrorKind::UpdateError))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_manual_date_taken(
|
||||||
|
&mut self,
|
||||||
|
context: &opentelemetry::Context,
|
||||||
|
library_id_val: i32,
|
||||||
|
rel_path_val: &str,
|
||||||
|
date_taken_val: i64,
|
||||||
|
) -> Result<ImageExif, DbError> {
|
||||||
|
trace_db_call(context, "update", "set_manual_date_taken", |_span| {
|
||||||
|
use schema::image_exif::dsl::*;
|
||||||
|
|
||||||
|
let mut connection = self.connection.lock().expect("Unable to get ExifDao");
|
||||||
|
|
||||||
|
// Read-modify-write under the dao mutex so the snapshot is
|
||||||
|
// consistent with the value being overwritten. The mutex holds
|
||||||
|
// for the duration of this closure — no other writer can race.
|
||||||
|
let current: ImageExif = image_exif
|
||||||
|
.filter(library_id.eq(library_id_val))
|
||||||
|
.filter(rel_path.eq(rel_path_val))
|
||||||
|
.first(connection.deref_mut())
|
||||||
|
.map_err(|_| anyhow::anyhow!("row not found"))?;
|
||||||
|
|
||||||
|
// Snapshot only on first override. Subsequent overrides keep
|
||||||
|
// the original snapshot intact so a single revert restores
|
||||||
|
// the resolver-derived value, not the prior override.
|
||||||
|
let (orig_dt, orig_src) = if current.original_date_taken.is_none() {
|
||||||
|
(current.date_taken, current.date_taken_source.clone())
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
current.original_date_taken,
|
||||||
|
current.original_date_taken_source.clone(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
diesel::update(
|
||||||
|
image_exif
|
||||||
|
.filter(library_id.eq(library_id_val))
|
||||||
|
.filter(rel_path.eq(rel_path_val)),
|
||||||
|
)
|
||||||
|
.set((
|
||||||
|
date_taken.eq(Some(date_taken_val)),
|
||||||
|
date_taken_source.eq(Some("manual".to_string())),
|
||||||
|
original_date_taken.eq(orig_dt),
|
||||||
|
original_date_taken_source.eq(orig_src),
|
||||||
|
))
|
||||||
|
.execute(connection.deref_mut())
|
||||||
|
.map_err(|_| anyhow::anyhow!("Update error"))?;
|
||||||
|
|
||||||
|
image_exif
|
||||||
|
.filter(library_id.eq(library_id_val))
|
||||||
|
.filter(rel_path.eq(rel_path_val))
|
||||||
|
.first::<ImageExif>(connection.deref_mut())
|
||||||
|
.map_err(|_| anyhow::anyhow!("Re-read error"))
|
||||||
|
})
|
||||||
|
.map_err(|_| DbError::new(DbErrorKind::UpdateError))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_manual_date_taken(
|
||||||
|
&mut self,
|
||||||
|
context: &opentelemetry::Context,
|
||||||
|
library_id_val: i32,
|
||||||
|
rel_path_val: &str,
|
||||||
|
) -> Result<ImageExif, DbError> {
|
||||||
|
trace_db_call(context, "update", "clear_manual_date_taken", |_span| {
|
||||||
|
use schema::image_exif::dsl::*;
|
||||||
|
|
||||||
|
let mut connection = self.connection.lock().expect("Unable to get ExifDao");
|
||||||
|
|
||||||
|
let current: ImageExif = image_exif
|
||||||
|
.filter(library_id.eq(library_id_val))
|
||||||
|
.filter(rel_path.eq(rel_path_val))
|
||||||
|
.first(connection.deref_mut())
|
||||||
|
.map_err(|_| anyhow::anyhow!("row not found"))?;
|
||||||
|
|
||||||
|
// No override active — nothing to revert. Return the current
|
||||||
|
// row unchanged so the endpoint is idempotent.
|
||||||
|
if current.original_date_taken.is_none() {
|
||||||
|
return Ok(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::update(
|
||||||
|
image_exif
|
||||||
|
.filter(library_id.eq(library_id_val))
|
||||||
|
.filter(rel_path.eq(rel_path_val)),
|
||||||
|
)
|
||||||
|
.set((
|
||||||
|
date_taken.eq(current.original_date_taken),
|
||||||
|
date_taken_source.eq(current.original_date_taken_source.clone()),
|
||||||
|
original_date_taken.eq::<Option<i64>>(None),
|
||||||
|
original_date_taken_source.eq::<Option<String>>(None),
|
||||||
|
))
|
||||||
|
.execute(connection.deref_mut())
|
||||||
|
.map_err(|_| anyhow::anyhow!("Update error"))?;
|
||||||
|
|
||||||
|
image_exif
|
||||||
|
.filter(library_id.eq(library_id_val))
|
||||||
|
.filter(rel_path.eq(rel_path_val))
|
||||||
|
.first::<ImageExif>(connection.deref_mut())
|
||||||
|
.map_err(|_| anyhow::anyhow!("Re-read error"))
|
||||||
|
})
|
||||||
|
.map_err(|_| DbError::new(DbErrorKind::UpdateError))
|
||||||
|
}
|
||||||
|
|
||||||
fn get_memories_in_window(
|
fn get_memories_in_window(
|
||||||
&mut self,
|
&mut self,
|
||||||
context: &opentelemetry::Context,
|
context: &opentelemetry::Context,
|
||||||
|
|||||||
@@ -105,7 +105,15 @@ pub struct ImageExif {
|
|||||||
/// Unix seconds at which the resolve was committed.
|
/// Unix seconds at which the resolve was committed.
|
||||||
pub duplicate_decided_at: Option<i64>,
|
pub duplicate_decided_at: Option<i64>,
|
||||||
/// Which step of the canonical-date waterfall populated `date_taken`.
|
/// Which step of the canonical-date waterfall populated `date_taken`.
|
||||||
|
/// Plus `"manual"` when the operator has set it via POST /image/exif/date.
|
||||||
pub date_taken_source: Option<String>,
|
pub date_taken_source: Option<String>,
|
||||||
|
/// Snapshot of the prior `date_taken` taken on first manual override.
|
||||||
|
/// NULL when no override is active. POST /image/exif/date/clear restores
|
||||||
|
/// `date_taken` from this column and nulls it back out.
|
||||||
|
pub original_date_taken: Option<i64>,
|
||||||
|
/// Snapshot of the prior `date_taken_source` taken on first manual
|
||||||
|
/// override. NULL when no override is active.
|
||||||
|
pub original_date_taken_source: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Insertable)]
|
#[derive(Insertable)]
|
||||||
|
|||||||
@@ -126,6 +126,8 @@ diesel::table! {
|
|||||||
duplicate_of_hash -> Nullable<Text>,
|
duplicate_of_hash -> Nullable<Text>,
|
||||||
duplicate_decided_at -> Nullable<BigInt>,
|
duplicate_decided_at -> Nullable<BigInt>,
|
||||||
date_taken_source -> Nullable<Text>,
|
date_taken_source -> Nullable<Text>,
|
||||||
|
original_date_taken -> Nullable<BigInt>,
|
||||||
|
original_date_taken_source -> Nullable<Text>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
64
src/files.rs
64
src/files.rs
@@ -1475,6 +1475,44 @@ mod tests {
|
|||||||
|
|
||||||
struct MockExifDao;
|
struct MockExifDao;
|
||||||
|
|
||||||
|
fn mock_exif_row(
|
||||||
|
library_id: i32,
|
||||||
|
rel_path: &str,
|
||||||
|
date_taken: Option<i64>,
|
||||||
|
date_taken_source: Option<String>,
|
||||||
|
) -> crate::database::models::ImageExif {
|
||||||
|
crate::database::models::ImageExif {
|
||||||
|
id: 1,
|
||||||
|
library_id,
|
||||||
|
file_path: rel_path.to_string(),
|
||||||
|
camera_make: None,
|
||||||
|
camera_model: None,
|
||||||
|
lens_model: None,
|
||||||
|
width: None,
|
||||||
|
height: None,
|
||||||
|
orientation: None,
|
||||||
|
gps_latitude: None,
|
||||||
|
gps_longitude: None,
|
||||||
|
gps_altitude: None,
|
||||||
|
focal_length: None,
|
||||||
|
aperture: None,
|
||||||
|
shutter_speed: None,
|
||||||
|
iso: None,
|
||||||
|
date_taken,
|
||||||
|
created_time: 0,
|
||||||
|
last_modified: 0,
|
||||||
|
content_hash: None,
|
||||||
|
size_bytes: None,
|
||||||
|
phash_64: None,
|
||||||
|
dhash_64: None,
|
||||||
|
duplicate_of_hash: None,
|
||||||
|
duplicate_decided_at: None,
|
||||||
|
date_taken_source,
|
||||||
|
original_date_taken: None,
|
||||||
|
original_date_taken_source: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ExifDao for MockExifDao {
|
impl ExifDao for MockExifDao {
|
||||||
fn store_exif(
|
fn store_exif(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -1509,6 +1547,8 @@ mod tests {
|
|||||||
duplicate_of_hash: None,
|
duplicate_of_hash: None,
|
||||||
duplicate_decided_at: None,
|
duplicate_decided_at: None,
|
||||||
date_taken_source: data.date_taken_source.clone(),
|
date_taken_source: data.date_taken_source.clone(),
|
||||||
|
original_date_taken: None,
|
||||||
|
original_date_taken_source: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1553,6 +1593,8 @@ mod tests {
|
|||||||
duplicate_of_hash: None,
|
duplicate_of_hash: None,
|
||||||
duplicate_decided_at: None,
|
duplicate_decided_at: None,
|
||||||
date_taken_source: data.date_taken_source.clone(),
|
date_taken_source: data.date_taken_source.clone(),
|
||||||
|
original_date_taken: None,
|
||||||
|
original_date_taken_source: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1666,6 +1708,28 @@ mod tests {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_manual_date_taken(
|
||||||
|
&mut self,
|
||||||
|
_context: &opentelemetry::Context,
|
||||||
|
library_id: i32,
|
||||||
|
rel_path: &str,
|
||||||
|
date_taken: i64,
|
||||||
|
) -> Result<crate::database::models::ImageExif, DbError> {
|
||||||
|
// Mock — files.rs tests don't exercise the date-override endpoints.
|
||||||
|
// Returning a synthetic row keeps the trait satisfied without
|
||||||
|
// depending on private DbError constructors.
|
||||||
|
Ok(mock_exif_row(library_id, rel_path, Some(date_taken), Some("manual".to_string())))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_manual_date_taken(
|
||||||
|
&mut self,
|
||||||
|
_context: &opentelemetry::Context,
|
||||||
|
library_id: i32,
|
||||||
|
rel_path: &str,
|
||||||
|
) -> Result<crate::database::models::ImageExif, DbError> {
|
||||||
|
Ok(mock_exif_row(library_id, rel_path, None, None))
|
||||||
|
}
|
||||||
|
|
||||||
fn get_memories_in_window(
|
fn get_memories_in_window(
|
||||||
&mut self,
|
&mut self,
|
||||||
_context: &opentelemetry::Context,
|
_context: &opentelemetry::Context,
|
||||||
|
|||||||
152
src/main.rs
152
src/main.rs
@@ -41,7 +41,7 @@ use urlencoding::decode;
|
|||||||
use crate::ai::InsightGenerator;
|
use crate::ai::InsightGenerator;
|
||||||
use crate::auth::login;
|
use crate::auth::login;
|
||||||
use crate::data::*;
|
use crate::data::*;
|
||||||
use crate::database::models::InsertImageExif;
|
use crate::database::models::{ImageExif, InsertImageExif};
|
||||||
use crate::database::*;
|
use crate::database::*;
|
||||||
use crate::files::{
|
use crate::files::{
|
||||||
RealFileSystem, RefreshThumbnailsMessage, is_image_or_video, is_valid_full_path, move_file,
|
RealFileSystem, RefreshThumbnailsMessage, is_image_or_video, is_valid_full_path, move_file,
|
||||||
@@ -593,6 +593,154 @@ async fn set_image_gps(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Body for `POST /image/exif/date` — operator-driven date_taken override.
|
||||||
|
/// `date_taken` is unix seconds (matches `image_exif.date_taken`'s convention
|
||||||
|
/// — naive local reinterpreted as UTC, not real UTC; the Apollo client passes
|
||||||
|
/// through the same value the photo carousel rendered before edit).
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
struct SetDateRequest {
|
||||||
|
path: String,
|
||||||
|
library: Option<String>,
|
||||||
|
date_taken: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Body for `POST /image/exif/date/clear` — revert a manual override and
|
||||||
|
/// restore the resolver-derived `(date_taken, date_taken_source)` pair from
|
||||||
|
/// the snapshot.
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
struct ClearDateRequest {
|
||||||
|
path: String,
|
||||||
|
library: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a `MetadataResponse` for the date endpoints. Mirrors
|
||||||
|
/// `get_file_metadata`'s shape so the client gets a single source of truth
|
||||||
|
/// after every mutation. Filesystem metadata is best-effort: if the file is
|
||||||
|
/// on a stale mount or moved, the DB-side override still succeeds and the
|
||||||
|
/// response carries `created=None, modified=None, size=0`. The DB row's
|
||||||
|
/// updated EXIF is what matters here.
|
||||||
|
fn build_metadata_response_for_date_mutation(
|
||||||
|
library: &libraries::Library,
|
||||||
|
rel_path: &str,
|
||||||
|
exif: ImageExif,
|
||||||
|
) -> MetadataResponse {
|
||||||
|
let full_path = is_valid_full_path(&library.root_path, &rel_path.to_string(), false);
|
||||||
|
let fs_meta = full_path
|
||||||
|
.as_ref()
|
||||||
|
.filter(|p| p.exists())
|
||||||
|
.and_then(|p| std::fs::metadata(p).ok());
|
||||||
|
let mut response: MetadataResponse = match fs_meta {
|
||||||
|
Some(m) => m.into(),
|
||||||
|
None => MetadataResponse {
|
||||||
|
created: None,
|
||||||
|
modified: None,
|
||||||
|
size: 0,
|
||||||
|
exif: None,
|
||||||
|
filename_date: None,
|
||||||
|
library_id: None,
|
||||||
|
library_name: None,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
response.exif = Some(exif.into());
|
||||||
|
response.library_id = Some(library.id);
|
||||||
|
response.library_name = Some(library.name.clone());
|
||||||
|
response.filename_date =
|
||||||
|
memories::extract_date_from_filename(rel_path).map(|dt| dt.timestamp());
|
||||||
|
response
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/image/exif/date")]
|
||||||
|
async fn set_image_date(
|
||||||
|
_: Claims,
|
||||||
|
request: HttpRequest,
|
||||||
|
body: web::Json<SetDateRequest>,
|
||||||
|
app_state: Data<AppState>,
|
||||||
|
exif_dao: Data<Mutex<Box<dyn ExifDao>>>,
|
||||||
|
) -> impl Responder {
|
||||||
|
let tracer = global_tracer();
|
||||||
|
let context = extract_context_from_request(&request);
|
||||||
|
let mut span = tracer.start_with_context("set_image_date", &context);
|
||||||
|
let span_context =
|
||||||
|
opentelemetry::Context::new().with_remote_span_context(span.span_context().clone());
|
||||||
|
|
||||||
|
let library = libraries::resolve_library_param(&app_state, body.library.as_deref())
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.unwrap_or_else(|| app_state.primary_library());
|
||||||
|
|
||||||
|
// Path normalization matches set_image_gps so a Windows-import client
|
||||||
|
// doesn't end up with a backslash variant that misses the row.
|
||||||
|
let normalized_path = body.path.replace('\\', "/");
|
||||||
|
|
||||||
|
let updated = {
|
||||||
|
let mut dao = exif_dao.lock().expect("Unable to lock ExifDao");
|
||||||
|
dao.set_manual_date_taken(&span_context, library.id, &normalized_path, body.date_taken)
|
||||||
|
};
|
||||||
|
|
||||||
|
match updated {
|
||||||
|
Ok(row) => {
|
||||||
|
span.set_status(Status::Ok);
|
||||||
|
HttpResponse::Ok().json(build_metadata_response_for_date_mutation(
|
||||||
|
&library,
|
||||||
|
&normalized_path,
|
||||||
|
row,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let msg = format!("set_manual_date_taken failed: {:?}", e);
|
||||||
|
error!("{}", msg);
|
||||||
|
span.set_status(Status::error(msg.clone()));
|
||||||
|
// Likely "row not found" — the file isn't indexed under this
|
||||||
|
// (library, path). 404 lets the client distinguish from a 5xx.
|
||||||
|
HttpResponse::NotFound().body(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/image/exif/date/clear")]
|
||||||
|
async fn clear_image_date(
|
||||||
|
_: Claims,
|
||||||
|
request: HttpRequest,
|
||||||
|
body: web::Json<ClearDateRequest>,
|
||||||
|
app_state: Data<AppState>,
|
||||||
|
exif_dao: Data<Mutex<Box<dyn ExifDao>>>,
|
||||||
|
) -> impl Responder {
|
||||||
|
let tracer = global_tracer();
|
||||||
|
let context = extract_context_from_request(&request);
|
||||||
|
let mut span = tracer.start_with_context("clear_image_date", &context);
|
||||||
|
let span_context =
|
||||||
|
opentelemetry::Context::new().with_remote_span_context(span.span_context().clone());
|
||||||
|
|
||||||
|
let library = libraries::resolve_library_param(&app_state, body.library.as_deref())
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.unwrap_or_else(|| app_state.primary_library());
|
||||||
|
|
||||||
|
let normalized_path = body.path.replace('\\', "/");
|
||||||
|
|
||||||
|
let updated = {
|
||||||
|
let mut dao = exif_dao.lock().expect("Unable to lock ExifDao");
|
||||||
|
dao.clear_manual_date_taken(&span_context, library.id, &normalized_path)
|
||||||
|
};
|
||||||
|
|
||||||
|
match updated {
|
||||||
|
Ok(row) => {
|
||||||
|
span.set_status(Status::Ok);
|
||||||
|
HttpResponse::Ok().json(build_metadata_response_for_date_mutation(
|
||||||
|
&library,
|
||||||
|
&normalized_path,
|
||||||
|
row,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let msg = format!("clear_manual_date_taken failed: {:?}", e);
|
||||||
|
error!("{}", msg);
|
||||||
|
span.set_status(Status::error(msg.clone()));
|
||||||
|
HttpResponse::NotFound().body(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
struct UploadQuery {
|
struct UploadQuery {
|
||||||
library: Option<String>,
|
library: Option<String>,
|
||||||
@@ -1697,6 +1845,8 @@ fn main() -> std::io::Result<()> {
|
|||||||
.service(delete_favorite)
|
.service(delete_favorite)
|
||||||
.service(get_file_metadata)
|
.service(get_file_metadata)
|
||||||
.service(set_image_gps)
|
.service(set_image_gps)
|
||||||
|
.service(set_image_date)
|
||||||
|
.service(clear_image_date)
|
||||||
.service(memories::list_memories)
|
.service(memories::list_memories)
|
||||||
.service(ai::generate_insight_handler)
|
.service(ai::generate_insight_handler)
|
||||||
.service(ai::generate_agentic_insight_handler)
|
.service(ai::generate_agentic_insight_handler)
|
||||||
|
|||||||
Reference in New Issue
Block a user