fix(files): actually delete physical files when removing event_file or hosted_file
event_file delete was broken since the module was first created (Oct 2024). delete_ae_obj_id__event_file passed delete_hosted_file=true + rm_orphan=true to the generic V3 CRUD endpoint, but delete_obj_template never handled those params — it only did a raw sql_delete on the event_file row. The hosted_file_link record, hosted_file DB record, and physical file on disk were never cleaned up. Fix: call api.delete_hosted_file (the action endpoint) BEFORE deleting the event_file record so the backend can still resolve link_to_id via Redis. Pass link_to_type='event_file', link_to_id=event_file_id, rm_orphan=true, method=delete. hosted_file_id is now an optional param on delete_ae_obj_id__event_file; component passes event_file_obj.hosted_file_id. Also fix hosted_file delete in /core/files/ admin page (same root cause): load links first, then call delete_hosted_file for each link with correct link_to_type/link_to_id_random and method=delete before removing the record. Also: add clickable navigation links in the files admin link sub-row. Direct types (event, journal, archive, post) resolve immediately; nested types (event_session, event_location, event_presenter, journal_entry, etc.) fetch their parent ID via the V3 CRUD endpoint and construct the correct route. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -365,21 +365,39 @@ 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: removes the hosted_file_link,
|
||||
// then deletes the hosted_file record and physical file if no other links remain.
|
||||
// Must be done BEFORE deleting the event_file record so the backend can still
|
||||
// resolve the link_to_id via Redis.
|
||||
hosted_file_id?: string;
|
||||
params?: key_val;
|
||||
try_cache?: boolean;
|
||||
log_lvl?: number;
|
||||
}) {
|
||||
if (hosted_file_id) {
|
||||
await api.delete_hosted_file({
|
||||
api_cfg,
|
||||
hosted_file_id,
|
||||
link_to_type: 'event_file',
|
||||
link_to_id: event_file_id,
|
||||
rm_orphan: true,
|
||||
params: { method: 'delete' },
|
||||
log_lvl
|
||||
});
|
||||
}
|
||||
|
||||
const result = await api.delete_ae_obj({
|
||||
api_cfg,
|
||||
obj_type: 'event_file',
|
||||
obj_id: event_file_id,
|
||||
params: { ...params, delete_hosted_file: true, rm_orphan: true },
|
||||
params,
|
||||
log_lvl
|
||||
});
|
||||
if (try_cache) await db_events.file.delete(event_file_id);
|
||||
|
||||
@@ -570,6 +570,8 @@ 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
|
||||
}
|
||||
);
|
||||
|
||||
@@ -235,6 +235,9 @@ async function copy_hash(hash: string) {
|
||||
// Keyed by hosted_file_id. Value: null = not loaded, [] = loaded/empty, [...] = loaded with links.
|
||||
let links_map = new SvelteMap<string, { link_to_type: string; link_to_id: number; link_to_id_random: string | null }[] | null>();
|
||||
let links_loading = new SvelteMap<string, boolean>();
|
||||
// Resolved navigation URLs per link — keyed as `${link_to_type}:${link_to_id_random}`.
|
||||
// Null means no routable page exists for that type.
|
||||
let link_url_map = new SvelteMap<string, string | null>();
|
||||
|
||||
async function toggle_links(file: ae_HostedFile) {
|
||||
const id = file.hosted_file_id;
|
||||
@@ -249,7 +252,10 @@ async function toggle_links(file: ae_HostedFile) {
|
||||
endpoint: `/v3/action/hosted_file/${id}/links`,
|
||||
log_lvl: 0
|
||||
});
|
||||
links_map.set(id, result?.data ?? result ?? []);
|
||||
const file_links = result?.data ?? result ?? [];
|
||||
links_map.set(id, file_links);
|
||||
// Resolve navigation URLs for each link in parallel.
|
||||
await Promise.all(file_links.map((lnk: any) => resolve_link_url(lnk.link_to_type, lnk.link_to_id_random)));
|
||||
} catch (e) {
|
||||
console.error('[hosted_file links]', e);
|
||||
links_map.set(id, []);
|
||||
@@ -258,6 +264,58 @@ async function toggle_links(file: ae_HostedFile) {
|
||||
}
|
||||
}
|
||||
|
||||
// Resolves a link record to a navigable app URL. Caches results to avoid repeat calls.
|
||||
// For nested types (session, presenter, location, journal_entry, etc.) we fetch the
|
||||
// object to get its parent ID, then construct the correct route.
|
||||
async function resolve_link_url(link_to_type: string, id_random: string | null): Promise<void> {
|
||||
if (!id_random) return;
|
||||
const cache_key = `${link_to_type}:${id_random}`;
|
||||
if (link_url_map.has(cache_key)) return;
|
||||
|
||||
// Direct types — no parent lookup needed.
|
||||
const direct: Record<string, string> = {
|
||||
event: `/events/${id_random}`,
|
||||
journal: `/journals/${id_random}`,
|
||||
archive: `/idaa/archives/${id_random}`,
|
||||
post: `/idaa/bb/${id_random}`
|
||||
};
|
||||
if (direct[link_to_type]) {
|
||||
link_url_map.set(cache_key, direct[link_to_type]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Nested types — look up parent ID from the object, then build URL.
|
||||
try {
|
||||
const result = await api.get_object({
|
||||
api_cfg: $ae_api,
|
||||
endpoint: `/v3/crud/${link_to_type}/${id_random}`,
|
||||
log_lvl: 0
|
||||
});
|
||||
const obj = result?.data ?? result;
|
||||
let url: string | null = null;
|
||||
|
||||
if (link_to_type === 'event_session' && obj?.event_id) {
|
||||
url = `/events/${obj.event_id}/session/${id_random}`;
|
||||
} else if (link_to_type === 'event_location' && obj?.event_id) {
|
||||
url = `/events/${obj.event_id}/location/${id_random}`;
|
||||
} else if (link_to_type === 'event_presenter' && obj?.event_id) {
|
||||
url = `/events/${obj.event_id}/presenter/${id_random}`;
|
||||
} else if (link_to_type === 'journal_entry' && obj?.journal_id) {
|
||||
url = `/journals/${obj.journal_id}/entry/${id_random}`;
|
||||
} else if (link_to_type === 'archive_content' && obj?.archive_id) {
|
||||
// No dedicated content detail page — link to the parent archive.
|
||||
url = `/idaa/archives/${obj.archive_id}`;
|
||||
} else if (link_to_type === 'post_comment' && obj?.post_id) {
|
||||
// No dedicated comment detail page — link to the parent post.
|
||||
url = `/idaa/bb/${obj.post_id}`;
|
||||
}
|
||||
|
||||
link_url_map.set(cache_key, url);
|
||||
} catch {
|
||||
link_url_map.set(cache_key, null);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
function fmt_size(bytes: number | undefined): string {
|
||||
if (!bytes) return '—';
|
||||
@@ -587,14 +645,29 @@ let total_size = $derived(results.reduce((sum, f) => sum + (f.size ?? 0), 0));
|
||||
<div class="flex flex-wrap items-center gap-1.5">
|
||||
<span class="text-[10px] font-bold uppercase tracking-wider opacity-40">Linked to:</span>
|
||||
{#each file_links as lnk}
|
||||
<span
|
||||
class="badge preset-tonal-secondary font-mono text-[10px]"
|
||||
title={lnk.link_to_id_random ?? String(lnk.link_to_id)}>
|
||||
{lnk.link_to_type}
|
||||
<span class="opacity-60 ml-0.5">
|
||||
/ {lnk.link_to_id_random ? lnk.link_to_id_random.slice(0, 8) + '…' : lnk.link_to_id}
|
||||
{@const link_key = `${lnk.link_to_type}:${lnk.link_to_id_random}`}
|
||||
{@const link_url = link_url_map.get(link_key)}
|
||||
{@const id_display = lnk.link_to_id_random ? lnk.link_to_id_random.slice(0, 8) + '…' : String(lnk.link_to_id)}
|
||||
{@const full_id = lnk.link_to_id_random ?? String(lnk.link_to_id)}
|
||||
{#if link_url}
|
||||
<a
|
||||
href={link_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="badge preset-tonal-tertiary font-mono text-[10px] hover:preset-filled-tertiary transition-all"
|
||||
title={`Open ${lnk.link_to_type} ${full_id}`}>
|
||||
{lnk.link_to_type}
|
||||
<span class="opacity-60 ml-0.5">/ {id_display}</span>
|
||||
<span class="ml-0.5 opacity-40">↗</span>
|
||||
</a>
|
||||
{:else}
|
||||
<span
|
||||
class="badge preset-tonal-secondary font-mono text-[10px]"
|
||||
title={full_id}>
|
||||
{lnk.link_to_type}
|
||||
<span class="opacity-60 ml-0.5">/ {id_display}</span>
|
||||
</span>
|
||||
</span>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user