fix(files): use new backend action endpoints for event_file delete + orphan scan

DELETE /v3/action/event_file/{id} now handles full atomic cleanup (link
removal, physical file, hosted_file record) in one call — replaces the
multi-step Redis pre-warm workaround. orphan_scan endpoint replaces the
N+1 per-file /links fetch on the admin files page.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-06-18 18:18:34 -04:00
parent f93bca1bd4
commit 5689bfebbc
3 changed files with 27 additions and 78 deletions

View File

@@ -365,70 +365,22 @@ export async function create_event_file_obj_from_hosted_file_async({
export async function delete_ae_obj_id__event_file({ export async function delete_ae_obj_id__event_file({
api_cfg, api_cfg,
event_file_id, event_file_id,
hosted_file_id,
params = {}, params = {},
try_cache = true, try_cache = true,
log_lvl = 0 log_lvl = 0
}: { }: {
api_cfg: any; api_cfg: any;
event_file_id: string; event_file_id: string;
// Providing hosted_file_id enables full cleanup of the physical file and
// hosted_file record. The /links fetch is done first because it calls
// get_id_random() on each linked object, which populates Redis. Without
// this pre-fetch, redis_lookup_id_random('event_file', id) raises 404
// in the delete handler — the cleanup is silently skipped.
hosted_file_id?: string;
params?: key_val; params?: key_val;
try_cache?: boolean; try_cache?: boolean;
log_lvl?: number; log_lvl?: number;
}) { }) {
if (hosted_file_id) { // Single atomic call: removes hosted_file_link, cleans up physical file +
// Step 1: Fetch links — populates Redis for all linked object IDs. // hosted_file record if no other links remain (rm_orphan=true is the default),
let file_links: { link_to_type: string; link_to_id_random: string | null }[] = []; // then deletes the event_file row. Replaces the old multi-step workaround.
try { const result = await api.delete_object({
const links_result = await api.get_object({
api_cfg,
endpoint: `/v3/action/hosted_file/${hosted_file_id}/links`,
log_lvl: 0
});
file_links = links_result?.data ?? links_result ?? [];
} catch (e) {
if (log_lvl) console.warn('[delete_event_file] /links fetch failed:', e);
}
// Step 2: Remove each link. rm_orphan=true on the last one triggers
// physical file + hosted_file record cleanup if nothing else uses it.
if (file_links.length > 0) {
for (let i = 0; i < file_links.length; i++) {
const lnk = file_links[i];
const is_last = i === file_links.length - 1;
await api.delete_hosted_file({
api_cfg,
hosted_file_id,
link_to_type: lnk.link_to_type,
link_to_id: lnk.link_to_id_random ?? undefined,
rm_orphan: is_last,
params: { method: 'delete' },
log_lvl
});
}
} else {
// No links recorded — attempt direct orphan cleanup.
await api.delete_hosted_file({
api_cfg,
hosted_file_id,
rm_orphan: true,
params: { method: 'delete' },
log_lvl
});
}
}
// Step 3: Delete the event_file record after the hosted_file cleanup.
const result = await api.delete_ae_obj({
api_cfg, api_cfg,
obj_type: 'event_file', endpoint: `/v3/action/event_file/${event_file_id}`,
obj_id: event_file_id,
params, params,
log_lvl log_lvl
}); });

View File

@@ -570,8 +570,6 @@ async function handle_convert_pdf_to_image(event_file_obj: key_val) {
api_cfg: $ae_api, api_cfg: $ae_api,
event_file_id: event_file_id:
event_file_obj.event_file_id, event_file_obj.event_file_id,
hosted_file_id:
event_file_obj.hosted_file_id,
log_lvl: 1 log_lvl: 1
} }
); );

View File

@@ -359,30 +359,29 @@ let displayed_results = $derived(
async function check_all_for_orphans() { async function check_all_for_orphans() {
orphan_checking = true; orphan_checking = true;
orphan_filter = false; orphan_filter = false;
// Fetch links for all visible results in parallel; skip already-cached entries. try {
await Promise.all( // Single backend query instead of N+1 per-file link fetches.
results.map(async (file) => { const result = await api.get_object({
const id = file.hosted_file_id; api_cfg: $ae_api,
if (links_map.has(id)) return; endpoint: `/v3/action/hosted_file/orphan_scan?limit=500`,
links_loading.set(id, true); log_lvl: 0
try { });
const result = await api.get_object({ const db_orphans: { hosted_file_id: string }[] = result?.db_orphans ?? result?.data?.db_orphans ?? [];
api_cfg: $ae_api, const orphan_id_set = new Set(db_orphans.map((o) => o.hosted_file_id));
endpoint: `/v3/action/hosted_file/${id}/links`, // Only mark confirmed orphans — empty array means no links found.
log_lvl: 0 // Non-orphans are left out of links_map entirely so the link toggle
}); // still works normally and orphan_ids only captures confirmed orphans.
links_map.set(id, result?.data ?? result ?? []); for (const file of results) {
// Also resolve nav URLs for any links found if (orphan_id_set.has(file.hosted_file_id)) {
await Promise.all((result?.data ?? result ?? []).map((lnk: any) => resolve_link_url(lnk.link_to_type, lnk.link_to_id_random))); links_map.set(file.hosted_file_id, []);
} catch {
links_map.set(id, []);
} finally {
links_loading.delete(id);
} }
}) }
); } catch (e) {
orphan_filter = true; console.error('[orphan_scan]', e);
orphan_checking = false; } finally {
orphan_filter = true;
orphan_checking = false;
}
} }
</script> </script>