mirror of
https://github.com/immich-app/immich.git
synced 2025-12-21 09:15:44 +03:00
test: update service specs
This commit is contained in:
@@ -29,7 +29,6 @@ import { Auth, Authenticated, FileResponse, GetLoginDetails } from 'src/middlewa
|
|||||||
import { StorageRepository } from 'src/repositories/storage.repository';
|
import { StorageRepository } from 'src/repositories/storage.repository';
|
||||||
import { LoginDetails } from 'src/services/auth.service';
|
import { LoginDetails } from 'src/services/auth.service';
|
||||||
import { MaintenanceService } from 'src/services/maintenance.service';
|
import { MaintenanceService } from 'src/services/maintenance.service';
|
||||||
import { integrityCheck } from 'src/utils/maintenance';
|
|
||||||
import { respondWithCookie } from 'src/utils/response';
|
import { respondWithCookie } from 'src/utils/response';
|
||||||
import { FilenameParamDto } from 'src/validation';
|
import { FilenameParamDto } from 'src/validation';
|
||||||
|
|
||||||
@@ -60,7 +59,7 @@ export class MaintenanceController {
|
|||||||
history: new HistoryBuilder().added('v9.9.9').alpha('v9.9.9'),
|
history: new HistoryBuilder().added('v9.9.9').alpha('v9.9.9'),
|
||||||
})
|
})
|
||||||
integrityCheck(): Promise<MaintenanceIntegrityResponseDto> {
|
integrityCheck(): Promise<MaintenanceIntegrityResponseDto> {
|
||||||
return integrityCheck(this.storageRepository);
|
return this.service.integrityCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('login')
|
@Post('login')
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import { MaintenanceWorkerService } from 'src/maintenance/maintenance-worker.ser
|
|||||||
import { GetLoginDetails } from 'src/middleware/auth.guard';
|
import { GetLoginDetails } from 'src/middleware/auth.guard';
|
||||||
import { StorageRepository } from 'src/repositories/storage.repository';
|
import { StorageRepository } from 'src/repositories/storage.repository';
|
||||||
import { LoginDetails } from 'src/services/auth.service';
|
import { LoginDetails } from 'src/services/auth.service';
|
||||||
import { integrityCheck } from 'src/utils/maintenance';
|
|
||||||
import { respondWithCookie } from 'src/utils/response';
|
import { respondWithCookie } from 'src/utils/response';
|
||||||
import { FilenameParamDto } from 'src/validation';
|
import { FilenameParamDto } from 'src/validation';
|
||||||
|
|
||||||
@@ -39,7 +38,7 @@ export class MaintenanceWorkerController {
|
|||||||
|
|
||||||
@Get('admin/maintenance/integrity')
|
@Get('admin/maintenance/integrity')
|
||||||
integrityCheck(): Promise<MaintenanceIntegrityResponseDto> {
|
integrityCheck(): Promise<MaintenanceIntegrityResponseDto> {
|
||||||
return integrityCheck(this.storageRepository);
|
return this.service.integrityCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('admin/maintenance/login')
|
@Post('admin/maintenance/login')
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { UnauthorizedException } from '@nestjs/common';
|
import { BadRequestException, UnauthorizedException } from '@nestjs/common';
|
||||||
import { SignJWT } from 'jose';
|
import { SignJWT } from 'jose';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { PassThrough, Readable } from 'node:stream';
|
import { PassThrough, Readable } from 'node:stream';
|
||||||
@@ -59,6 +59,32 @@ describe(MaintenanceWorkerService.name, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe.skip('ssr');
|
||||||
|
describe.skip('detectMediaLocation');
|
||||||
|
|
||||||
|
describe('setStatus', () => {
|
||||||
|
it('should broadcast status', async () => {
|
||||||
|
maintenanceEphemeralStateRepositoryMock.getPublicStatus.mockReturnValue({
|
||||||
|
action: MaintenanceAction.Start,
|
||||||
|
error: 'mock',
|
||||||
|
});
|
||||||
|
|
||||||
|
sut.setStatus({
|
||||||
|
action: MaintenanceAction.Start,
|
||||||
|
task: 'abc',
|
||||||
|
error: 'def',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(maintenanceEphemeralStateRepositoryMock.setStatus).toHaveBeenCalled();
|
||||||
|
expect(maintenanceWebsocketRepositoryMock.serverSend).toHaveBeenCalled();
|
||||||
|
expect(maintenanceWebsocketRepositoryMock.clientSend).toHaveBeenCalledTimes(2);
|
||||||
|
expect(maintenanceWebsocketRepositoryMock.clientSend).toHaveBeenCalledWith('MaintenanceStatusV1', 'public', {
|
||||||
|
action: 'start',
|
||||||
|
error: 'mock',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('logSecret', () => {
|
describe('logSecret', () => {
|
||||||
const RE_LOGIN_URL = /https:\/\/my.immich.app\/maintenance\?token=([A-Za-z0-9-_]*\.[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*)/;
|
const RE_LOGIN_URL = /https:\/\/my.immich.app\/maintenance\?token=([A-Za-z0-9-_]*\.[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*)/;
|
||||||
|
|
||||||
@@ -107,6 +133,95 @@ describe(MaintenanceWorkerService.name, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('status', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
maintenanceEphemeralStateRepositoryMock.getStatus.mockResolvedValue({
|
||||||
|
action: MaintenanceAction.Start,
|
||||||
|
error: 'secret value!',
|
||||||
|
});
|
||||||
|
|
||||||
|
maintenanceEphemeralStateRepositoryMock.getPublicStatus.mockResolvedValue({
|
||||||
|
action: MaintenanceAction.Start,
|
||||||
|
error: 'public mock',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('generates private status', async () => {
|
||||||
|
maintenanceEphemeralStateRepositoryMock.getSecret.mockReturnValue('secret');
|
||||||
|
|
||||||
|
const jwt = await new SignJWT({ _mockValue: true })
|
||||||
|
.setProtectedHeader({ alg: 'HS256' })
|
||||||
|
.setIssuedAt()
|
||||||
|
.setExpirationTime('4h')
|
||||||
|
.sign(new TextEncoder().encode('secret'));
|
||||||
|
|
||||||
|
await expect(sut.status(jwt)).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
error: 'secret value!',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('generates public status', async () => {
|
||||||
|
await expect(sut.status()).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
error: 'public mock',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('integrityCheck', () => {
|
||||||
|
it('generate integrity report', async () => {
|
||||||
|
mocks.storage.readdir.mockResolvedValue(['.immich', 'file1', 'file2']);
|
||||||
|
mocks.storage.readFile.mockResolvedValue(undefined as never);
|
||||||
|
mocks.storage.overwriteFile.mockRejectedValue(undefined as never);
|
||||||
|
|
||||||
|
await expect(sut.integrityCheck()).resolves.toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"storage": [
|
||||||
|
{
|
||||||
|
"files": 2,
|
||||||
|
"folder": "encoded-video",
|
||||||
|
"readable": true,
|
||||||
|
"writable": false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": 2,
|
||||||
|
"folder": "library",
|
||||||
|
"readable": true,
|
||||||
|
"writable": false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": 2,
|
||||||
|
"folder": "upload",
|
||||||
|
"readable": true,
|
||||||
|
"writable": false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": 2,
|
||||||
|
"folder": "profile",
|
||||||
|
"readable": true,
|
||||||
|
"writable": false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": 2,
|
||||||
|
"folder": "thumbs",
|
||||||
|
"readable": true,
|
||||||
|
"writable": false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": 2,
|
||||||
|
"folder": "backups",
|
||||||
|
"readable": true,
|
||||||
|
"writable": false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('login', () => {
|
describe('login', () => {
|
||||||
it('should fail without token', async () => {
|
it('should fail without token', async () => {
|
||||||
await expect(sut.login()).rejects.toThrowError(new UnauthorizedException('Missing JWT Token'));
|
await expect(sut.login()).rejects.toThrowError(new UnauthorizedException('Missing JWT Token'));
|
||||||
@@ -155,7 +270,23 @@ describe(MaintenanceWorkerService.name, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('endMaintenance', () => {
|
describe.skip('setAction'); // just calls setStatus+runAction
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actions
|
||||||
|
*/
|
||||||
|
|
||||||
|
describe('action: start', () => {
|
||||||
|
it('should not do anything', async () => {
|
||||||
|
await sut.runAction({
|
||||||
|
action: MaintenanceAction.Start,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mocks.logger.log).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('action: end', () => {
|
||||||
it('should set maintenance mode', async () => {
|
it('should set maintenance mode', async () => {
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ isMaintenanceMode: false });
|
mocks.systemMetadata.get.mockResolvedValue({ isMaintenanceMode: false });
|
||||||
await sut.runAction({
|
await sut.runAction({
|
||||||
@@ -176,20 +307,6 @@ describe(MaintenanceWorkerService.name, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Actions
|
|
||||||
*/
|
|
||||||
|
|
||||||
describe('action: start', () => {
|
|
||||||
it('should not do anything', async () => {
|
|
||||||
await sut.runAction({
|
|
||||||
action: MaintenanceAction.Start,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(mocks.logger.log).toHaveBeenCalledTimes(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('action: restore database', () => {
|
describe('action: restore database', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mocks.database.tryLock.mockResolvedValueOnce(true);
|
mocks.database.tryLock.mockResolvedValueOnce(true);
|
||||||
@@ -319,4 +436,27 @@ describe(MaintenanceWorkerService.name, () => {
|
|||||||
expect(mocks.storage.unlink).toHaveBeenCalledWith(`${StorageCore.getBaseFolder(StorageFolder.Backups)}/filename`);
|
expect(mocks.storage.unlink).toHaveBeenCalledWith(`${StorageCore.getBaseFolder(StorageFolder.Backups)}/filename`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('uploadBackup', () => {
|
||||||
|
it('should reject invalid file names', async () => {
|
||||||
|
await expect(sut.uploadBackup({ originalname: 'invalid backup' } as never)).rejects.toThrowError(
|
||||||
|
new BadRequestException('Not a valid backup name!'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should write file', async () => {
|
||||||
|
await sut.uploadBackup({ originalname: 'path.sql.gz' } as never);
|
||||||
|
expect(mocks.storage.overwriteFile).toBeCalledWith('/data/backups/uploaded-path.sql.gz', undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getBackupPath', () => {
|
||||||
|
it('should reject invalid file names', () => {
|
||||||
|
expect(() => sut.getBackupPath('invalid backup')).toThrowError(new BadRequestException('Invalid backup name!'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get backup path', () => {
|
||||||
|
expect(sut.getBackupPath('hello.sql.gz')).toEqual('/data/backups/hello.sql.gz');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ import { readFileSync } from 'node:fs';
|
|||||||
import { IncomingHttpHeaders } from 'node:http';
|
import { IncomingHttpHeaders } from 'node:http';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
import { MaintenanceAuthDto, MaintenanceStatusResponseDto, SetMaintenanceModeDto } from 'src/dtos/maintenance.dto';
|
import {
|
||||||
|
MaintenanceAuthDto,
|
||||||
|
MaintenanceIntegrityResponseDto,
|
||||||
|
MaintenanceStatusResponseDto,
|
||||||
|
SetMaintenanceModeDto,
|
||||||
|
} from 'src/dtos/maintenance.dto';
|
||||||
import { ServerConfigDto } from 'src/dtos/server.dto';
|
import { ServerConfigDto } from 'src/dtos/server.dto';
|
||||||
import { DatabaseLock, ImmichCookie, MaintenanceAction, StorageFolder, SystemMetadataKey } from 'src/enum';
|
import { DatabaseLock, ImmichCookie, MaintenanceAction, StorageFolder, SystemMetadataKey } from 'src/enum';
|
||||||
import { MaintenanceEphemeralStateRepository } from 'src/maintenance/maintenance-ephemeral-state.repository';
|
import { MaintenanceEphemeralStateRepository } from 'src/maintenance/maintenance-ephemeral-state.repository';
|
||||||
@@ -24,7 +29,7 @@ import { type ServerService as _ServerService } from 'src/services/server.servic
|
|||||||
import { MaintenanceModeState } from 'src/types';
|
import { MaintenanceModeState } from 'src/types';
|
||||||
import { deleteBackup, isValidBackupName, listBackups, restoreBackup, uploadBackup } from 'src/utils/backups';
|
import { deleteBackup, isValidBackupName, listBackups, restoreBackup, uploadBackup } from 'src/utils/backups';
|
||||||
import { getConfig } from 'src/utils/config';
|
import { getConfig } from 'src/utils/config';
|
||||||
import { createMaintenanceLoginUrl } from 'src/utils/maintenance';
|
import { createMaintenanceLoginUrl, integrityCheck } from 'src/utils/maintenance';
|
||||||
import { getExternalDomain } from 'src/utils/misc';
|
import { getExternalDomain } from 'src/utils/misc';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -172,6 +177,10 @@ export class MaintenanceWorkerService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
integrityCheck(): Promise<MaintenanceIntegrityResponseDto> {
|
||||||
|
return integrityCheck(this.storageRepository);
|
||||||
|
}
|
||||||
|
|
||||||
async login(jwt?: string): Promise<MaintenanceAuthDto> {
|
async login(jwt?: string): Promise<MaintenanceAuthDto> {
|
||||||
if (!jwt) {
|
if (!jwt) {
|
||||||
throw new UnauthorizedException('Missing JWT Token');
|
throw new UnauthorizedException('Missing JWT Token');
|
||||||
@@ -287,7 +296,7 @@ export class MaintenanceWorkerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async uploadBackup(file: Express.Multer.File): Promise<void> {
|
async uploadBackup(file: Express.Multer.File): Promise<void> {
|
||||||
return uploadBackup(file);
|
return uploadBackup(this.backupRepos, file);
|
||||||
}
|
}
|
||||||
|
|
||||||
getBackupPath(filename: string): string {
|
getBackupPath(filename: string): string {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { BadRequestException } from '@nestjs/common';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
import { MaintenanceAction, StorageFolder, SystemMetadataKey } from 'src/enum';
|
import { MaintenanceAction, StorageFolder, SystemMetadataKey } from 'src/enum';
|
||||||
@@ -56,6 +57,57 @@ describe(MaintenanceService.name, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('integrityCheck', () => {
|
||||||
|
it('generate integrity report', async () => {
|
||||||
|
mocks.storage.readdir.mockResolvedValue(['.immich', 'file1', 'file2']);
|
||||||
|
mocks.storage.readFile.mockResolvedValue(undefined as never);
|
||||||
|
mocks.storage.overwriteFile.mockRejectedValue(undefined as never);
|
||||||
|
|
||||||
|
await expect(sut.integrityCheck()).resolves.toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"storage": [
|
||||||
|
{
|
||||||
|
"files": 2,
|
||||||
|
"folder": "encoded-video",
|
||||||
|
"readable": true,
|
||||||
|
"writable": false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": 2,
|
||||||
|
"folder": "library",
|
||||||
|
"readable": true,
|
||||||
|
"writable": false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": 2,
|
||||||
|
"folder": "upload",
|
||||||
|
"readable": true,
|
||||||
|
"writable": false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": 2,
|
||||||
|
"folder": "profile",
|
||||||
|
"readable": true,
|
||||||
|
"writable": false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": 2,
|
||||||
|
"folder": "thumbs",
|
||||||
|
"readable": true,
|
||||||
|
"writable": false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": 2,
|
||||||
|
"folder": "backups",
|
||||||
|
"readable": true,
|
||||||
|
"writable": false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('startMaintenance', () => {
|
describe('startMaintenance', () => {
|
||||||
it('should set maintenance mode and return a secret', async () => {
|
it('should set maintenance mode and return a secret', async () => {
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ isMaintenanceMode: false });
|
mocks.systemMetadata.get.mockResolvedValue({ isMaintenanceMode: false });
|
||||||
@@ -137,7 +189,7 @@ describe(MaintenanceService.name, () => {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
describe('listBackups', () => {
|
describe('listBackups', () => {
|
||||||
it('should give us all valid and failed backups', async () => {
|
it('should give us all backups', async () => {
|
||||||
mocks.storage.readdir.mockResolvedValue([
|
mocks.storage.readdir.mockResolvedValue([
|
||||||
`immich-db-backup-${DateTime.fromISO('2025-07-25T11:02:16Z').toFormat("yyyyLLdd'T'HHmmss")}-v1.234.5-pg14.5.sql.gz.tmp`,
|
`immich-db-backup-${DateTime.fromISO('2025-07-25T11:02:16Z').toFormat("yyyyLLdd'T'HHmmss")}-v1.234.5-pg14.5.sql.gz.tmp`,
|
||||||
`immich-db-backup-${DateTime.fromISO('2025-07-27T11:01:16Z').toFormat("yyyyLLdd'T'HHmmss")}-v1.234.5-pg14.5.sql.gz`,
|
`immich-db-backup-${DateTime.fromISO('2025-07-27T11:01:16Z').toFormat("yyyyLLdd'T'HHmmss")}-v1.234.5-pg14.5.sql.gz`,
|
||||||
@@ -162,4 +214,27 @@ describe(MaintenanceService.name, () => {
|
|||||||
expect(mocks.storage.unlink).toHaveBeenCalledWith(`${StorageCore.getBaseFolder(StorageFolder.Backups)}/filename`);
|
expect(mocks.storage.unlink).toHaveBeenCalledWith(`${StorageCore.getBaseFolder(StorageFolder.Backups)}/filename`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('uploadBackup', () => {
|
||||||
|
it('should reject invalid file names', async () => {
|
||||||
|
await expect(sut.uploadBackup({ originalname: 'invalid backup' } as never)).rejects.toThrowError(
|
||||||
|
new BadRequestException('Not a valid backup name!'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should write file', async () => {
|
||||||
|
await sut.uploadBackup({ originalname: 'path.sql.gz' } as never);
|
||||||
|
expect(mocks.storage.overwriteFile).toBeCalledWith('/data/backups/uploaded-path.sql.gz', undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getBackupPath', () => {
|
||||||
|
it('should reject invalid file names', () => {
|
||||||
|
expect(() => sut.getBackupPath('invalid backup')).toThrowError(new BadRequestException('Invalid backup name!'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get backup path', () => {
|
||||||
|
expect(sut.getBackupPath('hello.sql.gz')).toEqual('/data/backups/hello.sql.gz');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,12 +2,17 @@ import { BadRequestException, Injectable } from '@nestjs/common';
|
|||||||
import { basename, join } from 'node:path';
|
import { basename, join } from 'node:path';
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
import { OnEvent } from 'src/decorators';
|
import { OnEvent } from 'src/decorators';
|
||||||
import { MaintenanceAuthDto, SetMaintenanceModeDto } from 'src/dtos/maintenance.dto';
|
import { MaintenanceAuthDto, MaintenanceIntegrityResponseDto, SetMaintenanceModeDto } from 'src/dtos/maintenance.dto';
|
||||||
import { MaintenanceAction, StorageFolder, SystemMetadataKey } from 'src/enum';
|
import { MaintenanceAction, StorageFolder, SystemMetadataKey } from 'src/enum';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { MaintenanceModeState } from 'src/types';
|
import { MaintenanceModeState } from 'src/types';
|
||||||
import { deleteBackup, isValidBackupName, listBackups, uploadBackup } from 'src/utils/backups';
|
import { deleteBackup, isValidBackupName, listBackups, uploadBackup } from 'src/utils/backups';
|
||||||
import { createMaintenanceLoginUrl, generateMaintenanceSecret, signMaintenanceJwt } from 'src/utils/maintenance';
|
import {
|
||||||
|
createMaintenanceLoginUrl,
|
||||||
|
generateMaintenanceSecret,
|
||||||
|
integrityCheck,
|
||||||
|
signMaintenanceJwt,
|
||||||
|
} from 'src/utils/maintenance';
|
||||||
import { getExternalDomain } from 'src/utils/misc';
|
import { getExternalDomain } from 'src/utils/misc';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -21,6 +26,10 @@ export class MaintenanceService extends BaseService {
|
|||||||
.then((state) => state ?? { isMaintenanceMode: false });
|
.then((state) => state ?? { isMaintenanceMode: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
integrityCheck(): Promise<MaintenanceIntegrityResponseDto> {
|
||||||
|
return integrityCheck(this.storageRepository);
|
||||||
|
}
|
||||||
|
|
||||||
async startMaintenance(action: SetMaintenanceModeDto, username: string): Promise<{ jwt: string }> {
|
async startMaintenance(action: SetMaintenanceModeDto, username: string): Promise<{ jwt: string }> {
|
||||||
const secret = generateMaintenanceSecret();
|
const secret = generateMaintenanceSecret();
|
||||||
await this.systemMetadataRepository.set(SystemMetadataKey.MaintenanceMode, {
|
await this.systemMetadataRepository.set(SystemMetadataKey.MaintenanceMode, {
|
||||||
@@ -86,7 +95,7 @@ export class MaintenanceService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async uploadBackup(file: Express.Multer.File): Promise<void> {
|
async uploadBackup(file: Express.Multer.File): Promise<void> {
|
||||||
return uploadBackup(file);
|
return uploadBackup(this.backupRepos, file);
|
||||||
}
|
}
|
||||||
|
|
||||||
getBackupPath(filename: string): string {
|
getBackupPath(filename: string): string {
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { BadRequestException } from '@nestjs/common';
|
import { BadRequestException } from '@nestjs/common';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { stat, writeFile } from 'node:fs/promises';
|
import path, { basename, join } from 'node:path';
|
||||||
import path, { join } from 'node:path';
|
|
||||||
import { PassThrough, Readable, Writable } from 'node:stream';
|
import { PassThrough, Readable, Writable } from 'node:stream';
|
||||||
import { pipeline } from 'node:stream/promises';
|
import { pipeline } from 'node:stream/promises';
|
||||||
import semver from 'semver';
|
import semver from 'semver';
|
||||||
@@ -273,21 +272,18 @@ export async function listBackups({ storage }: Pick<BackupRepos, 'storage'>): Pr
|
|||||||
.toReversed();
|
.toReversed();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function uploadBackup(file: Express.Multer.File): Promise<void> {
|
export async function uploadBackup(
|
||||||
|
{ storage }: Pick<BackupRepos, 'storage'>,
|
||||||
|
file: Express.Multer.File,
|
||||||
|
): Promise<void> {
|
||||||
const backupsFolder = StorageCore.getBaseFolder(StorageFolder.Backups);
|
const backupsFolder = StorageCore.getBaseFolder(StorageFolder.Backups);
|
||||||
const fn = file.originalname;
|
const fn = basename(file.originalname);
|
||||||
if (!isValidBackupName(fn)) {
|
if (!isValidBackupName(fn)) {
|
||||||
throw new BadRequestException('Not a valid backup name!');
|
throw new BadRequestException('Not a valid backup name!');
|
||||||
}
|
}
|
||||||
|
|
||||||
const path = join(backupsFolder, `uploaded-${fn}`);
|
const path = join(backupsFolder, `uploaded-${fn}`);
|
||||||
|
await storage.overwriteFile(path, file.buffer);
|
||||||
try {
|
|
||||||
await stat(path);
|
|
||||||
throw new BadRequestException('File already exists!');
|
|
||||||
} catch {
|
|
||||||
await writeFile(path, file.buffer);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createSqlProgressStreams(cb: (progress: number) => void) {
|
function createSqlProgressStreams(cb: (progress: number) => void) {
|
||||||
|
|||||||
Reference in New Issue
Block a user