-
Notifications
You must be signed in to change notification settings - Fork 27
fix: add orphan image cleanup function #87
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| # Cleanup Orphan Images | ||
|
|
||
| Deletes images from scan-images bucket if: | ||
|
|
||
| - older than 24 hours | ||
| - no matching record exists in scans table |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,46 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { createClient } from "@supabase/supabase-js"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const supabase = createClient( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Deno.env.get("SUPABASE_URL")!, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")! | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Deno.serve(async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const bucket = "scan-images"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { data: files, error } = await supabase.storage | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .from(bucket) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .list("", { limit: 1000 }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return new Response(error.message, { status: 500 }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+11
to
+17
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Implement pagination to handle more than 1000 files. The storage listing is capped at 1000 files with no pagination. If the ♻️ Proposed fix to add pagination const bucket = "scan-images";
+ let offset = 0;
+ const limit = 1000;
+ let hasMore = true;
+ while (hasMore) {
- const { data: files, error } = await supabase.storage
- .from(bucket)
- .list("", { limit: 1000 });
+ const { data: files, error } = await supabase.storage
+ .from(bucket)
+ .list("", { limit, offset });
- if (error) {
- return new Response(error.message, { status: 500 });
- }
+ if (error) {
+ return new Response(error.message, { status: 500 });
+ }
+ if (!files || files.length < limit) {
+ hasMore = false;
+ }
const now = Date.now();
for (const file of files ?? []) {
// ... existing processing logic
}
+ offset += limit;
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const now = Date.now(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const file of files ?? []) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!file.created_at) continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const ageHours = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (now - new Date(file.created_at).getTime()) / | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (1000 * 60 * 60); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (ageHours < 24) continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { data: scan } = await supabase | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .from("scans") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .select("id") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .contains("photo_urls", [file.name]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .maybeSingle(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+30
to
+34
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Critical: File path/URL mismatch will delete all old images, including those with valid scan records. The orphan detection logic compares This mismatch means:
Verify the mismatch by checking what values are actually stored in the database: #!/bin/bash
# Description: Confirm that photo_urls contains full public URLs, not just file names
# Show the upload function that generates photo_urls values
echo "=== Upload function (shows get_public_url is stored) ==="
rg -A 3 "get_public_url" backend/main.py
echo ""
echo "=== Scan insert (shows photo_urls receives the URL) ==="
rg -B 2 -A 2 'photo_urls.*photo_url' backend/main.py🔧 Proposed fix to construct the public URL for matching+ // Construct the public URL to match what's stored in photo_urls
+ const publicUrl = supabase.storage
+ .from(bucket)
+ .getPublicUrl(file.name).data.publicUrl;
+
const { data: scan } = await supabase
.from("scans")
.select("id")
- .contains("photo_urls", [file.name])
+ .contains("photo_urls", [publicUrl])
.maybeSingle();🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!scan) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await supabase.storage | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .from(bucket) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .remove([file.name]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+37
to
+39
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add error handling for storage deletion failures. The 🛡️ Proposed fix to handle deletion errors- await supabase.storage
+ const { error: removeError } = await supabase.storage
.from(bucket)
.remove([file.name]);
+ if (removeError) {
+ console.error(`Failed to delete ${file.name}: ${removeError.message}`);
+ continue;
+ }
+
console.log(`Deleted orphan image: ${file.name}`);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(`Deleted orphan image: ${file.name}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+30
to
+42
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. N+1 query problem: Optimize database access pattern. The function executes one database query per file in a loop (up to 1000 queries). This creates significant database load and extends execution time. Batch the operation by fetching all ♻️ Proposed optimization to eliminate N+1 queries const now = Date.now();
+
+ // Collect all files older than 24 hours
+ const oldFiles = [];
for (const file of files ?? []) {
if (!file.created_at) continue;
const ageHours =
(now - new Date(file.created_at).getTime()) /
(1000 * 60 * 60);
- if (ageHours < 24) continue;
+ if (ageHours >= 24) {
+ oldFiles.push(file.name);
+ }
+ }
+
+ if (oldFiles.length === 0) {
+ return new Response("Cleanup completed");
+ }
+
+ // Fetch all photo_urls in one query
+ const { data: scans } = await supabase
+ .from("scans")
+ .select("photo_urls");
+
+ // Build a Set of all referenced URLs for O(1) lookup
+ const referencedUrls = new Set<string>();
+ for (const scan of scans ?? []) {
+ for (const url of scan.photo_urls ?? []) {
+ referencedUrls.add(url);
+ }
+ }
- const { data: scan } = await supabase
- .from("scans")
- .select("id")
- .contains("photo_urls", [file.name])
- .maybeSingle();
+ // Delete orphans
+ for (const fileName of oldFiles) {
+ const publicUrl = supabase.storage
+ .from(bucket)
+ .getPublicUrl(fileName).data.publicUrl;
- if (!scan) {
+ if (!referencedUrls.has(publicUrl)) {
await supabase.storage
.from(bucket)
- .remove([file.name]);
+ .remove([fileName]);
- console.log(`Deleted orphan image: ${file.name}`);
+ console.log(`Deleted orphan image: ${fileName}`);
}
}🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return new Response("Cleanup completed"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add explicit error handling for missing environment variables.
The
!non-null assertion will throw a runtime error ifSUPABASE_URLorSUPABASE_SERVICE_ROLE_KEYare not set. While Supabase Edge Functions typically provide these variables, explicit validation would make the function more robust and provide clearer error messages.🛡️ Proposed fix to add validation
📝 Committable suggestion
🤖 Prompt for AI Agents