Skip to content

Commit 1454465

Browse files
committed
feat: add hourly background cleanup of expired uploads (>24h)
1 parent 3038784 commit 1454465

2 files changed

Lines changed: 83 additions & 0 deletions

File tree

agents/src/main.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,18 @@ async fn main() {
3030
config,
3131
});
3232

33+
tokio::spawn({
34+
let state = Arc::clone(&state);
35+
async move {
36+
let mut interval = tokio::time::interval(std::time::Duration::from_secs(60 * 60));
37+
interval.tick().await;
38+
loop {
39+
interval.tick().await;
40+
state.storage.cleanup_expired().await;
41+
}
42+
}
43+
});
44+
3345
let app = Router::new()
3446
.merge(routes::router())
3547
.layer(CorsLayer::permissive())

agents/src/storage.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use crate::config::Config;
88
use crate::error::AppError;
99

1010
const PRESIGN_EXPIRY: Duration = Duration::from_secs(24 * 60 * 60);
11+
const CLEANUP_MAX_AGE: Duration = Duration::from_secs(24 * 60 * 60);
1112

1213
pub struct Storage {
1314
client: Client,
@@ -81,6 +82,76 @@ impl Storage {
8182

8283
Ok(UploadResult { presigned_url })
8384
}
85+
86+
pub async fn cleanup_expired(&self) {
87+
let cutoff = aws_sdk_s3::primitives::DateTime::from_secs_f64(
88+
std::time::SystemTime::now()
89+
.duration_since(std::time::UNIX_EPOCH)
90+
.unwrap_or_default()
91+
.as_secs_f64()
92+
- CLEANUP_MAX_AGE.as_secs_f64(),
93+
);
94+
95+
let mut continuation_token: Option<String> = None;
96+
97+
loop {
98+
let mut request = self
99+
.client
100+
.list_objects_v2()
101+
.bucket(&self.bucket)
102+
.prefix("uploads/");
103+
104+
if let Some(token) = &continuation_token {
105+
request = request.continuation_token(token);
106+
}
107+
108+
let response = match request.send().await {
109+
Ok(response) => response,
110+
Err(e) => {
111+
tracing::error!("cleanup: failed to list objects: {e}");
112+
return;
113+
}
114+
};
115+
116+
let mut deleted = 0;
117+
for object in response.contents() {
118+
let is_expired = object
119+
.last_modified()
120+
.map(|modified| *modified < cutoff)
121+
.unwrap_or(false);
122+
123+
if !is_expired {
124+
continue;
125+
}
126+
127+
let Some(key) = object.key() else {
128+
continue;
129+
};
130+
131+
if let Err(e) = self
132+
.client
133+
.delete_object()
134+
.bucket(&self.bucket)
135+
.key(key)
136+
.send()
137+
.await
138+
{
139+
tracing::error!("cleanup: failed to delete {key}: {e}");
140+
} else {
141+
deleted += 1;
142+
}
143+
}
144+
145+
if deleted > 0 {
146+
tracing::info!("cleanup: deleted {deleted} expired files");
147+
}
148+
149+
match response.next_continuation_token() {
150+
Some(token) => continuation_token = Some(token.to_string()),
151+
None => break,
152+
}
153+
}
154+
}
84155
}
85156

86157
fn build_key(original_filename: Option<&str>, hash: &str) -> String {

0 commit comments

Comments
 (0)