#[allow(dead_code)] #[path = "../models.rs"] mod models; #[allow(dead_code)] #[path = "../uploads.rs"] mod uploads; use std::collections::BTreeMap; use cot::db::{Database, Model}; use models::{Media, Testimonial}; fn database_url() -> String { std::env::var("WEB_PETTING_DATABASE_URL") .or_else(|_| std::env::var("DATABASE_URL")) .unwrap_or_else(|_| "postgresql://postgres:postgres@localhost:5432/web_petting".to_string()) } async fn add_thumbnail( sources: &mut BTreeMap, db_path: &str, required: bool, ) -> bool { if !uploads::supports_thumbnail(db_path) { return true; } match uploads::ensure_local_thumbnail(db_path).await { Ok(thumbnail_path) => { sources .entry(thumbnail_path) .and_modify(|is_required| *is_required |= required) .or_insert(required); true } Err(error) => { eprintln!("Could not create thumbnail for {db_path}: {error}"); !required } } } async fn run() -> Result<(), Box> { let db = Database::new(database_url()).await?; let storage = uploads::Storage::load_configured_r2(&db).await?; let mut sources: BTreeMap = BTreeMap::new(); let mut preparation_failed = false; for media in Media::objects().all(&db).await? { let required = media.status == "active"; sources .entry(media.file_path.clone()) .and_modify(|is_required| *is_required |= required) .or_insert(required); if media.file_type == "photo" && !add_thumbnail(&mut sources, &media.file_path, required).await { preparation_failed = true; } } for testimonial in Testimonial::objects().all(&db).await? { let Some(image_path) = testimonial.image_path else { continue; }; let required = testimonial.status == "active"; sources .entry(image_path.clone()) .and_modify(|is_required| *is_required |= required) .or_insert(required); // Testimonial thumbnails are retained too, even though the landing page // currently displays the processed full-size image. let _ = add_thumbnail(&mut sources, &image_path, false).await; } let mut uploaded = 0usize; let mut skipped = 0usize; let mut missing = 0usize; let mut failed = 0usize; for (db_path, required) in sources { let local_path = uploads::resolve_db_path(&db_path); match tokio::fs::try_exists(&local_path).await { Ok(true) => {} Ok(false) => { eprintln!("Missing local file {}", local_path.display()); missing += 1; if required { failed += 1; } continue; } Err(error) => { eprintln!( "Could not inspect local file {}: {error}", local_path.display() ); missing += 1; if required { failed += 1; } continue; } }; if storage.exists(&db_path).await? { println!("Already in R2: {db_path}"); skipped += 1; continue; } match storage.upload_local_copy(&db_path, &local_path).await { Ok(()) => { println!("Uploaded: {db_path}"); uploaded += 1; } Err(error) => { eprintln!("Failed to upload {db_path}: {error}"); failed += 1; } } } db.close().await?; println!( "Migration summary: uploaded={uploaded}, already_present={skipped}, missing_local={missing}, failed={failed}" ); println!("Local files were not deleted."); if preparation_failed || failed > 0 { return Err("R2 migration did not complete successfully".into()); } Ok(()) } fn main() { let runtime = tokio::runtime::Builder::new_multi_thread() .enable_all() .build() .expect("failed to build Tokio runtime"); if let Err(error) = runtime.block_on(run()) { eprintln!("Migration failed: {error}"); std::process::exit(1); } }