pinakes-core: add error context to tag and collection writes; map serde_json errors to Serialization variant

pinakes-core: distinguish task panics from cancellations in import error
  handling
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Icf5686f34144630ebf1935c47b3979156a6a6964
This commit is contained in:
raf 2026-03-11 17:08:24 +03:00
commit b89c7a5dc5
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
3 changed files with 65 additions and 20 deletions

View file

@ -498,10 +498,14 @@ fn collect_import_result(
tracing::warn!(path = %path.display(), error = %e, "failed to import file");
results.push(Err(e));
},
Err(e) => {
tracing::error!(error = %e, "import task panicked");
Err(join_err) => {
if join_err.is_panic() {
tracing::error!(error = %join_err, "import task panicked");
} else {
tracing::warn!(error = %join_err, "import task was cancelled");
}
results.push(Err(PinakesError::InvalidOperation(format!(
"import task panicked: {e}"
"import task failed: {join_err}"
))));
},
}

View file

@ -4295,6 +4295,11 @@ impl StorageBackend for PostgresBackend {
&self,
metadata: &crate::model::BookMetadata,
) -> Result<()> {
if metadata.media_id.0.is_nil() {
return Err(PinakesError::Database(
"upsert_book_metadata: media_id must not be nil".to_string(),
));
}
let mut client = self
.pool
.get()

View file

@ -1116,7 +1116,8 @@ impl StorageBackend for SqliteBackend {
parent_id.map(|p| p.to_string()),
now.to_rfc3339(),
],
)?;
)
.map_err(crate::error::db_ctx("create_tag", &name))?;
drop(db);
Tag {
id,
@ -1192,7 +1193,8 @@ impl StorageBackend for SqliteBackend {
.lock()
.map_err(|e| PinakesError::Database(e.to_string()))?;
let changed = db
.execute("DELETE FROM tags WHERE id = ?1", params![id.to_string()])?;
.execute("DELETE FROM tags WHERE id = ?1", params![id.to_string()])
.map_err(crate::error::db_ctx("delete_tag", id))?;
drop(db);
if changed == 0 {
return Err(PinakesError::TagNotFound(id.to_string()));
@ -1214,7 +1216,11 @@ impl StorageBackend for SqliteBackend {
db.execute(
"INSERT OR IGNORE INTO media_tags (media_id, tag_id) VALUES (?1, ?2)",
params![media_id.0.to_string(), tag_id.to_string()],
)?;
)
.map_err(crate::error::db_ctx(
"tag_media",
format!("{media_id} x {tag_id}"),
))?;
}
Ok(())
})
@ -1232,7 +1238,11 @@ impl StorageBackend for SqliteBackend {
db.execute(
"DELETE FROM media_tags WHERE media_id = ?1 AND tag_id = ?2",
params![media_id.0.to_string(), tag_id.to_string()],
)?;
)
.map_err(crate::error::db_ctx(
"untag_media",
format!("{media_id} x {tag_id}"),
))?;
}
Ok(())
})
@ -1323,7 +1333,8 @@ impl StorageBackend for SqliteBackend {
now.to_rfc3339(),
now.to_rfc3339(),
],
)?;
)
.map_err(crate::error::db_ctx("create_collection", &name))?;
drop(db);
Collection {
id,
@ -1406,7 +1417,8 @@ impl StorageBackend for SqliteBackend {
let changed = db
.execute("DELETE FROM collections WHERE id = ?1", params![
id.to_string()
])?;
])
.map_err(crate::error::db_ctx("delete_collection", id))?;
drop(db);
if changed == 0 {
return Err(PinakesError::CollectionNotFound(id.to_string()));
@ -1440,7 +1452,11 @@ impl StorageBackend for SqliteBackend {
position,
now.to_rfc3339(),
],
)?;
)
.map_err(crate::error::db_ctx(
"add_to_collection",
format!("{collection_id} <- {media_id}"),
))?;
}
Ok(())
})
@ -1463,7 +1479,11 @@ impl StorageBackend for SqliteBackend {
"DELETE FROM collection_members WHERE collection_id = ?1 AND \
media_id = ?2",
params![collection_id.to_string(), media_id.0.to_string()],
)?;
)
.map_err(crate::error::db_ctx(
"remove_from_collection",
format!("{collection_id} <- {media_id}"),
))?;
}
Ok(())
})
@ -1863,20 +1883,27 @@ impl StorageBackend for SqliteBackend {
let db = conn
.lock()
.map_err(|e| PinakesError::Database(e.to_string()))?;
let tx = db.unchecked_transaction()?;
let ctx = format!("{} media x {} tags", media_ids.len(), tag_ids.len());
let tx = db
.unchecked_transaction()
.map_err(crate::error::db_ctx("batch_tag_media", &ctx))?;
// Prepare statement once for reuse
let mut stmt = tx.prepare_cached(
"INSERT OR IGNORE INTO media_tags (media_id, tag_id) VALUES (?1, ?2)",
)?;
)
.map_err(crate::error::db_ctx("batch_tag_media", &ctx))?;
let mut count = 0u64;
for mid in &media_ids {
for tid in &tag_ids {
let rows = stmt.execute(params![mid, tid])?;
let rows = stmt
.execute(params![mid, tid])
.map_err(crate::error::db_ctx("batch_tag_media", &ctx))?;
count += rows as u64; // INSERT OR IGNORE: rows=1 if new, 0 if existed
}
}
drop(stmt);
tx.commit()?;
tx.commit()
.map_err(crate::error::db_ctx("batch_tag_media", &ctx))?;
count
};
Ok(count)
@ -2695,7 +2722,7 @@ impl StorageBackend for SqliteBackend {
let id_str = id.0.to_string();
let now = chrono::Utc::now();
let role_str = serde_json::to_string(&role).map_err(|e| {
PinakesError::Database(format!("failed to serialize role: {e}"))
PinakesError::Serialization(format!("failed to serialize role: {e}"))
})?;
tx.execute(
@ -2714,7 +2741,7 @@ impl StorageBackend for SqliteBackend {
let user_profile = if let Some(prof) = profile.clone() {
let prefs_json =
serde_json::to_string(&prof.preferences).map_err(|e| {
PinakesError::Database(format!(
PinakesError::Serialization(format!(
"failed to serialize preferences: {e}"
))
})?;
@ -2796,7 +2823,9 @@ impl StorageBackend for SqliteBackend {
if let Some(ref r) = role {
updates.push("role = ?");
let role_str = serde_json::to_string(r).map_err(|e| {
PinakesError::Database(format!("failed to serialize role: {e}"))
PinakesError::Serialization(format!(
"failed to serialize role: {e}"
))
})?;
params.push(Box::new(role_str));
}
@ -2814,7 +2843,7 @@ impl StorageBackend for SqliteBackend {
if let Some(prof) = profile {
let prefs_json =
serde_json::to_string(&prof.preferences).map_err(|e| {
PinakesError::Database(format!(
PinakesError::Serialization(format!(
"failed to serialize preferences: {e}"
))
})?;
@ -2966,7 +2995,9 @@ impl StorageBackend for SqliteBackend {
PinakesError::Database(format!("failed to acquire database lock: {e}"))
})?;
let perm_str = serde_json::to_string(&permission).map_err(|e| {
PinakesError::Database(format!("failed to serialize permission: {e}"))
PinakesError::Serialization(format!(
"failed to serialize permission: {e}"
))
})?;
let now = chrono::Utc::now();
db.execute(
@ -5055,6 +5086,11 @@ impl StorageBackend for SqliteBackend {
&self,
metadata: &crate::model::BookMetadata,
) -> Result<()> {
if metadata.media_id.0.is_nil() {
return Err(PinakesError::Database(
"upsert_book_metadata: media_id must not be nil".to_string(),
));
}
let conn = Arc::clone(&self.conn);
let media_id_str = metadata.media_id.to_string();
let isbn = metadata.isbn.clone();