-
-
Notifications
You must be signed in to change notification settings - Fork 4.5k
feat: restore database backups #23978
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
test: update backup.service.ts tests with new util
fix: missing action in cli.service.ts
refactor: cache the secret in memory
| 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'), | ||
| }) | ||
| async startRestoreFlow( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We already have an endpoint for entering maintenance mode and I find it a bit weird that we now have another place where that happens. Can we just pass the file to the startMaintenance endpoint instead? Or, can you wait in the client for maintenance to start and then request a database restore after that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Motivation here is to have a separate route without the authentication/permission checks in favour of checking if the first admin has been created (much like the logic for the first account registration: https://github.com/immich-app/immich/blob/main/server/src/services/auth.service.ts#L167)
server/src/maintenance/maintenance-ephemeral-state.repository.ts
Outdated
Show resolved
Hide resolved
|
|
||
| @Get('admin/maintenance/backups/list') | ||
| @MaintenanceRoute() | ||
| listBackups(): Promise<MaintenanceListBackupsResponseDto> { | ||
| return this.service.listBackups(); | ||
| } | ||
|
|
||
| @Get('admin/maintenance/backups/:filename') | ||
| @MaintenanceRoute() | ||
| downloadBackup(@Param() { filename }: FilenameParamDto, @Res() res: Response) { | ||
| res.header('Content-Disposition', 'attachment'); | ||
| res.sendFile(this.service.getBackupPath(filename)); | ||
| } | ||
|
|
||
| @Delete('admin/maintenance/backups/:filename') | ||
| @MaintenanceRoute() | ||
| async deleteBackup(@Param() { filename }: FilenameParamDto): Promise<void> { | ||
| return this.service.deleteBackup(filename); | ||
| } | ||
|
|
||
| @Post('admin/maintenance/backups/upload') | ||
| @MaintenanceRoute() | ||
| @UseInterceptors(FileInterceptor('file')) | ||
| uploadBackup( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's just make a separate controller that works in both situations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I separated backups into their own service which, since it's being used in the usual API worker, just inherits from BaseService so cannot be used in maintenance worker. It could not inherit from BaseService but that causes other issues like needing to fundamentally change some bits about tests & how these services are mocked. (and in general I think that would cause some headaches)
We've previously set convention to just reimplement routes we need in the maintenance worker controller:
immich/server/src/maintenance/maintenance-worker.controller.ts
Lines 42 to 45 in f9d2a97
| @Get('server/config') | |
| getServerConfig(): ServerConfigDto { | |
| return this.service.getSystemConfig(); | |
| } |
Which I think is fine to do here again? i.e.
immich/server/src/maintenance/maintenance-worker.controller.ts
Lines 78 to 82 in f9d2a97
| @Get('admin/database-backups') | |
| @MaintenanceRoute() | |
| listBackups(): Promise<MaintenanceListBackupsResponseDto> { | |
| return this.service.listBackups(); | |
| } |
| } | ||
| case MaintenanceAction.RestoreDatabase: { | ||
| if (!action.restoreBackupFilename) { | ||
| return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be an error and you should validate it in the dto imo.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is intentional behaviour to either show the restore flow or start a restore depending on if the filename is provided.
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
refactor: add active flag to maintenance status
How Has This Been Tested?
Demo
restore-demo.mp4
Screenshots
... non-exhaustive
API Changes
GET /admin/maintenance/statusreturns the current action, task, progress, error (also sent over WS asMaintenanceStatusV1)GET /admin/maintenance/integritygenerates an integrity check and heuristics reportGET /admin/maintenance/backups/listprovides a list of valid backup files presentGET /admin/maintenance/backups/:filenameallows a user to download a backup fileDELETE /admin/maintenance/backups/:filenamedeletes the backup filePOST /admin/maintenance/backups/restorestarts the database restore flow (onboarding only)POST /admin/maintenance/backups/uploadallows user to upload a backup fileChecklist:
src/services/uses repositories implementations for database calls, filesystem operations, etc.src/repositories/is pretty basic/simple and does not have any immich specific logic (that belongs insrc/services/)Please describe to which degree, if any, an LLM was used in creating this pull request.
Implementation of Duplex stream code is derived from documentation / example code generated by Claude.
It has since been refined and thoroughly tested.
Further Work (out of scope)
shared_link_edit_expire_after_option_[...]andshared_link_expires_[...]are too specific to their actual contents....and…