fix: let a media file belong to more than one entry

media.entry_id modelled ownership as one entry per file, but link_to_entry
reassigns it on every save. Embedding the same image in a second entry
therefore moved the claim, and deleting that second entry deleted the file
out from under the first — a broken image in the entry and a 404 on its
share link.

A join table replaces the column. Deleting an entry now removes only the
files no other entry embeds, and link_to_entry runs in a transaction so an
entry is never briefly attached to nothing while its share link is being
read.
This commit is contained in:
nvrl committed 2026-09-05 09:06:22 +02:00
1 parent 8da0a30497
commit 1e0f2f8792
6 files changed
+94 -31

No files matched your search

+9 -4
View File
@@ -5,8 +5,13 @@
import { diary } from './store.svelte'
import { formatBytes, formatStamp } from './util'
async function remove(id: string, filename: string) {
if (!confirm(`delete ${filename}? entries that embed it will break.`)) return
const usage = (entryIds: number[]) =>
entryIds.length === 0 ? 'unattached' : entryIds.map((id) => `entry:${id}`).join(' ')
async function remove(id: string, filename: string, entryIds: number[]) {
const used = entryIds.length === 1 ? '1 entry' : `${entryIds.length} entries`
const warning = entryIds.length > 0 ? ` it is embedded in ${used}, which will break.` : ''
if (!confirm(`delete ${filename}?${warning}`)) return
await diary.guard(async () => {
await api.deleteMedia(id)
await diary.loadMedia()
@@ -36,7 +41,7 @@
<span class="name">{file.filename}</span>
<span class="faint">
{formatBytes(file.size)} · {formatStamp(file.created_at)} ·
{file.entry_id ? `entry:${file.entry_id}` : 'unattached'}
{usage(file.entry_ids)}
</span>
</div>
@@ -50,7 +55,7 @@
>
{/if}
<button onclick={() => diary.copy(location.origin + file.url)}>copy url</button>
<button onclick={() => remove(file.id, file.filename)}>rm</button>
<button onclick={() => remove(file.id, file.filename, file.entry_ids)}>rm</button>
</div>
</li>
{:else}
+2 -1
View File
@@ -33,7 +33,8 @@ export interface MediaFile {
size: number
created_at: number
url: string
entry_id: number | null
/** Every entry that embeds this file — a file may be used by several. */
entry_ids: number[]
}
export class ApiError extends Error {