CI / lint (push) Successful in 3s
Build images / sign-extension (push) Successful in 4s
CI / extension-version (push) Successful in 2s
Build images / build-agent (push) Successful in 6s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 33s
Build images / build-web (push) Successful in 1m9s
Build images / smoke-web (push) Skipped
Build images / build-ml (push) Successful in 2m3s
Build images / promote (push) Skipped
CI / integration (push) Successful in 2m35s
The operator chose a clean re-download over relinking in place. The ~1,600 broken files can't be relinked reliably: their message ids are gone, and their sidecars collided.
Settings → Maintenance → "Repair Discord downloads" previews, then applies:
- Deletes every image whose path is `…/discord/None/<8 digits>_None_…`. Both the folder and the name are required, because that pair is only what the bug produced. It reuses cleanup_service.delete_images for the record and file deletes.
- Sweeps the leftover collided sidecars from those folders and removes the emptied folders.
- Only then clears gallery-dl's archive rows `discord%`, excluding `discordasset_%`. Upstream keys message attachments as `discord{message_id}_{num}`. Since the broken files lost their message ids, per-source forgetting is impossible. Every pre-fix Discord download is broken, and files fetched after the fix still exist on disk, so gallery-dl's `skip` won't re-fetch them.
- Arms a fresh backfill on every Discord source.
The apply defaults to preview at both the route and the task, runs on maintenance_long, and is never on a beat. The card uses the confirm-dialog pattern of AttachmentReclaimCard.
Supporting refactors, with no behaviour change:
- gallery_dl.archive_path() is the single definition of the archive location.
- source_service.arm_backfill() is the mutation start_backfill already did, now shared with the sync repair.
Tests (tests/test_discord_repair.py):
- The archive clear leaves other platforms and Discord assets alone, and counting mutates nothing.
- Case-twin artist folders are both found.
- The folder sweep works.
- An integration run shows only the broken image goes. A correctly named Discord file and a `None` folder under Patreon survive, and only Discord sources are re-armed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SHQB1YukL3VyvMK8rcbmV9
119 lines
4.7 KiB
Vue
119 lines
4.7 KiB
Vue
<template>
|
|
<!-- #3999: Discord downloads made before the naming fix landed in a `None`
|
|
folder with no post and a download-time date. This re-downloads them
|
|
cleanly. Preview first; the apply deletes files. -->
|
|
<MaintenanceTile
|
|
icon="mdi-download-off-outline"
|
|
title="Repair Discord downloads"
|
|
blurb="Re-download Discord images that were saved with no post and the wrong date."
|
|
destructive
|
|
:open="applying || previewing"
|
|
>
|
|
<p class="text-body-2 mb-3">
|
|
Before the naming fix, every Discord download was saved into a folder
|
|
called <code>None</code>. Those images never got a post, so they show the
|
|
time Curator downloaded them rather than when they were posted.
|
|
<strong>Apply</strong> deletes those images, clears Discord from the
|
|
download history, and starts a fresh backfill of every Discord source so
|
|
they come back with their posts and dates. Tags you added by hand to those
|
|
images are lost. Nothing outside a Discord <code>None</code> folder is
|
|
touched.
|
|
</p>
|
|
|
|
<div class="d-flex align-center flex-wrap" style="gap: 12px;">
|
|
<v-btn
|
|
color="primary" variant="tonal" rounded="pill"
|
|
:loading="previewing" :disabled="applying" @click="preview"
|
|
>
|
|
<v-icon start>mdi-magnify</v-icon> Preview
|
|
</v-btn>
|
|
<v-btn
|
|
color="error" rounded="pill"
|
|
:loading="applying"
|
|
:disabled="previewing || !canApply"
|
|
@click="confirmOpen = true"
|
|
>
|
|
<v-icon start>mdi-download-off-outline</v-icon> Apply
|
|
</v-btn>
|
|
</div>
|
|
|
|
<v-alert
|
|
v-if="summary" :type="summaryType" variant="tonal" class="mt-4"
|
|
density="comfortable"
|
|
>
|
|
<span v-if="applied">
|
|
Deleted {{ summary.images_deleted }} image(s) and cleared
|
|
{{ summary.archive_entries }} Discord download record(s). Backfills
|
|
started on {{ summary.backfills_started }} Discord source(s).
|
|
</span>
|
|
<span v-else-if="hasWork">
|
|
{{ summary.images }} broken image(s) ({{ humanBytes(summary.bytes) }})
|
|
across {{ summary.directories }} folder(s), and
|
|
{{ summary.archive_entries }} Discord download record(s) to clear.
|
|
{{ summary.sources }} Discord source(s) will backfill again.
|
|
</span>
|
|
<span v-else>Nothing to repair — no broken Discord downloads found.</span>
|
|
|
|
<div v-if="applied && summary.files_failed" class="mt-1 text-caption">
|
|
{{ summary.files_failed }} file(s) could not be removed — see the worker log.
|
|
</div>
|
|
<div v-if="applied && summary.remaining" class="mt-1 text-caption">
|
|
{{ summary.remaining }} broken image(s) are still present. Run it again.
|
|
</div>
|
|
</v-alert>
|
|
|
|
<QueueStatusBar queue="maintenance_long" queue-label="Maintenance" />
|
|
|
|
<v-dialog v-model="confirmOpen" max-width="440">
|
|
<v-card>
|
|
<v-card-title>Repair Discord downloads?</v-card-title>
|
|
<v-card-text class="text-body-2">
|
|
This permanently deletes <strong>{{ summary?.images ?? 0 }}</strong>
|
|
Discord image(s) ({{ humanBytes(summary?.bytes) }}) and re-downloads
|
|
them from Discord with their posts and dates. Any tags you added to
|
|
those images by hand will not come back.
|
|
</v-card-text>
|
|
<v-card-actions>
|
|
<v-spacer />
|
|
<v-btn variant="text" @click="confirmOpen = false">Cancel</v-btn>
|
|
<v-btn color="error" @click="apply">Repair</v-btn>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</v-dialog>
|
|
</MaintenanceTile>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, ref } from 'vue'
|
|
|
|
import { useMaintenanceTask } from '../../composables/useMaintenanceTask.js'
|
|
import { humanBytes } from '../../utils/bytes.js'
|
|
import MaintenanceTile from '../common/MaintenanceTile.vue'
|
|
import QueueStatusBar from './QueueStatusBar.vue'
|
|
|
|
const confirmOpen = ref(false)
|
|
|
|
const { previewing, applying, summary, applied, preview, apply: applyTask } = useMaintenanceTask({
|
|
endpoint: '/api/admin/maintenance/repair-discord-downloads',
|
|
storageKey: 'fc.maint.repairDiscordDownloads',
|
|
appliedToast: 'Discord downloads cleared — backfills started',
|
|
})
|
|
|
|
// The archive count matters too: files could already be gone while gallery-dl
|
|
// still believes it has them, which would stop the backfill re-fetching.
|
|
const hasWork = computed(
|
|
() => !!summary.value && (summary.value.images > 0 || summary.value.archive_entries > 0),
|
|
)
|
|
const canApply = computed(() => hasWork.value && !applied.value)
|
|
const summaryType = computed(() => {
|
|
if (applied.value) return 'success'
|
|
return hasWork.value ? 'info' : 'success'
|
|
})
|
|
|
|
// The confirm dialog gates the destructive apply; close it, then run.
|
|
function apply () {
|
|
confirmOpen.value = false
|
|
applyTask()
|
|
}
|
|
</script>
|