From 58521c9efb1fd142f555d54a7d9496961209a9a7 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Tue, 29 Jul 2025 16:58:50 -0400 Subject: [PATCH] feat: change default media location to /data (#20367) * feat!: change default media location to /data * feat: dynamically detect media location --- .../mobile/container-compose-overrides.yml | 1 - .../server/container-compose-overrides.yml | 1 - docker/docker-compose.dev.yml | 1 - docker/docker-compose.prod.yml | 2 - docker/docker-compose.yml | 2 +- docs/docs/FAQ.mdx | 2 +- docs/docs/administration/server-commands.md | 17 ++- docs/docs/features/libraries.md | 2 +- docs/docs/guides/custom-locations.md | 12 +-- docs/docs/guides/external-library.md | 2 +- docs/docs/install/environment-variables.md | 2 +- docs/src/pages/errors.md | 19 ++++ e2e/docker-compose.yml | 1 - e2e/src/utils.ts | 12 --- server/Dockerfile | 2 +- server/src/commands/media-location.command.ts | 2 +- server/src/constants.ts | 2 - server/src/cores/storage.core.spec.ts | 5 +- server/src/cores/storage.core.ts | 19 +++- server/src/enum.ts | 2 + server/src/repositories/config.repository.ts | 2 + server/src/repositories/storage.repository.ts | 4 + .../src/services/asset-media.service.spec.ts | 16 +-- server/src/services/auth.service.spec.ts | 2 +- server/src/services/download.service.spec.ts | 21 ++-- server/src/services/job.service.spec.ts | 2 +- server/src/services/library.service.spec.ts | 26 ++--- server/src/services/media.service.spec.ts | 17 ++- server/src/services/metadata.service.spec.ts | 6 +- server/src/services/server.service.spec.ts | 12 +-- .../services/storage-template.service.spec.ts | 100 ++++++++---------- server/src/services/storage.service.spec.ts | 48 ++++++--- server/src/services/storage.service.ts | 83 ++++++++++----- server/src/services/user.service.spec.ts | 12 +-- server/test/fixtures/asset.stub.ts | 4 +- .../specs/services/storage.service.spec.ts | 46 ++++++++ .../repositories/storage.repository.mock.ts | 8 +- server/test/small.factory.ts | 2 +- web/src/lib/utils/asset-utils.spec.ts | 6 +- 39 files changed, 316 insertions(+), 209 deletions(-) create mode 100644 server/test/medium/specs/services/storage.service.spec.ts diff --git a/.devcontainer/mobile/container-compose-overrides.yml b/.devcontainer/mobile/container-compose-overrides.yml index 9fc7486fea..c0655e50e7 100644 --- a/.devcontainer/mobile/container-compose-overrides.yml +++ b/.devcontainer/mobile/container-compose-overrides.yml @@ -4,7 +4,6 @@ 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 diff --git a/.devcontainer/server/container-compose-overrides.yml b/.devcontainer/server/container-compose-overrides.yml index 951d763b4b..c4745ad199 100644 --- a/.devcontainer/server/container-compose-overrides.yml +++ b/.devcontainer/server/container-compose-overrides.yml @@ -6,7 +6,6 @@ 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 diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 6db9623b1a..0d918a3dbe 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -36,7 +36,6 @@ services: 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 fdeb6ef7a8..838aea4458 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -19,8 +19,6 @@ services: build: context: ../ dockerfile: server/Dockerfile - environment: - - IMMICH_MEDIA_LOCATION=/data volumes: - ${UPLOAD_LOCATION}/photos:/data - /etc/localtime:/etc/localtime:ro diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 6af0e0aa2a..2c0895a57a 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -18,7 +18,7 @@ services: # service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding volumes: # Do not edit the next line. If you want to change the media storage location on your system, edit the value of UPLOAD_LOCATION in the .env file - - ${UPLOAD_LOCATION}:/usr/src/app/upload + - ${UPLOAD_LOCATION}:/data - /etc/localtime:/etc/localtime:ro env_file: - .env diff --git a/docs/docs/FAQ.mdx b/docs/docs/FAQ.mdx index 9e14d10a0e..e57039a420 100644 --- a/docs/docs/FAQ.mdx +++ b/docs/docs/FAQ.mdx @@ -180,7 +180,7 @@ services: ... volumes: # Do not edit the next line. If you want to change the media storage location on your system, edit the value of UPLOAD_LOCATION in the .env file - - ${UPLOAD_LOCATION}:/usr/src/app/upload + - ${UPLOAD_LOCATION}:/data - /etc/localtime:/etc/localtime:ro + - originals:/usr/src/app/originals ... diff --git a/docs/docs/administration/server-commands.md b/docs/docs/administration/server-commands.md index b275d8fede..a25673abf2 100644 --- a/docs/docs/administration/server-commands.md +++ b/docs/docs/administration/server-commands.md @@ -94,19 +94,16 @@ Change media location ``` immich-admin change-media-location -? Enter the previous value of IMMICH_MEDIA_LOCATION: /usr/src/app/upload -? Enter the new value of IMMICH_MEDIA_LOCATION: /data +? Enter the previous value of IMMICH_MEDIA_LOCATION: /data +? Enter the new value of IMMICH_MEDIA_LOCATION: /my-data +... + Previous value: /data + Current value: /my-data - Previous value: /usr/src/app/upload - Current value: /data - - Changing database paths from "/usr/src/app/upload/*" to "/data/*" + Changing database paths from "/data/*" to "/my-data/*" ? Do you want to proceed? [Y/n] y Database file paths updated successfully! 🎉 - -You may now set IMMICH_MEDIA_LOCATION=/data and restart! - -(please remember to update applicable volume mounts e.g. ${UPLOAD_LOCATION}:/data) +... ``` diff --git a/docs/docs/features/libraries.md b/docs/docs/features/libraries.md index caf7ab5e55..f274ca3c70 100644 --- a/docs/docs/features/libraries.md +++ b/docs/docs/features/libraries.md @@ -93,7 +93,7 @@ The `immich-server` container will need access to the gallery. Modify your docke ```diff title="docker-compose.yml" immich-server: volumes: - - ${UPLOAD_LOCATION}:/usr/src/app/upload + - ${UPLOAD_LOCATION}:/data + - /mnt/nas/christmas-trip:/mnt/media/christmas-trip:ro + - /home/user/old-pics:/mnt/media/old-pics:ro + - /mnt/media/videos:/mnt/media/videos:ro diff --git a/docs/docs/guides/custom-locations.md b/docs/docs/guides/custom-locations.md index ba5caf4b26..af8ca438e7 100644 --- a/docs/docs/guides/custom-locations.md +++ b/docs/docs/guides/custom-locations.md @@ -27,11 +27,11 @@ After defining the locations of these files, we will edit the `docker-compose.ym services: immich-server: volumes: - - ${UPLOAD_LOCATION}:/usr/src/app/upload -+ - ${THUMB_LOCATION}:/usr/src/app/upload/thumbs -+ - ${ENCODED_VIDEO_LOCATION}:/usr/src/app/upload/encoded-video -+ - ${PROFILE_LOCATION}:/usr/src/app/upload/profile -+ - ${BACKUP_LOCATION}:/usr/src/app/upload/backups + - ${UPLOAD_LOCATION}:/data ++ - ${THUMB_LOCATION}:/data/thumbs ++ - ${ENCODED_VIDEO_LOCATION}:/data/encoded-video ++ - ${PROFILE_LOCATION}:/data/profile ++ - ${BACKUP_LOCATION}:/data/backups - /etc/localtime:/etc/localtime:ro ``` @@ -44,7 +44,7 @@ docker compose up -d :::note Because of the underlying properties of docker bind mounts, it is not recommended to mount the `upload/` and `library/` folders as separate bind mounts if they are on the same device. -For this reason, we mount the HDD or the network storage (NAS) to `/usr/src/app/upload` and then mount the folders we want to access under that folder. +For this reason, we mount the HDD or the network storage (NAS) to `/data` and then mount the folders we want to access under that folder. The `thumbs/` folder contains both the small thumbnails displayed in the timeline and the larger previews shown when clicking into an image. These cannot be separated. diff --git a/docs/docs/guides/external-library.md b/docs/docs/guides/external-library.md index 3ad1679423..7921843297 100644 --- a/docs/docs/guides/external-library.md +++ b/docs/docs/guides/external-library.md @@ -12,7 +12,7 @@ If you want Immich to be able to delete the images in the external library or ad ```diff immich-server: volumes: - - ${UPLOAD_LOCATION}:/usr/src/app/upload + - ${UPLOAD_LOCATION}:/data + - /home/user/photos1:/home/user/photos1:ro + - /mnt/photos2:/mnt/photos2:ro # you can delete this line if you only have one mount point, or you can add more lines if you have more than two ``` diff --git a/docs/docs/install/environment-variables.md b/docs/docs/install/environment-variables.md index d070e3485a..928e0b26e5 100644 --- a/docs/docs/install/environment-variables.md +++ b/docs/docs/install/environment-variables.md @@ -34,7 +34,7 @@ These environment variables are used by the `docker-compose.yml` file and do **N | `TZ` | Timezone | \*1 | server | microservices | | `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices | | `IMMICH_LOG_LEVEL` | Log level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices | -| `IMMICH_MEDIA_LOCATION` | Media location inside the container ⚠️**You probably shouldn't set this**\*2⚠️ | `/usr/src/app/upload` | server | api, microservices | +| `IMMICH_MEDIA_LOCATION` | Media location inside the container ⚠️**You probably shouldn't set this**\*2⚠️ | `/data` | server | api, microservices | | `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices | | `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | | | `CPU_CORES` | Number of cores available to the Immich server | auto-detected CPU core count | server | | diff --git a/docs/src/pages/errors.md b/docs/src/pages/errors.md index e9bf09770c..4a745877dc 100644 --- a/docs/src/pages/errors.md +++ b/docs/src/pages/errors.md @@ -3,3 +3,22 @@ ## TypeORM Upgrade The upgrade to Immich `v2.x.x` has a required upgrade path to `v1.132.0+`. This means it is required to start up the application at least once on version `1.132.0` (or later). Doing so will complete database schema upgrades that are required for `v2.0.0`. After Immich has successfully booted on this version, shut the system down and try the `v2.x.x` upgrade again. + +## Inconsistent Media Location + +:::caution +This error is related to the location of media files _inside the container_. Never move files on the host system when you run into this error message. +::: + +Immich automatically tries to detect where your Immich data is located. On start up, it compares the detected media location with the file paths in the database and throws an Inconsistent Media Location error when they do not match. + +To fix this issue, verify that the `IMMICH_MEDIA_LOCATION` environment variable and `UPLOAD_LOCATION` volume mount are in sync with the database paths. + +If you would like to migrate from one media location to another, simply successfully start Immich on `v1.136.0` or later, then do the following steps: + +1. Stop Immich +2. Update `IMMICH_MEDIA_LOCATION` to the new location +3. Update the right-hand side of the `UPLOAD_LOCATION` volume mount to the new location +4. Start up Immich + +After version `1.136.0`, Immich can detect when a media location has moved and will automatically update the database paths to keep them in sync. diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml index ed57f423bd..ccb21e4587 100644 --- a/e2e/docker-compose.yml +++ b/e2e/docker-compose.yml @@ -22,7 +22,6 @@ 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/e2e/src/utils.ts b/e2e/src/utils.ts index 3fcc4ab552..0f8ceeabfc 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -186,18 +186,6 @@ export const utils = { } }, - resetFilesystem: async () => { - const mediaInternal = '/usr/src/app/upload'; - const dirs = [ - `"${mediaInternal}/thumbs"`, - `"${mediaInternal}/upload"`, - `"${mediaInternal}/library"`, - `"${mediaInternal}/encoded-video"`, - ].join(' '); - - await execPromise(`docker exec -i "immich-e2e-server" /bin/bash -c "rm -rf ${dirs} && mkdir ${dirs}"`); - }, - unzip: async (input: string, output: string) => { await execPromise(`unzip -o -d "${output}" "${input}"`); }, diff --git a/server/Dockerfile b/server/Dockerfile index 9110d46f7d..c922c5b291 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -130,7 +130,7 @@ ENV IMMICH_SOURCE_REF=${BUILD_SOURCE_REF} ENV IMMICH_SOURCE_COMMIT=${BUILD_SOURCE_COMMIT} ENV IMMICH_SOURCE_URL=https://github.com/immich-app/immich/commit/${BUILD_SOURCE_COMMIT} -VOLUME /usr/src/app/upload +VOLUME /data EXPOSE 2283 ENTRYPOINT ["tini", "--", "/bin/bash", "-c"] CMD ["start.sh"] diff --git a/server/src/commands/media-location.command.ts b/server/src/commands/media-location.command.ts index 0935fe202d..0d32749c02 100644 --- a/server/src/commands/media-location.command.ts +++ b/server/src/commands/media-location.command.ts @@ -61,7 +61,7 @@ export class ChangeMediaLocationCommand extends CommandRunner { immich-server: ... volumes: - - \${UPLOAD_LOCATION}:/usr/src/app/upload + - \${UPLOAD_LOCATION}:/data ... )`; diff --git a/server/src/constants.ts b/server/src/constants.ts index 67d50a65f8..3215a58291 100644 --- a/server/src/constants.ts +++ b/server/src/constants.ts @@ -47,8 +47,6 @@ export const serverVersion = new SemVer(version); export const AUDIT_LOG_MAX_DURATION = Duration.fromObject({ days: 100 }); export const ONE_HOUR = Duration.fromObject({ hours: 1 }); -export const APP_MEDIA_LOCATION = process.env.IMMICH_MEDIA_LOCATION || '/usr/src/app/upload'; - export const MACHINE_LEARNING_PING_TIMEOUT = Number(process.env.MACHINE_LEARNING_PING_TIMEOUT || 2000); export const MACHINE_LEARNING_AVAILABILITY_BACKOFF_TIME = Number( process.env.MACHINE_LEARNING_AVAILABILITY_BACKOFF_TIME || 30_000, diff --git a/server/src/cores/storage.core.spec.ts b/server/src/cores/storage.core.spec.ts index 7bb2cdb1be..ed446f9259 100644 --- a/server/src/cores/storage.core.spec.ts +++ b/server/src/cores/storage.core.spec.ts @@ -2,7 +2,6 @@ import { StorageCore } from 'src/cores/storage.core'; import { vitest } from 'vitest'; vitest.mock('src/constants', () => ({ - APP_MEDIA_LOCATION: '/photos', ADDED_IN_PREFIX: 'This property was added in ', DEPRECATED_IN_PREFIX: 'This property was deprecated in ', IWorker: 'IWorker', @@ -10,6 +9,10 @@ vitest.mock('src/constants', () => ({ describe('StorageCore', () => { describe('isImmichPath', () => { + beforeAll(() => { + StorageCore.setMediaLocation('/photos'); + }); + it('should return true for APP_MEDIA_LOCATION path', () => { const immichPath = '/photos'; expect(StorageCore.isImmichPath(immichPath)).toBe(true); diff --git a/server/src/cores/storage.core.ts b/server/src/cores/storage.core.ts index 6576b397e3..06dde4644c 100644 --- a/server/src/cores/storage.core.ts +++ b/server/src/cores/storage.core.ts @@ -1,6 +1,5 @@ import { randomUUID } from 'node:crypto'; import { dirname, join, resolve } from 'node:path'; -import { APP_MEDIA_LOCATION } from 'src/constants'; import { StorageAsset } from 'src/database'; import { AssetFileType, AssetPathType, ImageFormat, PathType, PersonPathType, StorageFolder } from 'src/enum'; import { AssetRepository } from 'src/repositories/asset.repository'; @@ -32,6 +31,8 @@ export type ThumbnailPathEntity = { id: string; ownerId: string }; let instance: StorageCore | null; +let mediaLocation: string | undefined; + export class StorageCore { private constructor( private assetRepository: AssetRepository, @@ -74,6 +75,18 @@ export class StorageCore { instance = null; } + static getMediaLocation(): string { + if (mediaLocation === undefined) { + throw new Error('Media location is not set.'); + } + + return mediaLocation; + } + + static setMediaLocation(location: string) { + mediaLocation = location; + } + static getFolderLocation(folder: StorageFolder, userId: string) { return join(StorageCore.getBaseFolder(folder), userId); } @@ -83,7 +96,7 @@ export class StorageCore { } static getBaseFolder(folder: StorageFolder) { - return join(APP_MEDIA_LOCATION, folder); + return join(StorageCore.getMediaLocation(), folder); } static getPersonThumbnailPath(person: ThumbnailPathEntity) { @@ -108,7 +121,7 @@ export class StorageCore { static isImmichPath(path: string) { const resolvedPath = resolve(path); - const resolvedAppMediaLocation = resolve(APP_MEDIA_LOCATION); + const resolvedAppMediaLocation = StorageCore.getMediaLocation(); const normalizedPath = resolvedPath.endsWith('/') ? resolvedPath : resolvedPath + '/'; const normalizedAppMediaLocation = resolvedAppMediaLocation.endsWith('/') ? resolvedAppMediaLocation diff --git a/server/src/enum.ts b/server/src/enum.ts index d17b5dc901..666b9fc505 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -475,6 +475,8 @@ export enum DatabaseExtension { export enum BootstrapEventPriority { // Database service should be initialized before anything else, most other services need database access DatabaseService = -200, + // Detect and configure the media location before jobs are queued which may use it + StorageService = -195, // Other services may need to queue jobs on bootstrap. JobService = -190, // Initialise config after other bootstrap services, stop other services from using config on bootstrap diff --git a/server/src/repositories/config.repository.ts b/server/src/repositories/config.repository.ts index c9e96a1803..d5c279099c 100644 --- a/server/src/repositories/config.repository.ts +++ b/server/src/repositories/config.repository.ts @@ -97,6 +97,7 @@ export interface EnvData { storage: { ignoreMountCheckErrors: boolean; + mediaLocation?: string; }; workers: ImmichWorker[]; @@ -307,6 +308,7 @@ const getEnv = (): EnvData => { storage: { ignoreMountCheckErrors: !!dto.IMMICH_IGNORE_MOUNT_CHECK_ERRORS, + mediaLocation: dto.IMMICH_MEDIA_LOCATION, }, telemetry: { diff --git a/server/src/repositories/storage.repository.ts b/server/src/repositories/storage.repository.ts index 4d89b02a50..7d6b634845 100644 --- a/server/src/repositories/storage.repository.ts +++ b/server/src/repositories/storage.repository.ts @@ -162,6 +162,10 @@ export class StorageRepository { } } + existsSync(filepath: string) { + return existsSync(filepath); + } + async checkDiskUsage(folder: string): Promise { const stats = await fs.statfs(folder); return { diff --git a/server/src/services/asset-media.service.spec.ts b/server/src/services/asset-media.service.spec.ts index 0585a159ac..91bddeb6e9 100644 --- a/server/src/services/asset-media.service.spec.ts +++ b/server/src/services/asset-media.service.spec.ts @@ -29,7 +29,7 @@ const uploadFile = { file: { uuid: 'random-uuid', checksum: Buffer.from('checksum', 'utf8'), - originalPath: 'upload/admin/image.jpeg', + originalPath: '/data/library/admin/image.jpeg', originalName: 'image.jpeg', size: 1000, }, @@ -42,7 +42,7 @@ const uploadFile = { uuid: 'random-uuid', mimeType: 'image/jpeg', checksum: Buffer.from('checksum', 'utf8'), - originalPath: `upload/admin/${filename}`, + originalPath: `/data/admin/${filename}`, originalName: filename, size: 1000, }, @@ -294,16 +294,16 @@ describe(AssetMediaService.name, () => { it('should return profile for profile uploads', () => { expect(sut.getUploadFolder(uploadFile.filename(UploadFieldName.PROFILE_DATA, 'image.jpg'))).toEqual( - expect.stringContaining('upload/profile/admin_id'), + expect.stringContaining('/data/profile/admin_id'), ); - expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.stringContaining('upload/profile/admin_id')); + expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.stringContaining('/data/profile/admin_id')); }); it('should return upload for everything else', () => { expect(sut.getUploadFolder(uploadFile.filename(UploadFieldName.ASSET_DATA, 'image.jpg'))).toEqual( - expect.stringContaining('upload/upload/admin_id/ra/nd'), + expect.stringContaining('/data/upload/admin_id/ra/nd'), ); - expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.stringContaining('upload/upload/admin_id/ra/nd')); + expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.stringContaining('/data/upload/admin_id/ra/nd')); }); }); @@ -907,14 +907,14 @@ describe(AssetMediaService.name, () => { size: 1000, uuid: 'random-uuid', checksum: Buffer.from('checksum', 'utf8'), - originalPath: 'upload/upload/user-id/ra/nd/random-uuid.jpg', + originalPath: '/data/upload/user-id/ra/nd/random-uuid.jpg', } as unknown as Express.Multer.File; await sut.onUploadError(request, file); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.FileDelete, - data: { files: [expect.stringContaining('upload/upload/user-id/ra/nd/random-uuid.jpg')] }, + data: { files: [expect.stringContaining('/data/upload/user-id/ra/nd/random-uuid.jpg')] }, }); }); }); diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index 58c2542310..6e19292f71 100644 --- a/server/src/services/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -843,7 +843,7 @@ describe(AuthService.name, () => { ).resolves.toEqual(oauthResponse(user)); expect(mocks.user.update).toHaveBeenCalledWith(user.id, { - profileImagePath: expect.stringContaining(`upload/profile/${user.id}/${fileId}.jpg`), + profileImagePath: expect.stringContaining(`/data/profile/${user.id}/${fileId}.jpg`), profileChangedAt: expect.any(Date), }); expect(mocks.oauth.getProfilePicture).toHaveBeenCalledWith(pictureUrl); diff --git a/server/src/services/download.service.spec.ts b/server/src/services/download.service.spec.ts index 940767ff67..a85fd74c72 100644 --- a/server/src/services/download.service.spec.ts +++ b/server/src/services/download.service.spec.ts @@ -1,5 +1,4 @@ import { BadRequestException } from '@nestjs/common'; -import { APP_MEDIA_LOCATION } from 'src/constants'; import { DownloadResponseDto } from 'src/dtos/download.dto'; import { DownloadService } from 'src/services/download.service'; import { assetStub } from 'test/fixtures/asset.stub'; @@ -49,7 +48,7 @@ describe(DownloadService.name, () => { expect(archiveMock.addFile).toHaveBeenCalledTimes(1); expect(archiveMock.addFile).toHaveBeenNthCalledWith( 1, - expect.stringContaining('upload/library/IMG_123.jpg'), + expect.stringContaining('/data/library/IMG_123.jpg'), 'IMG_123.jpg', ); }); @@ -75,8 +74,8 @@ describe(DownloadService.name, () => { expect(mocks.logger.warn).toHaveBeenCalledTimes(2); expect(archiveMock.addFile).toHaveBeenCalledTimes(2); - expect(archiveMock.addFile).toHaveBeenNthCalledWith(1, 'upload/library/IMG_123.jpg', 'IMG_123.jpg'); - expect(archiveMock.addFile).toHaveBeenNthCalledWith(2, 'upload/library/IMG_456.jpg', 'IMG_456.jpg'); + expect(archiveMock.addFile).toHaveBeenNthCalledWith(1, '/data/library/IMG_123.jpg', 'IMG_123.jpg'); + expect(archiveMock.addFile).toHaveBeenNthCalledWith(2, '/data/library/IMG_456.jpg', 'IMG_456.jpg'); }); it('should download an archive', async () => { @@ -98,8 +97,8 @@ describe(DownloadService.name, () => { }); expect(archiveMock.addFile).toHaveBeenCalledTimes(2); - expect(archiveMock.addFile).toHaveBeenNthCalledWith(1, 'upload/library/IMG_123.jpg', 'IMG_123.jpg'); - expect(archiveMock.addFile).toHaveBeenNthCalledWith(2, 'upload/library/IMG_456.jpg', 'IMG_456.jpg'); + expect(archiveMock.addFile).toHaveBeenNthCalledWith(1, '/data/library/IMG_123.jpg', 'IMG_123.jpg'); + expect(archiveMock.addFile).toHaveBeenNthCalledWith(2, '/data/library/IMG_456.jpg', 'IMG_456.jpg'); }); it('should handle duplicate file names', async () => { @@ -121,8 +120,8 @@ describe(DownloadService.name, () => { }); expect(archiveMock.addFile).toHaveBeenCalledTimes(2); - expect(archiveMock.addFile).toHaveBeenNthCalledWith(1, 'upload/library/IMG_123.jpg', 'IMG_123.jpg'); - expect(archiveMock.addFile).toHaveBeenNthCalledWith(2, 'upload/library/IMG_123.jpg', 'IMG_123+1.jpg'); + expect(archiveMock.addFile).toHaveBeenNthCalledWith(1, '/data/library/IMG_123.jpg', 'IMG_123.jpg'); + expect(archiveMock.addFile).toHaveBeenNthCalledWith(2, '/data/library/IMG_123.jpg', 'IMG_123+1.jpg'); }); it('should be deterministic', async () => { @@ -144,8 +143,8 @@ describe(DownloadService.name, () => { }); expect(archiveMock.addFile).toHaveBeenCalledTimes(2); - expect(archiveMock.addFile).toHaveBeenNthCalledWith(1, 'upload/library/IMG_123.jpg', 'IMG_123.jpg'); - expect(archiveMock.addFile).toHaveBeenNthCalledWith(2, 'upload/library/IMG_123.jpg', 'IMG_123+1.jpg'); + expect(archiveMock.addFile).toHaveBeenNthCalledWith(1, '/data/library/IMG_123.jpg', 'IMG_123.jpg'); + expect(archiveMock.addFile).toHaveBeenNthCalledWith(2, '/data/library/IMG_123.jpg', 'IMG_123+1.jpg'); }); it('should resolve symlinks', async () => { @@ -291,7 +290,7 @@ describe(DownloadService.name, () => { id: 'asset-2', livePhotoVideoId: null, size: 23_456, - originalPath: APP_MEDIA_LOCATION + '/encoded-video/uuid-MP.mp4', + originalPath: '/data/encoded-video/uuid-MP.mp4', }, ]), ); diff --git a/server/src/services/job.service.spec.ts b/server/src/services/job.service.spec.ts index a57db736af..63d5fb2d06 100644 --- a/server/src/services/job.service.spec.ts +++ b/server/src/services/job.service.spec.ts @@ -11,7 +11,7 @@ describe(JobService.name, () => { let mocks: ServiceMocks; beforeEach(() => { - ({ sut, mocks } = newTestService(JobService, {})); + ({ sut, mocks } = newTestService(JobService)); mocks.config.getWorker.mockReturnValue(ImmichWorker.Microservices); }); diff --git a/server/src/services/library.service.spec.ts b/server/src/services/library.service.spec.ts index 308c80fb37..edd0d46bce 100644 --- a/server/src/services/library.service.spec.ts +++ b/server/src/services/library.service.spec.ts @@ -1,7 +1,7 @@ import { BadRequestException } from '@nestjs/common'; import { Stats } from 'node:fs'; import { defaults, SystemConfig } from 'src/config'; -import { APP_MEDIA_LOCATION, JOBS_LIBRARY_PAGINATION_SIZE } from 'src/constants'; +import { JOBS_LIBRARY_PAGINATION_SIZE } from 'src/constants'; import { mapLibrary } from 'src/dtos/library.dto'; import { AssetType, CronJob, ImmichWorker, JobName, JobStatus } from 'src/enum'; import { LibraryService } from 'src/services/library.service'; @@ -24,7 +24,7 @@ describe(LibraryService.name, () => { let mocks: ServiceMocks; beforeEach(() => { - ({ sut, mocks } = newTestService(LibraryService, {})); + ({ sut, mocks } = newTestService(LibraryService)); mocks.database.tryLock.mockResolvedValue(true); mocks.config.getWorker.mockReturnValue(ImmichWorker.Microservices); @@ -1171,10 +1171,10 @@ describe(LibraryService.name, () => { mocks.storage.checkFileExists.mockResolvedValue(true); - await expect(sut.validate('library-id', { importPaths: ['/data/user1/'] })).resolves.toEqual({ + await expect(sut.validate('library-id', { importPaths: ['/external/user1/'] })).resolves.toEqual({ importPaths: [ { - importPath: '/data/user1/', + importPath: '/external/user1/', isValid: true, message: undefined, }, @@ -1188,10 +1188,10 @@ describe(LibraryService.name, () => { throw error; }); - await expect(sut.validate('library-id', { importPaths: ['/data/user1/'] })).resolves.toEqual({ + await expect(sut.validate('library-id', { importPaths: ['/external/user1/'] })).resolves.toEqual({ importPaths: [ { - importPath: '/data/user1/', + importPath: '/external/user1/', isValid: false, message: 'Path does not exist (ENOENT)', }, @@ -1204,10 +1204,10 @@ describe(LibraryService.name, () => { isDirectory: () => false, } as Stats); - await expect(sut.validate('library-id', { importPaths: ['/data/user1/file'] })).resolves.toEqual({ + await expect(sut.validate('library-id', { importPaths: ['/external/user1/file'] })).resolves.toEqual({ importPaths: [ { - importPath: '/data/user1/file', + importPath: '/external/user1/file', isValid: false, message: 'Not a directory', }, @@ -1220,10 +1220,10 @@ describe(LibraryService.name, () => { throw new Error('Unknown error'); }); - await expect(sut.validate('library-id', { importPaths: ['/data/user1/'] })).resolves.toEqual({ + await expect(sut.validate('library-id', { importPaths: ['/external/user1/'] })).resolves.toEqual({ importPaths: [ { - importPath: '/data/user1/', + importPath: '/external/user1/', isValid: false, message: 'Error: Unknown error', }, @@ -1238,10 +1238,10 @@ describe(LibraryService.name, () => { mocks.storage.checkFileExists.mockResolvedValue(false); - await expect(sut.validate('library-id', { importPaths: ['/data/user1/'] })).resolves.toEqual({ + await expect(sut.validate('library-id', { importPaths: ['/external/user1/'] })).resolves.toEqual({ importPaths: [ { - importPath: '/data/user1/', + importPath: '/external/user1/', isValid: false, message: 'Lacking read permission for folder', }, @@ -1264,7 +1264,7 @@ describe(LibraryService.name, () => { }); it('should detect when import path is in immich media folder', async () => { - const importPaths = [APP_MEDIA_LOCATION + '/thumbs', `${process.cwd()}/xyz`, APP_MEDIA_LOCATION + '/library']; + const importPaths = ['/data/thumbs', `${process.cwd()}/xyz`, '/data/library']; const library = factory.library({ importPaths }); mocks.storage.stat.mockResolvedValue({ isDirectory: () => true } as Stats); diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index 0f4ba769c0..97351af37b 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -1,6 +1,5 @@ import { OutputInfo } from 'sharp'; import { SystemConfig } from 'src/config'; -import { APP_MEDIA_LOCATION } from 'src/constants'; import { Exif } from 'src/database'; import { AssetFileType, @@ -205,19 +204,19 @@ describe(MediaService.name, () => { entityId: assetStub.image.id, pathType: AssetPathType.FullSize, oldPath: '/uploads/user-id/fullsize/path.webp', - newPath: expect.stringContaining('upload/thumbs/user-id/as/se/asset-id-fullsize.jpeg'), + newPath: expect.stringContaining('/data/thumbs/user-id/as/se/asset-id-fullsize.jpeg'), }); expect(mocks.move.create).toHaveBeenCalledWith({ entityId: assetStub.image.id, pathType: AssetPathType.Preview, oldPath: '/uploads/user-id/thumbs/path.jpg', - newPath: expect.stringContaining('upload/thumbs/user-id/as/se/asset-id-preview.jpeg'), + newPath: expect.stringContaining('/data/thumbs/user-id/as/se/asset-id-preview.jpeg'), }); expect(mocks.move.create).toHaveBeenCalledWith({ entityId: assetStub.image.id, pathType: AssetPathType.Thumbnail, oldPath: '/uploads/user-id/webp/path.ext', - newPath: expect.stringContaining('upload/thumbs/user-id/as/se/asset-id-thumbnail.webp'), + newPath: expect.stringContaining('/data/thumbs/user-id/as/se/asset-id-thumbnail.webp'), }); expect(mocks.move.create).toHaveBeenCalledTimes(3); }); @@ -486,8 +485,8 @@ describe(MediaService.name, () => { mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image); const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8'); mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer); - const previewPath = APP_MEDIA_LOCATION + `/thumbs/user-id/as/se/asset-id-preview.${format}`; - const thumbnailPath = APP_MEDIA_LOCATION + `/thumbs/user-id/as/se/asset-id-thumbnail.webp`; + const previewPath = `/data/thumbs/user-id/as/se/asset-id-preview.${format}`; + const thumbnailPath = `/data/thumbs/user-id/as/se/asset-id-thumbnail.webp`; await sut.handleGenerateThumbnails({ id: assetStub.image.id }); @@ -531,8 +530,8 @@ describe(MediaService.name, () => { mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image); const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8'); mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer); - const previewPath = expect.stringContaining(`upload/thumbs/user-id/as/se/asset-id-preview.jpeg`); - const thumbnailPath = expect.stringContaining(`upload/thumbs/user-id/as/se/asset-id-thumbnail.${format}`); + const previewPath = expect.stringContaining(`/data/thumbs/user-id/as/se/asset-id-preview.jpeg`); + const thumbnailPath = expect.stringContaining(`/data/thumbs/user-id/as/se/asset-id-thumbnail.${format}`); await sut.handleGenerateThumbnails({ id: assetStub.image.id }); @@ -2895,7 +2894,7 @@ describe(MediaService.name, () => { expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - APP_MEDIA_LOCATION + '/encoded-video/user-id/as/se/asset-id.mp4', + '/data/encoded-video/user-id/as/se/asset-id.mp4', expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-c:a copy']), diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index cc0956b9a8..1e304137e4 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -587,7 +587,7 @@ describe(MetadataService.name, () => { libraryId: assetStub.livePhotoWithOriginalFileName.libraryId, localDateTime: assetStub.livePhotoWithOriginalFileName.fileCreatedAt, originalFileName: 'asset_1.mp4', - originalPath: expect.stringContaining('upload/encoded-video/user-id/li/ve/live-photo-motion-asset-MP.mp4'), + originalPath: expect.stringContaining('/data/encoded-video/user-id/li/ve/live-photo-motion-asset-MP.mp4'), ownerId: assetStub.livePhotoWithOriginalFileName.ownerId, type: AssetType.Video, }); @@ -645,7 +645,7 @@ describe(MetadataService.name, () => { libraryId: assetStub.livePhotoWithOriginalFileName.libraryId, localDateTime: assetStub.livePhotoWithOriginalFileName.fileCreatedAt, originalFileName: 'asset_1.mp4', - originalPath: expect.stringContaining('upload/encoded-video/user-id/li/ve/live-photo-motion-asset-MP.mp4'), + originalPath: expect.stringContaining('/data/encoded-video/user-id/li/ve/live-photo-motion-asset-MP.mp4'), ownerId: assetStub.livePhotoWithOriginalFileName.ownerId, type: AssetType.Video, }); @@ -703,7 +703,7 @@ describe(MetadataService.name, () => { libraryId: assetStub.livePhotoWithOriginalFileName.libraryId, localDateTime: assetStub.livePhotoWithOriginalFileName.fileCreatedAt, originalFileName: 'asset_1.mp4', - originalPath: expect.stringContaining('upload/encoded-video/user-id/li/ve/live-photo-motion-asset-MP.mp4'), + originalPath: expect.stringContaining('/data/encoded-video/user-id/li/ve/live-photo-motion-asset-MP.mp4'), ownerId: assetStub.livePhotoWithOriginalFileName.ownerId, type: AssetType.Video, }); diff --git a/server/src/services/server.service.spec.ts b/server/src/services/server.service.spec.ts index 06ddd32601..a96a9925db 100644 --- a/server/src/services/server.service.spec.ts +++ b/server/src/services/server.service.spec.ts @@ -28,7 +28,7 @@ describe(ServerService.name, () => { diskUseRaw: 300, }); - expect(mocks.storage.checkDiskUsage).toHaveBeenCalledWith(expect.stringContaining('upload/library')); + expect(mocks.storage.checkDiskUsage).toHaveBeenCalledWith(expect.stringContaining('/data/library')); }); it('should return the disk space as KiB', async () => { @@ -44,7 +44,7 @@ describe(ServerService.name, () => { diskUseRaw: 300_000, }); - expect(mocks.storage.checkDiskUsage).toHaveBeenCalledWith(expect.stringContaining('upload/library')); + expect(mocks.storage.checkDiskUsage).toHaveBeenCalledWith(expect.stringContaining('/data/library')); }); it('should return the disk space as MiB', async () => { @@ -60,7 +60,7 @@ describe(ServerService.name, () => { diskUseRaw: 300_000_000, }); - expect(mocks.storage.checkDiskUsage).toHaveBeenCalledWith(expect.stringContaining('upload/library')); + expect(mocks.storage.checkDiskUsage).toHaveBeenCalledWith(expect.stringContaining('/data/library')); }); it('should return the disk space as GiB', async () => { @@ -80,7 +80,7 @@ describe(ServerService.name, () => { diskUseRaw: 300_000_000_000, }); - expect(mocks.storage.checkDiskUsage).toHaveBeenCalledWith(expect.stringContaining('upload/library')); + expect(mocks.storage.checkDiskUsage).toHaveBeenCalledWith(expect.stringContaining('/data/library')); }); it('should return the disk space as TiB', async () => { @@ -100,7 +100,7 @@ describe(ServerService.name, () => { diskUseRaw: 300_000_000_000_000, }); - expect(mocks.storage.checkDiskUsage).toHaveBeenCalledWith(expect.stringContaining('upload/library')); + expect(mocks.storage.checkDiskUsage).toHaveBeenCalledWith(expect.stringContaining('/data/library')); }); it('should return the disk space as PiB', async () => { @@ -120,7 +120,7 @@ describe(ServerService.name, () => { diskUseRaw: 300_000_000_000_000_000, }); - expect(mocks.storage.checkDiskUsage).toHaveBeenCalledWith(expect.stringContaining('upload/library')); + expect(mocks.storage.checkDiskUsage).toHaveBeenCalledWith(expect.stringContaining('/data/library')); }); }); diff --git a/server/src/services/storage-template.service.spec.ts b/server/src/services/storage-template.service.spec.ts index 882ffcd328..d0d7ea3a3c 100644 --- a/server/src/services/storage-template.service.spec.ts +++ b/server/src/services/storage-template.service.spec.ts @@ -1,6 +1,5 @@ import { Stats } from 'node:fs'; import { defaults, SystemConfig } from 'src/config'; -import { APP_MEDIA_LOCATION } from 'src/constants'; import { AssetPathType, JobStatus } from 'src/enum'; import { StorageTemplateService } from 'src/services/storage-template.service'; import { albumStub } from 'test/fixtures/album.stub'; @@ -111,10 +110,8 @@ describe(StorageTemplateService.name, () => { it('should migrate single moving picture', async () => { mocks.user.get.mockResolvedValue(userStub.user1); - const newMotionPicturePath = - APP_MEDIA_LOCATION + `/library/${motionAsset.ownerId}/2022/2022-06-19/${motionAsset.originalFileName}`; - const newStillPicturePath = - APP_MEDIA_LOCATION + `/library/${stillAsset.ownerId}/2022/2022-06-19/${stillAsset.originalFileName}`; + const newMotionPicturePath = `/data/library/${motionAsset.ownerId}/2022/2022-06-19/${motionAsset.originalFileName}`; + const newStillPicturePath = `/data/library/${stillAsset.ownerId}/2022/2022-06-19/${stillAsset.originalFileName}`; mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(stillAsset); mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(motionAsset); @@ -160,7 +157,7 @@ describe(StorageTemplateService.name, () => { expect(mocks.move.create).toHaveBeenCalledWith({ entityId: asset.id, newPath: expect.stringContaining( - `upload/library/${user.id}/${asset.fileCreatedAt.getFullYear()}/${album.albumName}/${asset.originalFileName}`, + `/data/library/${user.id}/${asset.fileCreatedAt.getFullYear()}/${album.albumName}/${asset.originalFileName}`, ), oldPath: asset.originalPath, pathType: AssetPathType.Original, @@ -183,7 +180,7 @@ describe(StorageTemplateService.name, () => { expect(mocks.move.create).toHaveBeenCalledWith({ entityId: asset.id, newPath: expect.stringContaining( - `upload/library/${user.id}/${asset.fileCreatedAt.getFullYear()}/other/${month}/${asset.originalFileName}`, + `/data/library/${user.id}/${asset.fileCreatedAt.getFullYear()}/other/${month}/${asset.originalFileName}`, ), oldPath: asset.originalPath, pathType: AssetPathType.Original, @@ -219,7 +216,7 @@ describe(StorageTemplateService.name, () => { expect(mocks.move.create).toHaveBeenCalledWith({ entityId: asset.id, newPath: expect.stringContaining( - `upload/library/${user.id}/${asset.fileCreatedAt.getFullYear()}/${month} - ${album.albumName}/${asset.originalFileName}`, + `/data/library/${user.id}/${asset.fileCreatedAt.getFullYear()}/${month} - ${album.albumName}/${asset.originalFileName}`, ), oldPath: asset.originalPath, pathType: AssetPathType.Original, @@ -243,9 +240,7 @@ describe(StorageTemplateService.name, () => { const month = (asset.fileCreatedAt.getMonth() + 1).toString().padStart(2, '0'); expect(mocks.move.create).toHaveBeenCalledWith({ entityId: asset.id, - newPath: - APP_MEDIA_LOCATION + - `/library/${user.id}/${asset.fileCreatedAt.getFullYear()}/${month}/${asset.originalFileName}`, + newPath: `/data/library/${user.id}/${asset.fileCreatedAt.getFullYear()}/${month}/${asset.originalFileName}`, oldPath: asset.originalPath, pathType: AssetPathType.Original, }); @@ -255,9 +250,8 @@ describe(StorageTemplateService.name, () => { mocks.user.get.mockResolvedValue(userStub.user1); const asset = assetStub.storageAsset(); - const previousFailedNewPath = - APP_MEDIA_LOCATION + `/library/${userStub.user1.id}/2023/Feb/${asset.originalFileName}`; - const newPath = APP_MEDIA_LOCATION + `/library/${userStub.user1.id}/2022/2022-06-19/${asset.originalFileName}`; + const previousFailedNewPath = `/data/library/${userStub.user1.id}/2023/Feb/${asset.originalFileName}`; + const newPath = `/data/library/${userStub.user1.id}/2022/2022-06-19/${asset.originalFileName}`; mocks.storage.checkFileExists.mockImplementation((path) => Promise.resolve(path === asset.originalPath)); mocks.move.getByEntity.mockResolvedValue({ @@ -296,9 +290,8 @@ describe(StorageTemplateService.name, () => { mocks.user.get.mockResolvedValue(userStub.user1); const asset = assetStub.storageAsset({ fileSizeInByte: 5000 }); - const previousFailedNewPath = - APP_MEDIA_LOCATION + `/library/${asset.ownerId}/2022/June/${asset.originalFileName}`; - const newPath = APP_MEDIA_LOCATION + `/library/${asset.ownerId}/2022/2022-06-19/${asset.originalFileName}`; + const previousFailedNewPath = `/data/library/${asset.ownerId}/2022/June/${asset.originalFileName}`; + const newPath = `/data/library/${asset.ownerId}/2022/2022-06-19/${asset.originalFileName}`; mocks.storage.checkFileExists.mockImplementation((path) => Promise.resolve(path === previousFailedNewPath)); mocks.storage.stat.mockResolvedValue({ size: 5000 } as Stats); @@ -332,8 +325,7 @@ describe(StorageTemplateService.name, () => { it('should fail move if copying and hash of asset and the new file do not match', async () => { mocks.user.get.mockResolvedValue(userStub.user1); - const newPath = - APP_MEDIA_LOCATION + `/library/${userStub.user1.id}/2022/2022-06-19/${testAsset.originalFileName}`; + const newPath = `/data/library/${userStub.user1.id}/2022/2022-06-19/${testAsset.originalFileName}`; mocks.storage.rename.mockRejectedValue({ code: 'EXDEV' }); mocks.storage.stat.mockResolvedValue({ size: 5000 } as Stats); @@ -375,8 +367,8 @@ describe(StorageTemplateService.name, () => { 'should fail to migrate previously failed move from previous new path when old path no longer exists if $reason validation fails', async ({ failedPathChecksum, failedPathSize }) => { mocks.user.get.mockResolvedValue(userStub.user1); - const previousFailedNewPath = `upload/library/${userStub.user1.id}/2023/Feb/${testAsset.originalFileName}`; - const newPath = `upload/library/${userStub.user1.id}/2023/2023-02-23/${testAsset.originalFileName}`; + const previousFailedNewPath = `/data/library/${userStub.user1.id}/2023/Feb/${testAsset.originalFileName}`; + const newPath = `/data/library/${userStub.user1.id}/2023/2023-02-23/${testAsset.originalFileName}`; mocks.storage.checkFileExists.mockImplementation((path) => Promise.resolve(previousFailedNewPath === path)); mocks.storage.stat.mockResolvedValue({ size: failedPathSize } as Stats); @@ -423,7 +415,7 @@ describe(StorageTemplateService.name, () => { it('should handle an asset with a duplicate destination', async () => { const asset = assetStub.storageAsset(); const oldPath = asset.originalPath; - const newPath = APP_MEDIA_LOCATION + `/library/user-id/2022/2022-06-19/${asset.originalFileName}`; + const newPath = `/data/library/user-id/2022/2022-06-19/${asset.originalFileName}`; const newPath2 = newPath.replace('.jpg', '+1.jpg'); mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset])); @@ -448,7 +440,7 @@ describe(StorageTemplateService.name, () => { }); it('should skip when an asset already matches the template', async () => { - const asset = assetStub.storageAsset({ originalPath: 'upload/library/user-id/2023/2023-02-23/asset-id.jpg' }); + const asset = assetStub.storageAsset({ originalPath: '/data/library/user-id/2023/2023-02-23/asset-id.jpg' }); mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset])); mocks.user.getList.mockResolvedValue([userStub.user1]); @@ -463,7 +455,7 @@ describe(StorageTemplateService.name, () => { }); it('should skip when an asset is probably a duplicate', async () => { - const asset = assetStub.storageAsset({ originalPath: 'upload/library/user-id/2023/2023-02-23/asset-id+1.jpg' }); + const asset = assetStub.storageAsset({ originalPath: '/data/library/user-id/2023/2023-02-23/asset-id+1.jpg' }); mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset])); mocks.user.getList.mockResolvedValue([userStub.user1]); @@ -480,7 +472,7 @@ describe(StorageTemplateService.name, () => { it('should move an asset', async () => { const asset = assetStub.storageAsset(); const oldPath = asset.originalPath; - const newPath = APP_MEDIA_LOCATION + `/library/user-id/2022/2022-06-19/${asset.originalFileName}`; + const newPath = `/data/library/user-id/2022/2022-06-19/${asset.originalFileName}`; mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset])); mocks.user.getList.mockResolvedValue([userStub.user1]); mocks.move.create.mockResolvedValue({ @@ -508,7 +500,7 @@ describe(StorageTemplateService.name, () => { entityId: asset.id, pathType: AssetPathType.Original, oldPath: asset.originalPath, - newPath: `upload/library/${user.storageLabel}/2023/2023-02-23/${asset.originalFileName}`, + newPath: `/data/library/${user.storageLabel}/2023/2023-02-23/${asset.originalFileName}`, }); await sut.handleMigration(); @@ -516,12 +508,12 @@ describe(StorageTemplateService.name, () => { expect(mocks.assetJob.streamForStorageTemplateJob).toHaveBeenCalled(); expect(mocks.storage.rename).toHaveBeenCalledWith( '/original/path.jpg', - expect.stringContaining(`upload/library/${user.storageLabel}/2022/2022-06-19/${asset.originalFileName}`), + expect.stringContaining(`/data/library/${user.storageLabel}/2022/2022-06-19/${asset.originalFileName}`), ); expect(mocks.asset.update).toHaveBeenCalledWith({ id: asset.id, originalPath: expect.stringContaining( - `upload/library/${user.storageLabel}/2022/2022-06-19/${asset.originalFileName}`, + `/data/library/${user.storageLabel}/2022/2022-06-19/${asset.originalFileName}`, ), }); }); @@ -529,7 +521,7 @@ describe(StorageTemplateService.name, () => { it('should copy the file if rename fails due to EXDEV (rename across filesystems)', async () => { const asset = assetStub.storageAsset({ originalPath: '/path/to/original.jpg', fileSizeInByte: 5000 }); const oldPath = asset.originalPath; - const newPath = APP_MEDIA_LOCATION + `/library/user-id/2022/2022-06-19/${asset.originalFileName}`; + const newPath = `/data/library/user-id/2022/2022-06-19/${asset.originalFileName}`; mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset])); mocks.storage.rename.mockRejectedValue({ code: 'EXDEV' }); mocks.user.getList.mockResolvedValue([userStub.user1]); @@ -577,7 +569,7 @@ describe(StorageTemplateService.name, () => { entityId: asset.id, pathType: AssetPathType.Original, oldPath: asset.originalPath, - newPath: `upload/library/user-id/2022/2022-06-19/${asset.originalFileName}`, + newPath: `/data/library/user-id/2022/2022-06-19/${asset.originalFileName}`, }); mocks.storage.stat.mockResolvedValue({ size: 100, @@ -588,14 +580,14 @@ describe(StorageTemplateService.name, () => { expect(mocks.assetJob.streamForStorageTemplateJob).toHaveBeenCalled(); expect(mocks.storage.rename).toHaveBeenCalledWith( '/original/path.jpg', - expect.stringContaining(`upload/library/user-id/2022/2022-06-19/${asset.originalFileName}`), + expect.stringContaining(`/data/library/user-id/2022/2022-06-19/${asset.originalFileName}`), ); expect(mocks.storage.copyFile).toHaveBeenCalledWith( '/original/path.jpg', - expect.stringContaining(`upload/library/user-id/2022/2022-06-19/${asset.originalFileName}`), + expect.stringContaining(`/data/library/user-id/2022/2022-06-19/${asset.originalFileName}`), ); expect(mocks.storage.stat).toHaveBeenCalledWith( - expect.stringContaining(`upload/library/user-id/2022/2022-06-19/${asset.originalFileName}`), + expect.stringContaining(`/data/library/user-id/2022/2022-06-19/${asset.originalFileName}`), ); expect(mocks.asset.update).not.toHaveBeenCalled(); }); @@ -619,7 +611,7 @@ describe(StorageTemplateService.name, () => { expect(mocks.assetJob.streamForStorageTemplateJob).toHaveBeenCalled(); expect(mocks.storage.rename).toHaveBeenCalledWith( '/original/path.jpg', - expect.stringContaining(`upload/library/user-id/2022/2022-06-19/${asset.originalFileName}`), + expect.stringContaining(`/data/library/user-id/2022/2022-06-19/${asset.originalFileName}`), ); expect(mocks.asset.update).not.toHaveBeenCalled(); }); @@ -630,7 +622,7 @@ describe(StorageTemplateService.name, () => { const user = factory.userAdmin({ storageLabel: 'label-1' }); const asset = assetStub.storageAsset({ ownerId: user.id, - originalPath: `upload/library/${user.id}/2022/2022-06-19/IMG_7065.heic`, + originalPath: `/data/library/${user.id}/2022/2022-06-19/IMG_7065.heic`, originalFileName: 'IMG_7065.HEIC', }); mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset])); @@ -639,16 +631,16 @@ describe(StorageTemplateService.name, () => { id: '123', entityId: asset.id, pathType: AssetPathType.Original, - oldPath: `upload/library/${user.id}/2022/2022-06-19/IMG_7065.heic`, - newPath: `upload/library/${user.id}/2023/2023-02-23/IMG_7065.heic`, + oldPath: `/data/library/${user.id}/2022/2022-06-19/IMG_7065.heic`, + newPath: `/data/library/${user.id}/2023/2023-02-23/IMG_7065.heic`, }); await sut.handleMigration(); expect(mocks.assetJob.streamForStorageTemplateJob).toHaveBeenCalled(); expect(mocks.storage.rename).toHaveBeenCalledWith( - expect.stringContaining(`upload/library/${user.id}/2022/2022-06-19/IMG_7065.heic`), - expect.stringContaining(`upload/library/${user.storageLabel}/2022/2022-06-19/IMG_7065.heic`), + expect.stringContaining(`/data/library/${user.id}/2022/2022-06-19/IMG_7065.heic`), + expect.stringContaining(`/data/library/${user.storageLabel}/2022/2022-06-19/IMG_7065.heic`), ); }); @@ -656,7 +648,7 @@ describe(StorageTemplateService.name, () => { const user = factory.userAdmin(); const asset = assetStub.storageAsset({ ownerId: user.id, - originalPath: `upload/library/${user.id}/2022/2022-06-19/IMG_7065.HEIC`, + originalPath: `/data/library/${user.id}/2022/2022-06-19/IMG_7065.HEIC`, originalFileName: 'IMG_7065.HEIC', }); mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset])); @@ -665,16 +657,16 @@ describe(StorageTemplateService.name, () => { id: '123', entityId: asset.id, pathType: AssetPathType.Original, - oldPath: `upload/library/${user.id}/2022/2022-06-19/IMG_7065.HEIC`, - newPath: `upload/library/${user.id}/2023/2023-02-23/IMG_7065.heic`, + oldPath: `/data/library/${user.id}/2022/2022-06-19/IMG_7065.HEIC`, + newPath: `/data/library/${user.id}/2023/2023-02-23/IMG_7065.heic`, }); await sut.handleMigration(); expect(mocks.assetJob.streamForStorageTemplateJob).toHaveBeenCalled(); expect(mocks.storage.rename).toHaveBeenCalledWith( - expect.stringContaining(`upload/library/${user.id}/2022/2022-06-19/IMG_7065.HEIC`), - expect.stringContaining(`upload/library/${user.id}/2022/2022-06-19/IMG_7065.heic`), + expect.stringContaining(`/data/library/${user.id}/2022/2022-06-19/IMG_7065.HEIC`), + expect.stringContaining(`/data/library/${user.id}/2022/2022-06-19/IMG_7065.heic`), ); }); @@ -682,7 +674,7 @@ describe(StorageTemplateService.name, () => { const user = factory.userAdmin(); const asset = assetStub.storageAsset({ ownerId: user.id, - originalPath: `upload/library/${user.id}/2022/2022-06-19/IMG_7065.JPEG`, + originalPath: `/data/library/${user.id}/2022/2022-06-19/IMG_7065.JPEG`, originalFileName: 'IMG_7065.JPEG', }); mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset])); @@ -691,16 +683,16 @@ describe(StorageTemplateService.name, () => { id: '123', entityId: asset.id, pathType: AssetPathType.Original, - oldPath: `upload/library/${user.id}/2022/2022-06-19/IMG_7065.JPEG`, - newPath: `upload/library/${user.id}/2023/2023-02-23/IMG_7065.jpg`, + oldPath: `/data/library/${user.id}/2022/2022-06-19/IMG_7065.JPEG`, + newPath: `/data/library/${user.id}/2023/2023-02-23/IMG_7065.jpg`, }); await sut.handleMigration(); expect(mocks.assetJob.streamForStorageTemplateJob).toHaveBeenCalled(); expect(mocks.storage.rename).toHaveBeenCalledWith( - expect.stringContaining(`upload/library/${user.id}/2022/2022-06-19/IMG_7065.JPEG`), - expect.stringContaining(`upload/library/${user.id}/2022/2022-06-19/IMG_7065.jpg`), + expect.stringContaining(`/data/library/${user.id}/2022/2022-06-19/IMG_7065.JPEG`), + expect.stringContaining(`/data/library/${user.id}/2022/2022-06-19/IMG_7065.jpg`), ); }); @@ -708,7 +700,7 @@ describe(StorageTemplateService.name, () => { const user = factory.userAdmin(); const asset = assetStub.storageAsset({ ownerId: user.id, - originalPath: 'upload/library/user-id/2022/2022-06-19/IMG_7065.JPG', + originalPath: '/data/library/user-id/2022/2022-06-19/IMG_7065.JPG', originalFileName: 'IMG_7065.JPG', }); mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset])); @@ -717,16 +709,16 @@ describe(StorageTemplateService.name, () => { id: '123', entityId: asset.id, pathType: AssetPathType.Original, - oldPath: `upload/library/${user.id}/2022/2022-06-19/IMG_7065.JPG`, - newPath: `upload/library/${user.id}/2023/2023-02-23/IMG_7065.jpg`, + oldPath: `/data/library/${user.id}/2022/2022-06-19/IMG_7065.JPG`, + newPath: `/data/library/${user.id}/2023/2023-02-23/IMG_7065.jpg`, }); await sut.handleMigration(); expect(mocks.assetJob.streamForStorageTemplateJob).toHaveBeenCalled(); expect(mocks.storage.rename).toHaveBeenCalledWith( - expect.stringContaining(`upload/library/${user.id}/2022/2022-06-19/IMG_7065.JPG`), - expect.stringContaining(`upload/library/${user.id}/2022/2022-06-19/IMG_7065.jpg`), + expect.stringContaining(`/data/library/${user.id}/2022/2022-06-19/IMG_7065.JPG`), + expect.stringContaining(`/data/library/${user.id}/2022/2022-06-19/IMG_7065.jpg`), ); }); }); diff --git a/server/src/services/storage.service.spec.ts b/server/src/services/storage.service.spec.ts index 0303c11649..3ca9cd7ce2 100644 --- a/server/src/services/storage.service.spec.ts +++ b/server/src/services/storage.service.spec.ts @@ -20,6 +20,14 @@ describe(StorageService.name, () => { it('should enable mount folder checking', async () => { mocks.systemMetadata.get.mockResolvedValue(null); mocks.asset.getFileSamples.mockResolvedValue([]); + mocks.config.getEnv.mockReturnValue( + mockEnvData({ + storage: { + ignoreMountCheckErrors: false, + mediaLocation: '/data', + }, + }), + ); await expect(sut.onBootstrap()).resolves.toBeUndefined(); @@ -33,34 +41,34 @@ describe(StorageService.name, () => { upload: true, }, }); - expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.stringContaining('upload/encoded-video')); - expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.stringContaining('upload/library')); - expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.stringContaining('upload/profile')); - expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.stringContaining('upload/thumbs')); - expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.stringContaining('upload/upload')); - expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.stringContaining('upload/backups')); + expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.stringContaining('/data/encoded-video')); + expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.stringContaining('/data/library')); + expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.stringContaining('/data/profile')); + expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.stringContaining('/data/thumbs')); + expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.stringContaining('/data/upload')); + expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.stringContaining('/data/backups')); expect(mocks.storage.createFile).toHaveBeenCalledWith( - expect.stringContaining('upload/encoded-video/.immich'), + expect.stringContaining('/data/encoded-video/.immich'), expect.any(Buffer), ); expect(mocks.storage.createFile).toHaveBeenCalledWith( - expect.stringContaining('upload/library/.immich'), + expect.stringContaining('/data/library/.immich'), expect.any(Buffer), ); expect(mocks.storage.createFile).toHaveBeenCalledWith( - expect.stringContaining('upload/profile/.immich'), + expect.stringContaining('/data/profile/.immich'), expect.any(Buffer), ); expect(mocks.storage.createFile).toHaveBeenCalledWith( - expect.stringContaining('upload/thumbs/.immich'), + expect.stringContaining('/data/thumbs/.immich'), expect.any(Buffer), ); expect(mocks.storage.createFile).toHaveBeenCalledWith( - expect.stringContaining('upload/upload/.immich'), + expect.stringContaining('/data/upload/.immich'), expect.any(Buffer), ); expect(mocks.storage.createFile).toHaveBeenCalledWith( - expect.stringContaining('upload/backups/.immich'), + expect.stringContaining('/data/backups/.immich'), expect.any(Buffer), ); }); @@ -77,6 +85,14 @@ describe(StorageService.name, () => { }, }); mocks.asset.getFileSamples.mockResolvedValue([]); + mocks.config.getEnv.mockReturnValue( + mockEnvData({ + storage: { + ignoreMountCheckErrors: false, + mediaLocation: '/data', + }, + }), + ); await expect(sut.onBootstrap()).resolves.toBeUndefined(); @@ -91,15 +107,15 @@ describe(StorageService.name, () => { }, }); expect(mocks.storage.mkdirSync).toHaveBeenCalledTimes(2); - expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.stringContaining('upload/library')); - expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.stringContaining('upload/backups')); + expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.stringContaining('/data/library')); + expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.stringContaining('/data/backups')); expect(mocks.storage.createFile).toHaveBeenCalledTimes(2); expect(mocks.storage.createFile).toHaveBeenCalledWith( - expect.stringContaining('upload/library/.immich'), + expect.stringContaining('/data/library/.immich'), expect.any(Buffer), ); expect(mocks.storage.createFile).toHaveBeenCalledWith( - expect.stringContaining('upload/backups/.immich'), + expect.stringContaining('/data/backups/.immich'), expect.any(Buffer), ); }); diff --git a/server/src/services/storage.service.ts b/server/src/services/storage.service.ts index 6c99194abd..cd8910305a 100644 --- a/server/src/services/storage.service.ts +++ b/server/src/services/storage.service.ts @@ -1,9 +1,16 @@ import { Injectable } from '@nestjs/common'; import { join } from 'node:path'; -import { APP_MEDIA_LOCATION } from 'src/constants'; import { StorageCore } from 'src/cores/storage.core'; import { OnEvent, OnJob } from 'src/decorators'; -import { DatabaseLock, JobName, JobStatus, QueueName, StorageFolder, SystemMetadataKey } from 'src/enum'; +import { + BootstrapEventPriority, + DatabaseLock, + JobName, + JobStatus, + QueueName, + StorageFolder, + SystemMetadataKey, +} from 'src/enum'; import { BaseService } from 'src/services/base.service'; import { JobOf, SystemFlags } from 'src/types'; import { ImmichStartupError } from 'src/utils/misc'; @@ -12,9 +19,32 @@ const docsMessage = `Please see https://immich.app/docs/administration/system-in @Injectable() export class StorageService extends BaseService { - @OnEvent({ name: 'AppBootstrap' }) - async onBootstrap() { + private detectMediaLocation(): string { const envData = this.configRepository.getEnv(); + if (envData.storage.mediaLocation) { + return envData.storage.mediaLocation; + } + + const targets: string[] = []; + const candidates = ['/data', '/usr/src/app/upload']; + + for (const candidate of candidates) { + const exists = this.storageRepository.existsSync(candidate); + if (exists) { + targets.push(candidate); + } + } + + if (targets.length === 1) { + return targets[0]; + } + + return '/usr/src/app/upload'; + } + + @OnEvent({ name: 'AppBootstrap', priority: BootstrapEventPriority.StorageService }) + async onBootstrap() { + StorageCore.setMediaLocation(this.detectMediaLocation()); await this.databaseRepository.withLock(DatabaseLock.SystemFileMounts, async () => { const flags = @@ -53,6 +83,7 @@ export class StorageService extends BaseService { this.logger.log('Successfully verified system mount folder checks'); } catch (error) { + const envData = this.configRepository.getEnv(); if (envData.storage.ignoreMountCheckErrors) { this.logger.error(error as Error); this.logger.warn('Ignoring mount folder errors'); @@ -63,30 +94,34 @@ 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); - let previous = savedValue?.location || ''; + const current = StorageCore.getMediaLocation(); + const samples = await this.assetRepository.getFileSamples(); + if (samples.length > 0) { + const originalPath = samples[0].originalPath; + const savedValue = await this.systemMetadataRepository.get(SystemMetadataKey.MediaLocation); + 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); - } + if (!previous) { + previous = originalPath.startsWith('upload/') ? 'upload' : '/usr/src/app/upload'; } - await this.systemMetadataRepository.set(SystemMetadataKey.MediaLocation, { location: current }); + if (previous !== current) { + this.logger.log(`Media location changed (from=${previous}, to=${current})`); + + if (!originalPath.startsWith(previous)) { + throw new Error( + 'Detected an inconsistent media location. For more information, see https://immich.app/errors#inconsistent-media-location', + ); + } + + this.logger.warn( + `Detected a change to 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 }); }); } diff --git a/server/src/services/user.service.spec.ts b/server/src/services/user.service.spec.ts index b4e616974e..bd896ffc24 100644 --- a/server/src/services/user.service.spec.ts +++ b/server/src/services/user.service.spec.ts @@ -236,23 +236,23 @@ describe(UserService.name, () => { await sut.handleUserDelete({ id: user.id }); expect(mocks.storage.unlinkDir).toHaveBeenCalledWith( - expect.stringContaining('upload/library/deleted-user'), + expect.stringContaining('/data/library/deleted-user'), options, ); expect(mocks.storage.unlinkDir).toHaveBeenCalledWith( - expect.stringContaining('upload/upload/deleted-user'), + expect.stringContaining('/data/upload/deleted-user'), options, ); expect(mocks.storage.unlinkDir).toHaveBeenCalledWith( - expect.stringContaining('upload/profile/deleted-user'), + expect.stringContaining('/data/profile/deleted-user'), options, ); expect(mocks.storage.unlinkDir).toHaveBeenCalledWith( - expect.stringContaining('upload/thumbs/deleted-user'), + expect.stringContaining('/data/thumbs/deleted-user'), options, ); expect(mocks.storage.unlinkDir).toHaveBeenCalledWith( - expect.stringContaining('upload/encoded-video/deleted-user'), + expect.stringContaining('/data/encoded-video/deleted-user'), options, ); expect(mocks.album.deleteAll).toHaveBeenCalledWith(user.id); @@ -268,7 +268,7 @@ describe(UserService.name, () => { const options = { force: true, recursive: true }; - expect(mocks.storage.unlinkDir).toHaveBeenCalledWith(expect.stringContaining('upload/library/admin'), options); + expect(mocks.storage.unlinkDir).toHaveBeenCalledWith(expect.stringContaining('data/library/admin'), options); }); }); diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts index 76cf71d34d..066996ead5 100644 --- a/server/test/fixtures/asset.stub.ts +++ b/server/test/fixtures/asset.stub.ts @@ -65,7 +65,7 @@ export const assetStub = { owner: userStub.user1, ownerId: 'user-id', deviceId: 'device-id', - originalPath: 'upload/library/IMG_123.jpg', + originalPath: '/data/library/IMG_123.jpg', files: [thumbnailFile], checksum: Buffer.from('file hash', 'utf8'), type: AssetType.Image, @@ -101,7 +101,7 @@ export const assetStub = { owner: userStub.user1, ownerId: 'user-id', deviceId: 'device-id', - originalPath: 'upload/library/IMG_456.jpg', + originalPath: '/data/library/IMG_456.jpg', files: [previewFile], checksum: Buffer.from('file hash', 'utf8'), type: AssetType.Image, diff --git a/server/test/medium/specs/services/storage.service.spec.ts b/server/test/medium/specs/services/storage.service.spec.ts new file mode 100644 index 0000000000..6c3fc487d4 --- /dev/null +++ b/server/test/medium/specs/services/storage.service.spec.ts @@ -0,0 +1,46 @@ +import { Kysely } from 'kysely'; +import { AssetRepository } from 'src/repositories/asset.repository'; +import { ConfigRepository } from 'src/repositories/config.repository'; +import { DatabaseRepository } from 'src/repositories/database.repository'; +import { LoggingRepository } from 'src/repositories/logging.repository'; +import { StorageRepository } from 'src/repositories/storage.repository'; +import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository'; +import { DB } from 'src/schema'; +import { StorageService } from 'src/services/storage.service'; +import { newMediumService } from 'test/medium.factory'; +import { mockEnvData } from 'test/repositories/config.repository.mock'; +import { getKyselyDB } from 'test/utils'; + +let defaultDatabase: Kysely; + +const setup = (db?: Kysely) => { + return newMediumService(StorageService, { + database: db || defaultDatabase, + real: [AssetRepository, DatabaseRepository, SystemMetadataRepository], + mock: [StorageRepository, ConfigRepository, LoggingRepository], + }); +}; + +beforeAll(async () => { + defaultDatabase = await getKyselyDB(); +}); + +describe(StorageService.name, () => { + describe('onBoostrap', () => { + it('should work', async () => { + const { sut, ctx } = setup(); + + const configMock = ctx.getMock(ConfigRepository); + configMock.getEnv.mockReturnValue(mockEnvData({})); + + const storageMock = ctx.getMock(StorageRepository); + storageMock.mkdirSync.mockReturnValue(void 0); + storageMock.existsSync.mockReturnValue(true); + storageMock.createFile.mockResolvedValue(void 0); + storageMock.overwriteFile.mockResolvedValue(void 0); + storageMock.readFile.mockResolvedValue(Buffer.from('test content')); + + await expect(sut.onBootstrap()).resolves.toBeUndefined(); + }); + }); +}); diff --git a/server/test/repositories/storage.repository.mock.ts b/server/test/repositories/storage.repository.mock.ts index dc3fb3e16c..9752a39441 100644 --- a/server/test/repositories/storage.repository.mock.ts +++ b/server/test/repositories/storage.repository.mock.ts @@ -41,10 +41,9 @@ export const makeMockWatcher = return () => Promise.resolve(); }; -export const newStorageRepositoryMock = (reset = true): Mocked> => { - if (reset) { - StorageCore.reset(); - } +export const newStorageRepositoryMock = (): Mocked> => { + StorageCore.reset(); + StorageCore.setMediaLocation('/data'); return { createZipStream: vitest.fn(), @@ -53,6 +52,7 @@ export const newStorageRepositoryMock = (reset = true): Mocked = {}) => ({ livePhotoVideoId: null, localDateTime: newDate(), originalFileName: 'IMG_123.jpg', - originalPath: `upload/12/34/IMG_123.jpg`, + originalPath: `/data/12/34/IMG_123.jpg`, ownerId: newUuid(), sidecarPath: null, stackId: null, diff --git a/web/src/lib/utils/asset-utils.spec.ts b/web/src/lib/utils/asset-utils.spec.ts index b0f76f3bc2..52517975e2 100644 --- a/web/src/lib/utils/asset-utils.spec.ts +++ b/web/src/lib/utils/asset-utils.spec.ts @@ -33,21 +33,21 @@ describe('get asset filename', () => { { asset: { originalFileName: 'filename', - originalPath: 'upload/library/test/2016/2016-08-30/filename.jpg', + originalPath: '/data/library/test/2016/2016-08-30/filename.jpg', }, result: 'filename.jpg', }, { asset: { originalFileName: 'new-filename', - originalPath: 'upload/library/89d14e47-a40d-4cae-a347-a914cdef1f22/2016/2016-08-30/filename.jpg', + originalPath: '/data/library/89d14e47-a40d-4cae-a347-a914cdef1f22/2016/2016-08-30/filename.jpg', }, result: 'new-filename.jpg', }, { asset: { originalFileName: 'new-filename.txt', - originalPath: 'upload/library/test/2016/2016-08-30/filename.txt.jpg', + originalPath: '/data/library/test/2016/2016-08-30/filename.txt.jpg', }, result: 'new-filename.txt.jpg', },