adding suport for CRC32 SFV
This commit is contained in:
@@ -9,8 +9,8 @@ export class ComputeHashIndexCache implements ComputeInterface {
|
||||
hashComputer: HashComputer;
|
||||
|
||||
constructor(indexFilePath: string,private targetHash: CID_ALGORITHM_NAMES[] = [CID_ALGORITHM_NAMES.sha1, CID_ALGORITHM_NAMES.sha256],workerPath?:string) {
|
||||
this.hashComputer = new HashComputer(this.targetHash,workerPath);
|
||||
this.hashIndexManager = new HashIndexManager(indexFilePath);
|
||||
this.hashComputer = new HashComputer(targetHash,workerPath);
|
||||
this.hashIndexManager = new HashIndexManager(indexFilePath,targetHash);
|
||||
}
|
||||
|
||||
async computeMissingHash(filePath: string, metadata: MultiHashData): Promise<void> {
|
||||
|
||||
+72
-43
@@ -12,6 +12,7 @@ interface IndexLine extends Partial<Record<CID_ALGORITHM_NAMES, string>> {
|
||||
size: string;
|
||||
mtime: string;
|
||||
}
|
||||
|
||||
export const INDEX_HEADERS = ['path', 'size', 'mtime'];
|
||||
|
||||
export class HashIndexManager {
|
||||
@@ -22,11 +23,16 @@ export class HashIndexManager {
|
||||
private lastCacheFile: IndexLine[]; //state of the file last time it was read
|
||||
private indexOpsInProgress: boolean = false;
|
||||
private hasChanged: boolean = false;
|
||||
private initialLoad;
|
||||
private initialLoad: Promise<void>;
|
||||
private filePaths: { [key in CID_ALGORITHM_NAMES]?: string } = {};
|
||||
|
||||
constructor(private filePath: string,private targetHash:CID_ALGORITHM_NAMES[]= [CID_ALGORITHM_NAMES.sha1,CID_ALGORITHM_NAMES.sha256]) {
|
||||
if (!filePath) {
|
||||
throw new Error('Invalid index file path');
|
||||
constructor(filePath: string, private targetHash: CID_ALGORITHM_NAMES[] = [CID_ALGORITHM_NAMES.sha1, CID_ALGORITHM_NAMES.sha256]) {
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,19 +44,21 @@ export class HashIndexManager {
|
||||
* After init consseutively calls to this method will not reload the index
|
||||
* @param autosave
|
||||
*/
|
||||
async init(autosave = true) {
|
||||
public async init(autosave = true) {
|
||||
if (!this.initialLoad) {
|
||||
this.initialLoad = new Promise<void>(async (resolve, reject) => {
|
||||
try {
|
||||
if (!this.checkCSVHeaders(this.filePath)) {
|
||||
throw new Error('Invalid index file headers');
|
||||
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);
|
||||
}
|
||||
await this.loadIndex();
|
||||
if (autosave) {
|
||||
this.start();
|
||||
}
|
||||
resolve();
|
||||
}catch (e) {
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
@@ -59,7 +67,7 @@ export class HashIndexManager {
|
||||
}
|
||||
|
||||
// Function to check CSV headers
|
||||
checkCSVHeaders (csvContent: string): boolean {
|
||||
private checkCSVHeaders(csvContent: string, hash: CID_ALGORITHM_NAMES): boolean {
|
||||
const records = parseSync(csvContent, {
|
||||
bom: true,
|
||||
columns: true,
|
||||
@@ -76,7 +84,7 @@ export class HashIndexManager {
|
||||
|
||||
// Define required headers
|
||||
//from the IndexLine interface
|
||||
const requiredHeaders = ['path', 'size', 'mtime', ...this.targetHash];
|
||||
const requiredHeaders = [...INDEX_HEADERS, hash];
|
||||
|
||||
// Check if all required headers are present
|
||||
return requiredHeaders.every(header => headers.includes(header));
|
||||
@@ -98,15 +106,21 @@ export class HashIndexManager {
|
||||
this.intervalId = setInterval(() => this.saveCacheToFile(), time);
|
||||
}
|
||||
|
||||
public async loadIndex(): Promise<IndexLine[]> {
|
||||
if (await existsAsync(this.filePath)) {
|
||||
public async loadIndex(hash: CID_ALGORITHM_NAMES): Promise<IndexLine[]> {
|
||||
if (await existsAsync(this.filePaths[hash])) {
|
||||
// check the file size and if it did not change, do not read the file
|
||||
const stats = await fs.stat(this.filePath);
|
||||
const stats = await fs.stat(this.filePaths[hash]);
|
||||
if (this.lastIndexFileSize !== stats.size) {
|
||||
// Read existing file content and parse it
|
||||
const records: IndexLine[] = await this.readCsv();
|
||||
const records: IndexLine[] = await this.readCsv(hash);
|
||||
for (const record of records) {
|
||||
this.cache.set(record.path, record);
|
||||
let indexLine = this.cache.get(record.path);
|
||||
if (!indexLine) {
|
||||
this.cache.set(record.path, record);
|
||||
} else {
|
||||
//update the cache with the latest data
|
||||
indexLine[hash] = record[hash];
|
||||
}
|
||||
}
|
||||
this.lastIndexFileSize = stats.size;
|
||||
this.lastCacheFile = records;
|
||||
@@ -122,8 +136,8 @@ export class HashIndexManager {
|
||||
return Array.from(this.cache.values());
|
||||
}
|
||||
|
||||
private async readCsv(): Promise<IndexLine[]> {
|
||||
if (!(await existsAsync(this.filePath))) {
|
||||
private async readCsv(hash: CID_ALGORITHM_NAMES): Promise<IndexLine[]> {
|
||||
if (!(await existsAsync(this.filePaths[hash]))) {
|
||||
return [];
|
||||
}
|
||||
const start = performance.now();
|
||||
@@ -136,13 +150,13 @@ export class HashIndexManager {
|
||||
const records: IndexLine[] = [];
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
createReadStream(this.filePath)
|
||||
createReadStream(this.filePaths[hash])
|
||||
.pipe(parser)
|
||||
.on('data', (record) => {
|
||||
records.push(record);
|
||||
}).on('end', () => {
|
||||
resolve(records);
|
||||
console.log(`Index read time ${performance.now() - start}ms`);
|
||||
console.log(`Index read ${hash} time ${performance.now() - start}ms`);
|
||||
}).on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
@@ -156,27 +170,29 @@ export class HashIndexManager {
|
||||
this.hasChanged = false;
|
||||
this.indexOpsInProgress = true;
|
||||
const start = performance.now();
|
||||
let existingRows: IndexLine[] = await this.loadIndex();
|
||||
|
||||
if (this.cache.size !== 0) {
|
||||
const cacheRows: IndexLine[] = this.loadIndexFromCache();
|
||||
// Filter out cacheRows that are already in the file
|
||||
const newRows = cacheRows.filter(row => !existingRows.find(existingRow => existingRow.path === row.path));
|
||||
for (const hash of this.targetHash) {
|
||||
let existingRows: IndexLine[] = await this.loadIndex(hash);
|
||||
if (this.cache.size !== 0) {
|
||||
const cacheRows: IndexLine[] = this.loadIndexFromCache();
|
||||
// Filter out cacheRows that are already in the file
|
||||
const newRows = cacheRows.filter(row => !existingRows.find(existingRow => existingRow.path === row.path));
|
||||
|
||||
if (newRows.length !== 0) {
|
||||
// Serialize new cacheRows to CSV string
|
||||
const csvString = 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'},
|
||||
...this.targetHash.map(hash => ({key: hash, header: hash})),
|
||||
],
|
||||
});
|
||||
if (newRows.length !== 0) {
|
||||
// Serialize new cacheRows to CSV string
|
||||
const csvString = 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},
|
||||
],
|
||||
});
|
||||
|
||||
// Append new cacheRows to the file
|
||||
await fs.appendFile(this.filePath, csvString);
|
||||
// Append new cacheRows to the file
|
||||
await fs.appendFile(this.filePaths[hash], csvString);
|
||||
}
|
||||
}
|
||||
}
|
||||
const totalTime = performance.now() - start;
|
||||
@@ -192,13 +208,13 @@ export class HashIndexManager {
|
||||
public getCidForFile(filePath: string, fileSize: number, mtime: string): IndexLine {
|
||||
const fileName = path.basename(filePath);
|
||||
let fileNameIndex = this.cache.get(fileName);
|
||||
if(fileNameIndex) {
|
||||
if(fileNameIndex.mtime) {
|
||||
if (fileNameIndex) {
|
||||
if (fileNameIndex.mtime) {
|
||||
//if we have a mtime, we need to check it
|
||||
if (fileNameIndex.size === (fileSize + "") && fileNameIndex.mtime === mtime) {
|
||||
return fileNameIndex;
|
||||
}
|
||||
}else {
|
||||
} else {
|
||||
//mtime is optional
|
||||
if (fileNameIndex.size === (fileSize + "")) {
|
||||
return fileNameIndex;
|
||||
@@ -219,10 +235,23 @@ export class HashIndexManager {
|
||||
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 = path.basename(filePath);
|
||||
const data = {path: baseName, size: size, mtime: mtime, ...hashs};
|
||||
this.cache.set(baseName, data);
|
||||
let indexLine = this.cache.get(baseName);
|
||||
if (!indexLine) {
|
||||
const data = {path: baseName, size: size, mtime: mtime, ...hashs};
|
||||
this.cache.set(baseName, data);
|
||||
} else {
|
||||
//update the cache with the latest data
|
||||
for (const hash of this.targetHash) {
|
||||
indexLine[hash] = hashs[hash];
|
||||
}
|
||||
}
|
||||
this.hasChanged = true;
|
||||
}
|
||||
|
||||
|
||||
+17
-10
@@ -3,33 +3,40 @@ export interface MultiHashData {
|
||||
cid_sha1?: string; // 0x11 sha1
|
||||
cid_md5?: string; // 0xd5 md5
|
||||
"cid_sha3-256"?: string; // 0x16 sha3-256
|
||||
//cid_crc32?: string; //0x0132 CRC32 (SFV) TODO
|
||||
cid_crc32?: string; //0x0132 CRC32 (SFV)
|
||||
}
|
||||
|
||||
export interface ComputeInterface {
|
||||
computeMissingHash(filePath: string, metadata: MultiHashData): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* according to
|
||||
* https://ipfs.io/ipfs/QmXec1jjwzxWJoNbxQF5KffL8q6hFXm9QwUGaa3wKGk6dT/#title=Multicodecs&src=https://raw.githubusercontent.com/multiformats/multicodec/master/table.csv
|
||||
*/
|
||||
export enum CID_ALGORITHM_CODES {
|
||||
sha256 = 0x12,
|
||||
sha1 = 0x11,
|
||||
crc32 = 0x0132,
|
||||
md5 = 0xd5,
|
||||
sha1 = 0x11,
|
||||
sha256 = 0x12,
|
||||
sha3_256 = 0x16,
|
||||
//crc32 = 0x0132
|
||||
sha3_384 = 0x15,
|
||||
}
|
||||
|
||||
export enum CID_ALGORITHM_NAMES {
|
||||
sha256 = 'cid_sha2-256',
|
||||
sha1 = 'cid_sha1',
|
||||
crc32 = 'cid_crc32',
|
||||
md5 = 'cid_md5',
|
||||
sha1 = 'cid_sha1',
|
||||
sha256 = 'cid_sha2-256',
|
||||
sha3_256 = 'cid_sha3-256',
|
||||
//crc32 = 'cid_crc32'
|
||||
sha3_384 = 'cid_sha3-384',
|
||||
}
|
||||
|
||||
export enum CID_ALGORITHM {
|
||||
sha256 = 'sha2-256',
|
||||
sha1 = 'sha1',
|
||||
crc32 = 'crc32',
|
||||
md5 = 'md5',
|
||||
sha1 = 'sha1',
|
||||
sha256 = 'sha2-256',
|
||||
sha3_256 = 'sha3-256',
|
||||
//crc32 = 'crc32'
|
||||
sha3_384 = 'sha3-384',
|
||||
}
|
||||
@@ -1,14 +1,33 @@
|
||||
import { createReadStream } from "fs";
|
||||
import {BinaryLike, createHash} from 'crypto';
|
||||
import { CID } from 'multiformats/cid';
|
||||
import { create } from "multiformats/hashes/digest";
|
||||
import {CID_ALGORITHM, CID_ALGORITHM_CODES, CID_ALGORITHM_NAMES} from "../MultiHashData";
|
||||
import {createReadStream} from "fs";
|
||||
import {createHash} from 'crypto';
|
||||
import {CID} from 'multiformats/cid';
|
||||
import {create} from "multiformats/hashes/digest";
|
||||
import crc32 from 'crc-32';
|
||||
import {CID_ALGORITHM_CODES, CID_ALGORITHM_NAMES} from "../MultiHashData";
|
||||
|
||||
export interface SimpleHash {
|
||||
update(data: BinaryLike),
|
||||
update(data: Buffer),
|
||||
|
||||
digest(): Buffer;
|
||||
}
|
||||
|
||||
class Crc32Hash implements SimpleHash {
|
||||
private _crc: number = undefined;
|
||||
|
||||
update(data: Buffer): Crc32Hash {
|
||||
// Update the CRC-32 checksum with the new chunk of data
|
||||
this._crc = crc32.buf(data, this._crc);
|
||||
return this;
|
||||
}
|
||||
|
||||
digest(): Buffer {
|
||||
console.log((this._crc>>>0).toString(16)) ;
|
||||
const buffer = Buffer.alloc(4); // Create a buffer of 4 bytes (32 bits)
|
||||
buffer.writeInt32BE(this._crc, 0); // Write the unsigned integer to the buffer in big-endian format
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Compute the CIDs of a file using specific algorithms
|
||||
@@ -16,13 +35,19 @@ export interface SimpleHash {
|
||||
* @param algorithms Array of algorithms ('sha256', 'sha1')
|
||||
* @returns Array of CIDs (in the order of the algorithms)
|
||||
*/
|
||||
export default async function computeCIDs({ filePath, algorithms }: { filePath: string; algorithms: CID_ALGORITHM_NAMES[] }): Promise<string[]> {
|
||||
export default async function computeCIDs({filePath, algorithms}: {
|
||||
filePath: string;
|
||||
algorithms: CID_ALGORITHM_NAMES[]
|
||||
}): Promise<string[]> {
|
||||
const hashFunctions: Record<string, { code: number, hasher: SimpleHash }>
|
||||
= {
|
||||
[CID_ALGORITHM_NAMES.sha256]: { code: CID_ALGORITHM_CODES.sha256, hasher: createHash('sha256') },
|
||||
[CID_ALGORITHM_NAMES.sha1]: { code: CID_ALGORITHM_CODES.sha1, hasher: createHash('sha1') },
|
||||
[CID_ALGORITHM_NAMES.md5]: { code: CID_ALGORITHM_CODES.md5, hasher: createHash('md5') },
|
||||
[CID_ALGORITHM_NAMES.sha3_256]: { code: CID_ALGORITHM_CODES.sha3_256, hasher:createHash('sha3-256') }
|
||||
[CID_ALGORITHM_NAMES.sha256]: {code: CID_ALGORITHM_CODES.sha256, hasher: createHash('sha256')},
|
||||
[CID_ALGORITHM_NAMES.sha1]: {code: CID_ALGORITHM_CODES.sha1, hasher: createHash('sha1')},
|
||||
[CID_ALGORITHM_NAMES.md5]: {code: CID_ALGORITHM_CODES.md5, hasher: createHash('md5')},
|
||||
[CID_ALGORITHM_NAMES.sha3_256]: {code: CID_ALGORITHM_CODES.sha3_256, hasher: createHash('sha3-256')},
|
||||
[CID_ALGORITHM_NAMES.sha3_384]: {code: CID_ALGORITHM_CODES.sha3_384, hasher: createHash('sha3-384')},
|
||||
[CID_ALGORITHM_NAMES.crc32]: {code: CID_ALGORITHM_CODES.crc32, hasher: new Crc32Hash()}
|
||||
|
||||
};
|
||||
|
||||
const hashers = algorithms.map(algo => ({
|
||||
@@ -33,11 +58,11 @@ export default async function computeCIDs({ filePath, algorithms }: { filePath:
|
||||
const stream = createReadStream(filePath);
|
||||
for await (const chunk of stream) {
|
||||
for (const item of hashers) {
|
||||
item.hasher.update(chunk);
|
||||
item.hasher.update(chunk as Buffer);
|
||||
}
|
||||
}
|
||||
|
||||
const cids = await Promise.all(hashers.map(async ({ code, hasher }) => {
|
||||
const cids = await Promise.all(hashers.map(async ({code, hasher}) => {
|
||||
const hashBuffer = hasher.digest();
|
||||
const digest = create(code, hashBuffer);
|
||||
return CID.createV1(code, digest).toString();
|
||||
|
||||
Reference in New Issue
Block a user