feat: add a timestamp to flash backup (#877)

* feat: add a timestamp to flash backup

* feat: update gitignore

* feat: random interval is now 30 minutes
This commit is contained in:
Eli Bosley
2024-05-06 13:40:42 -04:00
committed by GitHub
parent 1f4c64d022
commit 1d944781cf
8 changed files with 163 additions and 24 deletions

4
.gitignore vendored
View File

@@ -83,4 +83,6 @@ deploy/*
.cache
.output
.env*
!.env.example
!.env.example
fb_keepalive

11
api/.env.test Normal file
View File

@@ -0,0 +1,11 @@
VERSION="THIS_WILL_BE_REPLACED_WHEN_BUILT"
PATHS_UNRAID_DATA=./dev/data # Where we store plugin data (e.g. permissions.json)
PATHS_STATES=./dev/states # Where .ini files live (e.g. vars.ini)
PATHS_DYNAMIX_BASE=./dev/dynamix # Dynamix's data directory
PATHS_DYNAMIX_CONFIG=./dev/dynamix/dynamix.cfg # Dynamix's config file
PATHS_MY_SERVERS_CONFIG=./dev/Unraid.net/myservers.cfg # My servers config file
PATHS_MY_SERVERS_FB=./dev/Unraid.net/fb_keepalive # My servers flashbackup timekeeper file
PATHS_KEYFILE_BASE=./dev/Unraid.net # Keyfile location
PORT=5000
NODE_ENV=test

View File

@@ -20,6 +20,7 @@ test('Returns paths', async () => {
"myservers-config",
"myservers-config-states",
"myservers-env",
"myservers-keepalive",
"keyfile-base",
"machine-id",
"log-base",

View File

@@ -34,6 +34,7 @@ export const FIVE_MINUTES_MS = 5 * ONE_MINUTE;
export const TEN_MINUTES_MS = 10 * ONE_MINUTE;
export const THIRTY_MINUTES_MS = 30 * ONE_MINUTE;
export const ONE_HOUR_MS = 60 * ONE_MINUTE;
export const ONE_DAY_MS = ONE_HOUR_MS * 24;
// Seconds
export const ONE_HOUR_SECS = 60 * 60;

View File

@@ -2,29 +2,54 @@ import { createSlice } from '@reduxjs/toolkit';
import { join, resolve as resolvePath } from 'path';
const initialState = {
core: __dirname,
'unraid-api-base': '/usr/local/bin/unraid-api/' as const,
'unraid-data': resolvePath(process.env.PATHS_UNRAID_DATA ?? '/boot/config/plugins/dynamix.my.servers/data/' as const),
'docker-autostart': '/var/lib/docker/unraid-autostart' as const,
'docker-socket': '/var/run/docker.sock' as const,
'parity-checks': '/boot/config/parity-checks.log' as const,
htpasswd: '/etc/nginx/htpasswd' as const,
'emhttpd-socket': '/var/run/emhttpd.socket' as const,
states: resolvePath(process.env.PATHS_STATES ?? '/usr/local/emhttp/state/' as const),
'dynamix-base': resolvePath(process.env.PATHS_DYNAMIX_BASE ?? '/boot/config/plugins/dynamix/' as const),
'dynamix-config': resolvePath(process.env.PATHS_DYNAMIX_CONFIG ?? '/boot/config/plugins/dynamix/dynamix.cfg' as const),
'myservers-base': '/boot/config/plugins/dynamix.my.servers/' as const,
'myservers-config': resolvePath(process.env.PATHS_MY_SERVERS_CONFIG ?? '/boot/config/plugins/dynamix.my.servers/myservers.cfg' as const),
'myservers-config-states': join(resolvePath(process.env.PATHS_STATES ?? '/usr/local/emhttp/state/' as const), 'myservers.cfg' as const),
'myservers-env': '/boot/config/plugins/dynamix.my.servers/env' as const,
'keyfile-base': resolvePath(process.env.PATHS_KEYFILE_BASE ?? '/boot/config' as const),
'machine-id': resolvePath(process.env.PATHS_MACHINE_ID ?? '/var/lib/dbus/machine-id' as const),
'log-base': resolvePath('/var/log/unraid-api/' as const),
'var-run': '/var/run' as const,
core: __dirname,
'unraid-api-base': '/usr/local/bin/unraid-api/' as const,
'unraid-data': resolvePath(
process.env.PATHS_UNRAID_DATA ??
('/boot/config/plugins/dynamix.my.servers/data/' as const)
),
'docker-autostart': '/var/lib/docker/unraid-autostart' as const,
'docker-socket': '/var/run/docker.sock' as const,
'parity-checks': '/boot/config/parity-checks.log' as const,
htpasswd: '/etc/nginx/htpasswd' as const,
'emhttpd-socket': '/var/run/emhttpd.socket' as const,
states: resolvePath(
process.env.PATHS_STATES ?? ('/usr/local/emhttp/state/' as const)
),
'dynamix-base': resolvePath(
process.env.PATHS_DYNAMIX_BASE ??
('/boot/config/plugins/dynamix/' as const)
),
'dynamix-config': resolvePath(
process.env.PATHS_DYNAMIX_CONFIG ??
('/boot/config/plugins/dynamix/dynamix.cfg' as const)
),
'myservers-base': '/boot/config/plugins/dynamix.my.servers/' as const,
'myservers-config': resolvePath(
process.env.PATHS_MY_SERVERS_CONFIG ??
('/boot/config/plugins/dynamix.my.servers/myservers.cfg' as const)
),
'myservers-config-states': join(
resolvePath(
process.env.PATHS_STATES ?? ('/usr/local/emhttp/state/' as const)
),
'myservers.cfg' as const
),
'myservers-env': '/boot/config/plugins/dynamix.my.servers/env' as const,
'myservers-keepalive':
process.env.PATHS_MY_SERVERS_FB ?? ('/boot/config/plugins/dynamix.my.servers/fb_keepalive' as const),
'keyfile-base': resolvePath(
process.env.PATHS_KEYFILE_BASE ?? ('/boot/config' as const)
),
'machine-id': resolvePath(
process.env.PATHS_MACHINE_ID ?? ('/var/lib/dbus/machine-id' as const)
),
'log-base': resolvePath('/var/log/unraid-api/' as const),
'var-run': '/var/run' as const,
};
export const paths = createSlice({
name: 'paths',
initialState,
reducers: {},
name: 'paths',
initialState,
reducers: {},
});

View File

@@ -1,9 +1,10 @@
import { LogCleanupService } from '@app/unraid-api/cron/log-cleanup.service';
import { WriteFlashFileService } from '@app/unraid-api/cron/write-flash-file.service';
import { Module } from '@nestjs/common';
import { ScheduleModule } from '@nestjs/schedule';
@Module({
imports: [ScheduleModule.forRoot()],
providers: [LogCleanupService],
providers: [LogCleanupService, WriteFlashFileService],
})
export class CronModule {}

View File

@@ -0,0 +1,55 @@
import { ONE_DAY_MS, THIRTY_MINUTES_MS } from '@app/consts';
import { sleep } from '@app/core/utils/misc/sleep';
import { convertToFuzzyTime } from '@app/mothership/utils/convert-to-fuzzy-time';
import { getters } from '@app/store/index';
import { Injectable, Logger } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { readFile, writeFile } from 'fs/promises';
@Injectable()
export class WriteFlashFileService {
constructor() {}
private readonly logger = new Logger(WriteFlashFileService.name);
private fileLocation = getters.paths()['myservers-keepalive'];
public randomizeWriteTime = true;
public writeNewTimestamp = async (): Promise<number> => {
const wait = this.randomizeWriteTime
? convertToFuzzyTime(0, THIRTY_MINUTES_MS)
: 0;
await sleep(wait);
const newDate = new Date();
try {
await writeFile(this.fileLocation, newDate.toISOString());
} catch (error) {
this.logger.error(error);
}
return newDate.getTime();
};
public getOrCreateTimestamp = async (): Promise<number> => {
try {
const file = (
await readFile(this.fileLocation, 'utf-8')
).toString();
return Date.parse(file);
} catch (error) {
return await this.writeNewTimestamp();
}
};
@Cron('0 * * * *')
async handleCron() {
try {
const currentDate = new Date().getTime();
const prevDate = await this.getOrCreateTimestamp();
if (currentDate - prevDate > ONE_DAY_MS * 7) {
// Write new timestamp
await this.writeNewTimestamp();
}
} catch (error) {
// File does not exist, write it
await this.writeNewTimestamp();
this.logger.error(error);
}
}
}

View File

@@ -0,0 +1,43 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { WriteFlashFileService } from './write-flash-file.service';
import { readFileSync, writeFileSync } from 'fs';
import { getters } from '@app/store/index';
describe('WriteFlashFileService', () => {
let service: WriteFlashFileService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [WriteFlashFileService],
}).compile();
service = module.get<WriteFlashFileService>(WriteFlashFileService);
service.randomizeWriteTime = false;
});
it('should be defined', () => {
expect(service).toBeDefined();
});
it('should write and update the file when called', async () => {
const timestamp = await service.writeNewTimestamp();
expect(timestamp).toBeGreaterThan(0);
const file = readFileSync(getters.paths()['myservers-keepalive'], 'utf8').toString();
expect(file).toBe(new Date(timestamp).toISOString(), 'file contents match the returned timestamp');
// Now make the file very old
writeFileSync(getters.paths()['myservers-keepalive'], '2021-01-01T00:00:00.000Z');
expect(readFileSync(getters.paths()['myservers-keepalive'], 'utf8').toString()).toBe('2021-01-01T00:00:00.000Z', 'file was updated');
await service.handleCron();
expect(readFileSync(getters.paths()['myservers-keepalive'], 'utf8').toString()).not.toBe('2021-01-01T00:00:00.000Z', 'file was updated');
// Now make the file kind of old (one day )
writeFileSync(getters.paths()['myservers-keepalive'], new Date(Date.now() - (1_000 * 60 * 60 * 24)).toISOString());
const now = Date.now();
await service.handleCron();
const contents = readFileSync(getters.paths()['myservers-keepalive'], 'utf8').toString();
expect(new Date(contents).getTime() + (1_000 * 60 * 60 * 12)).toBeLessThan(new Date(now).getTime(), 'file was updated but is still older than today');
});
});