arama_cache/core/
migrate.rs1use std::path::Path;
23
24use localcache::ConnectionPool;
25
26use crate::core::engine::{
27 CacheConfig, CacheError, DbLocation, IMAGE_PAYLOAD_VERSION, NAMESPACE_IMAGE, NAMESPACE_VIDEO,
28 Result, VIDEO_PAYLOAD_VERSION, cache_options,
29};
30use crate::core::payload::{ImagePayload, VideoPayload};
31use crate::core::thumbnail::thumbnail_dest_for_canonical;
32
33#[derive(Debug, Default)]
35pub struct MigrationReport {
36 pub imported: usize,
38 pub skipped: usize,
40}
41
42pub fn migrate_v1_if_present(v1_db: &Path, v2_db: &Path) -> Result<Option<MigrationReport>> {
45 if !v1_db.exists() || v2_db.exists() {
46 return Ok(None);
47 }
48
49 match import(v1_db, v2_db) {
50 Ok(report) => {
51 let backup = v1_db.with_extension("sqlite.v1.bak");
53 let _ = std::fs::rename(v1_db, backup);
54 Ok(Some(report))
55 }
56 Err(err) => {
57 let _ = std::fs::remove_file(v2_db);
61 Err(err)
62 }
63 }
64}
65
66fn import(v1_db: &Path, v2_db: &Path) -> Result<MigrationReport> {
67 let conn =
68 rusqlite::Connection::open_with_flags(v1_db, rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY)
69 .map_err(|e| CacheError::Migration(e.to_string()))?;
70
71 let config = CacheConfig {
72 db_location: DbLocation::Custom(v2_db.to_path_buf()),
73 ..CacheConfig::default()
74 };
75 let image_pool: ConnectionPool<ImagePayload> = ConnectionPool::open(cache_options(
76 &config,
77 NAMESPACE_IMAGE,
78 IMAGE_PAYLOAD_VERSION,
79 ))?;
80 let video_pool: ConnectionPool<VideoPayload> = ConnectionPool::open(cache_options(
81 &config,
82 NAMESPACE_VIDEO,
83 VIDEO_PAYLOAD_VERSION,
84 ))?;
85
86 let mut report = MigrationReport::default();
87
88 let mut stmt = conn
89 .prepare(
90 "SELECT f.id, f.path, f.mtime_ns,
91 t.thumbnail_path,
92 i.clip_vector,
93 v.clip_vector, v.wav2vec2_vector
94 FROM files f
95 LEFT JOIN thumbnails t ON t.id = f.id
96 LEFT JOIN image_features i ON i.id = f.id
97 LEFT JOIN video_features v ON v.id = f.id",
98 )
99 .map_err(|e| CacheError::Migration(e.to_string()))?;
100
101 let rows = stmt
102 .query_map([], |r| {
103 Ok(V1Row {
104 path: r.get::<_, String>(1)?,
105 mtime_ns: r.get::<_, Option<i64>>(2)?,
106 thumbnail_path: r.get::<_, Option<String>>(3)?,
107 image_clip: r.get::<_, Option<Vec<u8>>>(4)?,
108 video_clip: r.get::<_, Option<Vec<u8>>>(5)?,
109 video_wav: r.get::<_, Option<Vec<u8>>>(6)?,
110 })
111 })
112 .map_err(|e| CacheError::Migration(e.to_string()))?;
113
114 for row in rows {
115 let row = row.map_err(|e| CacheError::Migration(e.to_string()))?;
116
117 if !still_fresh(&row) {
118 report.skipped += 1;
119 continue;
120 }
121
122 let thumbnail_path = relocate_thumbnail(&row);
123
124 let outcome = if row.video_clip.is_some() || row.video_wav.is_some() {
127 video_pool.set(
128 &row.path,
129 &VideoPayload {
130 thumbnail_path,
131 clip_vector: row.video_clip.as_deref().map(blob_to_vec),
132 wav2vec2_vector: row.video_wav.as_deref().map(blob_to_vec),
133 },
134 )
135 } else {
136 image_pool.set(
137 &row.path,
138 &ImagePayload {
139 thumbnail_path,
140 clip_vector: row.image_clip.as_deref().map(blob_to_vec),
141 },
142 )
143 };
144
145 match outcome {
146 Ok(()) => report.imported += 1,
147 Err(_) => report.skipped += 1,
149 }
150 }
151
152 Ok(report)
153}
154
155struct V1Row {
156 path: String,
157 mtime_ns: Option<i64>,
158 thumbnail_path: Option<String>,
159 image_clip: Option<Vec<u8>>,
160 video_clip: Option<Vec<u8>>,
161 video_wav: Option<Vec<u8>>,
162}
163
164fn still_fresh(row: &V1Row) -> bool {
168 let Ok(meta) = std::fs::metadata(&row.path) else {
169 return false;
170 };
171 let Some(stored_ns) = row.mtime_ns else {
172 return false;
173 };
174 let current_ns = meta
175 .modified()
176 .ok()
177 .and_then(|m| m.duration_since(std::time::UNIX_EPOCH).ok())
178 .map(|d| d.as_nanos() as i64);
179 current_ns == Some(stored_ns)
180}
181
182fn relocate_thumbnail(row: &V1Row) -> Option<String> {
186 let old = Path::new(row.thumbnail_path.as_deref()?);
187 if !old.exists() {
188 return None;
189 }
190 let dir = old.parent()?;
191 let new = thumbnail_dest_for_canonical(dir, &row.path);
192 if new != old {
193 std::fs::rename(old, &new).ok()?;
194 }
195 Some(new.to_string_lossy().into_owned())
196}
197
198fn blob_to_vec(blob: &[u8]) -> Vec<f32> {
200 blob.chunks_exact(4)
201 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
202 .collect()
203}