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({
|
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: 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;
|
params?: key_val;
|
||||||
try_cache?: boolean;
|
try_cache?: boolean;
|
||||||
log_lvl?: number;
|
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({
|
const result = await api.delete_ae_obj({
|
||||||
api_cfg,
|
api_cfg,
|
||||||
obj_type: 'event_file',
|
obj_type: 'event_file',
|
||||||
obj_id: event_file_id,
|
obj_id: event_file_id,
|
||||||
params: { ...params, delete_hosted_file: true, rm_orphan: true },
|
params,
|
||||||
log_lvl
|
log_lvl
|
||||||
});
|
});
|
||||||
if (try_cache) await db_events.file.delete(event_file_id);
|
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,
|
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
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -235,6 +235,9 @@ async function copy_hash(hash: string) {
|
|||||||
// Keyed by hosted_file_id. Value: null = not loaded, [] = loaded/empty, [...] = loaded with links.
|
// 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_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>();
|
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) {
|
async function toggle_links(file: ae_HostedFile) {
|
||||||
const id = file.hosted_file_id;
|
const id = file.hosted_file_id;
|
||||||
@@ -249,7 +252,10 @@ async function toggle_links(file: ae_HostedFile) {
|
|||||||
endpoint: `/v3/action/hosted_file/${id}/links`,
|
endpoint: `/v3/action/hosted_file/${id}/links`,
|
||||||
log_lvl: 0
|
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) {
|
} catch (e) {
|
||||||
console.error('[hosted_file links]', e);
|
console.error('[hosted_file links]', e);
|
||||||
links_map.set(id, []);
|
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 ───────────────────────────────────────────────────────────────────
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||||
function fmt_size(bytes: number | undefined): string {
|
function fmt_size(bytes: number | undefined): string {
|
||||||
if (!bytes) return '—';
|
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">
|
<div class="flex flex-wrap items-center gap-1.5">
|
||||||
<span class="text-[10px] font-bold uppercase tracking-wider opacity-40">Linked to:</span>
|
<span class="text-[10px] font-bold uppercase tracking-wider opacity-40">Linked to:</span>
|
||||||
{#each file_links as lnk}
|
{#each file_links as lnk}
|
||||||
|
{@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
|
<span
|
||||||
class="badge preset-tonal-secondary font-mono text-[10px]"
|
class="badge preset-tonal-secondary font-mono text-[10px]"
|
||||||
title={lnk.link_to_id_random ?? String(lnk.link_to_id)}>
|
title={full_id}>
|
||||||
{lnk.link_to_type}
|
{lnk.link_to_type}
|
||||||
<span class="opacity-60 ml-0.5">
|
<span class="opacity-60 ml-0.5">/ {id_display}</span>
|
||||||
/ {lnk.link_to_id_random ? lnk.link_to_id_random.slice(0, 8) + '…' : lnk.link_to_id}
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
Reference in New Issue
Block a user