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({
api_cfg,
event_file_id,
hosted_file_id,
params = {},
try_cache = true,
log_lvl = 0
}: {
api_cfg: any;
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;
try_cache?: boolean;
log_lvl?: number;
}) {
if (hosted_file_id) {
// Step 1: Fetch links — populates Redis for all linked object IDs.
let file_links: { link_to_type: string; link_to_id_random: string | null }[] = [];
try {
const links_result = await api.get_object({
// Single atomic call: removes hosted_file_link, cleans up physical file +
// hosted_file record if no other links remain (rm_orphan=true is the default),
// then deletes the event_file row. Replaces the old multi-step workaround.
const result = await api.delete_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,
obj_type: 'event_file',
obj_id: event_file_id,
endpoint: `/v3/action/event_file/${event_file_id}`,
params,
log_lvl
});

View File

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

View File

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