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
JavaScript
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