| 224 |
224 |
|
let mut skipped: Vec<String> = Vec::new();
|
| 225 |
225 |
|
|
| 226 |
226 |
|
for (s3_key, zip_path_entry, db_size) in &files {
|
| 227 |
|
- |
// Per-file size pre-check BEFORE downloading so a single 20 GB video
|
| 228 |
|
- |
// can't blow the heap before the post-download total check fires.
|
| 229 |
|
- |
// The size comes from the DB column written at upload-confirm — no
|
| 230 |
|
- |
// S3 HEAD round-trip. The post-download total guard below is the
|
| 231 |
|
- |
// backstop for any row with a missing (None) size.
|
| 232 |
|
- |
if let Some(size) = db_size {
|
| 233 |
|
- |
let size = (*size).max(0) as u64;
|
| 234 |
|
- |
if size > MAX_FILE_SIZE {
|
| 235 |
|
- |
skipped.push(format!(
|
| 236 |
|
- |
"{} (exceeds 500 MB per-file export cap)",
|
| 237 |
|
- |
zip_path_entry
|
| 238 |
|
- |
));
|
| 239 |
|
- |
continue;
|
| 240 |
|
- |
}
|
| 241 |
|
- |
if total_size + size > MAX_TOTAL_SIZE {
|
| 242 |
|
- |
return Err("Content export exceeds the 2 GB limit. Try exporting a single project instead.".to_string());
|
| 243 |
|
- |
}
|
|
227 |
+ |
// Resolve the file size BEFORE downloading so a single huge object
|
|
228 |
+ |
// can't blow the heap before any guard fires. Prefer the DB column
|
|
229 |
+ |
// written at upload-confirm; fall back to a cheap S3 HEAD for legacy
|
|
230 |
+ |
// rows that predate it (a `None` size). A row whose size can't be
|
|
231 |
+ |
// resolved (HEAD error / object gone) is skipped rather than blindly
|
|
232 |
+ |
// buffered — without this, a legacy `None`-size object of arbitrary
|
|
233 |
+ |
// size landed fully in RAM before the post-download total check could
|
|
234 |
+ |
// fire (PERF-1).
|
|
235 |
+ |
let size = match db_size {
|
|
236 |
+ |
Some(s) => (*s).max(0) as u64,
|
|
237 |
+ |
None => match s3_clone.object_size(s3_key).await {
|
|
238 |
+ |
Ok(Some(s)) => s.max(0) as u64,
|
|
239 |
+ |
Ok(None) => {
|
|
240 |
+ |
tracing::warn!(s3_key = %s3_key, "export: object has no size (missing?) — skipping");
|
|
241 |
+ |
skipped.push(zip_path_entry.clone());
|
|
242 |
+ |
continue;
|
|
243 |
+ |
}
|
|
244 |
+ |
Err(e) => {
|
|
245 |
+ |
tracing::warn!(s3_key = %s3_key, error = %e, "export: HEAD failed — skipping");
|
|
246 |
+ |
skipped.push(zip_path_entry.clone());
|
|
247 |
+ |
continue;
|
|
248 |
+ |
}
|
|
249 |
+ |
},
|
|
250 |
+ |
};
|
|
251 |
+ |
if size > MAX_FILE_SIZE {
|
|
252 |
+ |
skipped.push(format!(
|
|
253 |
+ |
"{} (exceeds 500 MB per-file export cap)",
|
|
254 |
+ |
zip_path_entry
|
|
255 |
+ |
));
|
|
256 |
+ |
continue;
|
|
257 |
+ |
}
|
|
258 |
+ |
if total_size + size > MAX_TOTAL_SIZE {
|
|
259 |
+ |
return Err("Content export exceeds the 2 GB limit. Try exporting a single project instead.".to_string());
|
| 244 |
260 |
|
}
|
| 245 |
261 |
|
|
| 246 |
262 |
|
match s3_clone.download_object(s3_key).await {
|