UNPKG

igir

Version:

🕹 A zero-setup ROM collection manager that sorts, filters, extracts or archives, patches, and reports on collections of any size on any OS.

778 lines (777 loc) • 33.6 kB
import os from "node:os"; import path from "node:path"; import { ValidationResult } from "../../../packages/torrentzip/index.js"; import { ProgressBarSymbol } from "../../console/progressBar.js"; import { CacheMode } from "../../factories/fileFactory.js"; import ArchiveEntry from "../../models/files/archives/archiveEntry.js"; import ArchiveFile from "../../models/files/archives/archiveFile.js"; import Zip from "../../models/files/archives/zip.js"; import { ChecksumBitmask } from "../../models/files/fileChecksums.js"; import ZeroSizeFile from "../../models/files/zeroSizeFile.js"; import { LinkMode, ZipFormat } from "../../models/options.js"; import ArrayUtil from "../../utils/arrayUtil.js"; import FsUtil, { MoveResult } from "../../utils/fsUtil.js"; import IntlUtil from "../../utils/intlUtil.js"; import Module from "../module.js"; class CandidateWriter extends Module { // Keep track of written files, to warn on conflicts static OUTPUT_PATHS_WRITTEN = /* @__PURE__ */ new Map(); options; fileFactory; candidateSemaphore; moveMutex; filesQueuedForDeletion = []; constructor(options, progressBar, fileFactory, candidateSemaphore, moveMutex) { super(progressBar, CandidateWriter.name); this.options = options; this.fileFactory = fileFactory; this.candidateSemaphore = candidateSemaphore; this.moveMutex = moveMutex; } /** * Write & test candidates. */ async write(dat, candidates) { if (candidates.length === 0) { return { wrote: [], moved: [] }; } if (!this.options.shouldWrite() && !this.options.shouldTest()) { return { wrote: candidates, moved: [] }; } const inputToOutputFiles = /* @__PURE__ */ new Set(); const writableCandidates = candidates.filter((candidate) => candidate.canWrite()).map((candidate) => { const nonDuplicateRomsWithFiles = candidate.getRomsWithFiles().filter((romWithFiles) => { const inputToOutputFile = `${romWithFiles.getInputFile().toString()} \u2192 ${romWithFiles.getOutputFile().toString()}`; if (inputToOutputFiles.has(inputToOutputFile)) { return false; } inputToOutputFiles.add(inputToOutputFile); return true; }); if (nonDuplicateRomsWithFiles.length === candidate.getRomsWithFiles().length) { return candidate; } return candidate.withRomsWithFiles(nonDuplicateRomsWithFiles); }).filter((candidate) => candidate.getRomsWithFiles().length > 0); this.prefixedLogger.trace( `${dat.getName()}: ${this.options.shouldWrite() ? "writing" : "testing"} ${IntlUtil.toLocaleString(writableCandidates.length)} candidate${writableCandidates.length === 1 ? "" : "s"}` ); if (this.options.shouldTest() && !this.options.getOverwrite()) { this.progressBar.setSymbol(ProgressBarSymbol.TESTING); } else { this.progressBar.setSymbol(ProgressBarSymbol.WRITING); } this.progressBar.resetProgress(writableCandidates.length); await this.candidateSemaphore.map(writableCandidates, async (candidate) => { this.progressBar.incrementInProgress(); this.prefixedLogger.trace( `${dat.getName()}: ${candidate.getName()}: ${this.options.shouldWrite() ? "writing" : "testing"} candidate` ); if (this.options.shouldLink()) { await this.writeLink(dat, candidate); } else { await this.writeZip(dat, candidate); await this.writeRaw(dat, candidate); } this.prefixedLogger.trace( `${dat.getName()}: ${candidate.getName()}: done ${this.options.shouldWrite() ? "writing" : "testing"} candidate` ); this.progressBar.incrementCompleted(); }); this.prefixedLogger.trace( `${dat.getName()}: done ${this.options.shouldWrite() ? "writing" : "testing"} ${IntlUtil.toLocaleString(writableCandidates.length)} candidate${writableCandidates.length === 1 ? "" : "s"}` ); const writtenFilePaths = new Set( writableCandidates.flatMap( (c) => c.getRomsWithFiles().map((r) => r.getOutputFile().getFilePath()) ) ); const movedCandidates = this.filesQueuedForDeletion.filter( (writeCandidate) => writeCandidate.getRomsWithFiles().some((romWithFiles) => !writtenFilePaths.has(romWithFiles.getInputFile().getFilePath())) ); return { wrote: writableCandidates, moved: movedCandidates }; } static async ensureOutputDirExists(outputFilePath) { const outputDir = path.dirname(outputFilePath); if (!await FsUtil.exists(outputDir)) { await FsUtil.mkdir(outputDir, { recursive: true }); } } /** *********************** * * Zip Writing * * *********************** */ async writeZip(dat, candidate) { const inputToOutputZipEntries = candidate.getRomsWithFiles().filter((romWithFiles) => romWithFiles.getOutputFile() instanceof ArchiveEntry).map((romWithFiles) => [ romWithFiles.getInputFile(), romWithFiles.getOutputFile() ]); if (inputToOutputZipEntries.length === 0) { return; } const outputZip = inputToOutputZipEntries[0][1].getArchive(); const childBar = this.progressBar.addChildBar({ name: outputZip.getFilePath(), total: inputToOutputZipEntries.reduce( (sum, [, outputEntry]) => sum + outputEntry.getSize(), 0 ), progressFormatter: FsUtil.sizeReadable.bind(FsUtil) }); try { if (await FsUtil.exists(outputZip.getFilePath())) { if (this.options.shouldWrite() && !this.options.getOverwrite() && !this.options.getOverwriteInvalid()) { if (CandidateWriter.OUTPUT_PATHS_WRITTEN.has(outputZip.getFilePath())) { this.prefixedLogger.warn( `${dat.getName()}: ${candidate.getName()}: ${outputZip.getFilePath()}: not overwriting existing zip file already written by '${CandidateWriter.OUTPUT_PATHS_WRITTEN.get(outputZip.getFilePath())?.getName()}'` ); } else { this.prefixedLogger.debug( `${dat.getName()}: ${candidate.getName()}: ${outputZip.getFilePath()}: not overwriting existing zip file` ); } return; } if (!this.options.shouldWrite() || this.options.getOverwriteInvalid()) { const existingTest = await this.testZipContents( dat, candidate, outputZip.getFilePath(), inputToOutputZipEntries.map(([, outputEntry]) => outputEntry), childBar ); if (this.options.shouldWrite() && !existingTest) { this.prefixedLogger.debug( `${dat.getName()}: ${candidate.getName()}: ${outputZip.getFilePath()}: not overwriting existing zip file, the existing zip is correct` ); for (const [inputRomFile] of inputToOutputZipEntries) { this.enqueueFileDeletion(candidate, inputRomFile); } return; } if (!this.options.shouldWrite() && existingTest) { this.prefixedLogger.error( `${dat.getName()}: ${candidate.getName()}: ${outputZip.getFilePath()}: ${existingTest}` ); return; } } if (this.options.shouldWrite() && CandidateWriter.OUTPUT_PATHS_WRITTEN.has(outputZip.getFilePath())) { this.prefixedLogger.warn( `${dat.getName()}: ${candidate.getName()}: ${outputZip.getFilePath()}: overwriting existing zip file already written by '${CandidateWriter.OUTPUT_PATHS_WRITTEN.get(outputZip.getFilePath())?.getName()}'` ); } } if (!this.options.shouldWrite()) { return; } CandidateWriter.OUTPUT_PATHS_WRITTEN.set(outputZip.getFilePath(), dat); this.progressBar.setSymbol(ProgressBarSymbol.WRITING); let wasWritten = false; for (let i = 0; i <= this.options.getWriteRetry(); i += 1) { wasWritten = await this.writeZipFile( dat, candidate, outputZip, inputToOutputZipEntries, childBar ); if (wasWritten && !this.options.shouldTest()) { break; } if (wasWritten && this.options.shouldTest()) { const writtenTest = await this.testZipContents( dat, candidate, outputZip.getFilePath(), inputToOutputZipEntries.map((entry) => entry[1]), childBar ); if (!writtenTest) { break; } const message = `${dat.getName()}: ${candidate.getName()}: ${outputZip.getFilePath()}: written zip ${writtenTest}`; if (i < this.options.getWriteRetry()) { this.prefixedLogger.warn(`${message}, retrying`); } else { this.prefixedLogger.error(message); return; } } } if (wasWritten) { for (const [inputRomFile] of inputToOutputZipEntries) { this.enqueueFileDeletion(candidate, inputRomFile); } } } finally { childBar.delete(); } } async testZipContents(dat, candidate, zipFilePath, expectedArchiveEntries, progressBar) { this.prefixedLogger.trace( `${dat.getName()}: ${candidate.getName()}: ${zipFilePath}: testing zip` ); const zipFile = new Zip(zipFilePath); const expectedEntriesByPath = expectedArchiveEntries.reduce((map, entry) => { map.set(entry.getEntryPath(), entry); return map; }, /* @__PURE__ */ new Map()); const checksumBitmask = expectedArchiveEntries.reduce( (bitmask, entry) => bitmask | entry.getChecksumBitmask(), ChecksumBitmask.CRC32 ); let archiveEntries; try { archiveEntries = await this.fileFactory.entriesFromArchive( zipFile, checksumBitmask, CacheMode.IGNORE_CACHED_VALUE, (progress, total) => { progressBar.setCompleted(progress); if (total !== void 0) { progressBar.setTotal(total); } }, true ) ?? []; } catch (error) { return `failed to get archive contents: ${error}`; } const actualEntriesByPath = archiveEntries.reduce((map, entry) => { map.set(entry.getEntryPath(), entry); return map; }, /* @__PURE__ */ new Map()); if (actualEntriesByPath.size !== expectedEntriesByPath.size) { return `has ${IntlUtil.toLocaleString(actualEntriesByPath.size)} files, expected ${IntlUtil.toLocaleString(expectedEntriesByPath.size)}`; } for (const [entryPath, expectedFile] of expectedEntriesByPath) { if (!actualEntriesByPath.has(entryPath)) { return `is missing the file ${entryPath}`; } const actualFile = actualEntriesByPath.get(entryPath); if (actualFile.getSha256() && expectedFile.getSha256() && actualFile.getSha256() !== expectedFile.getSha256()) { return `entry '${entryPath}' has the SHA256 ${actualFile.getSha256()}, expected ${expectedFile.getSha256()}`; } if (actualFile.getSha1() && expectedFile.getSha1() && actualFile.getSha1() !== expectedFile.getSha1()) { return `entry '${entryPath}' has the SHA1 ${actualFile.getSha1()}, expected ${expectedFile.getSha1()}`; } if (actualFile.getMd5() && expectedFile.getMd5() && actualFile.getMd5() !== expectedFile.getMd5()) { return `entry '${entryPath}' has the MD5 ${actualFile.getMd5()}, expected ${expectedFile.getMd5()}`; } if (actualFile.getCrc32() && expectedFile.getCrc32() && actualFile.getCrc32() !== expectedFile.getCrc32()) { return `entry '${entryPath}' has the CRC32 ${actualFile.getCrc32()}, expected ${expectedFile.getCrc32()}`; } if (actualFile.getCrc32() && expectedFile.getCrc32() && actualFile.getSize() !== expectedFile.getSize()) { return `entry '${entryPath}' has the file ${entryPath} of size ${IntlUtil.toLocaleString(actualFile.getSize())}B, expected ${IntlUtil.toLocaleString(expectedFile.getSize())}B`; } } const tzValidationResult = await this.fileFactory.tzValidationFrom( zipFile, CacheMode.IGNORE_CACHED_VALUE ); if (this.options.getZipFormat() === ZipFormat.TORRENTZIP && tzValidationResult !== ValidationResult.VALID_TORRENTZIP) { return "is not a valid TorrentZip file"; } if (this.options.getZipFormat() === ZipFormat.RVZSTD && tzValidationResult !== ValidationResult.VALID_RVZSTD) { return "is not a valid RVZSTD file"; } this.prefixedLogger.trace( `${dat.getName()}: ${candidate.getName()}: ${zipFilePath}: test passed` ); return void 0; } async writeZipFile(dat, candidate, outputZip, inputToOutputZipEntries, progressBar) { const lockedFilePaths = this.options.shouldMove() ? inputToOutputZipEntries.map(([input]) => input.getFilePath()).reduce(ArrayUtil.reduceUnique(), []) : []; return await this.moveMutex.runExclusiveForKeys(lockedFilePaths, async () => { const movedInputToOutputZipEntries = this.options.shouldMove() ? inputToOutputZipEntries.map(([input, output]) => { const movedFilePath = this.moveMutex.getMovedLocationUnsafe(input.getFilePath()); if (movedFilePath === void 0) { return [input, output]; } return [input.withFilePath(movedFilePath), output]; }) : inputToOutputZipEntries; this.prefixedLogger.info( [ `${dat.getName()}: ${candidate.getName()}: creating zip archive '${outputZip.getFilePath()}' with the entr${movedInputToOutputZipEntries.length === 1 ? "y" : "ies"}:`, ...movedInputToOutputZipEntries.map(([input, output]) => { if (input.getFilePath() === output.getFilePath()) { return ` '${input.getExtractedFilePath()}' (${FsUtil.sizeReadable(input.getSize())}) \u2192 '${output.getExtractedFilePath()}' ${input.getExtractedFilePath() === output.getExtractedFilePath() ? "(rewriting)" : ""}`; } return ` '${input.toString()}' (${FsUtil.sizeReadable(input.getSize())}) \u2192 '${output.getExtractedFilePath()}'`; }) ].join("\n") ); try { await CandidateWriter.ensureOutputDirExists(outputZip.getFilePath()); const compressorThreads = Math.ceil( os.availableParallelism() / Math.max(this.candidateSemaphore.openLocks(), 1) ); await outputZip.createArchive( movedInputToOutputZipEntries, this.options.getZipFormat(), compressorThreads, (progress, total) => { progressBar.setCompleted(progress); progressBar.setTotal(total); } ); } catch (error) { this.prefixedLogger.error( `${dat.getName()}: ${candidate.getName()}: ${outputZip.getFilePath()}: failed to create zip: ${error}` ); return false; } this.prefixedLogger.trace( `${dat.getName()}: ${candidate.getName()}: ${outputZip.getFilePath()}: wrote ${IntlUtil.toLocaleString(inputToOutputZipEntries.length)} archive entr${inputToOutputZipEntries.length === 1 ? "y" : "ies"}` ); return true; }); } /** *********************** * * Raw Writing * * *********************** */ async writeRaw(dat, candidate) { const inputToOutputEntries = candidate.getRomsWithFiles().filter((romWithFiles) => !(romWithFiles.getOutputFile() instanceof ArchiveEntry)).map((romWithFiles) => [romWithFiles.getInputFile(), romWithFiles.getOutputFile()]); if (inputToOutputEntries.length === 0) { return; } const totalBytes = inputToOutputEntries.flatMap(([, outputFile]) => outputFile).reduce((sum, file) => sum + file.getSize(), 0); this.prefixedLogger.trace( `${dat.getName()}: ${candidate.getName()}: ${this.options.shouldMove() ? "moving" : "copying"} ${FsUtil.sizeReadable(totalBytes)} of ${IntlUtil.toLocaleString(inputToOutputEntries.length)} ${inputToOutputEntries.some(([input]) => input instanceof ArchiveFile) ? "archive" : "raw"} file${inputToOutputEntries.length === 1 ? "" : "s"}` ); const uniqueInputToOutputEntriesMap = inputToOutputEntries.reduce( (map, [inputRomFile, outputRomFile]) => { const key = inputRomFile.getFilePath(); if (map.has(key)) { map.get(key)?.push([inputRomFile, outputRomFile]); } else { map.set(key, [[inputRomFile, outputRomFile]]); } return map; }, /* @__PURE__ */ new Map() ); for (const groupedInputToOutput of uniqueInputToOutputEntriesMap.values()) { await Promise.all( groupedInputToOutput.map(async ([inputRomFile, outputRomFile]) => { await this.writeRawSingle(dat, candidate, inputRomFile, outputRomFile); }) ); } } async writeRawSingle(dat, candidate, inputRomFile, outputRomFile) { if (this.options.shouldWrite() && outputRomFile.equals(inputRomFile)) { const wasMoved = this.options.shouldMove() && !(inputRomFile instanceof ZeroSizeFile) && await this.moveMutex.moveFile(inputRomFile.getFilePath(), (movedInputPath) => [ // Return if this file was previously moved movedInputPath !== void 0, // If this file was previously moved then remember that location, otherwise, mark it as // moved so that if it's used in another input file it will be copied instead movedInputPath ?? inputRomFile.getFilePath() ]); if (!wasMoved) { this.prefixedLogger.debug( `${dat.getName()}: ${candidate.getName()}: ${outputRomFile.toString()}: input and output files are the same, skipping` ); return; } } const outputFilePath = outputRomFile.getFilePath(); const childBar = this.progressBar.addChildBar({ name: outputFilePath, // Files being patched might not have a known final size, just guess it as the input size total: outputRomFile.getSize() > 0 ? outputRomFile.getSize() : inputRomFile.getSize(), progressFormatter: FsUtil.sizeReadable.bind(FsUtil) }); try { if (await FsUtil.exists(outputFilePath)) { if (this.options.shouldWrite() && !this.options.getOverwrite() && !this.options.getOverwriteInvalid()) { if (CandidateWriter.OUTPUT_PATHS_WRITTEN.has(outputFilePath)) { this.prefixedLogger.warn( `${dat.getName()}: ${candidate.getName()}: ${outputFilePath}: not overwriting existing file already written by '${CandidateWriter.OUTPUT_PATHS_WRITTEN.get(outputFilePath)?.getName()}'` ); } else { this.prefixedLogger.debug( `${dat.getName()}: ${candidate.getName()}: ${outputFilePath}: not overwriting existing file` ); } return; } if (!this.options.shouldWrite() || this.options.getOverwriteInvalid()) { const existingTest = await this.testWrittenRaw( dat, candidate, outputFilePath, outputRomFile, childBar ); if (this.options.shouldWrite() && !existingTest) { this.prefixedLogger.debug( `${dat.getName()}: ${candidate.getName()}: ${outputFilePath}: not overwriting existing file, the existing file is correct` ); this.enqueueFileDeletion(candidate, inputRomFile); return; } if (!this.options.shouldWrite() && existingTest) { this.prefixedLogger.error( `${dat.getName()}: ${candidate.getName()}: ${outputFilePath}: ${existingTest}` ); return; } } if (this.options.shouldWrite() && CandidateWriter.OUTPUT_PATHS_WRITTEN.has(outputFilePath)) { this.prefixedLogger.warn( `${dat.getName()}: ${candidate.getName()}: ${outputFilePath}: overwriting existing file already written by '${CandidateWriter.OUTPUT_PATHS_WRITTEN.get(outputFilePath)?.getName()}'` ); } } if (!this.options.shouldWrite()) { return; } CandidateWriter.OUTPUT_PATHS_WRITTEN.set(outputFilePath, dat); this.progressBar.setSymbol(ProgressBarSymbol.WRITING); let written; for (let i = 0; i <= this.options.getWriteRetry(); i += 1) { if (this.options.shouldMove()) { written = await this.moveRawFile(dat, candidate, inputRomFile, outputFilePath, childBar); } else { written = await this.copyRawFile(dat, candidate, inputRomFile, outputFilePath, childBar); } if (written !== void 0 && !this.options.shouldTest()) { break; } if (written === MoveResult.COPIED && this.options.shouldTest()) { const writtenTest = await this.testWrittenRaw( dat, candidate, outputFilePath, outputRomFile, childBar ); if (!writtenTest) { break; } const message = `${dat.getName()}: ${candidate.getName()}: ${outputFilePath}: written file ${writtenTest}`; if (i < this.options.getWriteRetry()) { this.prefixedLogger.warn(`${message}, retrying`); } else { this.prefixedLogger.error(message); return; } } if (written === MoveResult.RENAMED && this.options.shouldTest()) { break; } } if (written) { this.enqueueFileDeletion(candidate, inputRomFile); } } finally { childBar.delete(); } } async moveRawFile(dat, candidate, inputRomFile, outputFilePath, progressBar) { if (inputRomFile instanceof ZeroSizeFile) { return await this.copyRawFile(dat, candidate, inputRomFile, outputFilePath, progressBar); } return await this.moveMutex.moveFile(inputRomFile.getFilePath(), async (movedInputPath) => { if (movedInputPath) { if (movedInputPath === outputFilePath) { return [MoveResult.RENAMED, void 0]; } const copyResult = await this.copyRawFile( dat, candidate, inputRomFile.withFilePath(movedInputPath), outputFilePath, progressBar ); return [copyResult, void 0]; } if (inputRomFile instanceof ArchiveEntry || inputRomFile.getFileHeader() !== void 0 || inputRomFile.getPatch() !== void 0) { const copyResult = await this.copyRawFile( dat, candidate, inputRomFile, outputFilePath, progressBar ); return [copyResult, void 0]; } this.prefixedLogger.info( `${dat.getName()}: ${candidate.getName()}: moving file '${inputRomFile.toString()}' (${FsUtil.sizeReadable(inputRomFile.getSize())}) \u2192 '${outputFilePath}'` ); try { await CandidateWriter.ensureOutputDirExists(outputFilePath); const moveResult = await FsUtil.mv( inputRomFile.getFilePath(), outputFilePath, (progress) => { progressBar.setCompleted(progress); } ); return [moveResult, outputFilePath]; } catch (error) { this.prefixedLogger.error( `${dat.getName()}: ${candidate.getName()}: failed to move file '${inputRomFile.toString()}' \u2192 '${outputFilePath}': ${error}` ); if (error.code === "ENOSPC") { throw error; } return [void 0, void 0]; } }); } async copyRawFile(dat, candidate, inputRomFile, outputFilePath, progressBar) { this.prefixedLogger.info( `${dat.getName()}: ${candidate.getName()}: ${inputRomFile instanceof ArchiveEntry ? "extracting" : "copying"} file '${inputRomFile.toString()}' (${FsUtil.sizeReadable(inputRomFile.getSize())}) \u2192 '${outputFilePath}'` ); try { await CandidateWriter.ensureOutputDirExists(outputFilePath); const tempRawFile = await FsUtil.mktemp(outputFilePath); await inputRomFile.extractAndTransformToFile(tempRawFile, (progress) => { progressBar.setCompleted(progress); }); await FsUtil.mv(tempRawFile, outputFilePath); return MoveResult.COPIED; } catch (error) { this.prefixedLogger.error( `${dat.getName()}: ${candidate.getName()}: failed to ${inputRomFile instanceof ArchiveEntry ? "extract" : "copy"} file '${inputRomFile.toString()}' \u2192 '${outputFilePath}': ${error}` ); if (error.code === "ENOSPC") { throw error; } return void 0; } } async testWrittenRaw(dat, candidate, outputFilePath, expectedFile, progressBar) { this.prefixedLogger.trace( `${dat.getName()}: ${candidate.getName()}: ${outputFilePath}: testing raw file` ); let actualFile; try { actualFile = await this.fileFactory.fileFrom( outputFilePath, expectedFile.getChecksumBitmask(), (progress) => { progressBar?.setCompleted(progress); }, CacheMode.IGNORE_CACHED_VALUE ); } catch (error) { return `failed to parse: ${error}`; } if (actualFile.getSha256() && expectedFile.getSha256() && actualFile.getSha256() !== expectedFile.getSha256()) { return `has the SHA256 ${actualFile.getSha256()}, expected ${expectedFile.getSha256()}`; } if (actualFile.getSha1() && expectedFile.getSha1() && actualFile.getSha1() !== expectedFile.getSha1()) { return `has the SHA1 ${actualFile.getSha1()}, expected ${expectedFile.getSha1()}`; } if (actualFile.getMd5() && expectedFile.getMd5() && actualFile.getMd5() !== expectedFile.getMd5()) { return `has the MD5 ${actualFile.getMd5()}, expected ${expectedFile.getMd5()}`; } if (actualFile.getCrc32() && expectedFile.getCrc32() && actualFile.getCrc32() !== expectedFile.getCrc32()) { return `has the CRC32 ${actualFile.getCrc32()}, expected ${expectedFile.getCrc32()}`; } if (actualFile.getCrc32() && actualFile.getSize() !== expectedFile.getSize()) { return `is of size ${IntlUtil.toLocaleString(actualFile.getSize())}B, expected ${IntlUtil.toLocaleString(expectedFile.getSize())}B`; } this.prefixedLogger.trace( `${dat.getName()}: ${candidate.getName()}: ${outputFilePath}: test passed` ); return void 0; } // Input files may be needed for multiple output files, such as an archive with hundreds of ROMs // in it. That means we need to "move" (delete) files at the very end after all DATs have // finished writing. enqueueFileDeletion(candidate, inputRomFile) { if (!this.options.shouldMove()) { return; } if (inputRomFile instanceof ZeroSizeFile) { return; } this.filesQueuedForDeletion.push(candidate); } /** ************************ * * Link Writing * * ************************ */ async writeLink(dat, candidate) { const inputToOutputEntries = candidate.getRomsWithFiles(); for (const inputToOutputEntry of inputToOutputEntries) { const inputRomFile = inputToOutputEntry.getInputFile(); const outputRomFile = inputToOutputEntry.getOutputFile(); await this.writeLinkSingle(dat, candidate, inputRomFile, outputRomFile); } } async writeLinkSingle(dat, candidate, inputRomFile, outputRomFile) { if (outputRomFile.equals(inputRomFile)) { this.prefixedLogger.debug( `${dat.getName()}: ${candidate.getName()}: ${outputRomFile.toString()}: input and output files are the same, skipping` ); return; } const linkPath = outputRomFile.getFilePath(); let targetPath = inputRomFile.getFilePath(); if (this.options.getLinkMode() === LinkMode.SYMLINK && this.options.getSymlinkRelative()) { await CandidateWriter.ensureOutputDirExists(linkPath); targetPath = await FsUtil.symlinkRelativePath(targetPath, linkPath); } if (await FsUtil.exists(linkPath)) { if (this.options.shouldWrite() && !this.options.getOverwrite() && !this.options.getOverwriteInvalid()) { if (CandidateWriter.OUTPUT_PATHS_WRITTEN.has(linkPath)) { this.prefixedLogger.warn( `${dat.getName()}: ${candidate.getName()}: ${linkPath}: not overwriting existing file already written by '${CandidateWriter.OUTPUT_PATHS_WRITTEN.get(linkPath)?.getName()}'` ); } else { this.prefixedLogger.debug( `${dat.getName()}: ${candidate.getName()}: ${linkPath}: not overwriting existing file` ); } return; } if (!this.options.shouldWrite() || this.options.getOverwriteInvalid()) { let existingTest; if (this.options.getLinkMode() === LinkMode.SYMLINK) { existingTest = await CandidateWriter.testWrittenSymlink(linkPath, targetPath); } else if (this.options.getLinkMode() === LinkMode.HARDLINK) { existingTest = await CandidateWriter.testWrittenHardlink( linkPath, inputRomFile.getFilePath() ); } else { existingTest = await this.testWrittenRaw(dat, candidate, linkPath, outputRomFile); } if (this.options.shouldWrite() && !existingTest) { this.prefixedLogger.debug( `${dat.getName()}: ${candidate.getName()}: ${linkPath}: not overwriting existing link, the existing link is correct` ); return; } if (!this.options.shouldWrite() && existingTest) { this.prefixedLogger.error( `${dat.getName()}: ${candidate.getName()}: ${linkPath}: ${existingTest}` ); return; } } if (this.options.shouldWrite() && CandidateWriter.OUTPUT_PATHS_WRITTEN.has(linkPath)) { this.prefixedLogger.warn( `${dat.getName()}: ${candidate.getName()}: ${linkPath}: overwriting existing zip file already written by '${CandidateWriter.OUTPUT_PATHS_WRITTEN.get(linkPath)?.getName()}'` ); } } if (!this.options.shouldWrite()) { return; } CandidateWriter.OUTPUT_PATHS_WRITTEN.set(linkPath, dat); this.progressBar.setSymbol(ProgressBarSymbol.WRITING); for (let i = 0; i <= this.options.getWriteRetry(); i += 1) { const wasWritten = await this.writeRawLink(dat, candidate, targetPath, linkPath); if (wasWritten && !this.options.shouldTest()) { break; } if (wasWritten && this.options.shouldTest()) { let writtenTest; if (this.options.getLinkMode() === LinkMode.SYMLINK) { writtenTest = await CandidateWriter.testWrittenSymlink(linkPath, targetPath); } else if (this.options.getLinkMode() === LinkMode.HARDLINK) { writtenTest = await CandidateWriter.testWrittenHardlink( linkPath, inputRomFile.getFilePath() ); } else { writtenTest = await this.testWrittenRaw(dat, candidate, linkPath, outputRomFile); } if (!writtenTest) { break; } const message = `${dat.getName()}: ${candidate.getName()} ${linkPath}: written link ${writtenTest}`; if (i < this.options.getWriteRetry()) { this.prefixedLogger.warn(`${message}, retrying`); } else { this.prefixedLogger.error(message); return; } } } } async writeRawLink(dat, candidate, targetPath, linkPath) { try { await CandidateWriter.ensureOutputDirExists(linkPath); if (this.options.getLinkMode() === LinkMode.SYMLINK) { this.prefixedLogger.info( `${dat.getName()}: ${candidate.getName()}: creating symlink '${targetPath}' \u2192 '${linkPath}'` ); await FsUtil.symlink(targetPath, linkPath); } else if (this.options.getLinkMode() === LinkMode.HARDLINK) { this.prefixedLogger.info( `${dat.getName()}: ${candidate.getName()}: creating hard link '${targetPath}' \u2192 '${linkPath}'` ); await FsUtil.hardlink(targetPath, linkPath); } else { this.prefixedLogger.info( `${dat.getName()}: ${candidate.getName()}: creating reflink '${targetPath}' \u2192 '${linkPath}'` ); await FsUtil.reflink(targetPath, linkPath); } return true; } catch (error) { this.prefixedLogger.error( `${dat.getName()}: ${candidate.getName()}: ${linkPath}: failed to link from ${targetPath}: ${error}` ); return false; } } static async testWrittenSymlink(linkPath, expectedTargetPath) { if (!await FsUtil.exists(linkPath)) { return "doesn't exist"; } if (!await FsUtil.isSymlink(linkPath)) { return "is not a symlink"; } const existingSourcePath = await FsUtil.readlink(linkPath); if (path.normalize(existingSourcePath) !== path.normalize(expectedTargetPath)) { return `has the target path '${existingSourcePath}', expected '${expectedTargetPath}`; } if (!await FsUtil.exists(await FsUtil.readlinkResolved(linkPath))) { return `has the target path '${existingSourcePath}' which doesn't exist`; } return void 0; } static async testWrittenHardlink(linkPath, inputRomPath) { if (!await FsUtil.exists(linkPath)) { return "doesn't exist"; } const targetInode = await FsUtil.inode(linkPath); const sourceInode = await FsUtil.inode(inputRomPath); if (targetInode !== sourceInode) { return `references a different file than '${inputRomPath}'`; } return void 0; } } export { CandidateWriter as default }; //# sourceMappingURL=candidateWriter.js.map