Add local automatic backups and Aniimo regions lists
All checks were successful
Deploy Sokko G / deploy (push) Successful in 18s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 18s
This commit is contained in:
parent
2255de94a7
commit
0a3bb4dffd
24 changed files with 908 additions and 16 deletions
140
website/src/features/toolboxes/storage/autoBackup.js
Normal file
140
website/src/features/toolboxes/storage/autoBackup.js
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
// Rôle : gère les sauvegardes JSON locales via la File System Access API.
|
||||
|
||||
export const AUTO_BACKUP_SETTING = "autoBackup";
|
||||
export const AUTO_BACKUP_FILE_PATTERN = /^sokkog-global-\d{4}-\d{2}-\d{2}\.json$/;
|
||||
const AUTO_BACKUP_LOCK = "sokkog-auto-backup";
|
||||
|
||||
export function isFileSystemAccessSupported(scope = globalThis) {
|
||||
return Boolean(scope && "showDirectoryPicker" in scope);
|
||||
}
|
||||
|
||||
export function getLocalDateKey(date = new Date()) {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
export function getBackupFileName(date = new Date()) {
|
||||
return `sokkog-global-${getLocalDateKey(date)}.json`;
|
||||
}
|
||||
|
||||
export function normalizeAutoBackupConfig(value) {
|
||||
return {
|
||||
enabled: value?.enabled === true,
|
||||
directoryHandle: value?.directoryHandle || null,
|
||||
lastBackup: typeof value?.lastBackup === "string" ? value.lastBackup : ""
|
||||
};
|
||||
}
|
||||
|
||||
async function withBackupLock(task) {
|
||||
if (globalThis.navigator?.locks?.request) {
|
||||
return globalThis.navigator.locks.request(AUTO_BACKUP_LOCK, { mode: "exclusive" }, task);
|
||||
}
|
||||
return task();
|
||||
}
|
||||
|
||||
async function queryReadWritePermission(directoryHandle) {
|
||||
if (!directoryHandle?.queryPermission) return "denied";
|
||||
return directoryHandle.queryPermission({ mode: "readwrite" });
|
||||
}
|
||||
|
||||
export async function getDirectoryPermission(directoryHandle) {
|
||||
try {
|
||||
return await queryReadWritePermission(directoryHandle);
|
||||
} catch {
|
||||
return "denied";
|
||||
}
|
||||
}
|
||||
|
||||
async function listBackupFiles(directoryHandle) {
|
||||
const files = [];
|
||||
for await (const entry of directoryHandle.values()) {
|
||||
if (entry.kind === "file" && AUTO_BACKUP_FILE_PATTERN.test(entry.name)) files.push(entry.name);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function sortBackupFiles(files) {
|
||||
return [...files].sort((left, right) => right.slice(14, 24).localeCompare(left.slice(14, 24)));
|
||||
}
|
||||
|
||||
async function cleanupBackups(directoryHandle) {
|
||||
const files = sortBackupFiles(await listBackupFiles(directoryHandle));
|
||||
for (const filename of files.slice(10)) await directoryHandle.removeEntry(filename);
|
||||
}
|
||||
|
||||
async function readBackupFile(directoryHandle, filename) {
|
||||
const fileHandle = await directoryHandle.getFileHandle(filename);
|
||||
const file = await fileHandle.getFile();
|
||||
return file.text();
|
||||
}
|
||||
|
||||
function comparableBackupContents(contents) {
|
||||
try {
|
||||
const payload = JSON.parse(contents);
|
||||
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return null;
|
||||
const withoutTimestamp = { ...payload };
|
||||
delete withoutTimestamp.exportedAt;
|
||||
return JSON.stringify(withoutTimestamp);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function backupContentsEqual(left, right) {
|
||||
const comparableLeft = comparableBackupContents(left);
|
||||
const comparableRight = comparableBackupContents(right);
|
||||
return comparableLeft !== null && comparableLeft === comparableRight;
|
||||
}
|
||||
|
||||
async function writeBackupFile(directoryHandle, filename, contents, preserveExisting) {
|
||||
const fileHandle = await directoryHandle.getFileHandle(filename, { create: true });
|
||||
let writable;
|
||||
try {
|
||||
writable = await fileHandle.createWritable();
|
||||
await writable.write(contents);
|
||||
await writable.close();
|
||||
} catch (error) {
|
||||
try { await writable?.abort(); } catch { /* Le fichier peut déjà être fermé. */ }
|
||||
if (!preserveExisting) {
|
||||
try { await directoryHandle.removeEntry(filename); } catch { /* Le fichier peut ne pas avoir été créé. */ }
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveBackup({ directoryHandle, generateJson, date = new Date(), force = false }) {
|
||||
if (!directoryHandle) return { ok: false, reason: "missing-directory" };
|
||||
const permission = await getDirectoryPermission(directoryHandle);
|
||||
if (permission !== "granted") return { ok: false, reason: "permission", permission };
|
||||
const filename = getBackupFileName(date);
|
||||
|
||||
return withBackupLock(async () => {
|
||||
try {
|
||||
const existing = await listBackupFiles(directoryHandle);
|
||||
if (!force && existing.includes(filename)) return { ok: true, created: false, filename };
|
||||
const contents = await generateJson();
|
||||
if (!force && existing.length) {
|
||||
const latestFilename = sortBackupFiles(existing)[0];
|
||||
try {
|
||||
if (backupContentsEqual(contents, await readBackupFile(directoryHandle, latestFilename))) {
|
||||
return { ok: true, created: false, unchanged: true, filename, comparedTo: latestFilename };
|
||||
}
|
||||
} catch {
|
||||
// Une lecture impossible ne doit pas empêcher la création d'une nouvelle version.
|
||||
}
|
||||
}
|
||||
await writeBackupFile(directoryHandle, filename, contents, existing.includes(filename));
|
||||
let cleanupError = "";
|
||||
try {
|
||||
await cleanupBackups(directoryHandle);
|
||||
} catch (error) {
|
||||
cleanupError = error?.message || "Erreur de nettoyage des anciennes sauvegardes.";
|
||||
}
|
||||
return { ok: true, created: true, filename, cleanupError };
|
||||
} catch (error) {
|
||||
return { ok: false, reason: "backup", error };
|
||||
}
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue