496 lines
16 KiB
JavaScript
496 lines
16 KiB
JavaScript
'use strict';
|
|
|
|
require('./chunk-KWYLOVWT.cjs');
|
|
require('./chunk-Y5FIE5OK.cjs');
|
|
require('./chunk-OHUFSPVO.cjs');
|
|
var chunkS7BSPELE_cjs = require('./chunk-S7BSPELE.cjs');
|
|
var piscina = require('piscina');
|
|
var fs = require('fs');
|
|
var csvParse = require('csv-parse');
|
|
var sync = require('csv-parse/sync');
|
|
var sync$1 = require('csv-stringify/sync');
|
|
var promises = require('fs/promises');
|
|
var path3 = require('path');
|
|
var timers = require('timers');
|
|
var chokidar = require('chokidar');
|
|
var PQueue = require('p-queue');
|
|
|
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
|
|
function _interopNamespace(e) {
|
|
if (e && e.__esModule) return e;
|
|
var n = Object.create(null);
|
|
if (e) {
|
|
Object.keys(e).forEach(function (k) {
|
|
if (k !== 'default') {
|
|
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
Object.defineProperty(n, k, d.get ? d : {
|
|
enumerable: true,
|
|
get: function () { return e[k]; }
|
|
});
|
|
}
|
|
});
|
|
}
|
|
n.default = e;
|
|
return Object.freeze(n);
|
|
}
|
|
|
|
var path3__namespace = /*#__PURE__*/_interopNamespace(path3);
|
|
var chokidar__default = /*#__PURE__*/_interopDefault(chokidar);
|
|
var PQueue__default = /*#__PURE__*/_interopDefault(PQueue);
|
|
|
|
var distFolder = undefined;
|
|
distFolder = distFolder.replace("src", "dist");
|
|
distFolder = distFolder + "/worker.js";
|
|
var workerUrl = new URL(distFolder).href;
|
|
console.log("Worker URL: ", workerUrl);
|
|
var FileIDComputerWorker = class {
|
|
piscina;
|
|
constructor(workerPath) {
|
|
this.piscina = new piscina.Piscina({
|
|
maxThreads: 4,
|
|
//filename: new URL('./ShaComputeWorker.ts', import.meta.url).href
|
|
filename: workerPath || process.env.WORKER_URL || workerUrl
|
|
});
|
|
}
|
|
/**
|
|
* Compute the CIDs of a file using specific algorithms
|
|
* @param filePath The path to the file
|
|
* @param algorithms Array of algorithms ('sha256', 'sha1')
|
|
* @returns Array of CIDs (in the order of the algorithms)
|
|
*/
|
|
async computeCIDs(filePath, algorithms) {
|
|
return this.piscina.run({ filePath, algorithms });
|
|
}
|
|
};
|
|
|
|
// src/lib/hash-compute/HashComputerWorker.ts
|
|
var HashComputerWorker = class {
|
|
constructor(targetHash, workerPath) {
|
|
this.targetHash = targetHash;
|
|
this.fileIDComputer = new FileIDComputerWorker(workerPath);
|
|
}
|
|
fileIDComputer;
|
|
async computeMissingHash(filePath, metadata) {
|
|
const neededHashes = this.targetHash.filter((hashName) => !metadata[hashName]);
|
|
if (neededHashes.length === 0) {
|
|
return;
|
|
}
|
|
const cids = await this.fileIDComputer.computeCIDs(filePath, neededHashes);
|
|
for (const [index, cid] of cids.entries()) {
|
|
const hashType = neededHashes[index];
|
|
metadata[hashType] = cid;
|
|
}
|
|
}
|
|
};
|
|
async function existsAsync(filePath) {
|
|
try {
|
|
await promises.access(filePath, promises.constants.F_OK);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
var INDEX_HEADERS = ["path", "size", "mtime"];
|
|
var HashIndexManager = class {
|
|
constructor(filePath, targetHash = ["cid_sha1" /* sha1 */, "cid_sha2-256" /* sha256 */]) {
|
|
this.targetHash = targetHash;
|
|
filePath = filePath.replace(".csv", "");
|
|
for (const hash of this.targetHash) {
|
|
this.filePaths[hash] = `${filePath}-${hash}.csv`;
|
|
if (!this.filePaths[hash]) {
|
|
throw new Error(`Invalid index file path for ${hash}`);
|
|
}
|
|
}
|
|
}
|
|
cache = /* @__PURE__ */ new Map();
|
|
intervalId;
|
|
intervalTime = 3e4;
|
|
lastIndexFileSize = 0;
|
|
//size of the index file last time it was read
|
|
lastCacheFile;
|
|
//state of the file last time it was read
|
|
indexOpsInProgress = false;
|
|
hasChanged = false;
|
|
initialLoad;
|
|
filePaths = {};
|
|
getCache() {
|
|
return new Map(this.cache);
|
|
}
|
|
/**
|
|
* After init consseutively calls to this method will not reload the index
|
|
* @param autosave
|
|
*/
|
|
async init(autosave = true) {
|
|
if (!this.initialLoad) {
|
|
this.initialLoad = new Promise(async (resolve, reject) => {
|
|
try {
|
|
for (const hash of this.targetHash) {
|
|
if (!this.checkCSVHeaders(this.filePaths[hash], hash)) {
|
|
throw new Error(`Invalid index file headers for ${hash}`);
|
|
}
|
|
await this.loadIndex(hash);
|
|
}
|
|
if (autosave) {
|
|
this.start();
|
|
}
|
|
resolve();
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
});
|
|
}
|
|
return this.initialLoad;
|
|
}
|
|
// Function to check CSV headers
|
|
checkCSVHeaders(csvContent, hash) {
|
|
const records = sync.parse(csvContent, {
|
|
bom: true,
|
|
columns: true,
|
|
skip_empty_lines: true
|
|
});
|
|
if (records.length === 0) {
|
|
return true;
|
|
}
|
|
const headers = Object.keys(records[0]);
|
|
const requiredHeaders = [...INDEX_HEADERS, hash];
|
|
return requiredHeaders.every((header) => headers.includes(header));
|
|
}
|
|
start() {
|
|
this.startAutoSave(this.intervalTime);
|
|
}
|
|
stopAutoSave() {
|
|
if (this.intervalId) {
|
|
timers.clearInterval(this.intervalId);
|
|
}
|
|
}
|
|
startAutoSave(time) {
|
|
this.stopAutoSave();
|
|
this.intervalTime = time;
|
|
this.intervalId = setInterval(() => this.saveCacheToFile(), time);
|
|
}
|
|
async loadIndex(hash) {
|
|
if (await existsAsync(this.filePaths[hash])) {
|
|
const stats = await fs.promises.stat(this.filePaths[hash]);
|
|
if (this.lastIndexFileSize !== stats.size) {
|
|
const records = await this.readCsv(hash);
|
|
for (const record of records) {
|
|
let indexLine = this.cache.get(record.path);
|
|
if (!indexLine) {
|
|
this.cache.set(record.path, record);
|
|
} else {
|
|
indexLine[hash] = record[hash];
|
|
}
|
|
}
|
|
this.lastIndexFileSize = stats.size;
|
|
this.lastCacheFile = records;
|
|
return records;
|
|
} else {
|
|
return this.lastCacheFile;
|
|
}
|
|
}
|
|
return [];
|
|
}
|
|
loadIndexFromCache() {
|
|
return Array.from(this.cache.values());
|
|
}
|
|
async readCsv(hash) {
|
|
if (!await existsAsync(this.filePaths[hash])) {
|
|
return [];
|
|
}
|
|
const start = performance.now();
|
|
const parser = csvParse.parse({
|
|
columns: true,
|
|
skip_empty_lines: true
|
|
});
|
|
const records = [];
|
|
return new Promise((resolve, reject) => {
|
|
fs.createReadStream(this.filePaths[hash]).pipe(parser).on("data", (record) => {
|
|
records.push(record);
|
|
}).on("end", () => {
|
|
resolve(records);
|
|
console.log(`Index read ${hash} time ${performance.now() - start}ms`);
|
|
}).on("error", (err) => {
|
|
reject(err);
|
|
});
|
|
});
|
|
}
|
|
async saveCacheToFile() {
|
|
if (this.indexOpsInProgress || !this.hasChanged) {
|
|
return;
|
|
}
|
|
this.hasChanged = false;
|
|
this.indexOpsInProgress = true;
|
|
const start = performance.now();
|
|
let cacheRows;
|
|
if (this.cache.size !== 0) {
|
|
cacheRows = this.loadIndexFromCache();
|
|
}
|
|
for (const hash of this.targetHash) {
|
|
let existingRows = await this.loadIndex(hash);
|
|
let existingRowsAsMap = new Map(existingRows.map((row) => [row.path, row]));
|
|
if (this.cache.size !== 0) {
|
|
const newRows = cacheRows.filter((row) => {
|
|
const newRow = !existingRowsAsMap.has(row.path);
|
|
return newRow && !!row[hash];
|
|
});
|
|
if (newRows.length !== 0) {
|
|
const csvString = sync$1.stringify(newRows, {
|
|
header: existingRows.length === 0,
|
|
// Only add header if the file was empty
|
|
columns: [
|
|
{ key: "path", header: "path" },
|
|
{ key: "size", header: "size" },
|
|
{ key: "mtime", header: "mtime" },
|
|
{ key: hash, header: hash }
|
|
]
|
|
});
|
|
await fs.promises.appendFile(this.filePaths[hash], csvString);
|
|
}
|
|
}
|
|
}
|
|
const totalTime = performance.now() - start;
|
|
console.log(`Index saved in ${totalTime}ms`);
|
|
if (totalTime * 10 > this.intervalTime) {
|
|
this.startAutoSave(totalTime * 10);
|
|
console.log(`Index save interval increased to ${totalTime * 10}ms`);
|
|
}
|
|
this.indexOpsInProgress = false;
|
|
}
|
|
async getCidForFileAsync(filePath) {
|
|
const fileName = path3__namespace.default.basename(filePath);
|
|
const stats = await promises.stat(filePath);
|
|
return this.getCidForFile(fileName, stats.size, stats.mtime.toISOString());
|
|
}
|
|
getCidForFile(filePath, fileSize, mtime) {
|
|
const fileName = path3__namespace.default.basename(filePath);
|
|
let fileNameIndex = this.cache.get(fileName);
|
|
for (const hash of this.targetHash) {
|
|
if (!fileNameIndex[hash]) {
|
|
delete fileNameIndex[hash];
|
|
}
|
|
}
|
|
if (fileNameIndex) {
|
|
if (fileNameIndex.mtime) {
|
|
if (fileNameIndex.size === fileSize + "" && fileNameIndex.mtime === mtime) {
|
|
return fileNameIndex;
|
|
}
|
|
} else {
|
|
if (fileNameIndex.size === fileSize + "") {
|
|
return fileNameIndex;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
addFileCid(filePath, fileSize, mtime, hashs) {
|
|
if (!filePath || !fileSize || !mtime || !hashs) {
|
|
throw new Error("Invalid parameters");
|
|
}
|
|
for (const hash of this.targetHash) {
|
|
if (!hashs[hash]) {
|
|
throw new Error(`Missing hash ${hash}`);
|
|
}
|
|
}
|
|
const size = fileSize + "";
|
|
const baseName = path3__namespace.default.basename(filePath);
|
|
let indexLine = this.cache.get(baseName);
|
|
if (!indexLine) {
|
|
let filteredHash = {};
|
|
for (const hash of this.targetHash) {
|
|
filteredHash[hash] = hashs[hash];
|
|
}
|
|
const data = { path: baseName, size, mtime, ...filteredHash };
|
|
this.cache.set(baseName, data);
|
|
} else {
|
|
for (const hash of this.targetHash) {
|
|
indexLine[hash] = hashs[hash];
|
|
}
|
|
}
|
|
this.hasChanged = true;
|
|
}
|
|
};
|
|
var HashComputerIndexCache = class {
|
|
constructor(indexFilePath, targetHash = ["cid_sha1" /* sha1 */, "cid_sha2-256" /* sha256 */], workerPath) {
|
|
this.targetHash = targetHash;
|
|
this.hashComputer = new HashComputerWorker(targetHash, workerPath);
|
|
this.hashIndexManager = new HashIndexManager(indexFilePath, targetHash);
|
|
}
|
|
hashIndexManager;
|
|
hashComputer;
|
|
async computeMissingHash(filePath, metadata) {
|
|
await this.hashIndexManager.init();
|
|
let stats = await promises.stat(filePath);
|
|
if (this.hashIndexManager.getCache().has(path3__namespace.default.basename(filePath))) {
|
|
const indexLine = this.hashIndexManager.getCidForFile(filePath, stats.size, stats.mtime.toISOString());
|
|
if (indexLine) {
|
|
for (const hash of this.targetHash) {
|
|
if (!metadata[hash] && indexLine[hash]) {
|
|
metadata[hash] = indexLine[hash];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
await this.hashComputer.computeMissingHash(filePath, metadata);
|
|
this.hashIndexManager.addFileCid(filePath, stats.size, stats.mtime.toISOString(), metadata);
|
|
}
|
|
async getHashIndexManager() {
|
|
await this.hashIndexManager.init();
|
|
return this.hashIndexManager;
|
|
}
|
|
};
|
|
var FolderWatcher = class {
|
|
constructor(fileProcessor, WATCH_FOLDER_LIST, config) {
|
|
this.WATCH_FOLDER_LIST = WATCH_FOLDER_LIST;
|
|
this.config = config;
|
|
this.fileProcessor = fileProcessor;
|
|
}
|
|
initialized = false;
|
|
queue = new PQueue__default.default({ concurrency: 100, autoStart: true });
|
|
fileProcessor;
|
|
queueSize = -1;
|
|
current = 0;
|
|
processing = /* @__PURE__ */ new Set();
|
|
/**
|
|
* Process the directory and its subdirectories
|
|
* @param directory
|
|
* @param promises
|
|
* @private
|
|
*/
|
|
async processDirectory(directory, promises$1) {
|
|
const entries = await promises.readdir(directory, { withFileTypes: true });
|
|
for (const entry of entries) {
|
|
const fullPath = path3__namespace.join(directory, entry.name);
|
|
if (entry.isDirectory()) {
|
|
await this.processDirectory(fullPath, promises$1);
|
|
} else {
|
|
const promise = this.processFile(fullPath);
|
|
promises$1.push(promise);
|
|
}
|
|
}
|
|
}
|
|
async countFile(directory) {
|
|
const entries = await promises.readdir(directory, { withFileTypes: true });
|
|
const countPromises = [];
|
|
for (const entry of entries) {
|
|
const fullPath = path3__namespace.join(directory, entry.name);
|
|
if (entry.isDirectory()) {
|
|
countPromises.push(this.countFile(fullPath));
|
|
} else {
|
|
countPromises.push(this.queue.add(
|
|
async () => await this.fileProcessor.canProcessFile(fullPath) ? 1 : 0
|
|
));
|
|
}
|
|
}
|
|
const counts = await Promise.all(countPromises);
|
|
return counts.reduce((acc, current) => acc + current, 0);
|
|
}
|
|
async processFile(filePath) {
|
|
if (await this.fileProcessor.canProcessFile(filePath)) {
|
|
if (!this.processing.has(filePath)) {
|
|
this.processing.add(filePath);
|
|
const current = ++this.current;
|
|
await this.queue.add(() => this.fileProcessor.processFile(current, this.queueSize, filePath));
|
|
this.processing.delete(filePath);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Process the file and its sibling file included other folder it is a recursive function
|
|
* @param filePath
|
|
* @private
|
|
*/
|
|
async processFileExtended(filePath) {
|
|
let dirname2 = path3__namespace.dirname(filePath);
|
|
await this.processDirectory(dirname2, []);
|
|
}
|
|
chokidarWatch(folderList) {
|
|
const chokidarconfig = {
|
|
ignoreInitial: true,
|
|
//ignore the initial scan we have our own function
|
|
persistent: true,
|
|
depth: Infinity,
|
|
awaitWriteFinish: {
|
|
stabilityThreshold: this.config.stabilityThreshold || 3e4,
|
|
pollInterval: this.config.pollInterval || 5e3
|
|
}
|
|
};
|
|
let watcher;
|
|
if ((this.config.interval || 0) <= 0) {
|
|
watcher = chokidar__default.default.watch(folderList, chokidarconfig);
|
|
} else {
|
|
watcher = chokidar__default.default.watch(folderList, {
|
|
...chokidarconfig,
|
|
usePolling: true,
|
|
interval: this.config.interval || 0
|
|
});
|
|
}
|
|
watcher.on("add", async (filePath) => {
|
|
try {
|
|
await this.processFileExtended(filePath);
|
|
} catch (error) {
|
|
console.error(`Error processing file ${filePath}:`, error);
|
|
}
|
|
}).on("change", async (filePath) => {
|
|
try {
|
|
await this.processFileExtended(filePath);
|
|
} catch (e) {
|
|
console.error(`Error processing file ${filePath}:`, e);
|
|
}
|
|
}).on("unlink", async (filePath) => {
|
|
try {
|
|
await this.fileProcessor.deleteFile(filePath);
|
|
await this.processFileExtended(filePath);
|
|
} catch (e) {
|
|
console.error(`Error processing file deletion:`, e);
|
|
}
|
|
}).on("error", (error) => console.error(`Watcher error: ${error}`)).on("ready", () => {
|
|
console.log(`Watching for file changes on ${folderList}`);
|
|
});
|
|
}
|
|
async watch() {
|
|
let folders = this.WATCH_FOLDER_LIST;
|
|
if (!folders) {
|
|
throw new Error(`No folder to watch`);
|
|
}
|
|
let folderList = folders.split(",");
|
|
this.chokidarWatch(folderList);
|
|
let countPromises = [];
|
|
for (let i = 0; i < folderList.length; i++) {
|
|
folderList[i] = path3__namespace.normalize(folderList[i]);
|
|
countPromises.push(this.countFile(folderList[i]));
|
|
}
|
|
const count = (await Promise.all(countPromises)).reduce((acc, current) => acc + current, 0);
|
|
console.log(`Found ${count} files to process`);
|
|
this.queueSize = count;
|
|
let promises = [];
|
|
for (let i = 0; i < folderList.length; i++) {
|
|
folderList[i] = path3__namespace.normalize(folderList[i]);
|
|
await this.processDirectory(folderList[i], promises);
|
|
}
|
|
await Promise.all(promises);
|
|
this.initialized = true;
|
|
console.log(`Watcher ready`);
|
|
}
|
|
};
|
|
|
|
Object.defineProperty(exports, "CID_ALGORITHM", {
|
|
enumerable: true,
|
|
get: function () { return chunkS7BSPELE_cjs.CID_ALGORITHM; }
|
|
});
|
|
Object.defineProperty(exports, "CID_ALGORITHM_CODES", {
|
|
enumerable: true,
|
|
get: function () { return chunkS7BSPELE_cjs.CID_ALGORITHM_CODES; }
|
|
});
|
|
Object.defineProperty(exports, "CID_ALGORITHM_NAMES", {
|
|
enumerable: true,
|
|
get: function () { return chunkS7BSPELE_cjs.CID_ALGORITHM_NAMES; }
|
|
});
|
|
exports.FileIDComputerWorker = FileIDComputerWorker;
|
|
exports.FolderWatcher = FolderWatcher;
|
|
exports.HashComputerIndexCache = HashComputerIndexCache;
|
|
exports.HashComputerWorker = HashComputerWorker;
|
|
exports.HashIndexManager = HashIndexManager;
|
|
exports.INDEX_HEADERS = INDEX_HEADERS;
|
|
//# sourceMappingURL=out.js.map
|
|
//# sourceMappingURL=index.cjs.map
|