Prevent duplicate settings auto-backups

This commit is contained in:
Cohee 2024-07-01 00:21:43 +03:00
parent 1b6e816525
commit f53c77b9d8
1 changed files with 57 additions and 6 deletions

View File

@ -3,7 +3,7 @@ const path = require('path');
const express = require('express'); const express = require('express');
const _ = require('lodash'); const _ = require('lodash');
const writeFileAtomicSync = require('write-file-atomic').sync; const writeFileAtomicSync = require('write-file-atomic').sync;
const { PUBLIC_DIRECTORIES, SETTINGS_FILE } = require('../constants'); const { SETTINGS_FILE } = require('../constants');
const { getConfigValue, generateTimestamp, removeOldBackups } = require('../util'); const { getConfigValue, generateTimestamp, removeOldBackups } = require('../util');
const { jsonParser } = require('../express-common'); const { jsonParser } = require('../express-common');
const { getAllUserHandles, getUserDirectories } = require('../users'); const { getAllUserHandles, getUserDirectories } = require('../users');
@ -27,12 +27,12 @@ const AUTOSAVE_FUNCTIONS = new Map();
*/ */
function triggerAutoSave(handle) { function triggerAutoSave(handle) {
if (!AUTOSAVE_FUNCTIONS.has(handle)) { if (!AUTOSAVE_FUNCTIONS.has(handle)) {
const throttledAutoSave = _.throttle(() => backupUserSettings(handle), AUTOSAVE_INTERVAL); const throttledAutoSave = _.throttle(() => backupUserSettings(handle, true), AUTOSAVE_INTERVAL);
AUTOSAVE_FUNCTIONS.set(handle, throttledAutoSave); AUTOSAVE_FUNCTIONS.set(handle, throttledAutoSave);
} }
const functionToCall = AUTOSAVE_FUNCTIONS.get(handle); const functionToCall = AUTOSAVE_FUNCTIONS.get(handle);
if (functionToCall) { if (functionToCall && typeof functionToCall === 'function') {
functionToCall(); functionToCall();
} }
} }
@ -113,7 +113,7 @@ async function backupSettings() {
const userHandles = await getAllUserHandles(); const userHandles = await getAllUserHandles();
for (const handle of userHandles) { for (const handle of userHandles) {
backupUserSettings(handle); backupUserSettings(handle, true);
} }
} catch (err) { } catch (err) {
console.log('Could not backup settings file', err); console.log('Could not backup settings file', err);
@ -123,13 +123,18 @@ async function backupSettings() {
/** /**
* Makes a backup of the user's settings file. * Makes a backup of the user's settings file.
* @param {string} handle User handle * @param {string} handle User handle
* @param {boolean} preventDuplicates Prevent duplicate backups
* @returns {void} * @returns {void}
*/ */
function backupUserSettings(handle) { function backupUserSettings(handle, preventDuplicates) {
const userDirectories = getUserDirectories(handle); const userDirectories = getUserDirectories(handle);
const backupFile = path.join(userDirectories.backups, `${getFilePrefix(handle)}${generateTimestamp()}.json`); const backupFile = path.join(userDirectories.backups, `${getFilePrefix(handle)}${generateTimestamp()}.json`);
const sourceFile = path.join(userDirectories.root, SETTINGS_FILE); const sourceFile = path.join(userDirectories.root, SETTINGS_FILE);
if (preventDuplicates && isDuplicateBackup(handle, sourceFile)) {
return;
}
if (!fs.existsSync(sourceFile)) { if (!fs.existsSync(sourceFile)) {
return; return;
} }
@ -138,6 +143,52 @@ function backupUserSettings(handle) {
removeOldBackups(userDirectories.backups, `settings_${handle}`); removeOldBackups(userDirectories.backups, `settings_${handle}`);
} }
/**
* Checks if the backup would be a duplicate.
* @param {string} handle User handle
* @param {string} sourceFile Source file path
* @returns {boolean} True if the backup is a duplicate
*/
function isDuplicateBackup(handle, sourceFile) {
const latestBackup = getLatestBackup(handle);
if (!latestBackup) {
return false;
}
return areFilesEqual(latestBackup, sourceFile);
}
/**
* Returns true if the two files are equal.
* @param {string} file1 File path
* @param {string} file2 File path
*/
function areFilesEqual(file1, file2) {
if (!fs.existsSync(file1) || !fs.existsSync(file2)) {
return false;
}
const content1 = fs.readFileSync(file1);
const content2 = fs.readFileSync(file2);
return content1.toString() === content2.toString();
}
/**
* Gets the latest backup file for a user.
* @param {string} handle User handle
* @returns {string|null} Latest backup file. Null if no backup exists.
*/
function getLatestBackup(handle) {
const userDirectories = getUserDirectories(handle);
const backupFiles = fs.readdirSync(userDirectories.backups)
.filter(x => x.startsWith(getFilePrefix(handle)))
.map(x => ({ name: x, ctime: fs.statSync(path.join(userDirectories.backups, x)).ctimeMs }));
const latestBackup = backupFiles.sort((a, b) => b.ctime - a.ctime)[0]?.name;
if (!latestBackup) {
return null;
}
return path.join(userDirectories.backups, latestBackup);
}
const router = express.Router(); const router = express.Router();
router.post('/save', jsonParser, function (request, response) { router.post('/save', jsonParser, function (request, response) {
@ -265,7 +316,7 @@ router.post('/load-snapshot', jsonParser, async (request, response) => {
router.post('/make-snapshot', jsonParser, async (request, response) => { router.post('/make-snapshot', jsonParser, async (request, response) => {
try { try {
backupUserSettings(request.user.profile.handle); backupUserSettings(request.user.profile.handle, false);
response.sendStatus(204); response.sendStatus(204);
} catch (error) { } catch (error) {
console.log(error); console.log(error);