pub fn purge_unattached(
ctx: &Ctx,
prefix: &str,
table: &str,
column: &str,
max_age: i64,
cursor: Option<&str>,
) -> impl Future<Output = Result<Purged>> + Send + use<>Expand description
Deletes the objects under prefix older than max_age seconds that no table.column row references, one listing page per call.
Direct uploads that were never attached (a closed tab, a failed form)
would stay in R2 forever: Rails’ ActiveStorage::Blob.unattached
purge. Each call lists one page of up to 1,000 keys from cursor
(None to start), keeps those uploaded more than max_age seconds
ago, looks them up with SELECT <column> FROM <table> WHERE <column> IN (...) (100 keys per query) and deletes the others in one call. Pass
the returned Purged::cursor to the next call until it is None.
table and column are identifiers from your code (letters, digits,
_); give the column an index (CREATE UNIQUE INDEX ... ON lessons(video_key)) so each lookup reads only matching rows. Keep
max_age well above 10 minutes (the lifetime of an upload URL) plus
the time a form stays open: a day is safe.
Free plan: one R2 class A operation per call (the listing), one D1 query per 100 old keys, deletes free; a few ms of CPU per 1,000 keys. Run it from a scheduled task, a few pages per run.
§Errors
Error::Internal (500) when table or
column is not a plain identifier, the STORAGE or DB binding is
missing, or R2 or D1 fails (nothing is deleted for the page then).
§Examples
use ocre::{Ctx, Result, storage};
// src/schedules/purge_uploads.rs, run every day: at most 5 pages (5 class A operations).
pub async fn run(ctx: &Ctx) -> Result<()> {
let mut cursor = None;
for _ in 0..5 {
let purged = storage::purge_unattached(ctx, "uploads/videos", "lessons", "video_key", 86_400, cursor.as_deref()).await?;
println!("purge_uploads: {} unattached uploads deleted", purged.deleted.len());
cursor = purged.cursor;
if cursor.is_none() {
break;
}
}
Ok(())
}