From 94af1bba4d147e6b05cfcf85c59313e6381d92ae Mon Sep 17 00:00:00 2001 From: izzy Date: Tue, 2 Dec 2025 16:47:31 +0000 Subject: [PATCH] refactor: `maintenanceStatus` -> `getMaintenanceStatus` refactor: `integrityCheck` -> `detectPriorInstall` chore: add `v2.4.0` version refactor: `/backups/list` -> `/backups` refactor: use sendFile in download route refactor: use separate backups permissions chore: correct descriptions refactor: permit handler that doesn't return promise for sendfile --- mobile/openapi/README.md | 8 +- .../lib/api/maintenance_admin_api.dart | 124 +++++++++--------- mobile/openapi/lib/model/permission.dart | 12 ++ open-api/immich-openapi-specs.json | 52 ++++---- open-api/typescript-sdk/src/fetch-client.ts | 16 ++- .../src/controllers/maintenance.controller.ts | 53 ++++---- server/src/enum.ts | 5 + .../maintenance-worker.controller.ts | 4 +- .../maintenance-worker.service.spec.ts | 2 +- .../maintenance/maintenance-worker.service.ts | 6 +- .../src/services/maintenance.service.spec.ts | 2 +- server/src/services/maintenance.service.ts | 18 ++- server/src/utils/file.ts | 2 +- server/src/utils/maintenance.ts | 4 +- .../maintenance/MaintenanceRestoreFlow.svelte | 4 +- web/src/lib/utils/maintenance.ts | 4 +- 16 files changed, 179 insertions(+), 137 deletions(-) diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 52556b6115..7925df0e99 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -162,14 +162,14 @@ Class | Method | HTTP request | Description *LibrariesApi* | [**updateLibrary**](doc//LibrariesApi.md#updatelibrary) | **PUT** /libraries/{id} | Update a library *LibrariesApi* | [**validate**](doc//LibrariesApi.md#validate) | **POST** /libraries/{id}/validate | Validate library settings *MaintenanceAdminApi* | [**deleteBackup**](doc//MaintenanceAdminApi.md#deletebackup) | **DELETE** /admin/maintenance/backups/{filename} | Delete backup +*MaintenanceAdminApi* | [**detectPriorInstall**](doc//MaintenanceAdminApi.md#detectpriorinstall) | **GET** /admin/maintenance/detect-install | Detect existing install *MaintenanceAdminApi* | [**downloadBackup**](doc//MaintenanceAdminApi.md#downloadbackup) | **GET** /admin/maintenance/backups/{filename} | Download backup -*MaintenanceAdminApi* | [**integrityCheck**](doc//MaintenanceAdminApi.md#integritycheck) | **GET** /admin/maintenance/integrity | Get integrity and heuristics -*MaintenanceAdminApi* | [**listBackups**](doc//MaintenanceAdminApi.md#listbackups) | **GET** /admin/maintenance/backups/list | List backups +*MaintenanceAdminApi* | [**getMaintenanceStatus**](doc//MaintenanceAdminApi.md#getmaintenancestatus) | **GET** /admin/maintenance/status | Get maintenance mode status +*MaintenanceAdminApi* | [**listBackups**](doc//MaintenanceAdminApi.md#listbackups) | **GET** /admin/maintenance/backups | List backups *MaintenanceAdminApi* | [**maintenanceLogin**](doc//MaintenanceAdminApi.md#maintenancelogin) | **POST** /admin/maintenance/login | Log into maintenance mode -*MaintenanceAdminApi* | [**maintenanceStatus**](doc//MaintenanceAdminApi.md#maintenancestatus) | **GET** /admin/maintenance/status | Get maintenance mode status *MaintenanceAdminApi* | [**setMaintenanceMode**](doc//MaintenanceAdminApi.md#setmaintenancemode) | **POST** /admin/maintenance | Set maintenance mode *MaintenanceAdminApi* | [**startRestoreFlow**](doc//MaintenanceAdminApi.md#startrestoreflow) | **POST** /admin/maintenance/backups/restore | Start backup restore flow -*MaintenanceAdminApi* | [**uploadBackup**](doc//MaintenanceAdminApi.md#uploadbackup) | **POST** /admin/maintenance/backups/upload | Upload asset +*MaintenanceAdminApi* | [**uploadBackup**](doc//MaintenanceAdminApi.md#uploadbackup) | **POST** /admin/maintenance/backups/upload | Upload database backup *MapApi* | [**getMapMarkers**](doc//MapApi.md#getmapmarkers) | **GET** /map/markers | Retrieve map markers *MapApi* | [**reverseGeocode**](doc//MapApi.md#reversegeocode) | **GET** /map/reverse-geocode | Reverse geocode coordinates *MemoriesApi* | [**addMemoryAssets**](doc//MemoriesApi.md#addmemoryassets) | **PUT** /memories/{id}/assets | Add assets to a memory diff --git a/mobile/openapi/lib/api/maintenance_admin_api.dart b/mobile/openapi/lib/api/maintenance_admin_api.dart index 8c6afc90cf..2c63f90d79 100644 --- a/mobile/openapi/lib/api/maintenance_admin_api.dart +++ b/mobile/openapi/lib/api/maintenance_admin_api.dart @@ -65,6 +65,54 @@ class MaintenanceAdminApi { } } + /// Detect existing install + /// + /// Collect integrity checks and other heuristics about local data. + /// + /// Note: This method returns the HTTP [Response]. + Future detectPriorInstallWithHttpInfo() async { + // ignore: prefer_const_declarations + final apiPath = r'/admin/maintenance/detect-install'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Detect existing install + /// + /// Collect integrity checks and other heuristics about local data. + Future detectPriorInstall() async { + final response = await detectPriorInstallWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'MaintenanceIntegrityResponseDto',) as MaintenanceIntegrityResponseDto; + + } + return null; + } + /// Download backup /// /// Downloads the database backup file @@ -122,14 +170,14 @@ class MaintenanceAdminApi { return null; } - /// Get integrity and heuristics + /// Get maintenance mode status /// - /// Collect integrity checks and other heuristics about local data. + /// Fetch information about the currently running maintenance action. /// /// Note: This method returns the HTTP [Response]. - Future integrityCheckWithHttpInfo() async { + Future getMaintenanceStatusWithHttpInfo() async { // ignore: prefer_const_declarations - final apiPath = r'/admin/maintenance/integrity'; + final apiPath = r'/admin/maintenance/status'; // ignore: prefer_final_locals Object? postBody; @@ -152,11 +200,11 @@ class MaintenanceAdminApi { ); } - /// Get integrity and heuristics + /// Get maintenance mode status /// - /// Collect integrity checks and other heuristics about local data. - Future integrityCheck() async { - final response = await integrityCheckWithHttpInfo(); + /// Fetch information about the currently running maintenance action. + Future getMaintenanceStatus() async { + final response = await getMaintenanceStatusWithHttpInfo(); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -164,7 +212,7 @@ class MaintenanceAdminApi { // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" // FormatException when trying to decode an empty string. if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'MaintenanceIntegrityResponseDto',) as MaintenanceIntegrityResponseDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'MaintenanceStatusResponseDto',) as MaintenanceStatusResponseDto; } return null; @@ -177,7 +225,7 @@ class MaintenanceAdminApi { /// Note: This method returns the HTTP [Response]. Future listBackupsWithHttpInfo() async { // ignore: prefer_const_declarations - final apiPath = r'/admin/maintenance/backups/list'; + final apiPath = r'/admin/maintenance/backups'; // ignore: prefer_final_locals Object? postBody; @@ -274,54 +322,6 @@ class MaintenanceAdminApi { return null; } - /// Get maintenance mode status - /// - /// Fetch information about the currently running maintenance action. - /// - /// Note: This method returns the HTTP [Response]. - Future maintenanceStatusWithHttpInfo() async { - // ignore: prefer_const_declarations - final apiPath = r'/admin/maintenance/status'; - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = []; - - - return apiClient.invokeAPI( - apiPath, - 'GET', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Get maintenance mode status - /// - /// Fetch information about the currently running maintenance action. - Future maintenanceStatus() async { - final response = await maintenanceStatusWithHttpInfo(); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'MaintenanceStatusResponseDto',) as MaintenanceStatusResponseDto; - - } - return null; - } - /// Set maintenance mode /// /// Put Immich into or take it out of maintenance mode @@ -410,9 +410,9 @@ class MaintenanceAdminApi { } } - /// Upload asset + /// Upload database backup /// - /// Uploads a new asset to the server. + /// Uploads .sql/.sql.gz file to restore backup from /// /// Note: This method returns the HTTP [Response]. /// @@ -454,9 +454,9 @@ class MaintenanceAdminApi { ); } - /// Upload asset + /// Upload database backup /// - /// Uploads a new asset to the server. + /// Uploads .sql/.sql.gz file to restore backup from /// /// Parameters: /// diff --git a/mobile/openapi/lib/model/permission.dart b/mobile/openapi/lib/model/permission.dart index 3b9a3964b6..555138f303 100644 --- a/mobile/openapi/lib/model/permission.dart +++ b/mobile/openapi/lib/model/permission.dart @@ -58,6 +58,10 @@ class Permission { static const authPeriodChangePassword = Permission._(r'auth.changePassword'); static const authDevicePeriodDelete = Permission._(r'authDevice.delete'); static const archivePeriodRead = Permission._(r'archive.read'); + static const backupPeriodList = Permission._(r'backup.list'); + static const backupPeriodDownload = Permission._(r'backup.download'); + static const backupPeriodUpload = Permission._(r'backup.upload'); + static const backupPeriodDelete = Permission._(r'backup.delete'); static const duplicatePeriodRead = Permission._(r'duplicate.read'); static const duplicatePeriodDelete = Permission._(r'duplicate.delete'); static const facePeriodCreate = Permission._(r'face.create'); @@ -206,6 +210,10 @@ class Permission { authPeriodChangePassword, authDevicePeriodDelete, archivePeriodRead, + backupPeriodList, + backupPeriodDownload, + backupPeriodUpload, + backupPeriodDelete, duplicatePeriodRead, duplicatePeriodDelete, facePeriodCreate, @@ -389,6 +397,10 @@ class PermissionTypeTransformer { case r'auth.changePassword': return Permission.authPeriodChangePassword; case r'authDevice.delete': return Permission.authDevicePeriodDelete; case r'archive.read': return Permission.archivePeriodRead; + case r'backup.list': return Permission.backupPeriodList; + case r'backup.download': return Permission.backupPeriodDownload; + case r'backup.upload': return Permission.backupPeriodUpload; + case r'backup.delete': return Permission.backupPeriodDelete; case r'duplicate.read': return Permission.duplicatePeriodRead; case r'duplicate.delete': return Permission.duplicatePeriodDelete; case r'face.create': return Permission.facePeriodCreate; diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 00c32cfcfa..90935706e3 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -372,7 +372,7 @@ "x-immich-state": "Alpha" } }, - "/admin/maintenance/backups/list": { + "/admin/maintenance/backups": { "get": { "description": "Get the list of the successful and failed backups", "operationId": "listBackups", @@ -407,11 +407,11 @@ "x-immich-admin-only": true, "x-immich-history": [ { - "version": "v9.9.9", + "version": "v2.4.0", "state": "Added" }, { - "version": "v9.9.9", + "version": "v2.4.0", "state": "Alpha" } ], @@ -435,11 +435,11 @@ ], "x-immich-history": [ { - "version": "v9.9.9", + "version": "v2.4.0", "state": "Added" }, { - "version": "v9.9.9", + "version": "v2.4.0", "state": "Alpha" } ], @@ -448,7 +448,7 @@ }, "/admin/maintenance/backups/upload": { "post": { - "description": "Uploads a new asset to the server.", + "description": "Uploads .sql/.sql.gz file to restore backup from", "operationId": "uploadBackup", "parameters": [], "requestBody": { @@ -478,22 +478,22 @@ "api_key": [] } ], - "summary": "Upload asset", + "summary": "Upload database backup", "tags": [ "Maintenance (admin)" ], "x-immich-admin-only": true, "x-immich-history": [ { - "version": "v9.9.9", + "version": "v2.4.0", "state": "Added" }, { - "version": "v9.9.9", + "version": "v2.4.0", "state": "Alpha" } ], - "x-immich-permission": "maintenance", + "x-immich-permission": "backup.upload", "x-immich-state": "Alpha" } }, @@ -535,15 +535,15 @@ "x-immich-admin-only": true, "x-immich-history": [ { - "version": "v9.9.9", + "version": "v2.4.0", "state": "Added" }, { - "version": "v9.9.9", + "version": "v2.4.0", "state": "Alpha" } ], - "x-immich-permission": "maintenance", + "x-immich-permission": "backup.delete", "x-immich-state": "Alpha" }, "get": { @@ -591,22 +591,22 @@ "x-immich-admin-only": true, "x-immich-history": [ { - "version": "v9.9.9", + "version": "v2.4.0", "state": "Added" }, { - "version": "v9.9.9", + "version": "v2.4.0", "state": "Alpha" } ], - "x-immich-permission": "maintenance", + "x-immich-permission": "backup.download", "x-immich-state": "Alpha" } }, - "/admin/maintenance/integrity": { + "/admin/maintenance/detect-install": { "get": { "description": "Collect integrity checks and other heuristics about local data.", - "operationId": "integrityCheck", + "operationId": "detectPriorInstall", "parameters": [], "responses": { "200": { @@ -620,17 +620,17 @@ "description": "" } }, - "summary": "Get integrity and heuristics", + "summary": "Detect existing install", "tags": [ "Maintenance (admin)" ], "x-immich-history": [ { - "version": "v9.9.9", + "version": "v2.4.0", "state": "Added" }, { - "version": "v9.9.9", + "version": "v2.4.0", "state": "Alpha" } ], @@ -684,7 +684,7 @@ "/admin/maintenance/status": { "get": { "description": "Fetch information about the currently running maintenance action.", - "operationId": "maintenanceStatus", + "operationId": "getMaintenanceStatus", "parameters": [], "responses": { "200": { @@ -704,11 +704,11 @@ ], "x-immich-history": [ { - "version": "v9.9.9", + "version": "v2.4.0", "state": "Added" }, { - "version": "v9.9.9", + "version": "v2.4.0", "state": "Alpha" } ], @@ -18250,6 +18250,10 @@ "auth.changePassword", "authDevice.delete", "archive.read", + "backup.list", + "backup.download", + "backup.upload", + "backup.delete", "duplicate.read", "duplicate.delete", "face.create", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 483caf38b1..81a15c4bc4 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1891,7 +1891,7 @@ export function listBackups(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; data: MaintenanceListBackupsResponseDto; - }>("/admin/maintenance/backups/list", { + }>("/admin/maintenance/backups", { ...opts })); } @@ -1905,7 +1905,7 @@ export function startRestoreFlow(opts?: Oazapfts.RequestOpts) { })); } /** - * Upload asset + * Upload database backup */ export function uploadBackup({ maintenanceUploadBackupDto }: { maintenanceUploadBackupDto: MaintenanceUploadBackupDto; @@ -1941,13 +1941,13 @@ export function downloadBackup({ filename }: { })); } /** - * Get integrity and heuristics + * Detect existing install */ -export function integrityCheck(opts?: Oazapfts.RequestOpts) { +export function detectPriorInstall(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; data: MaintenanceIntegrityResponseDto; - }>("/admin/maintenance/integrity", { + }>("/admin/maintenance/detect-install", { ...opts })); } @@ -1969,7 +1969,7 @@ export function maintenanceLogin({ maintenanceLoginDto }: { /** * Get maintenance mode status */ -export function maintenanceStatus(opts?: Oazapfts.RequestOpts) { +export function getMaintenanceStatus(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; data: MaintenanceStatusResponseDto; @@ -5343,6 +5343,10 @@ export enum Permission { AuthChangePassword = "auth.changePassword", AuthDeviceDelete = "authDevice.delete", ArchiveRead = "archive.read", + BackupList = "backup.list", + BackupDownload = "backup.download", + BackupUpload = "backup.upload", + BackupDelete = "backup.delete", DuplicateRead = "duplicate.read", DuplicateDelete = "duplicate.delete", FaceCreate = "face.create", diff --git a/server/src/controllers/maintenance.controller.ts b/server/src/controllers/maintenance.controller.ts index 28339daf4f..8a0c3f5c97 100644 --- a/server/src/controllers/maintenance.controller.ts +++ b/server/src/controllers/maintenance.controller.ts @@ -4,6 +4,7 @@ import { Controller, Delete, Get, + Next, Param, Post, Res, @@ -12,7 +13,7 @@ import { } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger'; -import { Response } from 'express'; +import { NextFunction, Response } from 'express'; import { Endpoint, HistoryBuilder } from 'src/decorators'; import { AuthDto } from 'src/dtos/auth.dto'; import { @@ -26,9 +27,10 @@ import { } from 'src/dtos/maintenance.dto'; import { ApiTag, ImmichCookie, MaintenanceAction, Permission } from 'src/enum'; import { Auth, Authenticated, FileResponse, GetLoginDetails } from 'src/middleware/auth.guard'; -import { StorageRepository } from 'src/repositories/storage.repository'; +import { LoggingRepository } from 'src/repositories/logging.repository'; import { LoginDetails } from 'src/services/auth.service'; import { MaintenanceService } from 'src/services/maintenance.service'; +import { sendFile } from 'src/utils/file'; import { respondWithCookie } from 'src/utils/response'; import { FilenameParamDto } from 'src/validation'; @@ -36,30 +38,30 @@ import { FilenameParamDto } from 'src/validation'; @Controller('admin/maintenance') export class MaintenanceController { constructor( + private logger: LoggingRepository, private service: MaintenanceService, - private storageRepository: StorageRepository, ) {} @Get('status') @Endpoint({ summary: 'Get maintenance mode status', description: 'Fetch information about the currently running maintenance action.', - history: new HistoryBuilder().added('v9.9.9').alpha('v9.9.9'), + history: new HistoryBuilder().added('v2.4.0').alpha('v2.4.0'), }) - maintenanceStatus(): MaintenanceStatusResponseDto { + getMaintenanceStatus(): MaintenanceStatusResponseDto { return { action: MaintenanceAction.End, }; } - @Get('integrity') + @Get('detect-install') @Endpoint({ - summary: 'Get integrity and heuristics', + summary: 'Detect existing install', description: 'Collect integrity checks and other heuristics about local data.', - history: new HistoryBuilder().added('v9.9.9').alpha('v9.9.9'), + history: new HistoryBuilder().added('v2.4.0').alpha('v2.4.0'), }) - integrityCheck(): Promise { - return this.service.integrityCheck(); + detectPriorInstall(): Promise { + return this.service.detectPriorInstall(); } @Post('login') @@ -94,11 +96,11 @@ export class MaintenanceController { } } - @Get('backups/list') + @Get('backups') @Endpoint({ summary: 'List backups', description: 'Get the list of the successful and failed backups', - history: new HistoryBuilder().added('v9.9.9').alpha('v9.9.9'), + history: new HistoryBuilder().added('v2.4.0').alpha('v2.4.0'), }) @Authenticated({ permission: Permission.Maintenance, admin: true }) listBackups(): Promise { @@ -110,21 +112,24 @@ export class MaintenanceController { @Endpoint({ summary: 'Download backup', description: 'Downloads the database backup file', - history: new HistoryBuilder().added('v9.9.9').alpha('v9.9.9'), + history: new HistoryBuilder().added('v2.4.0').alpha('v2.4.0'), }) - @Authenticated({ permission: Permission.Maintenance, admin: true }) - downloadBackup(@Param() { filename }: FilenameParamDto, @Res() res: Response) { - res.header('Content-Disposition', 'attachment'); - res.sendFile(this.service.getBackupPath(filename)); + @Authenticated({ permission: Permission.BackupDownload, admin: true }) + async downloadBackup( + @Param() { filename }: FilenameParamDto, + @Res() res: Response, + @Next() next: NextFunction, + ): Promise { + await sendFile(res, next, () => this.service.downloadBackup(filename), this.logger); } @Delete('backups/:filename') @Endpoint({ summary: 'Delete backup', description: 'Delete a backup by its filename', - history: new HistoryBuilder().added('v9.9.9').alpha('v9.9.9'), + history: new HistoryBuilder().added('v2.4.0').alpha('v2.4.0'), }) - @Authenticated({ permission: Permission.Maintenance, admin: true }) + @Authenticated({ permission: Permission.BackupDelete, admin: true }) async deleteBackup(@Param() { filename }: FilenameParamDto): Promise { return this.service.deleteBackup(filename); } @@ -133,7 +138,7 @@ export class MaintenanceController { @Endpoint({ summary: 'Start backup restore flow', description: 'Put Immich into maintenance mode to restore a backup (Immich must not be configured)', - history: new HistoryBuilder().added('v9.9.9').alpha('v9.9.9'), + history: new HistoryBuilder().added('v2.4.0').alpha('v2.4.0'), }) async startRestoreFlow( @GetLoginDetails() loginDetails: LoginDetails, @@ -147,13 +152,13 @@ export class MaintenanceController { } @Post('backups/upload') - @Authenticated({ permission: Permission.Maintenance, admin: true }) + @Authenticated({ permission: Permission.BackupUpload, admin: true }) @ApiConsumes('multipart/form-data') @ApiBody({ description: 'Backup Upload', type: MaintenanceUploadBackupDto }) @Endpoint({ - summary: 'Upload asset', - description: 'Uploads a new asset to the server.', - history: new HistoryBuilder().added('v9.9.9').alpha('v9.9.9'), + summary: 'Upload database backup', + description: 'Uploads .sql/.sql.gz file to restore backup from', + history: new HistoryBuilder().added('v2.4.0').alpha('v2.4.0'), }) @UseInterceptors(FileInterceptor('file')) uploadBackup( diff --git a/server/src/enum.ts b/server/src/enum.ts index 4647c673f7..71f4442913 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -127,6 +127,11 @@ export enum Permission { ArchiveRead = 'archive.read', + BackupList = 'backup.list', + BackupDownload = 'backup.download', + BackupUpload = 'backup.upload', + BackupDelete = 'backup.delete', + DuplicateRead = 'duplicate.read', DuplicateDelete = 'duplicate.delete', diff --git a/server/src/maintenance/maintenance-worker.controller.ts b/server/src/maintenance/maintenance-worker.controller.ts index 2b4a557877..134c91411e 100644 --- a/server/src/maintenance/maintenance-worker.controller.ts +++ b/server/src/maintenance/maintenance-worker.controller.ts @@ -37,8 +37,8 @@ export class MaintenanceWorkerController { } @Get('admin/maintenance/integrity') - integrityCheck(): Promise { - return this.service.integrityCheck(); + detectPriorInstall(): Promise { + return this.service.detectPriorInstall(); } @Post('admin/maintenance/login') diff --git a/server/src/maintenance/maintenance-worker.service.spec.ts b/server/src/maintenance/maintenance-worker.service.spec.ts index eab869592e..d22d148f24 100644 --- a/server/src/maintenance/maintenance-worker.service.spec.ts +++ b/server/src/maintenance/maintenance-worker.service.spec.ts @@ -177,7 +177,7 @@ describe(MaintenanceWorkerService.name, () => { mocks.storage.readFile.mockResolvedValue(undefined as never); mocks.storage.overwriteFile.mockRejectedValue(undefined as never); - await expect(sut.integrityCheck()).resolves.toMatchInlineSnapshot(` + await expect(sut.detectPriorInstall()).resolves.toMatchInlineSnapshot(` { "storage": [ { diff --git a/server/src/maintenance/maintenance-worker.service.ts b/server/src/maintenance/maintenance-worker.service.ts index 3605ae1af4..b4f79b5f49 100644 --- a/server/src/maintenance/maintenance-worker.service.ts +++ b/server/src/maintenance/maintenance-worker.service.ts @@ -29,7 +29,7 @@ import { type ServerService as _ServerService } from 'src/services/server.servic import { MaintenanceModeState } from 'src/types'; import { deleteBackup, isValidBackupName, listBackups, restoreBackup, uploadBackup } from 'src/utils/backups'; import { getConfig } from 'src/utils/config'; -import { createMaintenanceLoginUrl, integrityCheck } from 'src/utils/maintenance'; +import { createMaintenanceLoginUrl, detectPriorInstall } from 'src/utils/maintenance'; import { getExternalDomain } from 'src/utils/misc'; /** @@ -198,8 +198,8 @@ export class MaintenanceWorkerService { } } - integrityCheck(): Promise { - return integrityCheck(this.storageRepository); + detectPriorInstall(): Promise { + return detectPriorInstall(this.storageRepository); } async login(jwt?: string): Promise { diff --git a/server/src/services/maintenance.service.spec.ts b/server/src/services/maintenance.service.spec.ts index 812ea4dd10..540a5e8ea8 100644 --- a/server/src/services/maintenance.service.spec.ts +++ b/server/src/services/maintenance.service.spec.ts @@ -63,7 +63,7 @@ describe(MaintenanceService.name, () => { mocks.storage.readFile.mockResolvedValue(undefined as never); mocks.storage.overwriteFile.mockRejectedValue(undefined as never); - await expect(sut.integrityCheck()).resolves.toMatchInlineSnapshot(` + await expect(sut.detectPriorInstall()).resolves.toMatchInlineSnapshot(` { "storage": [ { diff --git a/server/src/services/maintenance.service.ts b/server/src/services/maintenance.service.ts index 983d0092c2..fe203f3651 100644 --- a/server/src/services/maintenance.service.ts +++ b/server/src/services/maintenance.service.ts @@ -3,14 +3,15 @@ import { basename, join } from 'node:path'; import { StorageCore } from 'src/cores/storage.core'; import { OnEvent } from 'src/decorators'; import { MaintenanceAuthDto, MaintenanceIntegrityResponseDto, SetMaintenanceModeDto } from 'src/dtos/maintenance.dto'; -import { MaintenanceAction, StorageFolder, SystemMetadataKey } from 'src/enum'; +import { CacheControl, MaintenanceAction, StorageFolder, SystemMetadataKey } from 'src/enum'; import { BaseService } from 'src/services/base.service'; import { MaintenanceModeState } from 'src/types'; import { deleteBackup, isValidBackupName, listBackups, uploadBackup } from 'src/utils/backups'; +import { ImmichFileResponse } from 'src/utils/file'; import { createMaintenanceLoginUrl, + detectPriorInstall, generateMaintenanceSecret, - integrityCheck, signMaintenanceJwt, } from 'src/utils/maintenance'; import { getExternalDomain } from 'src/utils/misc'; @@ -26,8 +27,8 @@ export class MaintenanceService extends BaseService { .then((state) => state ?? { isMaintenanceMode: false }); } - integrityCheck(): Promise { - return integrityCheck(this.storageRepository); + detectPriorInstall(): Promise { + return detectPriorInstall(this.storageRepository); } async startMaintenance(action: SetMaintenanceModeDto, username: string): Promise<{ jwt: string }> { @@ -98,6 +99,15 @@ export class MaintenanceService extends BaseService { return uploadBackup(this.backupRepos, file); } + downloadBackup(fileName: string): ImmichFileResponse { + return { + fileName, + cacheControl: CacheControl.PrivateWithoutCache, + contentType: fileName.endsWith('.gz') ? 'application/gzip' : 'application/sql', + path: this.getBackupPath(fileName), + }; + } + getBackupPath(filename: string): string { if (!isValidBackupName(filename)) { throw new BadRequestException('Invalid backup name!'); diff --git a/server/src/utils/file.ts b/server/src/utils/file.ts index 29c7f6f772..6ec78b0f21 100644 --- a/server/src/utils/file.ts +++ b/server/src/utils/file.ts @@ -42,7 +42,7 @@ const cacheControlHeaders: Record = { export const sendFile = async ( res: Response, next: NextFunction, - handler: () => Promise, + handler: () => Promise | ImmichFileResponse, logger: LoggingRepository, ): Promise => { // promisified version of 'res.sendFile' for cleaner async handling diff --git a/server/src/utils/maintenance.ts b/server/src/utils/maintenance.ts index 5ee701eb0b..8691f57985 100644 --- a/server/src/utils/maintenance.ts +++ b/server/src/utils/maintenance.ts @@ -77,7 +77,9 @@ export function generateMaintenanceSecret(): string { return randomBytes(64).toString('hex'); } -export async function integrityCheck(storageRepository: StorageRepository): Promise { +export async function detectPriorInstall( + storageRepository: StorageRepository, +): Promise { return { storage: await Promise.all( Object.values(StorageFolder).map(async (folder) => { diff --git a/web/src/lib/components/maintenance/MaintenanceRestoreFlow.svelte b/web/src/lib/components/maintenance/MaintenanceRestoreFlow.svelte index 730b26fedd..f8762a48d6 100644 --- a/web/src/lib/components/maintenance/MaintenanceRestoreFlow.svelte +++ b/web/src/lib/components/maintenance/MaintenanceRestoreFlow.svelte @@ -1,6 +1,6 @@