Compare commits

...

83 Commits

Author SHA1 Message Date
Alex
0d24a1cb28 Merge branch 'main' of github.com:immich-app/immich into docs-2026 2026-01-22 11:13:39 -06:00
Alex
d99b68cd59 restore and backup 2026-01-22 10:05:17 -06:00
bo0tzz
55477a8a1a chore: revert mise-action bump (#25451) 2026-01-22 16:53:14 +01:00
Alex
c395de66d5 Apply suggestions from code review
Co-authored-by: Mert <101130780+mertalev@users.noreply.github.com>
Co-authored-by: Mees Frensel <33722705+meesfrensel@users.noreply.github.com>
2026-01-22 09:18:46 -06:00
Alex
7cbfc12e0d chore: use context menu for user table (#25428)
* chore: use context menu for user table

* chore: reorder columns

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
2026-01-22 07:44:08 -05:00
Mees Frensel
c320146538 fix: add scoped API permissions to map endpoints (#25423) 2026-01-22 07:43:29 -05:00
solluh
3304c8efd8 docs: update README_de_DE.md (#25443) 2026-01-22 12:55:44 +01:00
Alex Tran
0235510bf9 promote to table of content 2026-01-22 04:15:22 +00:00
Alex Tran
fc63e17e72 backup information 2026-01-22 04:09:54 +00:00
Alex Tran
633bcf2ebe darker dark 2026-01-22 03:45:38 +00:00
Daniel Dietzler
2dcb4efc40 fix: lock tags column on update (#25435) 2026-01-21 21:20:05 -05:00
Alex
2a046ec896 docs: beginning of the year tune up and updates 2026-01-21 16:21:53 -06:00
Alex
2f1d1edf10 chore: use context menu for library table (#25429)
* chore: use context menu for library table

* chore: add user detail link and menu divider

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
2026-01-21 15:07:11 -06:00
Jason Rasmussen
1b032339aa refactor(web): asset job actions (#25426) 2026-01-21 13:13:16 -05:00
Jason Rasmussen
dc82c13ddc refactor(web): user setting actions (#25424) 2026-01-21 13:13:07 -05:00
Jason Rasmussen
417af66f30 refactor(web): on person thumbnail (#25422) 2026-01-21 13:13:02 -05:00
Min Idzelis
280f906e4b feat: handle-error minor improvments (#25288)
* feat: handle-error minor improvments

* review comments

* Update web/src/lib/utils/handle-error.ts

Co-authored-by: Jason Rasmussen <jason@rasm.me>

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
Co-authored-by: Jason Rasmussen <jason@rasm.me>
2026-01-21 16:46:08 +00:00
Alex
b669714bda chore: lower case text + facelift (#25263)
* chore: lower case text

* wip

* wip

* pr feedback

* pr feedback
2026-01-21 16:41:09 +00:00
Alex
0f6606848e fix: upload file without extension (#25419)
* fix: upload file without extension

* chore: fix foreground upload
2026-01-21 16:31:06 +00:00
aviv926
1a8671d940 feat(docs): add Free Up Space section (#25253)
* feat(docs): add Free Up Space tool section with usage details and warnings

* typo
2026-01-21 10:29:59 -06:00
shenlong
fb94ee80aa fix: prevent cloud id sync on app pause (#25332)
* fix: sever version not populated post auto-login

* saferun syncCloudIds

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2026-01-21 09:54:08 -06:00
Mees Frensel
083ee0b5fe fix(web): allow exiting pin setup flow (#25413) 2026-01-21 09:53:15 -06:00
Jason Rasmussen
0bae88bef6 refactor(web): person service actions (#25402)
* refactor(web): person service actions

* fix: timeline e2e tests
2026-01-21 10:40:09 -05:00
Daniel Dietzler
184f1a6d32 fix: tag update race condition (#25371) 2026-01-21 16:30:45 +01:00
Jason Rasmussen
248cb86143 chore: disable broken e2e timeline tests (#25417) 2026-01-21 10:14:08 -05:00
Daniel Dietzler
1649d87360 refactor: extract isEdited into its own column in asset_file (#25358) 2026-01-21 16:08:21 +01:00
Mees Frensel
8970566865 fix(web): handle deletion from asset viewer on map page (#25393) 2026-01-21 14:08:01 +00:00
Alex
0b4a96140e fix: don't include metadata when upload motion part of LivePhotos (#25400)
* fix: don't include metadata when upload motion part of LivePhotos

* fix: get original file name
2026-01-21 13:58:32 +00:00
Noel S
72caf8983c fix(mobile): indicators not showing on thumbnail tile after asset change in viewer (#25297)
* fixed indicators staying hidden

* remove logs

* explanation comment

* move import to correct place

* revert accidental change in null handling
2026-01-20 14:02:54 -06:00
Paul Makles
61a9d5cbc7 feat: restore database backups (#23978)
* feat: ProcessRepository#createSpawnDuplexStream

* test: write tests for ProcessRepository#createSpawnDuplexStream

* feat: StorageRepository#createGzip,createGunzip,createPlainReadStream

* feat: backups util (args, create, restore, progress)

* feat: wait on maintenance operation lock on boot

* chore: use backup util from backup.service.ts
test: update backup.service.ts tests with new util

* feat: list/delete backups (maintenance services)

* chore: open api
fix: missing action in cli.service.ts

* chore: add missing repositories to MaintenanceModule

* refactor: move logSecret into module init

* feat: initialise StorageCore in maintenance mode

* feat: authenticate websocket requests in maintenance mode

* test: add mock for new storage fns

* feat: add MaintenanceEphemeralStateRepository
refactor: cache the secret in memory

* test: update service worker tests

* feat: add external maintenance mode status

* feat: synchronised status, restore db action

* test: backup restore service tests

* refactor: DRY end maintenance

* feat: list and delete backup routes

* feat: start action on boot

* fix: should set status on restore end

* refactor: add maintenanceStore to hold writables

* feat: sync status to web app

* feat: web impl.

* test: various utils for testings

* test: web e2e tests

* test: e2e maintenance spec

* test: update cli spec

* chore: e2e lint

* chore: lint fixes

* chore: lint fixes

* feat: start restore flow route

* test: update e2e tests

* chore: remove neon lights on maintenance action pages

* fix: use 'startRestoreFlow' on onboarding page

* chore: ignore any library folder in `docker/`

* fix: load status on boot

* feat: upload backups

* refactor: permit any .sql(.gz) to be listed/restored

* feat: download backups from list

* fix: permit uploading just .sql files

* feat: restore just .sql files

* fix: don't show backups list if logged out

* feat: system integrity check in restore flow

* test: not providing failed backups in API anymore

* test: util should also not try to use failedBackups

* fix: actually assign inputStream

* test: correct test backup prep.

* fix: ensure task is defined to show error

* test: fix docker cp command

* test: update e2e web spec to select next button

* test: update e2e api tests

* test: refactor timeouts

* chore: remove `showDelete` from maint. settings

* chore: lint

* chore: lint

* fix: make sure backups are correctly sorted for clean up

* test: update service spec

* test: adjust e2e timeout

* test: increase web timeouts for ci

* chore: move gitignore changes

* chore: additional filename validation

* refactor: better typings for integrity API

* feat: higher accuracy progress tracking

* chore: delay lock retry

* refactor: remove old maintenance settings

* refactor: clean up tailwind classes

* refactor: use while loop rather than recursive calls

* test: update service specs

* chore: check canParse too

* chore: lint

* fix: logic error causing infinite loop

* refactor: use <ProgressBar /> from ui library

* fix: create or overwrite file

* chore: i18n pass, update progress bar

* fix: wrong translation string

* chore: update colour variables

* test: update web test for new maint. page

* chore: format, fix key

* test: update tests to be more linter complaint & use new routines

* chore: update onClick -> onAction, title -> breadcrumbs

* fix: use wrench icon in admin settings sidebar

* chore: add translation strings to accordion

* chore: lint

* refactor: move maintenance worker init into service

* 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

* refactor: move status impl into service
refactor: add active flag to maintenance status

* refactor: split into database backup controller

* test: split api e2e tests and passing

* fix: move end button into authed default maint page

* fix: also show in restore flow

* fix: import getMaintenanceStatus

* test: split web e2e tests

* refactor: ensure detect install is consistently named

* chore: ensure admin for detect install while out of maint.

* refactor: remove state repository

* test: update maint. worker service spec

* test: split backup service spec

* refactor: rename db backup routes

* refactor: instead of param, allow bulk backup deletion

* test: update sdk use in e2e test

* test: correct deleteBackup call

* fix: correct type for serverinstall response dto

* chore: validate filename for deletion

* test: wip

* test: backups no longer take path param

* refactor: scope util to database-backups instead of backups

* fix: update worker controller with new route

* chore: use new admin page actions

* chore: remove stray comment

* test: rename outdated test

* refactor: getter pattern for maintenance secret

* refactor: `createSpawnDuplexStream` -> `spawnDuplexStream`

* refactor: prefer `Object.assign`

* refactor: remove useless try {} block

* refactor: prefer `type Props`
refactor: prefer arrow function

* refactor: use luxon API for minutesAgo

* chore: remove change to gitignore

* refactor: prefer `type Props`

* refactor: remove async from onMount

* refactor: use luxon toRelative for relative time

* refactor: duplicate logic check

* chore: open api

* refactor: begin moving code into web//services

* refactor: don't use template string with $t

* test: use dialog role to match prompt

* refactor: split actions into flow/restore

* test: fix action value

* refactor: move more service calls into web//services

* chore: should void fn return

* chore: bump 2.4.0 to 2.5.0 in controller

* chore: bump 2.4.0 to 2.5.0 in controller

* refactor: use events for web//services

* chore: open api

* chore: open api

* refactor: don't await returned promise

* refactor: remove redundant check

* refactor: add `type: command` to actions

* refactor: split backup entries into own component

* refactor: split restore flow into separate components

* refactor(web): split BackupDelete event

* chore: stylings

* chore: stylings

* fix: don't log query failure on first boot

* feat: support pg_dumpall backups

* feat: display information about each backup

* chore: i18n

* feat: rollback to restore point on migrations failure

* feat: health check after restore

* chore: format

* refactor: split health check into separate function

* refactor: split health into repository
test: write tests covering rollbacks

* fix: omit 'health' requirement from createDbBackup

* test(e2e): rollback test

* fix: wrap text in backup entry

* fix: don't shrink context menu button

* fix: correct CREATE DB syntax for postgres

* test: rename backups generated by test

* feat: add filesize to backup response dto

* feat: restore list

* feat: ui work

* fix: e2e test

* fix: e2e test

* pr feedback

* pr feedback

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
Co-authored-by: Jason Rasmussen <jason@rasm.me>
2026-01-20 09:22:28 -06:00
Min Idzelis
ca0d4b283a feat: zoom image improvements for reactive prop handlings (#25286) 2026-01-20 13:18:54 +01:00
renovate[bot]
2b4e4051f0 fix(deps): update typescript-projects (#25377)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-01-20 11:20:27 +00:00
renovate[bot]
0f3956f654 chore(deps): update dependency @types/node to ^24.10.8 (#25376)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-01-20 10:44:39 +00:00
Alex
99bd7d5f27 chore: sharing action button position (#25381) 2026-01-20 01:43:57 +00:00
Alex
fe1d0edf4c chore: mobile font tuning (#25349)
* chore: mobile font tuning

* chore: fix some paddings

* setting page tune

* chore: album sort dropdown button styling

* pr feedback

* tweak sync status card

* chore: refactor
2026-01-19 14:56:35 -06:00
Arne Schwarck
4ef699e9fa feat: allow /memory?id= in AndroidManifest (#25373)
Allow /memory?id=

<!-- Allow singular memory route like /memory?id=... -->
2026-01-19 14:56:24 -06:00
Brandon Wees
3e21174dd8 chore: web editor improvements (#25169) 2026-01-19 18:57:15 +00:00
Brandon Wees
1b56bb84f9 fix: mobile edit handling (#25315)
Co-authored-by: mertalev <101130780+mertalev@users.noreply.github.com>
2026-01-19 12:22:53 -06:00
Marius
b3f5b8ede8 fix(mobile): album selector icon visibility (#25311)
Add explicit color to sort direction arrows and view mode toggle icons in album selector widget. Previously they were invisible in light view, when opening album selector from image viewer.
2026-01-19 12:18:32 -06:00
Jason Rasmussen
2b77dc8e1f refactor(web): workflow create action (#25369) 2026-01-19 12:41:28 -05:00
Jason Rasmussen
97a594556b refactor: sharing page actions (#25368) 2026-01-19 12:16:16 -05:00
Jason Rasmussen
4a7c4b6d15 refactor(web): routes (#25365) 2026-01-19 12:07:31 -05:00
Jason Rasmussen
a8198f9934 refactor: lock session (#25366)
refafctor: lock session
2026-01-19 11:47:58 -05:00
Jason Rasmussen
b123beae38 fix(server): api key update checks (#25363) 2026-01-19 10:20:06 -05:00
Mees Frensel
1ada7a8340 chore(deps): ignore @parcel/watcher build script (#25361) 2026-01-19 09:08:25 -05:00
Matthew Momjian
5d81cace23 chore(docs): update RAM req (#25344)
* RAM req

* Update requirements.md
2026-01-18 17:52:08 -06:00
yy
65f9a228ba fix: typos in comments and error messages (#25320) 2026-01-17 18:58:26 -06:00
Kolin
e6eca895ba fix(web): add min-width to setting input field (#25317)
Prevents input fields from collapsing in flex layouts, such as the extension field in storage template settings. Fixes #25298.
2026-01-16 16:31:06 -06:00
Jason Rasmussen
8196bd9bbd refactor(web): routes (#25313) 2026-01-16 16:11:09 -05:00
Daniel Dietzler
07675a2de4 feat: download original asset (#25302)
Co-authored-by: bwees <brandonwees@gmail.com>
2026-01-16 19:05:13 +00:00
Jason Rasmussen
a2b03f7650 refactor(web): user sidebar (#25292) 2026-01-16 11:17:35 -05:00
Savely Krasovsky
fdff591a11 feat: update intel compute driver (#25259) 2026-01-16 14:42:55 +00:00
Alex
e4443fa43e chore: dart http foreground upload (#24883)
* feat: bring back manual backup

* expose iCloud retrieval progress

* wip

* unify http upload method, check for connectivity on iOS

* handle LivePhotos progress

* feat: speed calculation

* wip

* better upload detail page

* handle error

* handle error

* pr feedback

* feat: share intent upload

* feat: manual upload

* feat: manual upload progress

* chore: styling

* refactor

* refactor

* remove unused logs

* fix: background android backup

* feat: add error section

* remove complete section

* remove empty state and prevent slot jumps

* more refactor

* fix: background test

* chore: add metadata to foreground upload

* fix: email and name get reset in auth provider

* pr feedback

* remove version check for metadata field in upload payload

* chore: fix unit test

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-01-15 20:10:08 -06:00
Jason Rasmussen
843d563178 refactor(web): admin page layout (#25281)
* refactor(web): admin page layout

* chore: remove unused props
2026-01-15 18:58:43 -05:00
Min Idzelis
256d62e22d feat: thumbhash improvments for reactive prop updates (#25287) 2026-01-15 18:57:43 -05:00
shenlong
91592aa48e fix(mobile): drop unique constraint on cloud_id (#25291)
fix: drop unique constraint on cloud_id

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-01-15 17:06:29 -06:00
shenlong
2ac113624b chore: remote unused sync_cloud_ids key (#25290)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-01-15 16:56:05 -06:00
renovate[bot]
0052979853 chore(deps): update dependency svelte to v5.46.4 [security] (#25284)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-15 22:10:17 +01:00
renovate[bot]
79b6c4ac70 chore(deps): update dependency @sveltejs/kit to v2.49.5 [security] (#25280)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-15 15:07:26 -05:00
Alex
95eb3e26c3 chore: sidebar spacing (#25278) 2026-01-15 10:35:01 -06:00
Alex
613dc858cb chore: tweak table text size (#25276) 2026-01-15 11:06:34 -05:00
shenlong
2f3fbd7dc5 fix: ignore duplicate cloud ID updates (#25271)
* fix: ignore duplicate remote updates

* update cloudId when any one of the ETag part is mismatched

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-01-15 09:15:56 -06:00
Min Idzelis
80a5444bf4 feat: redesign asset-viewer previous/next and hide when nav not possible (#24903) 2026-01-15 12:55:01 +01:00
Jason Rasmussen
d59ee7d2ae feat(web): immich/ui select component (#25268)
Co-authored-by: Alex <alex.tran1502@gmail.com>
2026-01-14 19:38:13 -06:00
idubnori
7b3a298c6a fix: Swagger UI generates incorrect double-prefixed URLs (/api/api/...) (#25266)
fix: add ignoreGlobalPrefix option to Swagger options
2026-01-14 16:55:17 -06:00
Alex
0a62ec7e29 chore: album option modal styling (#25269)
* chore: album option modal styling

* header action button color
2026-01-14 16:52:33 -06:00
Jason Rasmussen
21802ab5ba fix(server): prevent duplicate metadata items from being sent (#25267) 2026-01-14 16:52:06 -06:00
Daniel Dietzler
56dfdfd033 refactor: album share and options modals (#25212)
* refactor: album share modals

* chore: clean up

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
2026-01-14 15:18:02 -05:00
Jason Rasmussen
2190921c85 chore: await api key nested modal (#25265) 2026-01-14 14:02:44 -05:00
shenlong
9fa8de7baa feat: add cloud id during native sync (#20418)
* use adjustment time in iOS for hash reset

# Conflicts:
#	mobile/lib/infrastructure/repositories/local_album.repository.dart
#	mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart

* migration

* feat: sync cloudId and eTag on sync

* fixes fixes

* more fixes

* re-sync updated eTags

* add server version check & auto sync cloud ids on compatible servers

* fix test

* remove button from sync status page

* chore: modify for testing

* more changes

* chore: add commas in toString

* use cached provider in splash screen

* read upload service provider to prevent reset

* log errors from fetching cloud id mapping

* WIP: migrate cloud id - debug log

* ignore locked asset update

* bulk update metadata

* change log text

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2026-01-14 12:34:11 -06:00
Akash Karmakar
ed9448a6ee fix: dark mode appbar color (#24976)
* fix: dark mode appbar color

* update: using scrolledUnderElevation for sufaceTint change

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2026-01-14 17:47:34 +00:00
Noel S
15224a9ac5 fix(mobile): improve asset transition back to timeline (#24485)
* test

* wip

* fix: indicators popping in due to z height change of hero animation (fade in instead after animation)

* wip

* fix: selection outline changing to transparent before animation finish

* Remove unnecessary changes and follow conventions

* remove accidentally included files

* clean up

* new approach

* detect hero animation.

* wip

* Revert "new approach"

This reverts commit 13919f6d04.

* remove delayed animation

* wip

* wip (need to fix first open not triggering indicator hide)

* fix indicators not hiding on first hero animation

* Add back hiding selection background container

* revert accidental regression

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2026-01-14 10:40:24 -06:00
Alex
6e00fd92ef chore: use fontWeight for Text component (#25262) 2026-01-14 16:25:30 +00:00
Alex
6fdd1ce41a chore: use font-mono (#25250)
* chore: use font-mono

* chore: override variable
2026-01-14 11:21:48 -05:00
Jason Rasmussen
91d4cd6824 refactor: tables (#25226) 2026-01-14 07:56:09 -05:00
Ben
c7254a0c30 fix(docs): add missing mermaid dependency and configuration (#25247)
* fix(docs): add missing mermaid dependency and configuration

* fix: include pnpm-lock.yaml

* fix: docusaurus config format issue
2026-01-13 23:13:34 -05:00
Jason Rasmussen
38f01a6b7d fix(web): redirect to login (#25254) 2026-01-13 23:11:14 -05:00
Jason Rasmussen
f194a7ea3e fix: migration order (#25249) 2026-01-13 14:47:58 -06:00
Noel S
05a7ba98c1 fix(mobile): prevent system UI from hiding on drag down gesture (#25240)
* fix system ui briefly disappearing

* code style change
2026-01-13 19:40:24 +00:00
Alex
edc513a3df feat(web): 2026 font (#25174)
* feat(web): 2026 font

* chore: docs font

* spacing tweak

* tweak minimum font weight and update ui lib

* small tweaks

* docs: small tweaks

* more tweaks
2026-01-13 18:19:09 +00:00
Yaros
39212a049c feat(web): search albums by description (#25244)
feat: search albums by description
2026-01-13 11:56:59 -06:00
renovate[bot]
9b4f370834 chore(deps): update node.js to v24.13.0 (#25243)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-13 16:19:10 +00:00
Alex
aba85b036c feat(mobile): 2026 font (#25213) 2026-01-13 09:59:57 -06:00
576 changed files with 34412 additions and 6340 deletions

2
.github/.nvmrc vendored
View File

@@ -1 +1 @@
24.12.0
24.13.0

View File

@@ -131,7 +131,7 @@ jobs:
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@b868e6e7c8cc212beec876330b4059e661ee44bb # use-mise-action-v1.1.1
uses: immich-app/devtools/actions/use-mise@cd24790a7f5f6439ac32cc94f5523cb2de8bfa8c # use-mise-action-v1.1.0
- name: Load parameters
id: parameters

View File

@@ -29,7 +29,7 @@ jobs:
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@b868e6e7c8cc212beec876330b4059e661ee44bb # use-mise-action-v1.1.1
uses: immich-app/devtools/actions/use-mise@cd24790a7f5f6439ac32cc94f5523cb2de8bfa8c # use-mise-action-v1.1.0
- name: Destroy Docs Subdomain
env:

View File

@@ -1 +1 @@
24.12.0
24.13.0

View File

@@ -20,7 +20,7 @@
"@types/lodash-es": "^4.17.12",
"@types/micromatch": "^4.0.9",
"@types/mock-fs": "^4.13.1",
"@types/node": "^24.10.4",
"@types/node": "^24.10.8",
"@vitest/coverage-v8": "^3.0.0",
"byte-size": "^9.0.0",
"cli-progress": "^3.12.0",
@@ -69,6 +69,6 @@
"micromatch": "^4.0.8"
},
"volta": {
"node": "24.12.0"
"node": "24.13.0"
}
}

View File

@@ -1 +1 @@
24.12.0
24.13.0

View File

@@ -2,56 +2,113 @@
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import { mdiAlertCircle, mdiCheckCircle } from '@mdi/js';
import Icon from '@mdi/react';
A [3-2-1 backup strategy](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) is recommended to protect your data. You should keep copies of your uploaded photos/videos as well as the Immich database for a comprehensive backup solution. This page provides an overview on how to backup the database and the location of user-uploaded pictures and videos. A template bash script that can be run as a cron job is provided [here](/guides/template-backup-script.md)
:::danger
The instructions on this page show you how to prepare your Immich instance to be backed up, and which files to take a backup of. You still need to take care of using an actual backup tool to make a backup yourself.
:::
## Database
Immich stores [file paths in the database](https://github.com/immich-app/immich/discussions/3299), users metadata in the database, it does not scan the library folder, so database backups are essential
### Automatic Database Backups
Immich automatically creates database backups for disaster-recovery purposes. These backups are stored in `UPLOAD_LOCATION/backups` and can be managed through the web interface.
You can adjust the backup schedule and retention settings in **Administration > Settings > Backup** (default: keep last 14 backups, create daily at 2:00 AM).
:::caution
Immich saves [file paths in the database](https://github.com/immich-app/immich/discussions/3299), it does not scan the library folder to update the database so backups are crucial.
Database backups do **not** contain photos or videos — only metadata. They must be used together with a copy of the files in `UPLOAD_LOCATION` as outlined below.
:::
#### Creating a Backup
You can trigger a database backup manually:
1. Go to **Administration > Job Queues**
2. Click **Create job** in the top right
3. Select **Create Database Backup** and click **Confirm**
The backup will appear in `UPLOAD_LOCATION/backups` and counts toward your retention limit.
### Restoring a Database Backup
Immich provides two ways to restore a database backup: through the web interface or via the command line. The web interface is the recommended method for most users.
#### Restore from Settings {#restore-from-settings}
If you have an existing Immich installation:
<img
src={require('./img/restore-from-settings.webp').default}
title="Restore from settings"
/>
1. Go to **Administration > Maintenance**
2. Expand the **Restore database backup** section
3. You'll see a list of available backups with their version and creation date
4. Click **Restore** next to the backup you want to restore
5. Confirm the restore operation
:::info
Refer to the official [postgres documentation](https://www.postgresql.org/docs/current/backup.html) for details about backing up and restoring a postgres database.
Restoring a backup will wipe the current database and replace it with the backup. A restore point is automatically created before the operation begins, allowing rollback if the restore fails.
:::
:::caution
It is not recommended to directly backup the `DB_DATA_LOCATION` folder. Doing so while the database is running can lead to a corrupted backup that cannot be restored.
#### Restore from Onboarding {#restore-from-onboarding}
If you're setting up Immich on a fresh installation and want to restore from an existing backup:
<img
src={require('./img/restore-from-onboarding.webp').default}
title="Restore from onboarding"
/>
1. On the welcome screen, click **Restore from backup**
2. Immich will enter maintenance mode and display integrity checks for your storage folders
3. Review the folder status to ensure your library files are accessible
4. Click **Next** to proceed to backup selection
5. Select a backup from the list or upload a backup file (`.sql.gz`)
6. Click **Restore** to begin the restoration process
:::tip
Before restoring, ensure your `UPLOAD_LOCATION` folders contain the same files that existed when the backup was created. The integrity check will show you which folders are readable/writable and how many files they contain.
:::
### Automatic Database Dumps
### Uploading a Backup File {#uploading-backup}
You can upload a database backup file directly:
1. In the **Restore database backup** section, click **Select from computer**
2. Choose a `.sql.gz` file
3. The uploaded backup will appear in the list with an `uploaded-` prefix
4. Click **Restore** to restore from the uploaded file
### Backup Version Compatibility {#backup-compatibility}
When viewing backups, Immich displays compatibility indicators based on the current version and the information from the filename:
- <Icon path={mdiCheckCircle} size={1} color="green"/> Backup version matches current Immich version
- <Icon path={mdiAlertCircle} size={1} color="#feb001"/> Backup was created with a different Immich version
- <Icon path={mdiAlertCircle} size={1} color="red"/> Could not determine backup version
:::warning
The automatic database dumps can be used to restore the database in the event of damage to the Postgres database files.
There is no monitoring for these dumps and you will not be notified if they are unsuccessful.
Restoring a backup from a different Immich version may require database migrations. The restore process will attempt to run migrations automatically, but you should ensure you're restoring to a compatible version when possible.
:::
:::caution
The database dumps do **NOT** contain any pictures or videos, only metadata. They are only usable with a copy of the other files in `UPLOAD_LOCATION` as outlined below.
:::
### Restore Process {#restore-process}
For disaster-recovery purposes, Immich will automatically create database dumps. The dumps are stored in `UPLOAD_LOCATION/backups`.
Please be sure to make your own, independent backup of the database together with the asset folders as noted below.
You can adjust the schedule and amount of kept database dumps in the [admin settings](http://my.immich.app/admin/system-settings?isOpen=backup).
By default, Immich will keep the last 14 database dumps and create a new dump every day at 2:00 AM.
During restoration, Immich will:
#### Trigger Dump
1. Create a backup of the current database (restore point)
2. Restore the selected backup
3. Run database migrations if needed
4. Perform a health check to verify the restore succeeded
You are able to trigger a database dump in the [admin job status page](http://my.immich.app/admin/queues).
Visit the page, open the "Create job" modal from the top right, select "Create Database Dump" and click "Confirm".
A job will run and trigger a dump, you can verify this worked correctly by checking the logs or the `backups/` folder.
This dumps will count towards the last `X` dumps that will be kept based on your settings.
If the restore fails (e.g., corrupted backup or missing admin user), Immich will automatically roll back to the restore point.
#### Restoring
### Restore via Command Line {#restore-cli}
We hope to make restoring simpler in future versions, for now you can find the database dumps in the `UPLOAD_LOCATION/backups` folder on your host.
Then please follow the steps in the following section for restoring the database.
### Manual Backup and Restore
For advanced users or automated recovery scenarios, you can restore a database backup using the command line.
<Tabs>
<TabItem value="Linux system" label="Linux system" default>
@@ -106,10 +163,12 @@ docker compose up -d # Start remainder of Immich ap
</TabItem>
</Tabs>
Note that for the database restore to proceed properly, it requires a completely fresh install (i.e. the Immich server has never run since creating the Docker containers). If the Immich app has run, Postgres conflicts may be encountered upon database restoration (relation already exists, violated foreign key constraints, multiple primary keys, etc.), in which case you need to delete the `DB_DATA_LOCATION` folder to reset the database.
:::note
For the database restore to proceed properly, it requires a completely fresh install (i.e., the Immich server has never run since creating the Docker containers). If the Immich app has run, you may encounter Postgres conflicts (relation already exists, violated foreign key constraints, etc.). In this case, delete the `DB_DATA_LOCATION` folder to reset the database.
:::
:::tip
Some deployment methods make it difficult to start the database without also starting the server. In these cases, you may set the environment variable `DB_SKIP_MIGRATIONS=true` before starting the services. This will prevent the server from running migrations that interfere with the restore process. Be sure to remove this variable and restart the services after the database is restored.
Some deployment methods make it difficult to start the database without also starting the server. In these cases, set the environment variable `DB_SKIP_MIGRATIONS=true` before starting the services. This prevents the server from running migrations that interfere with the restore process. Remove this variable and restart services after the database is restored.
:::
## Filesystem
@@ -157,17 +216,14 @@ for more info read the [release notes](https://github.com/immich-app/immich/rele
- **Encoded Assets:**
- Videos that have been re-encoded from the original for wider compatibility. The original is not removed.
- Stored in `UPLOAD_LOCATION/encoded-video/<userID>`.
- **Database Dump Backups:**
- Automatic database backups created by Immich for disaster recovery.
- Stored in `UPLOAD_LOCATION/backups/`.
- **Postgres**
- The Immich database containing all the information to allow the system to function properly.
**Note:** This folder will only appear to users who have made the changes mentioned in [v1.102.0](https://github.com/immich-app/immich/discussions/8930) (an optional, non-mandatory change) or who started with this version.
- Stored in `DB_DATA_LOCATION`.
:::danger
A backup of this folder does not constitute a backup of your database!
Follow the instructions listed [here](/administration/backup-and-restore#database) to learn how to perform a proper backup.
:::
</TabItem>
<TabItem value="Storage Template On" label="Storage Template On">
@@ -203,16 +259,14 @@ When you turn off the storage template engine, it will leave the assets in `UPLO
- Files uploaded through mobile apps.
- Temporarily located in `UPLOAD_LOCATION/upload/<userID>`.
- Transferred to `UPLOAD_LOCATION/library/<userID>` upon successful upload.
- **Database Dump Backups:**
- Automatic database backups created by Immich for disaster recovery.
- Stored in `UPLOAD_LOCATION/backups/`.
- **Postgres**
- The Immich database containing all the information to allow the system to function properly.
**Note:** This folder will only appear to users who have made the changes mentioned in [v1.102.0](https://github.com/immich-app/immich/discussions/8930) (an optional, non-mandatory change) or who started with this version.
- Stored in `DB_DATA_LOCATION`.
:::danger
A backup of this folder does not constitute a backup of your database!
Follow the instructions listed [here](/administration/backup-and-restore#database) to learn how to perform a proper backup.
:::
</TabItem>
</Tabs>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 167 KiB

After

Width:  |  Height:  |  Size: 323 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -50,7 +50,7 @@ When a new asset is uploaded it kicks off a series of jobs, which include metada
Additionally, some jobs (such as memories generation) run on a schedule, which is every night at midnight by default. To change when they run or enable/disable a job navigate to System Settings -> [Nightly Tasks Settings](https://my.immich.app/admin/system-settings?isOpen=nightly-tasks).
<img src={require('./img/admin-nightly-tasks.webp').default} width="60%" title="Admin nightly tasks" />
<img src={require('./img/admin-nightly-tasks.webp').default} width="80%" title="Admin nightly tasks" />
:::note
Some jobs ([External Libraries](/features/libraries) scanning, Database Dump) are configured in their own sections in System Settings.

View File

@@ -4,7 +4,7 @@ Maintenance mode is used to perform administrative tasks such as restoring backu
You can enter maintenance mode by either:
- Selecting "enable maintenance mode" in system settings in administration.
- Selecting "Switch to maintenance mode" in `Maintenance` tab in administration.
- Running the enable maintenance mode [administration command](./server-commands.md).
## Logging in during maintenance

View File

@@ -31,7 +31,7 @@ Admin can send a welcome email if the Email option is set, you can learn here ho
Admin can specify the storage quota for the user as the instance's admin; once the limit is reached, the user won't be able to upload to the instance anymore.
In order to select a storage quota, click on the pencil icon and enter the storage quota in GiB. You can choose an unlimited quota by leaving it empty (default).
In order to select a storage quota, click on the edit user icon and enter the storage quota in GiB. You can choose an unlimited quota by leaving it empty (default).
:::tip
The system administrator can see the usage quota percentage of all users in Server Stats page.
@@ -41,12 +41,12 @@ The system administrator can see the usage quota percentage of all users in Serv
External libraries don't take up space from the storage quota.
:::
<img src={require('./img/user-quota-size.webp').default} width="40%" title="Set Quota Size" />
<img src={require('./img/user-quota-size.webp').default} width="80%" title="Set Quota Size" />
## Set Storage Label For User
The admin can add a custom label for each user, so instead of `upload/{userId}/your-template` it will be `upload/{custom_user_label}/your-template`.
To apply a storage template, go to the Administration page -> click on the pencil button next to the user.
To apply a storage template, go to the `Administration > Users`, then click on the context menu button next to the user.
:::note
To apply the Storage Label to previously uploaded assets, run the Storage Migration Job.
:::
@@ -55,25 +55,21 @@ To apply the Storage Label to previously uploaded assets, run the Storage Migrat
## Password Reset
To reset a user's password, click the pencil icon to edit a user, then click "Reset Password". The user's password will be reset to random password and they have to change it next time the sign in.
<img src={require('./img/user-edit-menu.webp').default} width="80%" title="Customize Delete User" />
<img src={require('./img/user-management-update.webp').default} width="40%" title="Reset Password" />
To reset a user's password, go to the `Administration > Users`, then click on the context menu button next to the user, then click "Reset Password". The user's password will be reset to random password and they have to change it next time the sign in.
## Delete a User
If you need to remove a user from Immich, head to "Administration", where users can be scheduled for deletion. The user account will immediately become disabled and their library and all associated data will be removed after 7 days by default.
<img src={require('./img/delete-user.webp').default} width="40%" title="Delete User" />
If you need to remove a user from Immich, go to the `Administration > Users`, then click on the context menu button next to the user. The user account will immediately become disabled and their library and all associated data will be removed after 7 days by default.
### Delete Delay
You can customize the time of the deletion of the users from the Administration -> Settings -> User Settings.
You can customize the time of the deletion of the users from the `Administration -> Settings -> User Settings`.
:::info user deletion job
The user deletion job runs at midnight to check for users that are ready for deletion. Changes to this setting will be evaluated at the next execution.
:::
<img src={require('./img/customize-delete-user.webp').default} width="80%" title="Customize Delete User" />
### Immediately Remove User
You can choose to delete a user immediately by checking the box

View File

@@ -37,7 +37,8 @@ All the services are packaged to run as with single Docker Compose command.
1. Clone the project repo.
2. Run `cp docker/example.env docker/.env`.
3. Edit `docker/.env` to provide values for the required variable `UPLOAD_LOCATION`.
4. From the root directory, run:
4. Install dependencies - `pnpm i`
5. From the root directory, run:
```bash title="Start development server"
make dev # required Makefile installed on the system.

View File

@@ -1,37 +1,42 @@
# Automatic Backup
## Overview
Immich supports uploading photos and videos from your mobile device to the server automatically.
---
When backup is enabled, Immich will upload new photos and videos from selected albums when you open or resume the app, as well as periodically in the background.
You can enable the settings by accessing the upload options from the upload page
<img
src={require('./img/enable-backup-button.webp').default}
width="300px"
title="Upload button"
/>
<img src={require('./img/backup-settings-access.webp').default} width="50%" title="Backup option selection" />
## Platform Specific Features
<img src={require('./img/background-foreground-backup.webp').default} width="50%" title="Foreground&Background Backup" />
### General
## Foreground backup
By default, Immich will only upload photos and videos when connected to Wi-Fi. You can change this behavior in the backup settings page.
If foreground backup is enabled: whenever the app is opened or resumed, it will check if any photos or videos in the selected album(s) have yet to be uploaded to the cloud (the remainder count). If there are any, they will be uploaded.
<img
src={require('./img/backup-options.webp').default}
width="500px"
title="Upload button"
/>
## Background backup
### Android
This feature is intended for everyday use. For initial bulk uploading, please use the foreground upload feature. For more information on why background upload is not working as expected, please refer to the [FAQ](/FAQ#why-does-foreground-backup-stop-when-i-navigate-away-from-the-app-shouldnt-it-transfer-the-job-to-background-backup).
If background backup is enabled. The app will periodically check if there are any new photos or videos in the selected album(s) to be uploaded to the server. If there are, it will upload them to the cloud in the background.
:::info Note
#### General
- The app must be in the background for the backup worker to start running.
- If you reopen the app and the first page you see is the backup page, the counts will not reflect the background uploaded result. You have to navigate out of the page and come back to see the updated counts.
#### Android
<img
src={require('./img/android-backup-options.webp').default}
width="500px"
title="Upload button"
/>
- It is a well-known problem that some Android models are very strict with battery optimization settings, which can cause a problem with the background worker. Please visit [Don't kill my app](https://dontkillmyapp.com/) for a guide on disabling this setting on your phone.
- You can allow the background task to run when the device is charging.
- You can set the minimum delay from the time a photo is taken to when the background upload task will run.
#### iOS
### iOS
- You must enable **Background App Refresh** for the app to work in the background. You can enable it in the Settings app under General > Background App Refresh.
@@ -39,4 +44,4 @@ If background backup is enabled. The app will periodically check if there are an
<img src={require('./img/background-app-refresh.webp').default} width="30%" title="background-app-refresh" />
</div>
:::
- iOS automatically manages background tasks; the app cannot control when the background upload task will run. The more frequently you open the app, the more often background tasks will run.

View File

@@ -188,6 +188,8 @@ immich upload --dry-run . | tail -n +6 | jq .newFiles[]
### Obtain the API Key
The API key can be obtained in the user setting panel on the web interface.
The API key can be obtained in the user setting panel on the web interface. You can also specify permissions for the key to limit its access.
![Obtain Api Key](./img/obtain-api-key.webp)
![Specify permissions for the key](./img/obtain-api-key-2.webp)

View File

@@ -21,14 +21,14 @@ The asset detail view will also show the faces that are recognized in the asset.
Additional actions you can do include:
- Changing the feature photo of the person
- Setting a person's date of birth
- Merging two or more detected faces into one person
- Hiding the faces of a person from the Explore page and detail view
- Assigning an unrecognized face to a person
- Setting a person's date of birth, so that the age of the person can be shown at the time the photo was taken
- Merging two or more detected people into one person
- Favoriting a person to pin them to the top of the list
It can be found from the app bar when you access the detail view of a person.
<img src={require('./img/facial-recognition-4.webp').default} title='Facial Recognition 4' width="70%"/>
<img src={require('./img/facial-recognition-4.webp').default} title='Facial Recognition 4' />
## How Face Detection Works

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 180 KiB

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 112 KiB

View File

@@ -118,46 +118,35 @@ _Remember to run `docker compose up -d` to register the changes. Make sure you c
These actions must be performed by the Immich administrator.
- Click on your avatar in the upper right corner
- Click on Administration -> External Libraries
- Click on Create an external library
- Select which user owns the library, this can not be changed later
- Enter `/mnt/media/christmas-trip` then click Add
- Click on Save
- Click the drop-down menu on the newly created library
- Click on Scan
- Click the drop-down menu on the newly created library
- Click on Rename Library and rename it to "Christmas Trip"
- Click on your avatar in the upper right corner.
- Click on `Administration -> External Libraries`.
- Click on `Create Library`.
- Select which user owns the library, this **can not** be changed later
- You are now entering the library management page.
- Click on `Add` in the `Folders` section.
- Enter `/mnt/media/christmas-trip` then click Add.
- Click on `Edit` Library and rename it to "Christmas Trip".
NOTE: We have to use the `/mnt/media/christmas-trip` path and not the `/mnt/nas/christmas-trip` path since all paths have to be what the Docker containers see.
Next, we'll add an exclusion pattern to filter out raw files.
- Click the drop-down menu on the newly-created Christmas library
- Click on Manage
- Click on Scan Settings
- Click on Add Exclusion Pattern
- Enter `**/Raw/**` and click save.
- Click save
- Click the drop-down menu on the newly created library
- Click on Scan
- Click on `Add` in the `Exclusion Patterns` section.
- Enter `**/Raw/**` and click Add.
- Click on `Scan`
The christmas trip library will now be scanned in the background. In the meantime, let's add the videos and old photos to another library.
- Click on Create External Library.
:::note
If you get an error here, please rename the other external library to something else. This is a bug that will be fixed in a future release.
:::
- Click the drop-down menu on the newly created library
- Click Edit Import Paths
- Click on Add Path
- Go back to `Administration -> External Libraries`.
- Click on `Create Library`.
- Select which user owns the library,
- You are now entering the library management page.
- Click on `Add` in the `Folders` section.
- Enter `/mnt/media/old-pics` then click Add
- Click on Add Path
- Click on `Add` in the `Folders` section.
- Enter `/mnt/media/videos` then click Add
- Click Save
- Click on Scan
- Click on `Scan`
- Click on `Edit` Library and rename it to "Old videos and photos".
Within seconds, the assets from the old-pics and videos folders should show up in the main timeline.

View File

@@ -20,14 +20,6 @@ Below are the SHA-256 fingerprints for the certificates signing the android appl
:::
:::info Beta Program
The beta release channel allows users to test upcoming changes before they are officially released. To join the channel use the links below.
- Android: Invitation link from [web](https://play.google.com/store/apps/details?id=app.alextran.immich) or from [mobile](https://play.google.com/store/apps/details?id=app.alextran.immich)
- iOS: [TestFlight invitation link](https://testflight.apple.com/join/1vYsAa8P)
:::
## Login
<MobileAppLogin />
@@ -36,10 +28,6 @@ The beta release channel allows users to test upcoming changes before they are o
<MobileAppBackup />
:::info
You can enable automatic backup on supported devices. For more information see [Automatic Backup](/features/automatic-backup.md).
:::
## Sync only selected photos
If you have a large number of photos on the device, and you would prefer not to backup all the photos, then it might be prudent to only backup selected photos from device to the Immich server.
@@ -57,17 +45,45 @@ This will enable a small cloud icon on the bottom right corner of the asset tile
Now make sure that the local album is selected in the backup screen (steps 1-2 above). You can find these albums listed in **<ins>Library -> On this device</ins>**. To selectively upload photos from these albums, simply select the local-only photos and tap on "Upload" button in the dynamic bottom menu.
<img
src={require('./img/mobile-upload-open-photo.webp').default}
width="50%"
title="Upload button on local asset preview"
/>
<img
src={require('./img/mobile-upload-selected-photos.webp').default}
width="40%"
title="Upload button after photos selection"
/>
## Free Up Space
The **Free Up Space** tool allows you to remove local media files from your device that have already been successfully backed up to your Immich server (and are not in the Immich trash). This helps reclaim storage on your mobile device without losing your memories.
### How it works
<img src={require('./img/free-up-space.webp').default} title="Free up space" />
1. **Configuration:**
- **Cutoff Date:** You can select a cutoff date. The tool will only look for photos and videos **on or before** this date.
- **Filter Options:** You can choose to remove **All** assets, or restrict removal to **Photos only** or **Videos only**.
- **Keep Favorites:** By default, local assets marked as favorites are preserved on your device, even if they match the cutoff date.
2. **Scan & Review:** Before any files are removed, you are presented with a review screen to verify which items will be deleted.
3. **Deletion:** Confirmed items are moved to your device's native Trash/Recycle Bin.
:::info reclaim storage
To permanently free up space, you must manually empty the system/gallery trash.
:::
### iCloud Photos
If you use **iCloud Photos** alongside Immich, it is vital to understand how deletion affects your data. iCloud utilizes a two-way sync; this means deleting a photo from your iPhone to free up space will **also delete it from iCloud**.
Assets that are part of an **iCloud Shared Album** are automatically excluded from the cleanup scan because iCloud does not allow removing the items from the device.
### External App Dependencies (WhatsApp, etc.)
Android applications like **WhatsApp** rely on local files to display media in chat history.
If Immich backs up your WhatsApp folder and you run **Free Up Space**, the local copies of these images will be deleted. Consequently, **media in your WhatsApp chats will appear blurry or missing.** You will only be able to view these photos inside the Immich app; they will no longer be visible within the WhatsApp interface.
**Recommendation:** If keeping chat history intact is important, please ensure you review the deletion list carefully or consider excluding WhatsApp folders from the backup if you intend to use this feature frequently.
## Album Sync
You can sync or mirror an album from your phone to the Immich server on your account. For example, if you select Recents, Camera and Videos album for backup, the corresponding album with the same name will be created on the server. Once the assets from those albums are uploaded, they will be put into the target albums automatically.

View File

@@ -11,45 +11,25 @@ Contextual CLIP search is powered by the [VectorChord](https://github.com/tensor
In addition, Immich offers advanced search functionality, allowing you to find specific content using customizable search filters. These filters include location, one or more faces, specific albums, and more. You can try out the search filters on the [Demo site](https://demo.immich.app).
The filters smart search allows you to search by include:
You can mix and match to search the following types of content:
- People
- Location
- Country
- State
- City
- Camera
- Make
- Model
- Date range
- File name or extension
- Media type
- Image (including live/motion photos)
- Video
- All
- Condition
- Not in any album
- Archived
- Favorited
- Rating
<Tabs>
<TabItem value="Computer" label="Computer" default>
Some search examples:
| Type | Description |
| ----------------------------------- | ----------------------------------------------------- |
| People | Faces that are recognized in your photos/videos. |
| Contextual | Content of the photos and videos. |
| File name or extension | Full or partial file's name, or file's extension |
| Description | Description added to assets. |
| Optical Character Recognition (OCR) | Text in images |
| Locations | Cities, states, and countries from reverse geocoding. |
| Tags | Tags assigned or extracted from assets. |
| Camera | make, model and lens model |
| Time frame | Start and end date of a specific time bucket |
| Media type | Image or video or both |
| Display options | In Archive, in Favorites or Not in any album |
| Start rating | User-assigned start rating |
<img src={require('./img/advanced-search-filters.webp').default} width="70%" title='Advanced search filters' />
<img src={require('./img/search-ex-1.webp').default} width="70%" title='Search Example 1' />
</TabItem>
<TabItem value="Mobile" label="Mobile">
<img src={require('./img/mobile-smart-search.webp').default} width="30%" title='Smart search on mobile' />
</TabItem>
</Tabs>
## Configuration
Navigating to `Administration > Settings > Machine Learning Settings > Smart Search` will show the options available.

View File

@@ -30,26 +30,17 @@ In the Immich web UI:
- click the **Administration** link in the upper right corner.
<img src={require('./img/administration-link.webp').default} width="50%" title="Administration link" />
- Select the **External Libraries** tab
<img src={require('./img/external-libraries.webp').default} width="50%" title="External Libraries tab" />
- Click the **Create Library** button
<img src={require('./img/create-external-library.webp').default} width="50%" title="Create Library button" />
- Select the **External Libraries** tab and click the **Create Library** button
<img src={require('./img/create-external-library.webp').default} width="80%" title="Create Library button" />
- In the dialog, select which user should own the new library
<img src={require('./img/library-owner.webp').default} width="50%" title="Library owner dialog" />
- Click the three-dots menu and select **Edit Import Paths**
<img src={require('./img/edit-import-paths.webp').default} width="50%" title="Edit Import Paths menu option" />
- You are now entering the library management page.
<img src={require('./img/library-management-page.webp').default} width="80%" title="Library management page" />
- Click Add path
<img src={require('./img/add-path-button.webp').default} width="50%" title="Add Path button" />
- Enter **/home/user/photos1** as the path and click Add
<img src={require('./img/add-path-field.webp').default} width="50%" title="Add Path field" />
- Save the new path
<img src={require('./img/path-save.webp').default} width="50%" title="Path Save button" />
- Click `Add` in the Folder section to specify a path for scanning and enter **/home/user/photos1** as the path and click Add
<img src={require('./img/edit-import-path.webp').default} width="50%" title="Add an import path" />
- Click the three-dots menu and select **Scan New Library Files**
<img src={require('./img/scan-new-library-files.webp').default} width="50%" title="Scan New Library Files menu option" />
@@ -64,4 +55,3 @@ In the Immich web UI:
- You should see non-zero Active jobs for
Library, Generate Thumbnails, and Extract Metadata.
<img src={require('./img/job-status.webp').default} width="50%" title="Job Status display" />

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -17,11 +17,11 @@ If this does not work, try running `docker compose up -d --force-recreate`.
## Docker Compose
| Variable | Description | Default | Containers |
| :----------------- | :------------------------------ | :-------: | :----------------------- |
| `IMMICH_VERSION` | Image tags | `release` | server, machine learning |
| `UPLOAD_LOCATION` | Host path for uploads | | server |
| `DB_DATA_LOCATION` | Host path for Postgres database | | database |
| Variable | Description | Default | Containers |
| :----------------- | :------------------------------ | :-----: | :----------------------- |
| `IMMICH_VERSION` | Image tags | `v2` | server, machine learning |
| `UPLOAD_LOCATION` | Host path for uploads | | server |
| `DB_DATA_LOCATION` | Host path for Postgres database | | database |
:::tip
These environment variables are used by the `docker-compose.yml` file and do **NOT** affect the containers directly.

View File

@@ -17,12 +17,17 @@ Hardware and software requirements for Immich:
- Immich runs well in a virtualized environment when running in a full virtual machine.
The use of Docker in LXC containers is [not recommended](https://pve.proxmox.com/wiki/Linux_Container), but may be possible for advanced users.
If you have issues, we recommend that you switch to a supported VM deployment.
- **RAM**: Minimum 4GB, recommended 6GB.
- **RAM**: Minimum 6GB, recommended 8GB.
- **CPU**: Minimum 2 cores, recommended 4 cores.
- **Storage**: Recommended Unix-compatible filesystem (EXT4, ZFS, APFS, etc.) with support for user/group ownership and permissions.
- The generation of thumbnails and transcoded video can increase the size of the photo library by 10-20% on average.
:::tip
:::note RAM requirements
For a smooth experience, especially during asset upload, Immich requires at least 6GB of RAM.
For systems with only 4GB of RAM, Immich can be run with machine learning features disabled.
:::
:::tip Postgres setup
Good performance and a stable connection to the Postgres database is critical to a smooth Immich experience.
The Postgres database files are typically between 1-3 GB in size.
For this reason, the Postgres database (`DB_DATA_LOCATION`) should ideally use local SSD storage, and never a network share of any kind.

View File

@@ -10,7 +10,7 @@ to install and use it.
## Requirements
- A system with at least 4GB of RAM and 2 CPU cores.
- A system with at least 6GB of RAM and 2 CPU cores.
- [Docker](https://docs.docker.com/engine/install/)
> For a more detailed list of requirements, see the [requirements page](/install/requirements).
@@ -63,9 +63,9 @@ The backup time differs depending on how many photos are on your mobile device.
take quite a while.
To quickly get going, you can selectively upload few photos first, by following this [guide](/features/mobile-app#sync-only-selected-photos).
You can select the **Jobs** tab to see Immich processing your photos.
You can select the **Job Queues** tab to see Immich processing your photos.
<img src={require('/docs/guides/img/jobs-tab.webp').default} title="Jobs tab" width={300} />
<img src={require('/docs/guides/img/jobs-tab.webp').default} title="Job Queues tab" width={300} />
---

View File

@@ -6,4 +6,4 @@
<img src={require('./img/album-selection.webp').default} width='50%' title='Backup button' />
3. Scroll down to the bottom and press "**Start Backup**" to start the backup process. This will upload all the assets in the selected albums.
3. Scroll down to the bottom and press "**Enable Backup**" to start the backup process. This will upload all the assets in the selected albums.

View File

@@ -2,6 +2,6 @@ If you have friends or family members who want to use the application as well, y
<img src={require('./img/create-new-user.webp').default} width="90%" title='New User Registration' />
In the Administration panel, you can click on the **Create user** button, and you'll be presented with the following dialog:
On the **Administration > Users** page, you can click on the **Create user** button, and you'll be presented with the following dialog:
<img src={require('./img/create-new-user-dialog.webp').default} width="90%" title='New User Registration Dialog' />
<img src={require('./img/create-new-user-dialog.webp').default} width="40%" title='New User Registration Dialog' />

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 260 KiB

View File

@@ -26,6 +26,12 @@ const config = {
locales: ['en'],
},
// Mermaid diagrams
markdown: {
mermaid: true,
},
themes: ['@docusaurus/theme-mermaid'],
plugins: [
async function myPlugin(context, options) {
return {
@@ -70,6 +76,10 @@ const config = {
autoCollapseCategories: false,
},
},
tableOfContents: {
minHeadingLevel: 2,
maxHeadingLevel: 4,
},
navbar: {
logo: {
alt: 'Immich Logo',

View File

@@ -20,6 +20,7 @@
"@docusaurus/core": "~3.9.0",
"@docusaurus/preset-classic": "~3.9.0",
"@docusaurus/theme-common": "~3.9.0",
"@docusaurus/theme-mermaid": "~3.9.0",
"@mdi/js": "^7.3.67",
"@mdi/react": "^1.6.1",
"@mdx-js/react": "^3.0.0",
@@ -57,6 +58,6 @@
"node": ">=20"
},
"volta": {
"node": "24.12.0"
"node": "24.13.0"
}
}

View File

@@ -8,19 +8,19 @@
@tailwind utilities;
@font-face {
font-family: 'Overpass';
src: url('/fonts/overpass/Overpass.ttf') format('truetype-variations');
font-weight: 1 999;
font-family: 'GoogleSans';
src: url('/fonts/GoogleSans/GoogleSans.ttf') format('truetype-variations');
font-weight: 410 900;
font-style: normal;
ascent-override: 106.25%;
size-adjust: 106.25%;
}
@font-face {
font-family: 'Overpass Mono';
src: url('/fonts/overpass/OverpassMono.ttf') format('truetype-variations');
font-weight: 1 999;
font-style: normal;
font-family: 'GoogleSansCode';
src: url('/fonts/GoogleSansCode/GoogleSansCode.ttf') format('truetype-variations');
font-weight: 1 900;
font-style: monospace;
ascent-override: 106.25%;
size-adjust: 106.25%;
}
@@ -37,7 +37,8 @@ img {
/* You can override the default Infima variables here. */
:root {
font-family: 'Overpass', sans-serif;
font-family: 'GoogleSans', sans-serif;
letter-spacing: 0.1px;
--ifm-color-primary: #4250af;
--ifm-color-primary-dark: #4250af;
--ifm-color-primary-darker: #4250af;
@@ -48,6 +49,16 @@ img {
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: 'GoogleSans', sans-serif;
letter-spacing: 0.1px;
}
/* For readability concerns, you should choose a lighter palette in dark mode. */
[data-theme='dark'] {
--ifm-color-primary: #adcbfa;
@@ -58,7 +69,13 @@ img {
--ifm-color-primary-lighter: #e9f1fe;
--ifm-color-primary-lightest: #ffffff;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
--ifm-background-color: #000000;
--ifm-navbar-background-color: #0c0c0c;
--ifm-footer-background-color: #0c0c0c;
}
[data-theme='dark'] body,
[data-theme='dark'] .main-wrapper {
background-color: #070707;
}
div[class^='announcementBar_'] {
@@ -71,15 +88,22 @@ div[class^='announcementBar_'] {
padding: 10px 10px 10px 16px;
border-radius: 24px;
margin-right: 16px;
font-weight: 500;
}
.menu__list-item-collapsible {
margin-right: 16px;
border-radius: 24px;
font-weight: 500;
}
.menu__link--active {
font-weight: 500;
font-weight: 600;
}
.table-of-contents__link {
font-size: 14px;
font-weight: 450;
}
/* workaround for version switcher PR 15894 */
@@ -88,13 +112,14 @@ div[class*='navbar__items'] > li:has(a[class*='version-switcher-34ab39']) {
}
code {
font-weight: 600;
font-weight: 500;
font-family: 'GoogleSansCode';
}
.buy-button {
padding: 8px 14px;
border: 1px solid transparent;
font-family: 'Overpass', sans-serif;
font-family: 'GoogleSans', sans-serif;
font-weight: 500;
cursor: pointer;
box-shadow: 0 0 5px 2px rgba(181, 206, 254, 0.4);

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -17,9 +17,9 @@ module.exports = {
// Dark Theme
'immich-dark-primary': '#adcbfa',
'immich-dark-bg': '#070a14',
'immich-dark-bg': '#000000',
'immich-dark-fg': '#e5e7eb',
'immich-dark-gray': '#212121',
'immich-dark-gray': '#111111',
},
},
},

View File

@@ -1 +1 @@
24.12.0
24.13.0

View File

@@ -27,7 +27,7 @@
"@playwright/test": "^1.44.1",
"@socket.io/component-emitter": "^3.1.2",
"@types/luxon": "^3.4.2",
"@types/node": "^24.10.4",
"@types/node": "^24.10.8",
"@types/pg": "^8.15.1",
"@types/pngjs": "^6.0.4",
"@types/supertest": "^6.0.2",
@@ -52,6 +52,6 @@
"vitest": "^3.0.0"
},
"volta": {
"node": "24.12.0"
"node": "24.13.0"
}
}

View File

@@ -8,7 +8,7 @@ dotenv.config({ path: resolve(import.meta.dirname, '.env') });
export const playwrightHost = process.env.PLAYWRIGHT_HOST ?? '127.0.0.1';
export const playwrightDbHost = process.env.PLAYWRIGHT_DB_HOST ?? '127.0.0.1';
export const playwriteBaseUrl = process.env.PLAYWRIGHT_BASE_URL ?? `http://${playwrightHost}:2285`;
export const playwriteSlowMo = parseInt(process.env.PLAYWRIGHT_SLOW_MO ?? '0');
export const playwriteSlowMo = Number.parseInt(process.env.PLAYWRIGHT_SLOW_MO ?? '0');
export const playwrightDisableWebserver = process.env.PLAYWRIGHT_DISABLE_WEBSERVER;
process.env.PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS = '1';
@@ -39,13 +39,13 @@ const config: PlaywrightTestConfig = {
testMatch: /.*\.e2e-spec\.ts/,
workers: 1,
},
{
name: 'parallel tests',
use: { ...devices['Desktop Chrome'] },
testMatch: /.*\.parallel-e2e-spec\.ts/,
fullyParallel: true,
workers: process.env.CI ? 3 : Math.max(1, Math.round(cpus().length * 0.75) - 1),
},
// {
// name: 'parallel tests',
// use: { ...devices['Desktop Chrome'] },
// testMatch: /.*\.parallel-e2e-spec\.ts/,
// fullyParallel: true,
// workers: process.env.CI ? 3 : Math.max(1, Math.round(cpus().length * 0.75) - 1),
// },
// {
// name: 'firefox',

View File

@@ -0,0 +1,350 @@
import { LoginResponseDto, ManualJobName } from '@immich/sdk';
import { errorDto } from 'src/responses';
import { app, utils } from 'src/utils';
import request from 'supertest';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
describe('/admin/database-backups', () => {
let cookie: string | undefined;
let admin: LoginResponseDto;
beforeAll(async () => {
await utils.resetDatabase();
admin = await utils.adminSetup();
await utils.resetBackups(admin.accessToken);
});
describe('GET /', async () => {
it('should succeed and be empty', async () => {
const { status, body } = await request(app)
.get('/admin/database-backups')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
backups: [],
});
});
it('should contain a created backup', async () => {
await utils.createJob(admin.accessToken, {
name: ManualJobName.BackupDatabase,
});
await utils.waitForQueueFinish(admin.accessToken, 'backupDatabase');
await expect
.poll(
async () => {
const { status, body } = await request(app)
.get('/admin/database-backups')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
return body;
},
{
interval: 500,
timeout: 10_000,
},
)
.toEqual(
expect.objectContaining({
backups: [
expect.objectContaining({
filename: expect.stringMatching(/immich-db-backup-\d{8}T\d{6}-v.*-pg.*\.sql\.gz$/),
filesize: expect.any(Number),
}),
],
}),
);
});
});
describe('DELETE /', async () => {
it('should delete backup', async () => {
const filename = await utils.createBackup(admin.accessToken);
const { status } = await request(app)
.delete(`/admin/database-backups`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ backups: [filename] });
expect(status).toBe(200);
const { status: listStatus, body } = await request(app)
.get('/admin/database-backups')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(listStatus).toBe(200);
expect(body).toEqual(
expect.objectContaining({
backups: [],
}),
);
});
});
// => action: restore database flow
describe.sequential('POST /start-restore', () => {
afterAll(async () => {
await request(app).post('/admin/maintenance').set('cookie', cookie!).send({ action: 'end' });
await utils.poll(
() => request(app).get('/server/config'),
({ status, body }) => status === 200 && !body.maintenanceMode,
);
admin = await utils.adminSetup();
});
it.sequential('should not work when the server is configured', async () => {
const { status, body } = await request(app).post('/admin/database-backups/start-restore').send();
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('The server already has an admin'));
});
it.sequential('should enter maintenance mode in "database restore mode"', async () => {
await utils.resetDatabase(); // reset database before running this test
const { status, headers } = await request(app).post('/admin/database-backups/start-restore').send();
expect(status).toBe(201);
cookie = headers['set-cookie'][0].split(';')[0];
await expect
.poll(
async () => {
const { status, body } = await request(app).get('/server/config');
expect(status).toBe(200);
return body.maintenanceMode;
},
{
interval: 500,
timeout: 10_000,
},
)
.toBeTruthy();
const { status: status2, body } = await request(app).get('/admin/maintenance/status').send({ token: 'token' });
expect(status2).toBe(200);
expect(body).toEqual({
active: true,
action: 'select_database_restore',
});
});
});
// => action: restore database
describe.sequential('POST /backups/restore', () => {
beforeAll(async () => {
await utils.disconnectDatabase();
});
afterAll(async () => {
await utils.connectDatabase();
});
it.sequential('should restore a backup', { timeout: 60_000 }, async () => {
let filename = await utils.createBackup(admin.accessToken);
// work-around until test is running on released version
await utils.move(
`/data/backups/${filename}`,
'/data/backups/immich-db-backup-20260114T184016-v2.5.0-pg14.19.sql.gz',
);
filename = 'immich-db-backup-20260114T184016-v2.5.0-pg14.19.sql.gz';
const { status } = await request(app)
.post('/admin/maintenance')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({
action: 'restore_database',
restoreBackupFilename: filename,
});
expect(status).toBe(201);
await expect
.poll(
async () => {
const { status, body } = await request(app).get('/server/config');
expect(status).toBe(200);
return body.maintenanceMode;
},
{
interval: 500,
timeout: 10_000,
},
)
.toBeTruthy();
const { status: status2, body } = await request(app).get('/admin/maintenance/status').send({ token: 'token' });
expect(status2).toBe(200);
expect(body).toEqual(
expect.objectContaining({
active: true,
action: 'restore_database',
}),
);
await expect
.poll(
async () => {
const { status, body } = await request(app).get('/server/config');
expect(status).toBe(200);
return body.maintenanceMode;
},
{
interval: 500,
timeout: 60_000,
},
)
.toBeFalsy();
});
it.sequential('fail to restore a corrupted backup', { timeout: 60_000 }, async () => {
await utils.prepareTestBackup('corrupted');
const { status, headers } = await request(app)
.post('/admin/maintenance')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({
action: 'restore_database',
restoreBackupFilename: 'development-corrupted.sql.gz',
});
expect(status).toBe(201);
cookie = headers['set-cookie'][0].split(';')[0];
await expect
.poll(
async () => {
const { status, body } = await request(app).get('/server/config');
expect(status).toBe(200);
return body.maintenanceMode;
},
{
interval: 500,
timeout: 10_000,
},
)
.toBeTruthy();
await expect
.poll(
async () => {
const { status, body } = await request(app).get('/admin/maintenance/status').send({ token: 'token' });
expect(status).toBe(200);
return body;
},
{
interval: 500,
timeout: 10_000,
},
)
.toEqual(
expect.objectContaining({
active: true,
action: 'restore_database',
error: 'Something went wrong, see logs!',
}),
);
const { status: status2, body: body2 } = await request(app)
.get('/admin/maintenance/status')
.set('cookie', cookie!)
.send({ token: 'token' });
expect(status2).toBe(200);
expect(body2).toEqual(
expect.objectContaining({
active: true,
action: 'restore_database',
error: expect.stringContaining('IM CORRUPTED'),
}),
);
await request(app).post('/admin/maintenance').set('cookie', cookie!).send({
action: 'end',
});
await utils.poll(
() => request(app).get('/server/config'),
({ status, body }) => status === 200 && !body.maintenanceMode,
);
});
it.sequential('rollback to restore point if backup is missing admin', { timeout: 60_000 }, async () => {
await utils.prepareTestBackup('empty');
const { status, headers } = await request(app)
.post('/admin/maintenance')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({
action: 'restore_database',
restoreBackupFilename: 'development-empty.sql.gz',
});
expect(status).toBe(201);
cookie = headers['set-cookie'][0].split(';')[0];
await expect
.poll(
async () => {
const { status, body } = await request(app).get('/server/config');
expect(status).toBe(200);
return body.maintenanceMode;
},
{
interval: 500,
timeout: 10_000,
},
)
.toBeTruthy();
await expect
.poll(
async () => {
const { status, body } = await request(app).get('/admin/maintenance/status').send({ token: 'token' });
expect(status).toBe(200);
return body;
},
{
interval: 500,
timeout: 30_000,
},
)
.toEqual(
expect.objectContaining({
active: true,
action: 'restore_database',
error: 'Something went wrong, see logs!',
}),
);
const { status: status2, body: body2 } = await request(app)
.get('/admin/maintenance/status')
.set('cookie', cookie!)
.send({ token: 'token' });
expect(status2).toBe(200);
expect(body2).toEqual(
expect.objectContaining({
active: true,
action: 'restore_database',
error: expect.stringContaining('Server health check failed, no admin exists.'),
}),
);
await request(app).post('/admin/maintenance').set('cookie', cookie!).send({
action: 'end',
});
await utils.poll(
() => request(app).get('/server/config'),
({ status, body }) => status === 200 && !body.maintenanceMode,
);
});
});
});

View File

@@ -14,6 +14,7 @@ describe('/admin/maintenance', () => {
await utils.resetDatabase();
admin = await utils.adminSetup();
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1);
await utils.resetBackups(admin.accessToken);
});
// => outside of maintenance mode
@@ -26,6 +27,17 @@ describe('/admin/maintenance', () => {
});
});
describe('GET /status', async () => {
it('to always indicate we are not in maintenance mode', async () => {
const { status, body } = await request(app).get('/admin/maintenance/status').send({ token: 'token' });
expect(status).toBe(200);
expect(body).toEqual({
active: false,
action: 'end',
});
});
});
describe('POST /login', async () => {
it('should not work out of maintenance mode', async () => {
const { status, body } = await request(app).post('/admin/maintenance/login').send({ token: 'token' });
@@ -39,6 +51,7 @@ describe('/admin/maintenance', () => {
describe.sequential('POST /', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post('/admin/maintenance').send({
active: false,
action: 'end',
});
expect(status).toBe(401);
@@ -69,6 +82,7 @@ describe('/admin/maintenance', () => {
.send({
action: 'start',
});
expect(status).toBe(201);
cookie = headers['set-cookie'][0].split(';')[0];
@@ -79,12 +93,13 @@ describe('/admin/maintenance', () => {
await expect
.poll(
async () => {
const { body } = await request(app).get('/server/config');
const { status, body } = await request(app).get('/server/config');
expect(status).toBe(200);
return body.maintenanceMode;
},
{
interval: 5e2,
timeout: 1e4,
interval: 500,
timeout: 10_000,
},
)
.toBeTruthy();
@@ -102,6 +117,17 @@ describe('/admin/maintenance', () => {
});
});
describe('GET /status', async () => {
it('to indicate we are in maintenance mode', async () => {
const { status, body } = await request(app).get('/admin/maintenance/status').send({ token: 'token' });
expect(status).toBe(200);
expect(body).toEqual({
active: true,
action: 'start',
});
});
});
describe('POST /login', async () => {
it('should fail without cookie or token in body', async () => {
const { status, body } = await request(app).post('/admin/maintenance/login').send({});
@@ -158,12 +184,13 @@ describe('/admin/maintenance', () => {
await expect
.poll(
async () => {
const { body } = await request(app).get('/server/config');
const { status, body } = await request(app).get('/server/config');
expect(status).toBe(200);
return body.maintenanceMode;
},
{
interval: 5e2,
timeout: 1e4,
interval: 500,
timeout: 10_000,
},
)
.toBeFalsy();

View File

@@ -348,6 +348,7 @@ export function toAssetResponseDto(asset: MockTimelineAsset, owner?: UserRespons
checksum: asset.checksum,
width: exifInfo.exifImageWidth ?? 1,
height: exifInfo.exifImageHeight ?? 1,
isEdited: false,
};
}

Some files were not shown because too many files have changed in this diff Show More