trash corrupt files
All checks were successful
Build and Push Docker Image / build (push) Successful in 57s

This commit is contained in:
Garret Patti
2026-04-20 11:44:30 -04:00
parent 7d2ae7e95c
commit dee9356004
4 changed files with 152 additions and 24 deletions

View File

@@ -5,6 +5,7 @@ import type { ComicIssue, ComicSeries } from '@/types'
import { getDb } from './db'
import { HIDDEN_FILES, thumbnailApiUrl } from './media-utils'
import { countZipImages, mapConcurrent } from './zip-utils'
import fsPromises from 'fs/promises'
const CBZ_EXTENSIONS = new Set(['.cbz'])
const CBZ_IMAGE_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.webp', '.gif'])
@@ -28,6 +29,30 @@ export interface ScannedComicSeries extends ComicSeries {
issues: ComicIssue[]
}
const TRASH_DIR = '.trash'
async function moveToTrash(absPath: string, libraryRoot: string): Promise<void> {
const trashDir = path.join(libraryRoot, TRASH_DIR)
await fsPromises.mkdir(trashDir, { recursive: true })
const filename = path.basename(absPath)
let dest = path.join(trashDir, filename)
if (fs.existsSync(dest)) {
const ext = path.extname(filename)
const base = path.basename(filename, ext)
dest = path.join(trashDir, `${base}_${Date.now()}${ext}`)
}
await fsPromises.rename(absPath, dest).catch(async (err: NodeJS.ErrnoException) => {
if (err.code === 'EXDEV') {
// Source and destination are on different filesystems — copy then delete.
await fsPromises.copyFile(absPath, dest)
await fsPromises.unlink(absPath)
} else {
throw err
}
})
console.log(`[scanner] Moved corrupt archive to trash: ${path.relative(libraryRoot, absPath)}`)
}
interface CollectedCbz {
absPath: string
filename: string
@@ -93,22 +118,38 @@ export async function scanComicsLibrary(
// Phase 2: Count pages for all CBZ files concurrently (10 at a time) by reading
// only each archive's central directory — no full-file reads.
const pageCounts = await mapConcurrent(collected, 10, (c) =>
const scanResults = await mapConcurrent(collected, 10, (c) =>
countZipImages(c.absPath, CBZ_IMAGE_EXTENSIONS)
)
// Phase 3: Build the result array from collected metadata + page counts.
// Move corrupt archives to the library's .trash folder and exclude them from indexing.
const movePromises: Promise<void>[] = []
const valid: Array<{ cbz: CollectedCbz; pageCount: number }> = []
for (let i = 0; i < collected.length; i++) {
const result = scanResults[i]
if (!result.valid) {
movePromises.push(
moveToTrash(collected[i].absPath, libraryRoot).catch((err) =>
console.warn(`[scanner] Could not move corrupt archive to trash: ${collected[i].absPath}`, err)
)
)
continue
}
valid.push({ cbz: collected[i], pageCount: result.pageCount })
}
if (movePromises.length > 0) await Promise.all(movePromises)
// Phase 3: Build the result array from valid files only.
const seriesMap = new Map<string, ScannedComicSeries>()
const standaloneIssues: ComicIssue[] = []
for (let i = 0; i < collected.length; i++) {
const c = collected[i]
for (const { cbz: c, pageCount } of valid) {
const coverUrl = thumbnailApiUrl(libraryId, c.relPath)
const issue: ComicIssue = {
id: encodeURIComponent(c.relPath),
title: path.basename(c.filename, path.extname(c.filename)),
issueNumber: parseIssueNumber(c.filename),
pageCount: pageCounts[i],
pageCount,
coverUrl,
filePath: c.relPath,
isStandalone: c.isStandalone,