diff --git a/crates/pinakes-core/src/import.rs b/crates/pinakes-core/src/import.rs index 6d3c657..27046e2 100644 --- a/crates/pinakes-core/src/import.rs +++ b/crates/pinakes-core/src/import.rs @@ -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}" )))); }, } diff --git a/crates/pinakes-core/src/storage/postgres.rs b/crates/pinakes-core/src/storage/postgres.rs index e0caeee..f9d2a43 100644 --- a/crates/pinakes-core/src/storage/postgres.rs +++ b/crates/pinakes-core/src/storage/postgres.rs @@ -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() diff --git a/crates/pinakes-core/src/storage/sqlite.rs b/crates/pinakes-core/src/storage/sqlite.rs index c377d9e..cfe08c9 100644 --- a/crates/pinakes-core/src/storage/sqlite.rs +++ b/crates/pinakes-core/src/storage/sqlite.rs @@ -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();