mirror of
https://github.com/unraid/api.git
synced 2025-12-30 21:19:49 -06:00
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:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -83,4 +83,6 @@ deploy/*
|
||||
.cache
|
||||
.output
|
||||
.env*
|
||||
!.env.example
|
||||
!.env.example
|
||||
|
||||
fb_keepalive
|
||||
11
api/.env.test
Normal file
11
api/.env.test
Normal 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
|
||||
@@ -20,6 +20,7 @@ test('Returns paths', async () => {
|
||||
"myservers-config",
|
||||
"myservers-config-states",
|
||||
"myservers-env",
|
||||
"myservers-keepalive",
|
||||
"keyfile-base",
|
||||
"machine-id",
|
||||
"log-base",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: {},
|
||||
});
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
55
api/src/unraid-api/cron/write-flash-file.service.ts
Normal file
55
api/src/unraid-api/cron/write-flash-file.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
43
api/src/unraid-api/cron/write-flash-file.spec.ts
Normal file
43
api/src/unraid-api/cron/write-flash-file.spec.ts
Normal 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');
|
||||
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user