From c6b25ef1116333569be1e4769766bc5205a34376 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Fri, 25 Jul 2025 15:25:36 -0400 Subject: [PATCH] feat: automatically detect media location changes (#20256) --- .../mobile/container-compose-overrides.yml | 5 +++-- .../server/container-compose-overrides.yml | 5 +++-- docker/docker-compose.dev.yml | 5 +++-- docker/docker-compose.prod.yml | 4 +++- e2e/docker-compose.yml | 1 + server/src/repositories/database.repository.ts | 9 +++++++++ server/src/services/storage.service.spec.ts | 4 ++++ server/src/services/storage.service.ts | 18 +++++++++++++++++- 8 files changed, 43 insertions(+), 8 deletions(-) diff --git a/.devcontainer/mobile/container-compose-overrides.yml b/.devcontainer/mobile/container-compose-overrides.yml index 0543f42317..9fc7486fea 100644 --- a/.devcontainer/mobile/container-compose-overrides.yml +++ b/.devcontainer/mobile/container-compose-overrides.yml @@ -4,6 +4,7 @@ services: target: dev-container-mobile environment: - IMMICH_SERVER_URL=http://127.0.0.1:2283/ + - IMMICH_MEDIA_LOCATION=/data volumes: !override # bind mount host to /workspaces/immich - ..:/workspaces/immich - cli_node_modules:/workspaces/immich/cli/node_modules @@ -11,8 +12,8 @@ services: - open_api_node_modules:/workspaces/immich/open-api/typescript-sdk/node_modules - server_node_modules:/workspaces/immich/server/node_modules - web_node_modules:/workspaces/immich/web/node_modules - - ${UPLOAD_LOCATION}/photos:/usr/src/app/upload - - ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/upload + - ${UPLOAD_LOCATION}/photos:/data + - ${UPLOAD_LOCATION}/photos/upload:/data/upload - /etc/localtime:/etc/localtime:ro database: diff --git a/.devcontainer/server/container-compose-overrides.yml b/.devcontainer/server/container-compose-overrides.yml index 24ac9734b1..951d763b4b 100644 --- a/.devcontainer/server/container-compose-overrides.yml +++ b/.devcontainer/server/container-compose-overrides.yml @@ -6,6 +6,7 @@ services: hostname: immich-dev environment: - IMMICH_SERVER_URL=http://127.0.0.1:2283/ + - IMMICH_MEDIA_LOCATION=/data volumes: !override - ..:/workspaces/immich - cli_node_modules:/workspaces/immich/cli/node_modules @@ -13,8 +14,8 @@ services: - open_api_node_modules:/workspaces/immich/open-api/typescript-sdk/node_modules - server_node_modules:/workspaces/immich/server/node_modules - web_node_modules:/workspaces/immich/web/node_modules - - ${UPLOAD_LOCATION:-upload1-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/usr/src/app/upload - - ${UPLOAD_LOCATION:-upload2-devcontainer-volume}${UPLOAD_LOCATION:+/photos/upload}:/usr/src/app/upload/upload + - ${UPLOAD_LOCATION:-upload1-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/data + - ${UPLOAD_LOCATION:-upload2-devcontainer-volume}${UPLOAD_LOCATION:+/photos/upload}:/data/upload - /etc/localtime:/etc/localtime:ro immich-web: diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 32ff115102..6db9623b1a 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -29,13 +29,14 @@ services: volumes: - ../server:/usr/src/app/server - ../open-api:/usr/src/app/open-api - - ${UPLOAD_LOCATION}/photos:/usr/src/app/upload - - ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/upload/upload + - ${UPLOAD_LOCATION}/photos:/data + - ${UPLOAD_LOCATION}/photos/upload:/data/upload - /usr/src/app/server/node_modules - /etc/localtime:/etc/localtime:ro env_file: - .env environment: + IMMICH_MEDIA_LOCATION: /data IMMICH_REPOSITORY: immich-app/immich IMMICH_REPOSITORY_URL: https://github.com/immich-app/immich IMMICH_SOURCE_REF: local diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index a025ff4cc5..fdeb6ef7a8 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -19,8 +19,10 @@ services: build: context: ../ dockerfile: server/Dockerfile + environment: + - IMMICH_MEDIA_LOCATION=/data volumes: - - ${UPLOAD_LOCATION}/photos:/usr/src/app/upload + - ${UPLOAD_LOCATION}/photos:/data - /etc/localtime:/etc/localtime:ro env_file: - .env diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml index ccb21e4587..ed57f423bd 100644 --- a/e2e/docker-compose.yml +++ b/e2e/docker-compose.yml @@ -22,6 +22,7 @@ services: - IMMICH_ENV=testing - IMMICH_PORT=2285 - IMMICH_IGNORE_MOUNT_CHECK_ERRORS=true + - IMMICH_MEDIA_LOCATION=/data volumes: - ./test-assets:/test-assets extra_hosts: diff --git a/server/src/repositories/database.repository.ts b/server/src/repositories/database.repository.ts index 1f83630cfa..f334896ce1 100644 --- a/server/src/repositories/database.repository.ts +++ b/server/src/repositories/database.repository.ts @@ -437,6 +437,15 @@ export class DatabaseRepository { } async migrateFilePaths(sourceFolder: string, targetFolder: string): Promise { + // remove trailing slashes + if (sourceFolder.endsWith('/')) { + sourceFolder = sourceFolder.slice(0, -1); + } + + if (targetFolder.endsWith('/')) { + targetFolder = targetFolder.slice(0, -1); + } + // escaping regex special characters with a backslash const sourceRegex = '^' + sourceFolder.replaceAll(/[-[\]{}()*+?.,\\^$|#\s]/g, String.raw`\$&`); const source = sql.raw(`'${sourceRegex}'`); diff --git a/server/src/services/storage.service.spec.ts b/server/src/services/storage.service.spec.ts index 567b78ac09..0303c11649 100644 --- a/server/src/services/storage.service.spec.ts +++ b/server/src/services/storage.service.spec.ts @@ -19,6 +19,7 @@ describe(StorageService.name, () => { describe('onBootstrap', () => { it('should enable mount folder checking', async () => { mocks.systemMetadata.get.mockResolvedValue(null); + mocks.asset.getFileSamples.mockResolvedValue([]); await expect(sut.onBootstrap()).resolves.toBeUndefined(); @@ -75,6 +76,7 @@ describe(StorageService.name, () => { upload: true, }, }); + mocks.asset.getFileSamples.mockResolvedValue([]); await expect(sut.onBootstrap()).resolves.toBeUndefined(); @@ -128,6 +130,7 @@ describe(StorageService.name, () => { error.code = 'EEXIST'; mocks.systemMetadata.get.mockResolvedValue({ mountChecks: {} }); mocks.storage.createFile.mockRejectedValue(error); + mocks.asset.getFileSamples.mockResolvedValue([]); await expect(sut.onBootstrap()).resolves.toBeUndefined(); @@ -149,6 +152,7 @@ describe(StorageService.name, () => { storage: { ignoreMountCheckErrors: true }, }), ); + mocks.asset.getFileSamples.mockResolvedValue([]); mocks.storage.overwriteFile.mockRejectedValue( new Error("ENOENT: no such file or directory, open '/app/.immich'"), ); diff --git a/server/src/services/storage.service.ts b/server/src/services/storage.service.ts index 632e0c1385..6c99194abd 100644 --- a/server/src/services/storage.service.ts +++ b/server/src/services/storage.service.ts @@ -65,10 +65,26 @@ export class StorageService extends BaseService { await this.databaseRepository.withLock(DatabaseLock.MediaLocation, async () => { const current = APP_MEDIA_LOCATION; const savedValue = await this.systemMetadataRepository.get(SystemMetadataKey.MediaLocation); - const previous = savedValue?.location || ''; + let previous = savedValue?.location || ''; if (previous !== current) { this.logger.log(`Media location changed (from=${previous}, to=${current})`); + + const samples = await this.assetRepository.getFileSamples(); + if (samples.length > 0) { + const originalPath = samples[0].originalPath; + if (!previous) { + previous = originalPath.startsWith('upload/') ? 'upload' : '/usr/src/app/upload'; + } + + if (previous && originalPath.startsWith(previous)) { + this.logger.warn( + `Detected a change to IMMICH_MEDIA_LOCATION, performing an automatic migration of file paths from ${previous} to ${current}, this may take awhile`, + ); + await this.databaseRepository.migrateFilePaths(previous, current); + } + } + await this.systemMetadataRepository.set(SystemMetadataKey.MediaLocation, { location: current }); } });