diff --git a/.gitignore b/.gitignore index 0dd42452f..b442b9d6c 100644 --- a/.gitignore +++ b/.gitignore @@ -83,4 +83,6 @@ deploy/* .cache .output .env* -!.env.example \ No newline at end of file +!.env.example + +fb_keepalive \ No newline at end of file diff --git a/api/.env.test b/api/.env.test new file mode 100644 index 000000000..03efd9ac0 --- /dev/null +++ b/api/.env.test @@ -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 \ No newline at end of file diff --git a/api/src/__test__/store/modules/paths.test.ts b/api/src/__test__/store/modules/paths.test.ts index 6cdaea253..bcbb0b1ac 100644 --- a/api/src/__test__/store/modules/paths.test.ts +++ b/api/src/__test__/store/modules/paths.test.ts @@ -20,6 +20,7 @@ test('Returns paths', async () => { "myservers-config", "myservers-config-states", "myservers-env", + "myservers-keepalive", "keyfile-base", "machine-id", "log-base", diff --git a/api/src/consts.ts b/api/src/consts.ts index 7389b78f8..4be249f3e 100644 --- a/api/src/consts.ts +++ b/api/src/consts.ts @@ -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; diff --git a/api/src/store/modules/paths.ts b/api/src/store/modules/paths.ts index 0249ead4b..a78f817cb 100644 --- a/api/src/store/modules/paths.ts +++ b/api/src/store/modules/paths.ts @@ -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: {}, }); diff --git a/api/src/unraid-api/cron/cron.module.ts b/api/src/unraid-api/cron/cron.module.ts index dca7177b8..4d02c334d 100644 --- a/api/src/unraid-api/cron/cron.module.ts +++ b/api/src/unraid-api/cron/cron.module.ts @@ -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 {} diff --git a/api/src/unraid-api/cron/write-flash-file.service.ts b/api/src/unraid-api/cron/write-flash-file.service.ts new file mode 100644 index 000000000..20712a538 --- /dev/null +++ b/api/src/unraid-api/cron/write-flash-file.service.ts @@ -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 => { + 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 => { + 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); + } + } +} diff --git a/api/src/unraid-api/cron/write-flash-file.spec.ts b/api/src/unraid-api/cron/write-flash-file.spec.ts new file mode 100644 index 000000000..4cbc61318 --- /dev/null +++ b/api/src/unraid-api/cron/write-flash-file.spec.ts @@ -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); + 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'); + + }); +});