1use crate::{model::error::Result, storage::file::Metadata};
2use serde::Serialize;
3use sqlx::{SqlitePool, query};
4use uuid::Uuid;
5
6#[derive(sqlx::FromRow)]
14#[allow(unused)]
15pub struct Asset {
16 uuid: Uuid,
17 hash: Vec<u8>,
18 last_modified: i64,
19 user: i64,
20 size: i64,
21 tag: AssetTag,
22}
23
24#[derive(sqlx::Type, Copy, Clone)]
30#[repr(i32)]
31pub enum AssetTag {
32 GalleryMeta = 0,
34 GalleryItem = 1,
36 DriveMeta = 2,
38 DriveItem = 3,
40}
41
42impl Asset {
43 pub async fn exists(pool: &SqlitePool, hash: Vec<u8>) -> Result<bool> {
48 let result =
49 query!("SELECT 1 AS matched FROM assets WHERE hash = ?", hash)
50 .fetch_optional(pool)
51 .await?;
52
53 Ok(result.is_some())
54 }
55
56 pub async fn create(
63 pool: &SqlitePool,
64 user: i64,
65 tag: AssetTag,
66 metadata: &Metadata,
67 ) -> Result<()> {
68 sqlx::query!(
69 r#"
70 INSERT INTO assets (id, user_id, hash, size_bytes, last_modified,tag)
71 VALUES(?, ?, ?, ?, ?, ?)
72 "#,
73 Uuid::new_v4().as_bytes().to_vec(),
74 user,
75 metadata.hash.as_bytes().to_vec(),
76 metadata.size,
77 metadata.last_modified,
78 tag
79 ).execute(pool)
80 .await?;
81
82 Ok(())
83 }
84
85 pub async fn delete(
92 pool: &SqlitePool,
93 user: i64,
94 hash: &[u8],
95 ) -> Result<()> {
96 sqlx::query!(
97 r#"
98 DELETE FROM assets WHERE user_id = ? AND hash = ?
99 "#,
100 user,
101 hash
102 )
103 .execute(pool)
104 .await?;
105
106 Ok(())
107 }
108
109 pub async fn owned_by(
117 pool: &SqlitePool,
118 user: i64,
119 hash: &[u8],
120 ) -> Result<bool> {
121 let result = sqlx::query!(
122 r#"
123 SELECT 1 AS matched FROM assets WHERE user_id = ? AND hash = ?
124 "#,
125 user,
126 hash,
127 )
128 .fetch_optional(pool)
129 .await?;
130
131 Ok(result.is_some())
132 }
133}
134
135#[derive(Serialize)]
136pub struct AssetMetadataRow {
137 pub hash: String,
138 pub size_bytes: i64,
139 pub last_modified: i64,
140 pub tag: i16,
141}
142
143impl Asset {
144 pub async fn list(
145 pool: &SqlitePool,
146 user_id: i64,
147 tag_filter: Option<i16>,
148 ) -> Result<Vec<AssetMetadataRow>> {
149 let tag_str = tag_filter.map(|t| t.to_string());
150 let rows = sqlx::query!(
151 r#"
152 SELECT hash, size_bytes, last_modified, tag
153 FROM assets
154 WHERE user_id = ?
155 AND (? IS NULL OR tag = ?)
156 ORDER BY last_modified DESC
157 "#,
158 user_id,
159 tag_str,
160 tag_str,
161 )
162 .fetch_all(pool)
163 .await?;
164
165 let items = rows
166 .into_iter()
167 .map(|row| AssetMetadataRow {
168 hash: hex::encode(row.hash),
169 size_bytes: row.size_bytes,
170 last_modified: row.last_modified,
171 tag: row.tag.parse::<i64>().unwrap_or(0) as i16,
172 })
173 .collect();
174
175 Ok(items)
176 }
177}
178
179impl TryFrom<&str> for AssetTag {
180 type Error = ();
181 fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
182 use AssetTag::{DriveItem, DriveMeta, GalleryItem, GalleryMeta};
183
184 Ok(match value {
185 "0" => GalleryMeta,
186 "1" => GalleryItem,
187 "2" => DriveMeta,
188 "3" => DriveItem,
189 _ => Err(())?,
190 })
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use anyhow::Result;
197 use sqlx::SqlitePool;
198 use uuid::Uuid;
199
200 use crate::model::asset::Asset;
201
202 async fn setup_db() -> Result<SqlitePool> {
203 let pool = SqlitePool::connect("sqlite::memory:").await?;
204 sqlx::migrate!().run(&pool).await?;
205 Ok(pool)
206 }
207
208 async fn insert_asset(
209 pool: &SqlitePool,
210 user_id: i64,
211 hash: &[u8],
212 ) -> Result<()> {
213 let dummy_id = format!("id_hash_{user_id}");
214
215 sqlx::query!(r#"
216 INSERT OR IGNORE INTO users (id, identity_hash, auth_verifier) VALUES (?, ?, ?)
217 "#,
218 user_id,
219 dummy_id,
220 "dummy_verifier"
221 ).execute(pool)
222 .await?;
223
224 sqlx::query!(r#"
225 INSERT INTO assets (id, user_id, hash, size_bytes, last_modified, tag) VALUES (?, ?, ?, ?, ?, ?)
226 "#,
227 Uuid::new_v4().as_bytes().to_vec(),
228 user_id,
229 hash,
230 100,
231 0,
232 0
233 ).execute(pool)
234 .await?;
235 Ok(())
236 }
237
238 async fn count_owners(pool: &SqlitePool, hash: &[u8]) -> Result<i64> {
239 let count = sqlx::query_scalar!(
240 "SELECT COUNT(*) FROM assets WHERE hash = ?",
241 hash as &[u8]
242 )
243 .fetch_one(pool)
244 .await?;
245
246 Ok(count)
247 }
248
249 #[tokio::test]
250 async fn delete_asset_with_single_owner() -> Result<()> {
251 let pool = setup_db().await?;
252 let hash = b"hello_fellas_i_m_deleting_a_file";
253
254 insert_asset(&pool, 10, hash).await?;
255
256 Asset::delete(&pool, 10, hash).await?;
257
258 let count = count_owners(&pool, hash).await?;
259
260 assert_eq!(count, 0);
262
263 Ok(())
264 }
265
266 #[tokio::test]
267 async fn attempt_to_delete_unowned_asset() -> Result<()> {
268 let pool = setup_db().await?;
269 let hash = b"a_dude_uploads_a_file_with_cache";
270
271 insert_asset(&pool, 10, hash).await?;
272
273 Asset::delete(&pool, 12, hash).await?;
274
275 let count = count_owners(&pool, hash).await?;
276
277 assert_eq!(count, 1);
279
280 Ok(())
281 }
282
283 #[tokio::test]
284 async fn list_assets_filters_by_tag_and_orders_newest_first() -> Result<()>
285 {
286 let pool = setup_db().await?;
287 let user_id = 99;
288
289 sqlx::query!(
290 "INSERT INTO users (id, identity_hash, auth_verifier) VALUES (?, ?, ?)",
291 user_id, "dummy_hash_99", "dummy_verifier"
292 ).execute(&pool).await?;
293
294 let insert = async move |pool: &SqlitePool,
295 hash: &[u8],
296 last_modified: i64,
297 tag: &str| {
298 sqlx::query!(
299 r#"
300 INSERT INTO assets (id, user_id, hash, size_bytes, last_modified, tag)
301 VALUES (?, ?, ?, ?, ?, ?)
302 "#,
303 Uuid::new_v4().as_bytes().to_vec(),
304 user_id,
305 hash,
306 1024,
307 last_modified,
308 tag
309 ).execute(&pool.clone()).await
310 };
311
312 insert(&pool, b"hash_a", 100, "0").await?;
313 insert(&pool, b"hash_b", 200, "1").await?;
314 insert(&pool, b"hash_c", 300, "0").await?;
315
316 let all_assets = Asset::list(&pool, user_id, None).await?;
317 assert_eq!(all_assets.len(), 3);
318 assert_eq!(
319 all_assets[0].last_modified, 300,
320 "Expected newest asset to be listed, first"
321 );
322 assert_eq!(
323 all_assets[2].last_modified, 100,
324 "Expected oldest item to be listed last"
325 );
326
327 let meta_assets = Asset::list(&pool, user_id, Some(0)).await?;
328 assert_eq!(meta_assets.len(), 2, "Should filter out tag 1");
329 assert_eq!(meta_assets[0].tag, 0);
330 assert_eq!(meta_assets[1].tag, 0);
331 assert_eq!(
332 meta_assets[0].last_modified, 300,
333 "Newest meta should be listed first"
334 );
335
336 Ok(())
337 }
338}