mirror of
https://github.com/immich-app/immich.git
synced 2025-12-21 01:11:16 +03:00
refactor: scope util to database-backups instead of backups
This commit is contained in:
@@ -26,8 +26,14 @@ 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 { deleteBackups, downloadBackup, listBackups, restoreBackup, uploadBackup } from 'src/utils/backups';
|
|
||||||
import { getConfig } from 'src/utils/config';
|
import { getConfig } from 'src/utils/config';
|
||||||
|
import {
|
||||||
|
deleteDatabaseBackup,
|
||||||
|
downloadDatabaseBackup,
|
||||||
|
listDatabaseBackups,
|
||||||
|
restoreDatabaseBackup,
|
||||||
|
uploadDatabaseBackup,
|
||||||
|
} from 'src/utils/database-backups';
|
||||||
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';
|
||||||
import { getExternalDomain } from 'src/utils/misc';
|
import { getExternalDomain } from 'src/utils/misc';
|
||||||
@@ -171,28 +177,28 @@ export class MaintenanceWorkerService {
|
|||||||
* {@link _DatabaseBackupService.listBackups}
|
* {@link _DatabaseBackupService.listBackups}
|
||||||
*/
|
*/
|
||||||
async listBackups(): Promise<{ backups: string[] }> {
|
async listBackups(): Promise<{ backups: string[] }> {
|
||||||
return { backups: await listBackups(this.backupRepos) };
|
return { backups: await listDatabaseBackups(this.backupRepos) };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link _DatabaseBackupService.deleteBackup}
|
* {@link _DatabaseBackupService.deleteBackup}
|
||||||
*/
|
*/
|
||||||
async deleteBackup(files: string[]): Promise<void> {
|
async deleteBackup(files: string[]): Promise<void> {
|
||||||
return deleteBackups(this.backupRepos, files);
|
return deleteDatabaseBackup(this.backupRepos, files);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link _DatabaseBackupService.uploadBackup}
|
* {@link _DatabaseBackupService.uploadBackup}
|
||||||
*/
|
*/
|
||||||
async uploadBackup(file: Express.Multer.File): Promise<void> {
|
async uploadBackup(file: Express.Multer.File): Promise<void> {
|
||||||
return uploadBackup(this.backupRepos, file);
|
return uploadDatabaseBackup(this.backupRepos, file);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link _DatabaseBackupService.downloadBackup}
|
* {@link _DatabaseBackupService.downloadBackup}
|
||||||
*/
|
*/
|
||||||
downloadBackup(fileName: string): ImmichFileResponse {
|
downloadBackup(fileName: string): ImmichFileResponse {
|
||||||
return downloadBackup(fileName);
|
return downloadDatabaseBackup(fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
private get backupRepos() {
|
private get backupRepos() {
|
||||||
@@ -339,7 +345,7 @@ export class MaintenanceWorkerService {
|
|||||||
progress: 0,
|
progress: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
await restoreBackup(this.backupRepos, filename, (task, progress) =>
|
await restoreDatabaseBackup(this.backupRepos, filename, (task, progress) =>
|
||||||
this.setStatus({
|
this.setStatus({
|
||||||
active: true,
|
active: true,
|
||||||
action: MaintenanceAction.RestoreDatabase,
|
action: MaintenanceAction.RestoreDatabase,
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ import { DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName, StorageFolde
|
|||||||
import { ArgOf } from 'src/repositories/event.repository';
|
import { ArgOf } from 'src/repositories/event.repository';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import {
|
import {
|
||||||
createBackup,
|
createDatabaseBackup,
|
||||||
isFailedBackupName,
|
isFailedDatabaseBackupName,
|
||||||
isValidRoutineBackupName,
|
isValidDatabaseRoutineBackupName,
|
||||||
UnsupportedPostgresError,
|
UnsupportedPostgresError,
|
||||||
} from 'src/utils/backups';
|
} from 'src/utils/database-backups';
|
||||||
import { handlePromiseError } from 'src/utils/misc';
|
import { handlePromiseError } from 'src/utils/misc';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -57,10 +57,10 @@ export class BackupService extends BaseService {
|
|||||||
const backupsFolder = StorageCore.getBaseFolder(StorageFolder.Backups);
|
const backupsFolder = StorageCore.getBaseFolder(StorageFolder.Backups);
|
||||||
const files = await this.storageRepository.readdir(backupsFolder);
|
const files = await this.storageRepository.readdir(backupsFolder);
|
||||||
const backups = files
|
const backups = files
|
||||||
.filter((fn) => isValidRoutineBackupName(fn))
|
.filter((fn) => isValidDatabaseRoutineBackupName(fn))
|
||||||
.toSorted()
|
.toSorted()
|
||||||
.toReversed();
|
.toReversed();
|
||||||
const failedBackups = files.filter((fn) => isFailedBackupName(fn));
|
const failedBackups = files.filter((fn) => isFailedDatabaseBackupName(fn));
|
||||||
|
|
||||||
const toDelete = backups.slice(config.keepLastAmount);
|
const toDelete = backups.slice(config.keepLastAmount);
|
||||||
toDelete.push(...failedBackups);
|
toDelete.push(...failedBackups);
|
||||||
@@ -74,7 +74,7 @@ export class BackupService extends BaseService {
|
|||||||
@OnJob({ name: JobName.DatabaseBackup, queue: QueueName.BackupDatabase })
|
@OnJob({ name: JobName.DatabaseBackup, queue: QueueName.BackupDatabase })
|
||||||
async handleBackupDatabase(): Promise<JobStatus> {
|
async handleBackupDatabase(): Promise<JobStatus> {
|
||||||
try {
|
try {
|
||||||
await createBackup(this.backupRepos);
|
await createDatabaseBackup(this.backupRepos);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof UnsupportedPostgresError) {
|
if (error instanceof UnsupportedPostgresError) {
|
||||||
return JobStatus.Failed;
|
return JobStatus.Failed;
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { deleteBackups, downloadBackup, listBackups, uploadBackup } from 'src/utils/backups';
|
import {
|
||||||
|
deleteDatabaseBackup,
|
||||||
|
downloadDatabaseBackup,
|
||||||
|
listDatabaseBackups,
|
||||||
|
uploadDatabaseBackup,
|
||||||
|
} from 'src/utils/database-backups';
|
||||||
import { ImmichFileResponse } from 'src/utils/file';
|
import { ImmichFileResponse } from 'src/utils/file';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -9,19 +14,19 @@ import { ImmichFileResponse } from 'src/utils/file';
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class DatabaseBackupService extends BaseService {
|
export class DatabaseBackupService extends BaseService {
|
||||||
async listBackups(): Promise<{ backups: string[] }> {
|
async listBackups(): Promise<{ backups: string[] }> {
|
||||||
return { backups: await listBackups(this.backupRepos) };
|
return { backups: await listDatabaseBackups(this.backupRepos) };
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteBackup(files: string[]): Promise<void> {
|
deleteBackup(files: string[]): Promise<void> {
|
||||||
return deleteBackups(this.backupRepos, files);
|
return deleteDatabaseBackup(this.backupRepos, files);
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadBackup(file: Express.Multer.File): Promise<void> {
|
async uploadBackup(file: Express.Multer.File): Promise<void> {
|
||||||
return uploadBackup(this.backupRepos, file);
|
return uploadDatabaseBackup(this.backupRepos, file);
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadBackup(fileName: string): ImmichFileResponse {
|
downloadBackup(fileName: string): ImmichFileResponse {
|
||||||
return downloadBackup(fileName);
|
return downloadDatabaseBackup(fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
private get backupRepos() {
|
private get backupRepos() {
|
||||||
|
|||||||
@@ -14,18 +14,18 @@ import { LoggingRepository } from 'src/repositories/logging.repository';
|
|||||||
import { ProcessRepository } from 'src/repositories/process.repository';
|
import { ProcessRepository } from 'src/repositories/process.repository';
|
||||||
import { StorageRepository } from 'src/repositories/storage.repository';
|
import { StorageRepository } from 'src/repositories/storage.repository';
|
||||||
|
|
||||||
export function isValidBackupName(filename: string) {
|
export function isValidDatabaseBackupName(filename: string) {
|
||||||
return filename.match(/^[\d\w-.]+\.sql(?:\.gz)?$/);
|
return filename.match(/^[\d\w-.]+\.sql(?:\.gz)?$/);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isValidRoutineBackupName(filename: string) {
|
export function isValidDatabaseRoutineBackupName(filename: string) {
|
||||||
const oldBackupStyle = filename.match(/^immich-db-backup-\d+\.sql\.gz$/);
|
const oldBackupStyle = filename.match(/^immich-db-backup-\d+\.sql\.gz$/);
|
||||||
//immich-db-backup-20250729T114018-v1.136.0-pg14.17.sql.gz
|
//immich-db-backup-20250729T114018-v1.136.0-pg14.17.sql.gz
|
||||||
const newBackupStyle = filename.match(/^immich-db-backup-\d{8}T\d{6}-v.*-pg.*\.sql\.gz$/);
|
const newBackupStyle = filename.match(/^immich-db-backup-\d{8}T\d{6}-v.*-pg.*\.sql\.gz$/);
|
||||||
return oldBackupStyle || newBackupStyle;
|
return oldBackupStyle || newBackupStyle;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isFailedBackupName(filename: string) {
|
export function isFailedDatabaseBackupName(filename: string) {
|
||||||
return filename.match(/^immich-db-backup-.*\.sql\.gz\.tmp$/);
|
return filename.match(/^immich-db-backup-.*\.sql\.gz\.tmp$/);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,7 +137,7 @@ export async function buildPostgresLaunchArguments(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createBackup(
|
export async function createDatabaseBackup(
|
||||||
{ logger, storage, process: processRepository, ...pgRepos }: BackupRepos,
|
{ logger, storage, process: processRepository, ...pgRepos }: BackupRepos,
|
||||||
filenamePrefix: string = '',
|
filenamePrefix: string = '',
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@@ -179,7 +179,7 @@ export async function createBackup(
|
|||||||
logger.log(`Database Backup Success`);
|
logger.log(`Database Backup Success`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function restoreBackup(
|
export async function restoreDatabaseBackup(
|
||||||
{ logger, storage, process: processRepository, ...pgRepos }: BackupRepos,
|
{ logger, storage, process: processRepository, ...pgRepos }: BackupRepos,
|
||||||
filename: string,
|
filename: string,
|
||||||
progressCb?: (action: 'backup' | 'restore', progress: number) => void,
|
progressCb?: (action: 'backup' | 'restore', progress: number) => void,
|
||||||
@@ -188,7 +188,7 @@ export async function restoreBackup(
|
|||||||
|
|
||||||
let complete = false;
|
let complete = false;
|
||||||
try {
|
try {
|
||||||
if (!isValidBackupName(filename)) {
|
if (!isValidDatabaseBackupName(filename)) {
|
||||||
throw new Error('Invalid backup file format!');
|
throw new Error('Invalid backup file format!');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,7 +202,7 @@ export async function restoreBackup(
|
|||||||
|
|
||||||
progressCb?.('backup', 0.05);
|
progressCb?.('backup', 0.05);
|
||||||
|
|
||||||
await createBackup({ logger, storage, process: processRepository, ...pgRepos }, 'restore-point-');
|
await createDatabaseBackup({ logger, storage, process: processRepository, ...pgRepos }, 'restore-point-');
|
||||||
|
|
||||||
logger.log(`Database Restore Starting. Database Version: ${databaseMajorVersion}`);
|
logger.log(`Database Restore Starting. Database Version: ${databaseMajorVersion}`);
|
||||||
|
|
||||||
@@ -266,32 +266,32 @@ export async function restoreBackup(
|
|||||||
logger.log(`Database Restore Success`);
|
logger.log(`Database Restore Success`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteBackups({ storage }: Pick<BackupRepos, 'storage'>, files: string[]): Promise<void> {
|
export async function deleteDatabaseBackup({ storage }: Pick<BackupRepos, 'storage'>, files: string[]): Promise<void> {
|
||||||
const backupsFolder = StorageCore.getBaseFolder(StorageFolder.Backups);
|
const backupsFolder = StorageCore.getBaseFolder(StorageFolder.Backups);
|
||||||
|
|
||||||
if (files.some((filename) => !isValidBackupName(filename))) {
|
if (files.some((filename) => !isValidDatabaseBackupName(filename))) {
|
||||||
throw new BadRequestException('Invalid backup name!');
|
throw new BadRequestException('Invalid backup name!');
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(files.map((filename) => storage.unlink(path.join(backupsFolder, filename))));
|
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 listDatabaseBackups({ storage }: Pick<BackupRepos, 'storage'>): Promise<string[]> {
|
||||||
const backupsFolder = StorageCore.getBaseFolder(StorageFolder.Backups);
|
const backupsFolder = StorageCore.getBaseFolder(StorageFolder.Backups);
|
||||||
const files = await storage.readdir(backupsFolder);
|
const files = await storage.readdir(backupsFolder);
|
||||||
return files
|
return files
|
||||||
.filter((fn) => isValidBackupName(fn))
|
.filter((fn) => isValidDatabaseBackupName(fn))
|
||||||
.toSorted((a, b) => (a.startsWith('uploaded-') === b.startsWith('uploaded-') ? a.localeCompare(b) : 1))
|
.toSorted((a, b) => (a.startsWith('uploaded-') === b.startsWith('uploaded-') ? a.localeCompare(b) : 1))
|
||||||
.toReversed();
|
.toReversed();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function uploadBackup(
|
export async function uploadDatabaseBackup(
|
||||||
{ storage }: Pick<BackupRepos, 'storage'>,
|
{ storage }: Pick<BackupRepos, 'storage'>,
|
||||||
file: Express.Multer.File,
|
file: Express.Multer.File,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
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 (!isValidDatabaseBackupName(fn)) {
|
||||||
throw new BadRequestException('Invalid backup name!');
|
throw new BadRequestException('Invalid backup name!');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,8 +299,8 @@ export async function uploadBackup(
|
|||||||
await storage.createOrOverwriteFile(path, file.buffer);
|
await storage.createOrOverwriteFile(path, file.buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function downloadBackup(fileName: string) {
|
export function downloadDatabaseBackup(fileName: string) {
|
||||||
if (!isValidBackupName(fileName)) {
|
if (!isValidDatabaseBackupName(fileName)) {
|
||||||
throw new BadRequestException('Invalid backup name!');
|
throw new BadRequestException('Invalid backup name!');
|
||||||
}
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user