Added Cloudflare R2 media storage support
Build and Publish / Build and Publish Docker Image (push) Successful in 1m26s
Build and Publish / Build and Publish Docker Image (push) Successful in 1m26s
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
#[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<String, bool>,
|
||||
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<dyn std::error::Error>> {
|
||||
let db = Database::new(database_url()).await?;
|
||||
let storage = uploads::Storage::load_configured_r2(&db).await?;
|
||||
let mut sources: BTreeMap<String, bool> = 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user