mirror of
https://github.com/immich-app/immich.git
synced 2025-07-31 15:08:44 -04:00
feat: automatically detect media location changes (#20256)
This commit is contained in:
parent
0fdeac0417
commit
c6b25ef111
@ -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:
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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}'`);
|
||||||
|
@ -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'"),
|
||||||
);
|
);
|
||||||
|
@ -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 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user