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.

172 lines (171 loc) • 7.58 kB
import async from "async"; import { ProgressBarSymbol } from "../../console/progressBar.js"; import Defaults from "../../globals/defaults.js"; import ArchiveEntry from "../../models/files/archives/archiveEntry.js"; import ArchiveFile from "../../models/files/archives/archiveFile.js"; import ChdBinCue from "../../models/files/archives/chd/chdBinCue.js"; import ArrayUtil from "../../utils/arrayUtil.js"; import FsUtil from "../../utils/fsUtil.js"; import IntlUtil from "../../utils/intlUtil.js"; import Module from "../module.js"; class MovedROMDeleter extends Module { options; constructor(options, progressBar) { super(progressBar, MovedROMDeleter.name); this.options = options; } /** * Delete input files that were moved. */ async delete(indexedRoms, movedWriteCandidates, writtenFilesToExclude) { if (!this.options.shouldMove()) { return []; } if (movedWriteCandidates.length === 0) { return []; } this.prefixedLogger.trace("deleting moved ROMs"); this.progressBar.setSymbol(ProgressBarSymbol.DAT_FILTERING); const inputFiles = /* @__PURE__ */ new Set(); for (const candidate of movedWriteCandidates) { for (const romWithFiles of candidate.getRomsWithFiles()) inputFiles.add(romWithFiles.getInputFile().getFilePath()); } this.progressBar.resetProgress(inputFiles.size); this.prefixedLogger.trace( `considering ${IntlUtil.toLocaleString(inputFiles.size)} unique input paths for deletion` ); const movedRoms = /* @__PURE__ */ new Set(); for (const writeCandidate of movedWriteCandidates) { for (const romsWithFiles of writeCandidate.getRomsWithFiles()) { const inputFile = romsWithFiles.getInputFile(); let possibleDuplicates = inputFile instanceof ArchiveFile ? [inputFile] : indexedRoms.findFiles(inputFile); if (this.options.shouldExtractRom(romsWithFiles.getRom()) || this.options.shouldZipRom(romsWithFiles.getRom())) { } else if (inputFile instanceof ArchiveFile) { possibleDuplicates = possibleDuplicates.filter( (matchedFile) => (matchedFile instanceof ArchiveEntry || matchedFile instanceof ArchiveFile) && matchedFile.getArchive().constructor.name === inputFile.getArchive().constructor.name ); } else { possibleDuplicates = possibleDuplicates.filter( (matchedFile) => !(matchedFile instanceof ArchiveEntry || matchedFile instanceof ArchiveFile) ); } for (const duplicate of possibleDuplicates) movedRoms.add(duplicate); } } this.prefixedLogger.trace( `expanded to ${IntlUtil.toLocaleString(movedRoms.size)} possible input files` ); const fullyConsumedFiles = this.filterOutPartiallyConsumedArchives([...movedRoms], indexedRoms); this.prefixedLogger.trace( `filtered to ${IntlUtil.toLocaleString(fullyConsumedFiles.length)} fully used input files` ); const filePathsToDelete = MovedROMDeleter.filterOutWrittenFiles( fullyConsumedFiles, writtenFilesToExclude ); this.prefixedLogger.trace( `filtered to ${IntlUtil.toLocaleString(filePathsToDelete.length)} non-output files` ); this.progressBar.resetProgress(filePathsToDelete.length); const existingFilePathsCheck = await async.mapLimit( filePathsToDelete, Defaults.MAX_FS_THREADS, async (filePath) => await FsUtil.exists(filePath) ); const existingFilePaths = filePathsToDelete.filter( (_filePath, idx) => existingFilePathsCheck.at(idx) === true ); if (existingFilePaths.length > 0) { this.progressBar.setSymbol(ProgressBarSymbol.DELETING); this.progressBar.resetProgress(existingFilePaths.length); this.prefixedLogger.trace( `deleting ${IntlUtil.toLocaleString(existingFilePaths.length)} moved file${existingFilePaths.length === 1 ? "" : "s"}` ); } const filePathChunks = existingFilePaths.reduce( ArrayUtil.reduceChunk(Defaults.OUTPUT_CLEANER_BATCH_SIZE), [] ); for (const filePathChunk of filePathChunks) { this.progressBar.setInProgress(filePathChunk.length); this.prefixedLogger.info( `deleting moved file${filePathChunk.length === 1 ? "" : "s"}: ${filePathChunk.map((filePath) => ` ${filePath}`).join("\n")}` ); await Promise.all( filePathChunk.map(async (filePath) => { try { await FsUtil.rm(filePath, { force: true }); } catch (error) { this.prefixedLogger.error(`${filePath}: failed to delete: ${error}`); } }) ); this.progressBar.incrementCompleted(filePathChunk.length); this.progressBar.setInProgress(0); } this.prefixedLogger.trace("done deleting moved ROMs"); return existingFilePaths; } /** * Archives can contain a mixture of ROMs from many different games, so it is possible that not * every entry from the archive (or a duplicate of it) was used as an input file for a * WriteCandidate. These partially used archives are not safe to delete and need to be filtered * out. */ filterOutPartiallyConsumedArchives(movedRoms, indexedRoms) { const groupedInputRoms = indexedRoms.getFilesByFilePath(); const groupedMovedRoms = MovedROMDeleter.groupFilesByFilePath(movedRoms); return [...groupedMovedRoms].map(([filePath, movedEntries]) => { if (movedEntries.length === 1 && !(movedEntries[0] instanceof ArchiveEntry)) { return filePath; } const movedEntryHashCodes = new Set(movedEntries.map((file) => file.hashCode())); const inputFilesForPath = groupedInputRoms.get(filePath) ?? []; const unmovedArchiveEntries = inputFilesForPath.filter( (inputFile) => { if (!(inputFile instanceof ArchiveEntry)) { return false; } if (movedEntryHashCodes.has(inputFile.hashCode())) { return false; } return !(inputFile.getArchive() instanceof ChdBinCue && inputFile.getExtractedFilePath().toLowerCase().endsWith(".cue")); } ); if (unmovedArchiveEntries.length === 0) { return filePath; } this.prefixedLogger.warn( `${filePath}: not deleting moved file, ${IntlUtil.toLocaleString(unmovedArchiveEntries.length)} archive entr${unmovedArchiveEntries.length === 1 ? "y was" : "ies were"} unmatched: ${unmovedArchiveEntries.toSorted((a, b) => a.getEntryPath().localeCompare(b.getEntryPath())).map((entry) => ` ${entry.toString()}`).join("\n")}` ); return void 0; }).filter((filePath) => filePath !== void 0); } static groupFilesByFilePath(files) { return files.reduce((map, file) => { const key = file.getFilePath(); const filesForKey = map.get(key) ?? []; filesForKey.push(file); const uniqueFilesForKey = filesForKey.filter( ArrayUtil.filterUniqueMapped((fileForKey) => fileForKey.toString()) ); map.set(key, uniqueFilesForKey); return map; }, /* @__PURE__ */ new Map()); } /** * When an input directory is also used as the output directory, and a file is matched multiple * times, don't delete the input file if it is in a correct location. */ static filterOutWrittenFiles(movedRoms, writtenFilesToExclude) { const writtenFilePaths = new Set(writtenFilesToExclude.map((file) => file.getFilePath())); return movedRoms.filter((filePath) => !writtenFilePaths.has(filePath)); } } export { MovedROMDeleter as default }; //# sourceMappingURL=movedRomDeleter.js.map