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

214 lines
7.7 KiB
JavaScript

(function (app) {
'use strict';
var dom = app.dom;
var util = app.util;
var filesModule = app.modules.files;
var ARCHIVE_DIR_NAME = '.archive';
function sanitizeUrlSegment(value, fallback) {
var str = (value || '').toString().trim();
if (!str) {
return fallback;
}
var cleaned = str
.replace(/[^A-Za-z0-9._~-]+/g, '-')
.replace(/-+/g, '-')
.replace(/^-+|-+$/g, '');
return cleaned || fallback;
}
function encodeRelativePath(path) {
return path.split('/').map(function (segment) {
return encodeURIComponent(segment);
}).join('/');
}
function buildRedirectHtml(targetHref, displayName) {
var safeHrefAttr = util.escapeHtmlAttribute(targetHref);
var safeLabel = util.escapeHtml(displayName || targetHref);
return '<!DOCTYPE html>\n' +
'<html lang="en">\n' +
'<head>\n' +
'<meta charset="utf-8">\n' +
'<meta http-equiv="refresh" content="0; url=' + safeHrefAttr + '">\n' +
'<title>' + safeLabel + '</title>\n' +
'</head>\n' +
'<body>\n' +
'<p>Redirecting to <a href="' + safeHrefAttr + '">' + safeLabel + '</a>.</p>\n' +
'</body>\n' +
'</html>\n';
}
async function ensureArchiveDirectory(rootHandle) {
return rootHandle.getDirectoryHandle(ARCHIVE_DIR_NAME, { create: true });
}
async function writeRedirectFile(directoryHandle, filename, html) {
var fileHandle = await directoryHandle.getFileHandle(filename, { create: true });
var writable = await fileHandle.createWritable();
await writable.write(new Blob([html], { type: 'text/html' }));
await writable.close();
}
function groupFilesByTracking(files) {
var map = new Map();
files.forEach(function (file) {
var key = file.trackingNumber || '';
if (!map.has(key)) {
map.set(key, []);
}
map.get(key).push(file);
});
return map;
}
function determineLatestByTracking(grouped) {
var latest = new Map();
grouped.forEach(function (files, tracking) {
if (!files || !files.length) {
return;
}
var sorted = files.slice().sort(function (a, b) {
var revisionCompare = util.compareRevisionPriority(a.revision, b.revision);
if (revisionCompare !== 0) {
return revisionCompare;
}
var pathCompare = (a.path || '').localeCompare(b.path || '');
if (pathCompare !== 0) {
return pathCompare;
}
return (a.name || '').localeCompare(b.name || '');
});
latest.set(tracking, sorted[sorted.length - 1]);
});
return latest;
}
function buildRedirectFilenames(file) {
var trackingSegment = sanitizeUrlSegment(file.trackingNumber || '', 'tracking');
var rawRevision = (file.revision || '').trim();
var revisionSegment = rawRevision ? sanitizeUrlSegment(rawRevision, 'rev') : '';
var rawHash = (file.sha256 || '').trim();
var hashSegment = rawHash ? sanitizeUrlSegment(rawHash, 'hash') : '';
var htmlSuffix = '.html';
var revisionFilename = revisionSegment ? (trackingSegment + '_' + revisionSegment + htmlSuffix) : '';
var hashFilename = hashSegment ? (hashSegment + htmlSuffix) : '';
var latestFilename = trackingSegment + htmlSuffix;
return {
latestFilename: latestFilename,
revisionFilename: revisionFilename,
hashFilename: hashFilename
};
}
async function resolveFileMetadata(entry) {
var parsed = zddc.parseFilename(entry.name || entry.path || '') || {};
if (!parsed.trackingNumber) {
return null;
}
var file = await entry.handle.getFile();
var sha256 = await util.hashFile(file);
return {
path: entry.path,
name: file.name,
extension: parsed.extension || zddc.splitExtension(file.name).extension,
trackingNumber: parsed.trackingNumber,
revision: parsed.revision,
status: parsed.status,
title: parsed.title,
sha256: sha256,
size: file.size
};
}
function buildRelativeHref(relativePath) {
var encoded = encodeRelativePath(relativePath);
return '../' + encoded;
}
async function createRedirectFiles(archiveHandle, file, isLatest) {
var filenames = buildRedirectFilenames(file);
var href = buildRelativeHref(file.path);
var html = buildRedirectHtml(href, file.name);
var writes = [];
if (filenames.revisionFilename) {
writes.push(writeRedirectFile(archiveHandle, filenames.revisionFilename, html));
}
if (filenames.hashFilename) {
writes.push(writeRedirectFile(archiveHandle, filenames.hashFilename, html));
}
if (isLatest) {
writes.push(writeRedirectFile(archiveHandle, filenames.latestFilename, html));
}
await Promise.all(writes);
return true;
}
async function pickIndexDirectory() {
if (typeof window.showDirectoryPicker !== 'function') {
throw new Error('File System Access API showDirectoryPicker is required');
}
return window.showDirectoryPicker();
}
filesModule.generateArchiveRedirects = async function generateArchiveRedirects() {
var rootHandle;
try {
rootHandle = await pickIndexDirectory();
} catch (err) {
if (err && (err.name === 'AbortError' || err.name === 'NotAllowedError')) {
console.log('[transmittal] Create Index cancelled.');
return;
}
throw err;
}
var overallStart = filesModule.nowMs();
var entries = await filesModule.collectDirectoryEntries(rootHandle);
if (!entries.length) {
console.log('[transmittal] Create Index: No files found.');
return;
}
var archiveHandle = await ensureArchiveDirectory(rootHandle);
var files = [];
for (var i = 0; i < entries.length; i++) {
try {
var metadata = await resolveFileMetadata(entries[i]);
if (metadata) {
files.push(metadata);
}
} catch (err) {
console.warn('[transmittal] create-index skip', entries[i].path, err);
}
}
if (!files.length) {
console.log('[transmittal] Create Index: No ZDDC files found.');
return;
}
var grouped = groupFilesByTracking(files);
var latest = determineLatestByTracking(grouped);
var writePromises = files.map(function (file) {
var isLatest = latest.get(file.trackingNumber) === file;
return createRedirectFiles(archiveHandle, file, isLatest)
.catch(function (err) {
console.error('[transmittal] create-index write failed', file.path, err);
return null;
});
});
var results = await Promise.all(writePromises);
var created = results.filter(function (result) { return result !== null; }).length;
var totalElapsed = filesModule.nowMs() - overallStart;
console.log('[transmittal] Create Index: Generated redirects for ' + created + ' files in ' + filesModule.formatDuration(totalElapsed) + '.');
};
})(window.transmittalApp);