trash corrupt files
All checks were successful
Build and Push Docker Image / build (push) Successful in 57s
All checks were successful
Build and Push Docker Image / build (push) Successful in 57s
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user