test: wip

This commit is contained in:
izzy
2025-12-03 12:26:47 +00:00
parent 02265ba224
commit 6b9cc855a5
5 changed files with 57 additions and 58 deletions

View File

@@ -450,17 +450,25 @@ describe(MaintenanceWorkerService.name, () => {
}); });
describe('deleteBackup', () => { describe('deleteBackup', () => {
it('should reject invalid file names', async () => {
await expect(sut.deleteBackup(['filename'])).rejects.toThrowError(
new BadRequestException('Invalid backup name!'),
);
});
it('should unlink the target file', async () => { it('should unlink the target file', async () => {
await sut.deleteBackup('filename'); await sut.deleteBackup(['filename.sql']);
expect(mocks.storage.unlink).toHaveBeenCalledTimes(1); expect(mocks.storage.unlink).toHaveBeenCalledTimes(1);
expect(mocks.storage.unlink).toHaveBeenCalledWith(`${StorageCore.getBaseFolder(StorageFolder.Backups)}/filename`); expect(mocks.storage.unlink).toHaveBeenCalledWith(
`${StorageCore.getBaseFolder(StorageFolder.Backups)}/filename.sql`,
);
}); });
}); });
describe('uploadBackup', () => { describe('uploadBackup', () => {
it('should reject invalid file names', async () => { it('should reject invalid file names', async () => {
await expect(sut.uploadBackup({ originalname: 'invalid backup' } as never)).rejects.toThrowError( await expect(sut.uploadBackup({ originalname: 'invalid backup' } as never)).rejects.toThrowError(
new BadRequestException('Not a valid backup name!'), new BadRequestException('Invalid backup name!'),
); );
}); });

View File

@@ -1,10 +1,9 @@
import { BadRequestException, Injectable, UnauthorizedException } from '@nestjs/common'; import { Injectable, UnauthorizedException } from '@nestjs/common';
import { parse } from 'cookie'; import { parse } from 'cookie';
import { NextFunction, Request, Response } from 'express'; import { NextFunction, Request, Response } from 'express';
import { jwtVerify } from 'jose'; import { jwtVerify } from 'jose';
import { readFileSync } from 'node:fs'; import { readFileSync } from 'node:fs';
import { IncomingHttpHeaders } from 'node:http'; import { IncomingHttpHeaders } from 'node:http';
import { join } from 'node:path';
import { StorageCore } from 'src/cores/storage.core'; import { StorageCore } from 'src/cores/storage.core';
import { import {
MaintenanceAuthDto, MaintenanceAuthDto,
@@ -13,14 +12,7 @@ import {
SetMaintenanceModeDto, SetMaintenanceModeDto,
} from 'src/dtos/maintenance.dto'; } from 'src/dtos/maintenance.dto';
import { ServerConfigDto } from 'src/dtos/server.dto'; import { ServerConfigDto } from 'src/dtos/server.dto';
import { import { DatabaseLock, ImmichCookie, MaintenanceAction, SystemMetadataKey } from 'src/enum';
CacheControl,
DatabaseLock,
ImmichCookie,
MaintenanceAction,
StorageFolder,
SystemMetadataKey,
} from 'src/enum';
import { MaintenanceWebsocketRepository } from 'src/maintenance/maintenance-websocket.repository'; import { MaintenanceWebsocketRepository } from 'src/maintenance/maintenance-websocket.repository';
import { AppRepository } from 'src/repositories/app.repository'; import { AppRepository } from 'src/repositories/app.repository';
import { ConfigRepository } from 'src/repositories/config.repository'; import { ConfigRepository } from 'src/repositories/config.repository';
@@ -34,7 +26,7 @@ import { type BaseService as _BaseService } from 'src/services/base.service';
import { type DatabaseBackupService as _DatabaseBackupService } from 'src/services/database-backup.service'; import { type DatabaseBackupService as _DatabaseBackupService } from 'src/services/database-backup.service';
import { type ServerService as _ServerService } from 'src/services/server.service'; import { type ServerService as _ServerService } from 'src/services/server.service';
import { MaintenanceModeState } from 'src/types'; import { MaintenanceModeState } from 'src/types';
import { deleteBackup, isValidBackupName, listBackups, restoreBackup, uploadBackup } from 'src/utils/backups'; import { deleteBackups, downloadBackup, listBackups, restoreBackup, uploadBackup } from 'src/utils/backups';
import { getConfig } from 'src/utils/config'; import { getConfig } from 'src/utils/config';
import { ImmichFileResponse } from 'src/utils/file'; import { ImmichFileResponse } from 'src/utils/file';
import { createMaintenanceLoginUrl, detectPriorInstall } from 'src/utils/maintenance'; import { createMaintenanceLoginUrl, detectPriorInstall } from 'src/utils/maintenance';
@@ -185,8 +177,8 @@ export class MaintenanceWorkerService {
/** /**
* {@link _DatabaseBackupService.deleteBackup} * {@link _DatabaseBackupService.deleteBackup}
*/ */
async deleteBackup(filename: string): Promise<void> { async deleteBackup(files: string[]): Promise<void> {
return deleteBackup(this.backupRepos, filename); return deleteBackups(this.backupRepos, files);
} }
/** /**
@@ -200,18 +192,7 @@ export class MaintenanceWorkerService {
* {@link _DatabaseBackupService.downloadBackup} * {@link _DatabaseBackupService.downloadBackup}
*/ */
downloadBackup(fileName: string): ImmichFileResponse { downloadBackup(fileName: string): ImmichFileResponse {
if (!isValidBackupName(fileName)) { return downloadBackup(fileName);
throw new BadRequestException('Invalid backup name!');
}
const path = join(StorageCore.getBaseFolder(StorageFolder.Backups), fileName);
return {
path,
fileName,
cacheControl: CacheControl.PrivateWithoutCache,
contentType: fileName.endsWith('.gz') ? 'application/gzip' : 'application/sql',
};
} }
private get backupRepos() { private get backupRepos() {

View File

@@ -38,17 +38,25 @@ describe(MaintenanceService.name, () => {
}); });
describe('deleteBackup', () => { describe('deleteBackup', () => {
it('should reject invalid file names', async () => {
await expect(sut.deleteBackup(['filename'])).rejects.toThrowError(
new BadRequestException('Invalid backup name!'),
);
});
it('should unlink the target file', async () => { it('should unlink the target file', async () => {
await sut.deleteBackup(['filename']); await sut.deleteBackup(['filename.sql']);
expect(mocks.storage.unlink).toHaveBeenCalledTimes(1); expect(mocks.storage.unlink).toHaveBeenCalledTimes(1);
expect(mocks.storage.unlink).toHaveBeenCalledWith(`${StorageCore.getBaseFolder(StorageFolder.Backups)}/filename`); expect(mocks.storage.unlink).toHaveBeenCalledWith(
`${StorageCore.getBaseFolder(StorageFolder.Backups)}/filename.sql`,
);
}); });
}); });
describe('uploadBackup', () => { describe('uploadBackup', () => {
it('should reject invalid file names', async () => { it('should reject invalid file names', async () => {
await expect(sut.uploadBackup({ originalname: 'invalid backup' } as never)).rejects.toThrowError( await expect(sut.uploadBackup({ originalname: 'invalid backup' } as never)).rejects.toThrowError(
new BadRequestException('Not a valid backup name!'), new BadRequestException('Invalid backup name!'),
); );
}); });

View File

@@ -1,9 +1,6 @@
import { BadRequestException, Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { basename, join } from 'node:path';
import { StorageCore } from 'src/cores/storage.core';
import { CacheControl, StorageFolder } from 'src/enum';
import { BaseService } from 'src/services/base.service'; import { BaseService } from 'src/services/base.service';
import { deleteBackup, isValidBackupName, listBackups, uploadBackup } from 'src/utils/backups'; import { deleteBackups, downloadBackup, listBackups, uploadBackup } from 'src/utils/backups';
import { ImmichFileResponse } from 'src/utils/file'; import { ImmichFileResponse } from 'src/utils/file';
/** /**
@@ -15,12 +12,8 @@ export class DatabaseBackupService extends BaseService {
return { backups: await listBackups(this.backupRepos) }; return { backups: await listBackups(this.backupRepos) };
} }
async deleteBackup(files: string[]): Promise<void> { deleteBackup(files: string[]): Promise<void> {
if (files.some((filename) => !isValidBackupName(filename))) { return deleteBackups(this.backupRepos, files);
throw new BadRequestException('Invalid backup name!');
}
await Promise.all(files.map((filename) => deleteBackup(this.backupRepos, basename(filename))));
} }
async uploadBackup(file: Express.Multer.File): Promise<void> { async uploadBackup(file: Express.Multer.File): Promise<void> {
@@ -28,18 +21,7 @@ export class DatabaseBackupService extends BaseService {
} }
downloadBackup(fileName: string): ImmichFileResponse { downloadBackup(fileName: string): ImmichFileResponse {
if (!isValidBackupName(fileName)) { return downloadBackup(fileName);
throw new BadRequestException('Invalid backup name!');
}
const path = join(StorageCore.getBaseFolder(StorageFolder.Backups), fileName);
return {
path,
fileName,
cacheControl: CacheControl.PrivateWithoutCache,
contentType: fileName.endsWith('.gz') ? 'application/gzip' : 'application/sql',
};
} }
private get backupRepos() { private get backupRepos() {

View File

@@ -7,7 +7,7 @@ import { pipeline } from 'node:stream/promises';
import semver from 'semver'; import semver from 'semver';
import { serverVersion } from 'src/constants'; import { serverVersion } from 'src/constants';
import { StorageCore } from 'src/cores/storage.core'; import { StorageCore } from 'src/cores/storage.core';
import { StorageFolder } from 'src/enum'; import { CacheControl, StorageFolder } from 'src/enum';
import { ConfigRepository } from 'src/repositories/config.repository'; import { ConfigRepository } from 'src/repositories/config.repository';
import { DatabaseRepository } from 'src/repositories/database.repository'; import { DatabaseRepository } from 'src/repositories/database.repository';
import { LoggingRepository } from 'src/repositories/logging.repository'; import { LoggingRepository } from 'src/repositories/logging.repository';
@@ -266,9 +266,14 @@ export async function restoreBackup(
logger.log(`Database Restore Success`); logger.log(`Database Restore Success`);
} }
export async function deleteBackup({ storage }: Pick<BackupRepos, 'storage'>, filename: string): Promise<void> { export async function deleteBackups({ storage }: Pick<BackupRepos, 'storage'>, files: string[]): Promise<void> {
const backupsFolder = StorageCore.getBaseFolder(StorageFolder.Backups); const backupsFolder = StorageCore.getBaseFolder(StorageFolder.Backups);
await storage.unlink(path.join(backupsFolder, filename));
if (files.some((filename) => !isValidBackupName(filename))) {
throw new BadRequestException('Invalid backup name!');
}
await Promise.all(files.map((filename) => storage.unlink(path.join(backupsFolder, filename))));
} }
export async function listBackups({ storage }: Pick<BackupRepos, 'storage'>): Promise<string[]> { export async function listBackups({ storage }: Pick<BackupRepos, 'storage'>): Promise<string[]> {
@@ -287,13 +292,28 @@ export async function uploadBackup(
const backupsFolder = StorageCore.getBaseFolder(StorageFolder.Backups); const backupsFolder = StorageCore.getBaseFolder(StorageFolder.Backups);
const fn = basename(file.originalname); const fn = basename(file.originalname);
if (!isValidBackupName(fn)) { if (!isValidBackupName(fn)) {
throw new BadRequestException('Not a valid backup name!'); throw new BadRequestException('Invalid backup name!');
} }
const path = join(backupsFolder, `uploaded-${fn}`); const path = join(backupsFolder, `uploaded-${fn}`);
await storage.createOrOverwriteFile(path, file.buffer); await storage.createOrOverwriteFile(path, file.buffer);
} }
export function downloadBackup(fileName: string) {
if (!isValidBackupName(fileName)) {
throw new BadRequestException('Invalid backup name!');
}
const path = join(StorageCore.getBaseFolder(StorageFolder.Backups), fileName);
return {
path,
fileName,
cacheControl: CacheControl.PrivateWithoutCache,
contentType: fileName.endsWith('.gz') ? 'application/gzip' : 'application/sql',
};
}
function createSqlProgressStreams(cb: (progress: number) => void) { function createSqlProgressStreams(cb: (progress: number) => void) {
const STDIN_START_MARKER = new TextEncoder().encode('FROM stdin'); const STDIN_START_MARKER = new TextEncoder().encode('FROM stdin');
const STDIN_END_MARKER = new TextEncoder().encode(String.raw`\.`); const STDIN_END_MARKER = new TextEncoder().encode(String.raw`\.`);