104 lines
5.8 KiB
JavaScript
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 });
|
|
});
|