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.
194 lines (193 loc) • 7.32 kB
JavaScript
import fs from "node:fs";
import path from "node:path";
import async from "async";
import { isNotJunk } from "junk";
import trash from "trash";
import MappableSemaphore from "../../async/mappableSemaphore.js";
import { ProgressBarSymbol } from "../../console/progressBar.js";
import Defaults from "../../globals/defaults.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 DirectoryCleaner extends Module {
options;
constructor(options, progressBar) {
super(progressBar, DirectoryCleaner.name);
this.options = options;
}
/**
* Clean some directories, excluding some files.
*/
async clean(dirsToClean, filesToExclude) {
if (!this.options.shouldWrite()) {
return [];
}
if (filesToExclude.length === 0) {
this.prefixedLogger.trace("no files were written, not cleaning output");
return [];
}
this.prefixedLogger.trace("cleaning files in output");
this.progressBar.setSymbol(ProgressBarSymbol.FILE_SCANNING);
this.progressBar.resetProgress(0);
const filesToClean = await this.options.scanOutputFilesWithoutCleanExclusions(
dirsToClean,
filesToExclude,
(increment) => {
this.progressBar.incrementTotal(increment);
}
);
if (filesToClean.length === 0) {
this.prefixedLogger.debug("no files to clean");
return [];
}
this.progressBar.setSymbol(ProgressBarSymbol.RECYCLING);
try {
this.prefixedLogger.trace(
`cleaning ${IntlUtil.toLocaleString(filesToClean.length)} file${filesToClean.length === 1 ? "" : "s"}`
);
this.progressBar.resetProgress(filesToClean.length);
if (this.options.getCleanDryRun()) {
this.prefixedLogger.info(
`paths skipped from cleaning (dry run):
${filesToClean.map((filePath) => ` ${filePath}`).join("\n")}`
);
} else {
const cleanBackupDir = this.options.getCleanBackup();
if (cleanBackupDir === void 0) {
await this.trashOrDelete(filesToClean);
} else {
await this.backupFiles(cleanBackupDir, filesToClean);
}
}
} catch (error) {
this.prefixedLogger.error(`failed to clean unmatched files: ${error}`);
return [];
}
try {
let emptyDirs = await DirectoryCleaner.getEmptyDirs(dirsToClean);
while (emptyDirs.length > 0) {
this.progressBar.resetProgress(emptyDirs.length);
this.prefixedLogger.trace(
`cleaning ${IntlUtil.toLocaleString(emptyDirs.length)} empty director${emptyDirs.length === 1 ? "y" : "ies"}`
);
if (this.options.getCleanDryRun()) {
this.prefixedLogger.info(
`paths skipped from cleaning (dry run):
${emptyDirs.map((filePath) => ` ${filePath}`).join("\n")}`
);
} else {
await this.trashOrDelete(emptyDirs);
}
emptyDirs = await DirectoryCleaner.getEmptyDirs(dirsToClean);
}
} catch (error) {
this.prefixedLogger.error(`failed to clean empty directories: ${error}`);
}
this.prefixedLogger.trace("done cleaning files in output");
return filesToClean.toSorted((a, b) => a.localeCompare(b));
}
async trashOrDelete(filePaths) {
for (let i = 0; i < filePaths.length; i += Defaults.OUTPUT_CLEANER_BATCH_SIZE) {
const filePathsChunk = filePaths.slice(i, i + Defaults.OUTPUT_CLEANER_BATCH_SIZE);
this.progressBar.setInProgress(filePathsChunk.length);
this.prefixedLogger.info(
`recycling cleaned path${filePathsChunk.length === 1 ? "" : "s"}:
${filePathsChunk.map((filePath) => ` ${filePath}`).join("\n")}`
);
try {
await trash(filePathsChunk);
} catch (error) {
this.prefixedLogger.warn(
`failed to recycle ${filePathsChunk.length} path${filePathsChunk.length === 1 ? "" : "s"}: ${error}`
);
}
this.progressBar.incrementCompleted(filePathsChunk.length);
}
const existSemaphore = new MappableSemaphore(Defaults.OUTPUT_CLEANER_BATCH_SIZE);
const existingFilePathsCheck = await existSemaphore.map(
filePaths,
async (filePath) => await FsUtil.exists(filePath)
);
const existingFilePaths = filePaths.filter(
(_filePath, idx) => existingFilePathsCheck.at(idx) === true
);
if (existingFilePaths.length > 0) {
this.progressBar.setSymbol(ProgressBarSymbol.DELETING);
}
for (let i = 0; i < existingFilePaths.length; i += Defaults.OUTPUT_CLEANER_BATCH_SIZE) {
const filePathsChunk = existingFilePaths.slice(i, i + Defaults.OUTPUT_CLEANER_BATCH_SIZE);
this.prefixedLogger.info(
`deleting cleaned path${filePathsChunk.length === 1 ? "" : "s"}:
${filePathsChunk.map((filePath) => ` ${filePath}`).join("\n")}`
);
await Promise.all(
filePathsChunk.map(async (filePath) => {
try {
await FsUtil.rm(filePath, { force: true });
} catch (error) {
this.prefixedLogger.error(`${filePath}: failed to delete: ${error}`);
}
})
);
}
}
async backupFiles(backupDir, filePaths) {
const semaphore = new MappableSemaphore(this.options.getWriterThreads());
await semaphore.map(filePaths, async (filePath) => {
this.progressBar.incrementInProgress();
let backupPath = path.join(backupDir, path.basename(filePath));
let increment = 0;
while (await FsUtil.exists(backupPath)) {
increment += 1;
const { name, ext } = path.parse(filePath);
backupPath = path.join(backupDir, `${name} (${increment})${ext}`);
}
this.prefixedLogger.info(`moving cleaned path: ${filePath} -> ${backupPath}`);
const backupPathDir = path.dirname(backupPath);
if (!await FsUtil.exists(backupPathDir)) {
await FsUtil.mkdir(backupPathDir, { recursive: true });
}
try {
await FsUtil.mv(filePath, backupPath);
} catch (error) {
this.prefixedLogger.warn(`failed to move ${filePath} -> ${backupPath}: ${error}`);
}
this.progressBar.incrementCompleted();
});
}
static async getEmptyDirs(dirsToClean) {
if (Array.isArray(dirsToClean)) {
return (await async.mapLimit(
dirsToClean,
Defaults.MAX_FS_THREADS,
async (dirToClean) => await this.getEmptyDirs(dirToClean)
)).flat().reduce(ArrayUtil.reduceUnique(), []);
}
if (!await FsUtil.exists(dirsToClean)) {
return [];
}
const subPaths = (await fs.promises.readdir(dirsToClean)).filter((basename) => isNotJunk(basename)).map((basename) => path.join(dirsToClean, basename));
const subDirs = [];
const subFiles = [];
await async.mapLimit(subPaths, Defaults.MAX_FS_THREADS, async (subPath) => {
if (await FsUtil.isDirectory(subPath)) {
subDirs.push(subPath);
} else {
subFiles.push(subPath);
}
});
if (subDirs.length === 0 && subFiles.length === 0) {
return [dirsToClean];
}
return (await async.mapLimit(
subDirs,
Defaults.MAX_FS_THREADS,
async (subDir) => await this.getEmptyDirs(subDir)
)).flat();
}
}
export {
DirectoryCleaner as default
};
//# sourceMappingURL=directoryCleaner.js.map