Skip to content

Conversation

@insertish
Copy link
Contributor

@insertish insertish commented Nov 18, 2025

  • Maintenance mode is now its own settings page with a section for restoring database backups.
  • Users can now manage their backups, incl. ability to restore and delete from the settings interface.
  • Users can now restore a database backup from onboarding / initial instance setup.
  • Basic integrity/heuristic checks for the library folders (displayed during onboarding "restore from backup").
  • Users can upload .sql or .sql.gz database dumps to restore.
  • Backup creation job now uses pg_dump instead of pg_dumpall.

How Has This Been Tested?

  • Updated maintenance e2e tests for API & web.
  • Update maintenance (& worker) service tests to cover new functionality.
  • Added new process repository tests to ensure duplex stream functions correctly.
  • Updated existing CLI & backup service tests where necessary to account for changes.

Demo

restore-demo.mp4

Screenshots

Screenshot 2025-11-25 at 11-16-33 Maintenance - Immich
Screenshot 2025-11-25 at 11-16-40 Maintenance - Immich Screenshot 2025-11-25 at 11-16-47 Maintenance - Immich

... non-exhaustive

API Changes

  • GET /admin/maintenance/status returns the current action, task, progress, error (also sent over WS as MaintenanceStatusV1)
  • GET /admin/maintenance/integrity generates an integrity check and heuristics report
  • GET /admin/maintenance/backups/list provides a list of valid backup files present
  • GET /admin/maintenance/backups/:filename allows a user to download a backup file
  • DELETE /admin/maintenance/backups/:filename deletes the backup file
  • POST /admin/maintenance/backups/restore starts the database restore flow (onboarding only)
  • POST /admin/maintenance/backups/upload allows user to upload a backup file

Checklist:

  • I have performed a self-review of my own code
  • I have made corresponding changes to the documentation if applicable
  • I have no unrelated changes in the PR.
  • I have confirmed that any new dependencies are strictly necessary.
  • I have written tests for new code (if applicable)
  • I have followed naming conventions/patterns in the surrounding code
  • All code in src/services/ uses repositories implementations for database calls, filesystem operations, etc.
  • All code in src/repositories/ is pretty basic/simple and does not have any immich specific logic (that belongs in src/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)

  • The translation keys shared_link_edit_expire_after_option_[...] and shared_link_expires_[...] are too specific to their actual contents.
  • Ellipsis usage in source translations is inconsistent, mix of ... and

@alextran1502 alextran1502 requested a review from jrasm91 December 1, 2025 15:20
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(
Copy link
Member

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?

Copy link
Contributor Author

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)

Comment on lines 64 to 87

@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(
Copy link
Member

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.

Copy link
Contributor Author

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:

@Get('server/config')
getServerConfig(): ServerConfigDto {
return this.service.getSystemConfig();
}

Which I think is fine to do here again? i.e.

@Get('admin/database-backups')
@MaintenanceRoute()
listBackups(): Promise<MaintenanceListBackupsResponseDto> {
return this.service.listBackups();
}

}
case MaintenanceAction.RestoreDatabase: {
if (!action.restoreBackupFilename) {
return;
Copy link
Member

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.

Copy link
Contributor Author

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants