mirror of
https://github.com/immich-app/immich.git
synced 2025-12-22 01:11:20 +03:00
test: wip
This commit is contained in:
@@ -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!'),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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!'),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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`\.`);
|
||||||
|
|||||||
Reference in New Issue
Block a user