Files

104 lines
5.8 KiB
JavaScript

(function initProtectionModel(root, factory) {
const api = factory();
if (typeof module === 'object' && module.exports) module.exports = api;
if (root) root.chimeraProtectionModel = api;
})(typeof window !== 'undefined' ? window : globalThis, function createProtectionModel() {
'use strict';
const list = value => Array.isArray(value) ? value : [];
const value = input => input == null || input === '' ? null : input;
const first = (...inputs) => inputs.map(value).find(item => item != null) ?? null;
const lower = input => String(input || '').trim().toLowerCase();
const missingStates = new Set(['missing', 'unavailable', 'disconnected', 'offline', 'not_mounted', 'absent']);
const failedIntegrityStates = new Set(['failed', 'corrupt', 'mismatch', 'invalid']);
function normalizeCopy(title, copy, index) {
const volume = copy.volume || {};
const device = copy.device || volume.device || copy.storageDevice || {};
const id = first(copy.id, copy.copyId, copy.copy_id);
const volumeId = first(copy.volumeId, copy.volume_id, volume.id);
const deviceId = first(copy.deviceId, copy.device_id, device.id, volume.deviceId, volume.device_id);
const deviceFingerprint = first(copy.deviceFingerprint, copy.stableFingerprint, device.stableFingerprint, device.stable_fingerprint);
const volumeKey = first(copy.stableVolumeKey, copy.stable_volume_key, volume.stableVolumeKey, volume.stable_volume_key);
const deviceKey = deviceId != null ? `device:${deviceId}` : deviceFingerprint ? `fingerprint:${deviceFingerprint}` : 'device:unknown';
const relativePath = first(copy.relativePath, copy.relative_path, copy.path, '');
const mountPath = first(copy.mountPath, copy.mount_path, volume.mountPath, volume.mount_path, '');
const fullPath = mountPath && relativePath
? `${String(mountPath).replace(/[\\/]+$/, '')}/${String(relativePath).replace(/^[\\/]+/, '')}`
: String(first(relativePath, mountPath, 'Unknown path'));
const availabilityState = lower(first(copy.availabilityState, copy.availability_state, volume.availabilityState, volume.availability_state, device.availabilityState, device.availability_state, 'unknown'));
const integrityState = lower(first(copy.integrityState, copy.integrity_state, 'unknown'));
const hashState = lower(first(copy.hashState, copy.hash_state, copy.inventoryHash || copy.inventory_hash ? 'recorded' : 'missing'));
const missing = missingStates.has(availabilityState);
const integrityFailed = failedIntegrityStates.has(integrityState);
return Object.freeze({
copyId: id != null ? String(id) : `unidentified-${index + 1}`,
titleId: String(first(title.game_id, title.titleId, title.title_id, title.id, 'unknown')),
titleName: String(first(title.prospero_name, title.name, title.title, title.game_id, 'Unknown title')),
region: String(first(title.prospero_region, title.region, '')),
deviceId: deviceId == null ? null : String(deviceId),
deviceKey,
deviceName: String(first(copy.deviceName, copy.displayName, device.displayName, device.display_name, deviceFingerprint, 'Unknown device')),
deviceFingerprint: deviceFingerprint == null ? null : String(deviceFingerprint),
volumeId: volumeId == null ? null : String(volumeId),
volumeKey: volumeKey == null ? null : String(volumeKey),
path: fullPath,
sizeBytes: Number(first(copy.sizeBytes, copy.size_bytes, 0)) || 0,
hash: first(copy.inventoryHash, copy.inventory_hash),
hashAlgorithm: String(first(copy.hashAlgorithm, copy.hash_algorithm, 'sha256')),
hashState,
integrityState,
lastVerifiedAt: first(copy.lastVerifiedAt, copy.last_verified_at),
lastSeenAt: first(copy.lastSeenAt, copy.last_seen_at),
availabilityState,
missing,
integrityFailed,
restoreTarget: Object.freeze({
copyId: id != null ? String(id) : `unidentified-${index + 1}`,
titleId: String(first(title.game_id, title.titleId, title.title_id, title.id, 'unknown')),
volumeId: volumeId == null ? null : String(volumeId),
deviceId: deviceId == null ? null : String(deviceId),
path: fullPath
})
});
}
function evaluate(copies, requiredDistinctDevices = 2) {
const concrete = list(copies);
const healthyCopies = concrete.filter(copy => !copy.missing && !copy.integrityFailed);
const knownDevices = new Set(healthyCopies.map(copy => copy.deviceKey).filter(key => key !== 'device:unknown'));
const unknownDeviceCopies = healthyCopies.filter(copy => copy.deviceKey === 'device:unknown').length;
const distinctDevices = knownDevices.size + (unknownDeviceCopies ? 1 : 0);
const required = Math.max(1, Number(requiredDistinctDevices) || 2);
const missingCopies = concrete.filter(copy => copy.missing).length;
const failedCopies = concrete.filter(copy => copy.integrityFailed).length;
const healthy = distinctDevices >= required;
const sameDeviceExcess = Math.max(0, healthyCopies.length - distinctDevices);
const status = healthy ? 'healthy' : healthyCopies.length ? 'needs-diversity' : concrete.length ? 'degraded' : 'unprotected';
return Object.freeze({
copyCount: concrete.length,
healthyCopyCount: healthyCopies.length,
distinctDeviceCount: distinctDevices,
requiredDistinctDevices: required,
missingCopyCount: missingCopies,
failedCopyCount: failedCopies,
sameDeviceExcess,
healthy,
status
});
}
function flatten(titles, options = {}) {
const result = [];
for (const title of list(titles)) {
const copies = list(title.localCopies).map((copy, index) => normalizeCopy(title, copy, index));
const policy = evaluate(copies, options.requiredDistinctDevices);
copies.forEach(copy => result.push(Object.freeze({ ...copy, policy })));
}
return result;
}
return Object.freeze({ normalizeCopy, evaluate, flatten });
});