ZDDC/archive/js/hash.js
2026-06-11 13:32:31 -05:00

194 lines
6.6 KiB
JavaScript

(function() {
'use strict';
// SHA-256 hashing and cache management
const HASH_CACHE_FILENAME = '.hashes.json';
// Calculate SHA-256 hash for a file (delegates to shared/hash.js)
async function calculateFileHash(file) {
return zddc.crypto.sha256File(file);
}
// Load hash cache for a directory
async function loadHashCache(dirHandle) {
try {
const cacheHandle = await dirHandle.getFileHandle(HASH_CACHE_FILENAME);
const cacheFile = await cacheHandle.getFile();
const cacheText = await cacheFile.text();
return JSON.parse(cacheText);
} catch (err) {
// Cache doesn't exist or can't be read
return {};
}
}
// Save hash cache for a directory
async function saveHashCache(dirHandle, cache) {
try {
const cacheHandle = await dirHandle.getFileHandle(HASH_CACHE_FILENAME, { create: true });
const writable = await cacheHandle.createWritable();
await writable.write(JSON.stringify(cache, null, 2));
await writable.close();
return true;
} catch (err) {
console.warn('Unable to save hash cache:', err);
return false;
}
}
// Hash files in a directory with caching
async function hashDirectoryFiles(dirHandle, files) {
const cache = await loadHashCache(dirHandle);
const updatedCache = {};
const results = {};
for (const fileInfo of files) {
try {
const file = await fileInfo.handle.getFile();
const cacheKey = file.name;
const lastModified = file.lastModified;
// Check if we have a cached hash
if (cache[cacheKey] && cache[cacheKey].lastModified === lastModified) {
// Use cached hash
results[fileInfo.id] = cache[cacheKey].hash;
updatedCache[cacheKey] = cache[cacheKey];
} else {
// Calculate new hash
const hash = await calculateFileHash(file);
results[fileInfo.id] = hash;
updatedCache[cacheKey] = {
hash: hash,
lastModified: lastModified,
size: file.size
};
}
} catch (err) {
console.error(`Error hashing file ${fileInfo.name}:`, err);
}
}
// Try to save updated cache
await saveHashCache(dirHandle, updatedCache);
return results;
}
// Add hash information to files
async function addHashesToFiles() {
if (!window.app.files.length) return;
// Hash operations require local file handles — not available in HTTP mode
if (window.app.sourceMode === 'http') {
console.log('Hash operations not available in HTTP mode.');
return;
}
window.app.modules.export.showProgress('Calculating file hashes...', 0, window.app.files.length);
try {
// Group files by directory
const filesByDir = {};
window.app.files.forEach(file => {
const dirPath = file.folderPath;
if (!filesByDir[dirPath]) {
filesByDir[dirPath] = {
handle: null,
files: []
};
}
filesByDir[dirPath].files.push(file);
});
// Find directory handles
for (const dirPath in filesByDir) {
const folder = window.app.transmittalFolders.find(f => f.path === dirPath);
if (folder) {
filesByDir[dirPath].handle = folder.handle;
}
}
// Hash files in each directory
let processed = 0;
for (const dirPath in filesByDir) {
const dirInfo = filesByDir[dirPath];
if (dirInfo.handle) {
const hashes = await hashDirectoryFiles(dirInfo.handle, dirInfo.files);
// Update file objects with hashes
dirInfo.files.forEach(file => {
if (hashes[file.id]) {
file.hash = hashes[file.id];
}
});
}
processed += dirInfo.files.length;
window.app.modules.export.showProgress('Calculating file hashes...', processed, window.app.files.length);
}
window.app.modules.export.hideProgress();
} catch (err) {
window.app.modules.export.hideProgress();
console.error('Error calculating hashes:', err);
}
}
// Verify file integrity
async function verifyFileIntegrity(fileId) {
const file = window.app.files.find(f => f.id === fileId);
if (!file || !file.hash) {
alert('No hash available for this file.');
return;
}
if (!file.handle) {
alert('File integrity verification is not available in HTTP mode.');
return;
}
try {
const fileData = await file.handle.getFile();
const currentHash = await calculateFileHash(fileData);
if (currentHash === file.hash) {
alert('File integrity verified. Hash matches.');
} else {
alert('WARNING: File has been modified! Hash does not match.');
}
} catch (err) {
alert('Error verifying file: ' + err.message);
}
}
// Export hash report
function exportHashReport() {
const headers = ['File Path', 'SHA-256 Hash', 'Size', 'Last Modified'];
const rows = [headers];
window.app.filteredFiles.forEach(file => {
if (file.hash) {
rows.push([
file.path,
file.hash,
window.app.modules.export.formatFileSize(file.size),
new Date(file.modified).toISOString()
]);
}
});
window.app.modules.export.downloadFile(window.app.modules.export.rowsToCSV(rows), 'file-hashes.csv', 'text/csv');
}
window.app.modules.hash = {
calculateFileHash,
loadHashCache,
saveHashCache,
hashDirectoryFiles,
addHashesToFiles,
verifyFileIntegrity,
exportHashReport
};
})();