feat: automatically detect media location changes (#20256)

This commit is contained in:
Jason Rasmussen 2025-07-25 15:25:36 -04:00 committed by GitHub
parent 0fdeac0417
commit c6b25ef111
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 43 additions and 8 deletions

View File

@ -4,6 +4,7 @@ services:
target: dev-container-mobile target: dev-container-mobile
environment: environment:
- IMMICH_SERVER_URL=http://127.0.0.1:2283/ - IMMICH_SERVER_URL=http://127.0.0.1:2283/
- IMMICH_MEDIA_LOCATION=/data
volumes: !override # bind mount host to /workspaces/immich volumes: !override # bind mount host to /workspaces/immich
- ..:/workspaces/immich - ..:/workspaces/immich
- cli_node_modules:/workspaces/immich/cli/node_modules - 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 - open_api_node_modules:/workspaces/immich/open-api/typescript-sdk/node_modules
- server_node_modules:/workspaces/immich/server/node_modules - server_node_modules:/workspaces/immich/server/node_modules
- web_node_modules:/workspaces/immich/web/node_modules - web_node_modules:/workspaces/immich/web/node_modules
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload - ${UPLOAD_LOCATION}/photos:/data
- ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/upload - ${UPLOAD_LOCATION}/photos/upload:/data/upload
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
database: database:

View File

@ -6,6 +6,7 @@ services:
hostname: immich-dev hostname: immich-dev
environment: environment:
- IMMICH_SERVER_URL=http://127.0.0.1:2283/ - IMMICH_SERVER_URL=http://127.0.0.1:2283/
- IMMICH_MEDIA_LOCATION=/data
volumes: !override volumes: !override
- ..:/workspaces/immich - ..:/workspaces/immich
- cli_node_modules:/workspaces/immich/cli/node_modules - 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 - open_api_node_modules:/workspaces/immich/open-api/typescript-sdk/node_modules
- server_node_modules:/workspaces/immich/server/node_modules - server_node_modules:/workspaces/immich/server/node_modules
- web_node_modules:/workspaces/immich/web/node_modules - web_node_modules:/workspaces/immich/web/node_modules
- ${UPLOAD_LOCATION:-upload1-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/usr/src/app/upload - ${UPLOAD_LOCATION:-upload1-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/data
- ${UPLOAD_LOCATION:-upload2-devcontainer-volume}${UPLOAD_LOCATION:+/photos/upload}:/usr/src/app/upload/upload - ${UPLOAD_LOCATION:-upload2-devcontainer-volume}${UPLOAD_LOCATION:+/photos/upload}:/data/upload
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
immich-web: immich-web:

View File

@ -29,13 +29,14 @@ services:
volumes: volumes:
- ../server:/usr/src/app/server - ../server:/usr/src/app/server
- ../open-api:/usr/src/app/open-api - ../open-api:/usr/src/app/open-api
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload - ${UPLOAD_LOCATION}/photos:/data
- ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/upload/upload - ${UPLOAD_LOCATION}/photos/upload:/data/upload
- /usr/src/app/server/node_modules - /usr/src/app/server/node_modules
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
env_file: env_file:
- .env - .env
environment: environment:
IMMICH_MEDIA_LOCATION: /data
IMMICH_REPOSITORY: immich-app/immich IMMICH_REPOSITORY: immich-app/immich
IMMICH_REPOSITORY_URL: https://github.com/immich-app/immich IMMICH_REPOSITORY_URL: https://github.com/immich-app/immich
IMMICH_SOURCE_REF: local IMMICH_SOURCE_REF: local

View File

@ -19,8 +19,10 @@ services:
build: build:
context: ../ context: ../
dockerfile: server/Dockerfile dockerfile: server/Dockerfile
environment:
- IMMICH_MEDIA_LOCATION=/data
volumes: volumes:
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload - ${UPLOAD_LOCATION}/photos:/data
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
env_file: env_file:
- .env - .env

View File

@ -22,6 +22,7 @@ services:
- IMMICH_ENV=testing - IMMICH_ENV=testing
- IMMICH_PORT=2285 - IMMICH_PORT=2285
- IMMICH_IGNORE_MOUNT_CHECK_ERRORS=true - IMMICH_IGNORE_MOUNT_CHECK_ERRORS=true
- IMMICH_MEDIA_LOCATION=/data
volumes: volumes:
- ./test-assets:/test-assets - ./test-assets:/test-assets
extra_hosts: extra_hosts:

View File

@ -437,6 +437,15 @@ export class DatabaseRepository {
} }
async migrateFilePaths(sourceFolder: string, targetFolder: string): Promise<void> { async migrateFilePaths(sourceFolder: string, targetFolder: string): Promise<void> {
// 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 // escaping regex special characters with a backslash
const sourceRegex = '^' + sourceFolder.replaceAll(/[-[\]{}()*+?.,\\^$|#\s]/g, String.raw`\$&`); const sourceRegex = '^' + sourceFolder.replaceAll(/[-[\]{}()*+?.,\\^$|#\s]/g, String.raw`\$&`);
const source = sql.raw(`'${sourceRegex}'`); const source = sql.raw(`'${sourceRegex}'`);

View File

@ -19,6 +19,7 @@ describe(StorageService.name, () => {
describe('onBootstrap', () => { describe('onBootstrap', () => {
it('should enable mount folder checking', async () => { it('should enable mount folder checking', async () => {
mocks.systemMetadata.get.mockResolvedValue(null); mocks.systemMetadata.get.mockResolvedValue(null);
mocks.asset.getFileSamples.mockResolvedValue([]);
await expect(sut.onBootstrap()).resolves.toBeUndefined(); await expect(sut.onBootstrap()).resolves.toBeUndefined();
@ -75,6 +76,7 @@ describe(StorageService.name, () => {
upload: true, upload: true,
}, },
}); });
mocks.asset.getFileSamples.mockResolvedValue([]);
await expect(sut.onBootstrap()).resolves.toBeUndefined(); await expect(sut.onBootstrap()).resolves.toBeUndefined();
@ -128,6 +130,7 @@ describe(StorageService.name, () => {
error.code = 'EEXIST'; error.code = 'EEXIST';
mocks.systemMetadata.get.mockResolvedValue({ mountChecks: {} }); mocks.systemMetadata.get.mockResolvedValue({ mountChecks: {} });
mocks.storage.createFile.mockRejectedValue(error); mocks.storage.createFile.mockRejectedValue(error);
mocks.asset.getFileSamples.mockResolvedValue([]);
await expect(sut.onBootstrap()).resolves.toBeUndefined(); await expect(sut.onBootstrap()).resolves.toBeUndefined();
@ -149,6 +152,7 @@ describe(StorageService.name, () => {
storage: { ignoreMountCheckErrors: true }, storage: { ignoreMountCheckErrors: true },
}), }),
); );
mocks.asset.getFileSamples.mockResolvedValue([]);
mocks.storage.overwriteFile.mockRejectedValue( mocks.storage.overwriteFile.mockRejectedValue(
new Error("ENOENT: no such file or directory, open '/app/.immich'"), new Error("ENOENT: no such file or directory, open '/app/.immich'"),
); );

View File

@ -65,10 +65,26 @@ export class StorageService extends BaseService {
await this.databaseRepository.withLock(DatabaseLock.MediaLocation, async () => { await this.databaseRepository.withLock(DatabaseLock.MediaLocation, async () => {
const current = APP_MEDIA_LOCATION; const current = APP_MEDIA_LOCATION;
const savedValue = await this.systemMetadataRepository.get(SystemMetadataKey.MediaLocation); const savedValue = await this.systemMetadataRepository.get(SystemMetadataKey.MediaLocation);
const previous = savedValue?.location || ''; let previous = savedValue?.location || '';
if (previous !== current) { if (previous !== current) {
this.logger.log(`Media location changed (from=${previous}, to=${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 }); await this.systemMetadataRepository.set(SystemMetadataKey.MediaLocation, { location: current });
} }
}); });