217 lines
7.1 KiB
Rust
217 lines
7.1 KiB
Rust
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<PostMedia> {
|
|
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<Vec<Media>> {
|
|
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<Vec<Post>> {
|
|
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<String> {
|
|
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");
|
|
}
|
|
}
|