fix(external): path-safe unlink + per-link staging + orphan repair (#859)
External downloads import IN PLACE, so the post-attach dedup-skip unlink could delete a file that IS an ImageRecord's backing file — orphaning the record and 404-ing on playback. Two sources of that: - Two links on the same post (same film from mega + gdrive) emitted the same filename into one external/<post_id>/ dir; the second overwrote the first. Stage per-LINK now (external/<post_id>/<link_id>/) so each file keeps its path. - The duplicate_hash/duplicate_phash branch unlinked `f` unconditionally. Make it path-safe: only unlink when `f` is NOT the existing record's canonical file. Plus an operator-triggered orphan-repair maintenance task (prune_missing_file_records_task) to clean up records already orphaned by the bug: scans ImageRecords, deletes those whose file is gone (cascade), with an NFS-stall guard that aborts without deleting if a large sample is mostly missing. Wired through POST /api/admin/maintenance/prune-missing-files and a MissingFileRepairCard in the Maintenance panel. Tests: refetch-same-link keeps the canonical file; orphan repair deletes only real orphans and aborts on the mostly-missing guard. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
<AliasTable class="mt-4" />
|
||||
<DbMaintenanceCard class="mt-6" />
|
||||
<ArchiveReextractCard class="mt-6" />
|
||||
<MissingFileRepairCard class="mt-6" />
|
||||
<BackupCard class="mt-6" />
|
||||
<!-- TagMaintenanceCard moved to Cleanup tab (v26.05.25.7) — it
|
||||
operates on the existing library which fits the Cleanup-tab
|
||||
@@ -35,6 +36,7 @@ import AllowlistTable from './AllowlistTable.vue'
|
||||
import AliasTable from './AliasTable.vue'
|
||||
import DbMaintenanceCard from './DbMaintenanceCard.vue'
|
||||
import ArchiveReextractCard from './ArchiveReextractCard.vue'
|
||||
import MissingFileRepairCard from './MissingFileRepairCard.vue'
|
||||
import BackupCard from './BackupCard.vue'
|
||||
import { useSystemActivityStore } from '../../stores/systemActivity.js'
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<!-- #859: delete ImageRecords whose backing file is gone from disk (orphans
|
||||
left by the external-attach unlink bug) so they stop 404-ing on playback. -->
|
||||
<v-card>
|
||||
<v-card-title>Repair missing-file records</v-card-title>
|
||||
<v-card-text>
|
||||
<p class="text-body-2 mb-3">
|
||||
Scans every image/video record and removes any whose underlying file is
|
||||
gone from disk — orphaned records that otherwise 404 when you open the
|
||||
post. Safe: it <strong>aborts without deleting</strong> if a large share of
|
||||
files look missing (which means a storage/NFS hiccup, not real orphans).
|
||||
Run it only when storage is healthy.
|
||||
</p>
|
||||
<v-btn color="primary" rounded="pill" :loading="busy" @click="run">
|
||||
<v-icon start>mdi-file-remove-outline</v-icon> Repair missing-file records
|
||||
</v-btn>
|
||||
<span v-if="queued" class="ml-3 text-caption text-success">Queued ✓</span>
|
||||
<QueueStatusBar queue="maintenance" queue-label="Maintenance" />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { useApi } from '../../composables/useApi.js'
|
||||
import { toast } from '../../utils/toast.js'
|
||||
import QueueStatusBar from './QueueStatusBar.vue'
|
||||
|
||||
const api = useApi()
|
||||
const busy = ref(false)
|
||||
const queued = ref(false)
|
||||
|
||||
async function run () {
|
||||
busy.value = true
|
||||
queued.value = false
|
||||
try {
|
||||
await api.post('/api/admin/maintenance/prune-missing-files')
|
||||
queued.value = true
|
||||
toast({ text: 'Missing-file repair queued', type: 'success' })
|
||||
} catch (e) {
|
||||
toast({ text: e?.body?.detail || e?.message || 'Failed to queue', type: 'error' })
|
||||
} finally {
|
||||
busy.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user