Alternative ATProto PDS implementation

prototype actor_store sql_blob

Changed files
+129 -326
src
actor_store
+129 -326
src/actor_store/sql_blob.rs
··· 3 clippy::single_char_lifetime_names, 4 unused_qualifications 5 )] 6 - use std::{path::PathBuf, str::FromStr as _, sync::Arc}; 7 - 8 use anyhow::{Context, Result}; 9 use cidv10::Cid; 10 - use diesel::*; 11 use rsky_common::get_random_str; 12 - use tokio::fs; 13 14 use crate::db::DbConn; 15 - 16 - /// Type for stream of blob data 17 - pub type BlobStream = Box<dyn std::io::Read + Send>; 18 19 /// ByteStream implementation for blob data 20 pub struct ByteStream { ··· 38 pub db: Arc<DbConn>, 39 /// DID of the actor 40 pub did: String, 41 - /// Path for blob storage 42 - pub path: PathBuf, 43 - } 44 - 45 - /// Configuration for the blob store 46 - pub struct BlobConfig { 47 - /// Base path for blob storage 48 - pub path: PathBuf, 49 - } 50 - 51 - /// Represents a move operation for blobs 52 - struct MoveObject { 53 - from: String, 54 - to: String, 55 } 56 57 /// Blob table structure for SQL operations ··· 60 struct BlobEntry { 61 cid: String, 62 did: String, 63 - path: String, 64 size: i32, 65 mime_type: String, 66 quarantined: bool, 67 temp: bool, 68 } 69 70 // Table definition for blobs ··· 72 blobs (cid, did) { 73 cid -> Text, 74 did -> Text, 75 - path -> Text, 76 size -> Integer, 77 mime_type -> Text, 78 quarantined -> Bool, 79 temp -> Bool, 80 } 81 } 82 83 impl BlobStoreSql { 84 /// Create a new SQL-based blob store for the given DID 85 - pub fn new(did: String, cfg: &BlobConfig, db: Arc<DbConn>) -> Self { 86 - let actor_path = cfg.path.join(&did); 87 - 88 - // Create actor directory if it doesn't exist 89 - if !actor_path.exists() { 90 - // Use blocking to avoid complicating this constructor 91 - std::fs::create_dir_all(&actor_path).unwrap_or_else(|_| { 92 - panic!("Failed to create blob directory: {}", actor_path.display()) 93 - }); 94 - } 95 - 96 - BlobStoreSql { 97 - db, 98 - did, 99 - path: actor_path, 100 - } 101 } 102 103 /// Create a factory function for blob stores 104 - pub fn creator(cfg: &BlobConfig, db: Arc<DbConn>) -> Box<dyn Fn(String) -> BlobStoreSql + '_> { 105 let db_clone = db.clone(); 106 - Box::new(move |did: String| BlobStoreSql::new(did, cfg, db_clone.clone())) 107 } 108 109 /// Generate a random key for temporary blobs ··· 111 get_random_str() 112 } 113 114 - /// Get the path for a temporary blob 115 - fn get_tmp_path(&self, key: &str) -> PathBuf { 116 - self.path.join("tmp").join(key) 117 - } 118 - 119 - /// Get the filesystem path for a stored blob 120 - fn get_stored_path(&self, cid: &Cid) -> PathBuf { 121 - self.path.join("blocks").join(cid.to_string()) 122 - } 123 - 124 - /// Get the filesystem path for a quarantined blob 125 - fn get_quarantined_path(&self, cid: &Cid) -> PathBuf { 126 - self.path.join("quarantine").join(cid.to_string()) 127 - } 128 - 129 /// Store a blob temporarily 130 pub async fn put_temp(&self, bytes: Vec<u8>) -> Result<String> { 131 let key = self.gen_key(); 132 - let tmp_path = self.get_tmp_path(&key); 133 - 134 - // Ensure the directory exists 135 - if let Some(parent) = tmp_path.parent() { 136 - fs::create_dir_all(parent) 137 - .await 138 - .context("Failed to create temp directory")?; 139 - } 140 - 141 - // Write the blob data to the file 142 - fs::write(&tmp_path, &bytes) 143 - .await 144 - .context("Failed to write temporary blob")?; 145 - 146 - // Clone values to be used in the closure 147 let did_clone = self.did.clone(); 148 - let tmp_path_str = tmp_path.to_string_lossy().to_string(); 149 let bytes_len = bytes.len() as i32; 150 151 - // Store metadata in the database (will be updated when made permanent) 152 self.db 153 .run(move |conn| { 154 let entry = BlobEntry { 155 cid: "temp".to_string(), // Will be updated when made permanent 156 did: did_clone, 157 - path: tmp_path_str, 158 size: bytes_len, 159 mime_type: "application/octet-stream".to_string(), // Will be updated when made permanent 160 quarantined: false, 161 temp: true, 162 }; 163 164 diesel::insert_into(blobs::table) 165 .values(&entry) 166 .execute(conn) 167 - .context("Failed to insert temporary blob metadata") 168 }) 169 .await?; 170 ··· 175 pub async fn make_permanent(&self, key: String, cid: Cid) -> Result<()> { 176 let already_has = self.has_stored(cid).await?; 177 if !already_has { 178 - let tmp_path = self.get_tmp_path(&key); 179 - let stored_path = self.get_stored_path(&cid); 180 - 181 - // Ensure parent directory exists 182 - if let Some(parent) = stored_path.parent() { 183 - fs::create_dir_all(parent) 184 - .await 185 - .context("Failed to create blocks directory")?; 186 - } 187 - 188 - // Read the bytes 189 - let bytes = fs::read(&tmp_path) 190 - .await 191 - .context("Failed to read temporary blob")?; 192 - 193 - // Write to permanent location 194 - fs::write(&stored_path, &bytes) 195 - .await 196 - .context("Failed to write permanent blob")?; 197 198 - // Update database metadata 199 - let tmp_path_clone = tmp_path.clone(); 200 self.db 201 .run(move |conn| { 202 - // Update the entry with the correct CID and path 203 diesel::update(blobs::table) 204 - .filter(blobs::path.eq(tmp_path_clone.to_string_lossy().to_string())) 205 .set(( 206 - blobs::cid.eq(cid.to_string()), 207 - blobs::path.eq(stored_path.to_string_lossy().to_string()), 208 blobs::temp.eq(false), 209 )) 210 .execute(conn) 211 - .context("Failed to update blob metadata") 212 }) 213 .await?; 214 - 215 - // Remove the temporary file 216 - fs::remove_file(tmp_path) 217 - .await 218 - .context("Failed to remove temporary blob")?; 219 220 Ok(()) 221 } else { 222 - // Already saved, so delete the temp file 223 - let tmp_path = self.get_tmp_path(&key); 224 - if tmp_path.exists() { 225 - fs::remove_file(tmp_path) 226 - .await 227 - .context("Failed to remove existing temporary blob")?; 228 - } 229 Ok(()) 230 } 231 } 232 233 /// Store a blob directly as permanent 234 pub async fn put_permanent(&self, cid: Cid, bytes: Vec<u8>) -> Result<()> { 235 - let stored_path = self.get_stored_path(&cid); 236 - 237 - // Ensure parent directory exists 238 - if let Some(parent) = stored_path.parent() { 239 - fs::create_dir_all(parent) 240 - .await 241 - .context("Failed to create blocks directory")?; 242 - } 243 - 244 - // Write to permanent location 245 - fs::write(&stored_path, &bytes) 246 - .await 247 - .context("Failed to write permanent blob")?; 248 - 249 - let stored_path_str = stored_path.to_string_lossy().to_string(); 250 let cid_str = cid.to_string(); 251 let did_clone = self.did.clone(); 252 let bytes_len = bytes.len() as i32; 253 254 - // Update database metadata 255 self.db 256 .run(move |conn| { 257 let entry = BlobEntry { 258 - cid: cid_str, 259 - did: did_clone, 260 - path: stored_path_str.clone(), 261 size: bytes_len, 262 mime_type: "application/octet-stream".to_string(), // Could be improved with MIME detection 263 quarantined: false, 264 temp: false, 265 }; 266 267 diesel::insert_into(blobs::table) 268 .values(&entry) 269 .on_conflict((blobs::cid, blobs::did)) 270 .do_update() 271 - .set(blobs::path.eq(stored_path_str)) 272 .execute(conn) 273 - .context("Failed to insert permanent blob metadata") 274 }) 275 .await?; 276 ··· 279 280 /// Quarantine a blob 281 pub async fn quarantine(&self, cid: Cid) -> Result<()> { 282 - let stored_path = self.get_stored_path(&cid); 283 - let quarantined_path = self.get_quarantined_path(&cid); 284 285 - // Ensure parent directory exists 286 - if let Some(parent) = quarantined_path.parent() { 287 - fs::create_dir_all(parent) 288 - .await 289 - .context("Failed to create quarantine directory")?; 290 - } 291 - 292 - // Move the blob if it exists 293 - if stored_path.exists() { 294 - // Read the bytes 295 - let bytes = fs::read(&stored_path) 296 - .await 297 - .context("Failed to read stored blob")?; 298 - 299 - // Write to quarantine location 300 - fs::write(&quarantined_path, &bytes) 301 - .await 302 - .context("Failed to write quarantined blob")?; 303 - 304 - // Update database metadata 305 - let cid_str = cid.to_string(); 306 - let did_clone = self.did.clone(); 307 - let quarantined_path_str = quarantined_path.to_string_lossy().to_string(); 308 - 309 - self.db 310 - .run(move |conn| { 311 - diesel::update(blobs::table) 312 - .filter(blobs::cid.eq(cid_str)) 313 - .filter(blobs::did.eq(did_clone)) 314 - .set(( 315 - blobs::path.eq(quarantined_path_str), 316 - blobs::quarantined.eq(true), 317 - )) 318 - .execute(conn) 319 - .context("Failed to update blob metadata for quarantine") 320 - }) 321 - .await?; 322 - 323 - // Remove the original file 324 - fs::remove_file(stored_path) 325 - .await 326 - .context("Failed to remove quarantined blob")?; 327 - } 328 329 Ok(()) 330 } 331 332 /// Unquarantine a blob 333 pub async fn unquarantine(&self, cid: Cid) -> Result<()> { 334 - let quarantined_path = self.get_quarantined_path(&cid); 335 - let stored_path = self.get_stored_path(&cid); 336 337 - // Ensure parent directory exists 338 - if let Some(parent) = stored_path.parent() { 339 - fs::create_dir_all(parent) 340 - .await 341 - .context("Failed to create blocks directory")?; 342 - } 343 - 344 - // Move the blob if it exists 345 - if quarantined_path.exists() { 346 - // Read the bytes 347 - let bytes = fs::read(&quarantined_path) 348 - .await 349 - .context("Failed to read quarantined blob")?; 350 - 351 - // Write to normal location 352 - fs::write(&stored_path, &bytes) 353 - .await 354 - .context("Failed to write unquarantined blob")?; 355 - 356 - // Update database metadata 357 - let stored_path_str = stored_path.to_string_lossy().to_string(); 358 - let cid_str = cid.to_string(); 359 - let did_clone = self.did.clone(); 360 - 361 - self.db 362 - .run(move |conn| { 363 - diesel::update(blobs::table) 364 - .filter(blobs::cid.eq(cid_str)) 365 - .filter(blobs::did.eq(did_clone)) 366 - .set(( 367 - blobs::path.eq(stored_path_str), 368 - blobs::quarantined.eq(false), 369 - )) 370 - .execute(conn) 371 - .context("Failed to update blob metadata for unquarantine") 372 - }) 373 - .await?; 374 - 375 - // Remove the quarantined file 376 - fs::remove_file(quarantined_path) 377 - .await 378 - .context("Failed to remove from quarantine")?; 379 - } 380 381 Ok(()) 382 } 383 384 /// Get a blob as a stream 385 - pub async fn get_object(&self, cid_param: Cid) -> Result<ByteStream> { 386 use self::blobs::dsl::*; 387 388 - // Get the blob path from the database 389 - let cid_string = cid_param.to_string(); 390 let did_clone = self.did.clone(); 391 392 - let blob_record = self 393 .db 394 .run(move |conn| { 395 blobs 396 - .filter(cid.eq(&cid_string)) 397 .filter(did.eq(&did_clone)) 398 .filter(quarantined.eq(false)) 399 - .select(path) 400 - .first::<String>(conn) 401 .optional() 402 - .context("Failed to query blob metadata") 403 }) 404 .await?; 405 406 - if let Some(blob_path) = blob_record { 407 - // Read the blob data 408 - let bytes = fs::read(blob_path) 409 - .await 410 - .context("Failed to read blob data")?; 411 Ok(ByteStream::new(bytes)) 412 } else { 413 - anyhow::bail!("Blob not found: {:?}", cid) 414 } 415 } 416 ··· 426 } 427 428 /// Delete a blob by CID string 429 - pub async fn delete(&self, cid: String) -> Result<()> { 430 - self.delete_key(self.get_stored_path(&Cid::from_str(&cid)?)) 431 - .await 432 } 433 434 /// Delete multiple blobs by CID 435 pub async fn delete_many(&self, cids: Vec<Cid>) -> Result<()> { 436 - for cid in cids { 437 - self.delete_key(self.get_stored_path(&cid)).await?; 438 - } 439 Ok(()) 440 } 441 442 /// Check if a blob is stored 443 - pub async fn has_stored(&self, cid_param: Cid) -> Result<bool> { 444 use self::blobs::dsl::*; 445 446 - let cid_string = cid_param.to_string(); 447 let did_clone = self.did.clone(); 448 449 let exists = self ··· 451 .run(move |conn| { 452 diesel::select(diesel::dsl::exists( 453 blobs 454 - .filter(cid.eq(&cid_string)) 455 .filter(did.eq(&did_clone)) 456 .filter(temp.eq(false)), 457 )) ··· 465 466 /// Check if a temporary blob exists 467 pub async fn has_temp(&self, key: String) -> Result<bool> { 468 - let tmp_path = self.get_tmp_path(&key); 469 - Ok(tmp_path.exists()) 470 - } 471 472 - /// Check if a blob exists by key 473 - async fn has_key(&self, key_path: PathBuf) -> bool { 474 - key_path.exists() 475 - } 476 477 - /// Delete a blob by its key path 478 - async fn delete_key(&self, key_path: PathBuf) -> Result<()> { 479 - use self::blobs::dsl::*; 480 - 481 - // Delete from database first 482 - let key_path_clone = key_path.clone(); 483 - self.db 484 .run(move |conn| { 485 - diesel::delete(blobs) 486 - .filter(path.eq(key_path_clone.to_string_lossy().to_string())) 487 - .execute(conn) 488 - .context("Failed to delete blob metadata") 489 }) 490 .await?; 491 492 - // Then delete the file if it exists 493 - if key_path.exists() { 494 - fs::remove_file(key_path) 495 - .await 496 - .context("Failed to delete blob file")?; 497 - } 498 - 499 - Ok(()) 500 - } 501 - 502 - /// Delete multiple blobs by key path 503 - async fn delete_many_keys(&self, keys: Vec<String>) -> Result<()> { 504 - for key in keys { 505 - self.delete_key(PathBuf::from(key)).await?; 506 - } 507 - Ok(()) 508 - } 509 - 510 - /// Move a blob from one location to another 511 - async fn move_object(&self, keys: MoveObject) -> Result<()> { 512 - let from_path = PathBuf::from(&keys.from); 513 - let to_path = PathBuf::from(&keys.to); 514 - 515 - // Ensure parent directory exists 516 - if let Some(parent) = to_path.parent() { 517 - fs::create_dir_all(parent) 518 - .await 519 - .context("Failed to create directory")?; 520 - } 521 - 522 - // Only move if the source exists 523 - if from_path.exists() { 524 - // Read the data 525 - let data = fs::read(&from_path) 526 - .await 527 - .context("Failed to read source blob")?; 528 - 529 - // Write to the destination 530 - fs::write(&to_path, data) 531 - .await 532 - .context("Failed to write destination blob")?; 533 - 534 - // Update the database record 535 - let from_path_clone = from_path.clone(); 536 - self.db 537 - .run(move |conn| { 538 - diesel::update(blobs::table) 539 - .filter(blobs::path.eq(from_path_clone.to_string_lossy().to_string())) 540 - .set(blobs::path.eq(to_path.to_string_lossy().to_string())) 541 - .execute(conn) 542 - .context("Failed to update blob path") 543 - }) 544 - .await?; 545 - 546 - // Delete the source file 547 - fs::remove_file(from_path) 548 - .await 549 - .context("Failed to remove source blob")?; 550 - } 551 - 552 - Ok(()) 553 } 554 }
··· 3 clippy::single_char_lifetime_names, 4 unused_qualifications 5 )] 6 use anyhow::{Context, Result}; 7 use cidv10::Cid; 8 + use diesel::prelude::*; 9 use rsky_common::get_random_str; 10 + use std::sync::Arc; 11 12 use crate::db::DbConn; 13 14 /// ByteStream implementation for blob data 15 pub struct ByteStream { ··· 33 pub db: Arc<DbConn>, 34 /// DID of the actor 35 pub did: String, 36 } 37 38 /// Blob table structure for SQL operations ··· 41 struct BlobEntry { 42 cid: String, 43 did: String, 44 + data: Vec<u8>, 45 size: i32, 46 mime_type: String, 47 quarantined: bool, 48 temp: bool, 49 + temp_key: Option<String>, 50 } 51 52 // Table definition for blobs ··· 54 blobs (cid, did) { 55 cid -> Text, 56 did -> Text, 57 + data -> Binary, 58 size -> Integer, 59 mime_type -> Text, 60 quarantined -> Bool, 61 temp -> Bool, 62 + temp_key -> Nullable<Text>, 63 } 64 } 65 66 impl BlobStoreSql { 67 /// Create a new SQL-based blob store for the given DID 68 + pub fn new(did: String, db: Arc<DbConn>) -> Self { 69 + BlobStoreSql { db, did } 70 } 71 72 /// Create a factory function for blob stores 73 + pub fn creator(db: Arc<DbConn>) -> Box<dyn Fn(String) -> BlobStoreSql> { 74 let db_clone = db.clone(); 75 + Box::new(move |did: String| BlobStoreSql::new(did, db_clone.clone())) 76 } 77 78 /// Generate a random key for temporary blobs ··· 80 get_random_str() 81 } 82 83 /// Store a blob temporarily 84 pub async fn put_temp(&self, bytes: Vec<u8>) -> Result<String> { 85 let key = self.gen_key(); 86 let did_clone = self.did.clone(); 87 let bytes_len = bytes.len() as i32; 88 89 + // Store in the database with temp flag 90 + let key_clone = key.clone(); 91 self.db 92 .run(move |conn| { 93 let entry = BlobEntry { 94 cid: "temp".to_string(), // Will be updated when made permanent 95 did: did_clone, 96 + data: bytes, 97 size: bytes_len, 98 mime_type: "application/octet-stream".to_string(), // Will be updated when made permanent 99 quarantined: false, 100 temp: true, 101 + temp_key: Some(key_clone), 102 }; 103 104 diesel::insert_into(blobs::table) 105 .values(&entry) 106 .execute(conn) 107 + .context("Failed to insert temporary blob data") 108 }) 109 .await?; 110 ··· 115 pub async fn make_permanent(&self, key: String, cid: Cid) -> Result<()> { 116 let already_has = self.has_stored(cid).await?; 117 if !already_has { 118 + let cid_str = cid.to_string(); 119 + let did_clone = self.did.clone(); 120 121 + // Update database record to make it permanent 122 self.db 123 .run(move |conn| { 124 diesel::update(blobs::table) 125 + .filter(blobs::temp_key.eq(&key)) 126 + .filter(blobs::did.eq(&did_clone)) 127 .set(( 128 + blobs::cid.eq(&cid_str), 129 blobs::temp.eq(false), 130 + blobs::temp_key.eq::<Option<String>>(None), 131 )) 132 .execute(conn) 133 + .context("Failed to update blob to permanent status") 134 }) 135 .await?; 136 137 Ok(()) 138 } else { 139 + // Already exists, so delete the temporary one 140 + let did_clone = self.did.clone(); 141 + 142 + self.db 143 + .run(move |conn| { 144 + diesel::delete(blobs::table) 145 + .filter(blobs::temp_key.eq(&key)) 146 + .filter(blobs::did.eq(&did_clone)) 147 + .execute(conn) 148 + .context("Failed to delete redundant temporary blob") 149 + }) 150 + .await?; 151 + 152 Ok(()) 153 } 154 } 155 156 /// Store a blob directly as permanent 157 pub async fn put_permanent(&self, cid: Cid, bytes: Vec<u8>) -> Result<()> { 158 let cid_str = cid.to_string(); 159 let did_clone = self.did.clone(); 160 let bytes_len = bytes.len() as i32; 161 162 + // Store directly in the database 163 self.db 164 .run(move |conn| { 165 + let data_clone = bytes.clone(); 166 let entry = BlobEntry { 167 + cid: cid_str.clone(), 168 + did: did_clone.clone(), 169 + data: bytes, 170 size: bytes_len, 171 mime_type: "application/octet-stream".to_string(), // Could be improved with MIME detection 172 quarantined: false, 173 temp: false, 174 + temp_key: None, 175 }; 176 177 diesel::insert_into(blobs::table) 178 .values(&entry) 179 .on_conflict((blobs::cid, blobs::did)) 180 .do_update() 181 + .set(blobs::data.eq(data_clone)) 182 .execute(conn) 183 + .context("Failed to insert permanent blob data") 184 }) 185 .await?; 186 ··· 189 190 /// Quarantine a blob 191 pub async fn quarantine(&self, cid: Cid) -> Result<()> { 192 + let cid_str = cid.to_string(); 193 + let did_clone = self.did.clone(); 194 195 + // Update the quarantine flag in the database 196 + self.db 197 + .run(move |conn| { 198 + diesel::update(blobs::table) 199 + .filter(blobs::cid.eq(&cid_str)) 200 + .filter(blobs::did.eq(&did_clone)) 201 + .set(blobs::quarantined.eq(true)) 202 + .execute(conn) 203 + .context("Failed to quarantine blob") 204 + }) 205 + .await?; 206 207 Ok(()) 208 } 209 210 /// Unquarantine a blob 211 pub async fn unquarantine(&self, cid: Cid) -> Result<()> { 212 + let cid_str = cid.to_string(); 213 + let did_clone = self.did.clone(); 214 215 + // Update the quarantine flag in the database 216 + self.db 217 + .run(move |conn| { 218 + diesel::update(blobs::table) 219 + .filter(blobs::cid.eq(&cid_str)) 220 + .filter(blobs::did.eq(&did_clone)) 221 + .set(blobs::quarantined.eq(false)) 222 + .execute(conn) 223 + .context("Failed to unquarantine blob") 224 + }) 225 + .await?; 226 227 Ok(()) 228 } 229 230 /// Get a blob as a stream 231 + pub async fn get_object(&self, blob_cid: Cid) -> Result<ByteStream> { 232 use self::blobs::dsl::*; 233 234 + let cid_str = blob_cid.to_string(); 235 let did_clone = self.did.clone(); 236 237 + // Get the blob data from the database 238 + let blob_data = self 239 .db 240 .run(move |conn| { 241 blobs 242 + .filter(self::blobs::cid.eq(&cid_str)) 243 .filter(did.eq(&did_clone)) 244 .filter(quarantined.eq(false)) 245 + .select(data) 246 + .first::<Vec<u8>>(conn) 247 .optional() 248 + .context("Failed to query blob data") 249 }) 250 .await?; 251 252 + if let Some(bytes) = blob_data { 253 Ok(ByteStream::new(bytes)) 254 } else { 255 + anyhow::bail!("Blob not found: {}", blob_cid) 256 } 257 } 258 ··· 268 } 269 270 /// Delete a blob by CID string 271 + pub async fn delete(&self, blob_cid: String) -> Result<()> { 272 + use self::blobs::dsl::*; 273 + 274 + let did_clone = self.did.clone(); 275 + 276 + // Delete from database 277 + self.db 278 + .run(move |conn| { 279 + diesel::delete(blobs) 280 + .filter(self::blobs::cid.eq(&blob_cid)) 281 + .filter(did.eq(&did_clone)) 282 + .execute(conn) 283 + .context("Failed to delete blob") 284 + }) 285 + .await?; 286 + 287 + Ok(()) 288 } 289 290 /// Delete multiple blobs by CID 291 pub async fn delete_many(&self, cids: Vec<Cid>) -> Result<()> { 292 + use self::blobs::dsl::*; 293 + 294 + let cid_strings: Vec<String> = cids.into_iter().map(|c| c.to_string()).collect(); 295 + let did_clone = self.did.clone(); 296 + 297 + // Delete all blobs in one operation 298 + self.db 299 + .run(move |conn| { 300 + diesel::delete(blobs) 301 + .filter(self::blobs::cid.eq_any(cid_strings)) 302 + .filter(did.eq(&did_clone)) 303 + .execute(conn) 304 + .context("Failed to delete multiple blobs") 305 + }) 306 + .await?; 307 + 308 Ok(()) 309 } 310 311 /// Check if a blob is stored 312 + pub async fn has_stored(&self, blob_cid: Cid) -> Result<bool> { 313 use self::blobs::dsl::*; 314 315 + let cid_str = blob_cid.to_string(); 316 let did_clone = self.did.clone(); 317 318 let exists = self ··· 320 .run(move |conn| { 321 diesel::select(diesel::dsl::exists( 322 blobs 323 + .filter(self::blobs::cid.eq(&cid_str)) 324 .filter(did.eq(&did_clone)) 325 .filter(temp.eq(false)), 326 )) ··· 334 335 /// Check if a temporary blob exists 336 pub async fn has_temp(&self, key: String) -> Result<bool> { 337 + use self::blobs::dsl::*; 338 339 + let did_clone = self.did.clone(); 340 341 + let exists = self 342 + .db 343 .run(move |conn| { 344 + diesel::select(diesel::dsl::exists( 345 + blobs 346 + .filter(temp_key.eq(&key)) 347 + .filter(did.eq(&did_clone)) 348 + .filter(temp.eq(true)), 349 + )) 350 + .get_result::<bool>(conn) 351 + .context("Failed to check if temporary blob exists") 352 }) 353 .await?; 354 355 + Ok(exists) 356 } 357 }