use std::path::Path; use crate::db::DbConnection as Connection; use uuid::Uuid; use crate::db::queries::media as qm; use crate::db::queries::post as qp; use crate::db::queries::post_media as qpm; use crate::engine::EngineResult; use crate::model::{Media, Post, PostMedia}; use crate::util::now_unix_ms; /// Link a media item to a post and sync the media sidecar. pub fn link_media_to_post( conn: &Connection, data_dir: &Path, project_id: &str, post_id: &str, media_id: &str, sort_order: i32, ) -> EngineResult { let id = Uuid::new_v4().to_string(); let now = now_unix_ms(); let pm = PostMedia { id, project_id: project_id.to_string(), post_id: post_id.to_string(), media_id: media_id.to_string(), sort_order, created_at: now, }; qpm::link_media(conn, &pm)?; crate::engine::media::sync_media_sidecar(conn, data_dir, media_id)?; Ok(pm) } /// Unlink a media item from a post and sync the media sidecar. pub fn unlink_media_from_post( conn: &Connection, data_dir: &Path, post_id: &str, media_id: &str, ) -> EngineResult<()> { qpm::unlink_media(conn, post_id, media_id)?; crate::engine::media::sync_media_sidecar(conn, data_dir, media_id)?; Ok(()) } /// Reorder media items for a post. `media_ids` contains the new ordering; /// each entry gets sort_order = its index. pub fn reorder_post_media( conn: &Connection, post_id: &str, media_ids: &[String], ) -> EngineResult<()> { for (i, media_id) in media_ids.iter().enumerate() { qpm::update_sort_order(conn, post_id, media_id, i as i32)?; } Ok(()) } /// List media items currently linked to a post. pub fn list_media_for_post(conn: &Connection, post_id: &str) -> EngineResult> { let links = qpm::list_post_media_by_post(conn, post_id)?; let mut media = Vec::with_capacity(links.len()); for link in links { if let Ok(item) = qm::get_media_by_id(conn, &link.media_id) { media.push(item); } } Ok(media) } /// List posts currently linked to a media item. pub fn list_posts_for_media(conn: &Connection, media_id: &str) -> EngineResult> { let links = qpm::list_post_media_by_media(conn, media_id)?; let mut posts = Vec::with_capacity(links.len()); for link in links { if let Ok(post) = qp::get_post_by_id(conn, &link.post_id) { posts.push(post); } } Ok(posts) } #[cfg(test)] mod tests { use super::*; use std::fs; use tempfile::TempDir; use crate::db::Database; use crate::db::queries::media::{insert_media, make_test_media}; use crate::db::queries::post::{insert_post, make_test_post}; use crate::db::queries::post_media::list_post_media_by_post; use crate::db::queries::project::{insert_project, make_test_project}; use crate::util::sidecar::MediaSidecar; use crate::util::sidecar::read_sidecar; fn setup() -> (Database, TempDir) { let db = Database::open_in_memory().unwrap(); db.migrate().unwrap(); insert_project(db.conn(), &make_test_project("p1", "blog")).unwrap(); let dir = TempDir::new().unwrap(); insert_post(db.conn(), &make_test_post("post1", "p1", "test")).unwrap(); (db, dir) } /// Insert a media item whose sidecar_path is inside the temp dir. fn insert_test_media(db: &Database, dir: &Path, id: &str) { let sidecar_rel = format!("media/{id}.jpg.meta"); let mut media = make_test_media(id, "p1"); media.file_path = format!("media/{id}.jpg"); media.sidecar_path = sidecar_rel.clone(); insert_media(db.conn(), &media).unwrap(); // Write an initial sidecar so the directory exists let initial = MediaSidecar::from_media(&media, &[]); let abs_sidecar = dir.join(&sidecar_rel); fs::create_dir_all(abs_sidecar.parent().unwrap()).unwrap(); fs::write(&abs_sidecar, initial.to_string()).unwrap(); } fn read_linked_ids(dir: &Path, sidecar_rel: &str) -> Vec { let content = fs::read_to_string(dir.join(sidecar_rel)).unwrap(); let sc = read_sidecar(&content).unwrap(); sc.linked_post_ids } #[test] fn link_and_verify_sidecar() { let (db, dir) = setup(); insert_test_media(&db, dir.path(), "m1"); link_media_to_post(db.conn(), dir.path(), "p1", "post1", "m1", 0).unwrap(); let ids = read_linked_ids(dir.path(), "media/m1.jpg.meta"); assert_eq!(ids, vec!["post1"]); } #[test] fn unlink_removes_from_sidecar() { let (db, dir) = setup(); insert_test_media(&db, dir.path(), "m1"); link_media_to_post(db.conn(), dir.path(), "p1", "post1", "m1", 0).unwrap(); unlink_media_from_post(db.conn(), dir.path(), "post1", "m1").unwrap(); let ids = read_linked_ids(dir.path(), "media/m1.jpg.meta"); assert!(ids.is_empty()); } #[test] fn reorder_updates_sort_order() { let (db, dir) = setup(); insert_test_media(&db, dir.path(), "m1"); insert_test_media(&db, dir.path(), "m2"); link_media_to_post(db.conn(), dir.path(), "p1", "post1", "m1", 0).unwrap(); link_media_to_post(db.conn(), dir.path(), "p1", "post1", "m2", 1).unwrap(); // Reverse the order: m2 first, m1 second reorder_post_media(db.conn(), "post1", &["m2".to_string(), "m1".to_string()]).unwrap(); let list = list_post_media_by_post(db.conn(), "post1").unwrap(); assert_eq!(list.len(), 2); assert_eq!(list[0].media_id, "m2"); assert_eq!(list[0].sort_order, 0); assert_eq!(list[1].media_id, "m1"); assert_eq!(list[1].sort_order, 1); } #[test] fn multiple_posts_linked() { let (db, dir) = setup(); insert_test_media(&db, dir.path(), "m1"); let mut post = make_test_post("post2", "p1", "test2"); post.created_at = 2000; post.updated_at = 2000; insert_post(db.conn(), &post).unwrap(); link_media_to_post(db.conn(), dir.path(), "p1", "post1", "m1", 0).unwrap(); link_media_to_post(db.conn(), dir.path(), "p1", "post2", "m1", 0).unwrap(); let ids = read_linked_ids(dir.path(), "media/m1.jpg.meta"); assert_eq!(ids.len(), 2); assert!(ids.contains(&"post1".to_string())); assert!(ids.contains(&"post2".to_string())); } #[test] fn list_media_for_post_returns_resolved_media() { let (db, dir) = setup(); insert_test_media(&db, dir.path(), "m1"); link_media_to_post(db.conn(), dir.path(), "p1", "post1", "m1", 0).unwrap(); let media = list_media_for_post(db.conn(), "post1").unwrap(); assert_eq!(media.len(), 1); assert_eq!(media[0].id, "m1"); } #[test] fn list_posts_for_media_returns_resolved_posts() { let (db, dir) = setup(); insert_test_media(&db, dir.path(), "m1"); link_media_to_post(db.conn(), dir.path(), "p1", "post1", "m1", 0).unwrap(); let posts = list_posts_for_media(db.conn(), "m1").unwrap(); assert_eq!(posts.len(), 1); assert_eq!(posts[0].id, "post1"); } }