diff --git a/server/src/immich/api-v1/asset/asset-repository.ts b/server/src/immich/api-v1/asset/asset-repository.ts index c8b76d9674..7d55fa790e 100644 --- a/server/src/immich/api-v1/asset/asset-repository.ts +++ b/server/src/immich/api-v1/asset/asset-repository.ts @@ -1,4 +1,3 @@ -import { AssetCreate } from '@app/domain'; import { AssetEntity, ExifEntity } from '@app/infra/entities'; import { OptionalBetween } from '@app/infra/infra.utils'; import { Injectable } from '@nestjs/common'; @@ -22,8 +21,6 @@ export interface AssetOwnerCheck extends AssetCheck { export interface IAssetRepositoryV1 { get(id: string): Promise; - create(asset: AssetCreate): Promise; - upsertExif(exif: Partial): Promise; getAllByUserId(userId: string, dto: AssetSearchDto): Promise; getLocationsByUserId(userId: string): Promise; getDetectedObjectsByUserId(userId: string): Promise; @@ -132,14 +129,6 @@ export class AssetRepositoryV1 implements IAssetRepositoryV1 { }); } - create(asset: AssetCreate): Promise { - return this.assetRepository.save(asset); - } - - async upsertExif(exif: Partial): Promise { - await this.exifRepository.upsert(exif, { conflictPaths: ['assetId'] }); - } - /** * Get assets by checksums on the database * @param ownerId diff --git a/server/src/immich/api-v1/asset/asset.core.ts b/server/src/immich/api-v1/asset/asset.core.ts deleted file mode 100644 index 0688a65dd6..0000000000 --- a/server/src/immich/api-v1/asset/asset.core.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { AuthDto, IJobRepository, JobName, mimeTypes, UploadFile } from '@app/domain'; -import { AssetEntity } from '@app/infra/entities'; -import { BadRequestException } from '@nestjs/common'; -import { parse } from 'node:path'; -import { IAssetRepositoryV1 } from './asset-repository'; -import { CreateAssetDto } from './dto/create-asset.dto'; - -export class AssetCore { - constructor( - private repository: IAssetRepositoryV1, - private jobRepository: IJobRepository, - ) {} - - async create( - auth: AuthDto, - dto: CreateAssetDto & { libraryId: string }, - file: UploadFile, - livePhotoAssetId?: string, - sidecarPath?: string, - ): Promise { - const asset = await this.repository.create({ - ownerId: auth.user.id, - libraryId: dto.libraryId, - - checksum: file.checksum, - originalPath: file.originalPath, - - deviceAssetId: dto.deviceAssetId, - deviceId: dto.deviceId, - - fileCreatedAt: dto.fileCreatedAt, - fileModifiedAt: dto.fileModifiedAt, - localDateTime: dto.fileCreatedAt, - deletedAt: null, - - type: mimeTypes.assetType(file.originalPath), - isFavorite: dto.isFavorite, - isArchived: dto.isArchived ?? false, - duration: dto.duration || null, - isVisible: dto.isVisible ?? true, - livePhotoVideo: livePhotoAssetId === null ? null : ({ id: livePhotoAssetId } as AssetEntity), - resizePath: null, - webpPath: null, - thumbhash: null, - encodedVideoPath: null, - tags: [], - sharedLinks: [], - originalFileName: parse(file.originalName).name, - faces: [], - sidecarPath: sidecarPath || null, - isReadOnly: dto.isReadOnly ?? false, - isExternal: dto.isExternal ?? false, - isOffline: dto.isOffline ?? false, - }); - - await this.repository.upsertExif({ assetId: asset.id, fileSizeInByte: file.size }); - await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: { id: asset.id, source: 'upload' } }); - - return asset; - } - - static requireQuota(auth: AuthDto, size: number) { - if (auth.user.quotaSizeInBytes && auth.user.quotaSizeInBytes < auth.user.quotaUsageInBytes + size) { - throw new BadRequestException('Quota has been exceeded!'); - } - } -} diff --git a/server/src/immich/api-v1/asset/asset.service.spec.ts b/server/src/immich/api-v1/asset/asset.service.spec.ts index d5fde4a625..0e5bafa5f0 100644 --- a/server/src/immich/api-v1/asset/asset.service.spec.ts +++ b/server/src/immich/api-v1/asset/asset.service.spec.ts @@ -68,9 +68,6 @@ describe('AssetService', () => { beforeEach(() => { assetRepositoryMockV1 = { get: jest.fn(), - create: jest.fn(), - upsertExif: jest.fn(), - getAllByUserId: jest.fn(), getDetectedObjectsByUserId: jest.fn(), getLocationsByUserId: jest.fn(), @@ -109,12 +106,12 @@ describe('AssetService', () => { }; const dto = _getCreateAssetDto(); - assetRepositoryMockV1.create.mockResolvedValue(assetEntity); + assetMock.create.mockResolvedValue(assetEntity); accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([dto.libraryId!])); await expect(sut.uploadFile(authStub.user1, dto, file)).resolves.toEqual({ duplicate: false, id: 'id_1' }); - expect(assetRepositoryMockV1.create).toHaveBeenCalled(); + expect(assetMock.create).toHaveBeenCalled(); expect(userMock.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, file.size); }); @@ -131,7 +128,7 @@ describe('AssetService', () => { const error = new QueryFailedError('', [], new Error('unique key violation')); (error as any).constraint = ASSET_CHECKSUM_CONSTRAINT; - assetRepositoryMockV1.create.mockRejectedValue(error); + assetMock.create.mockRejectedValue(error); assetRepositoryMockV1.getAssetsByChecksums.mockResolvedValue([_getAsset_1()]); accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([dto.libraryId!])); @@ -149,8 +146,8 @@ describe('AssetService', () => { const error = new QueryFailedError('', [], new Error('unique key violation')); (error as any).constraint = ASSET_CHECKSUM_CONSTRAINT; - assetRepositoryMockV1.create.mockResolvedValueOnce(assetStub.livePhotoMotionAsset); - assetRepositoryMockV1.create.mockResolvedValueOnce(assetStub.livePhotoStillAsset); + assetMock.create.mockResolvedValueOnce(assetStub.livePhotoMotionAsset); + assetMock.create.mockResolvedValueOnce(assetStub.livePhotoStillAsset); accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([dto.libraryId!])); await expect( diff --git a/server/src/immich/api-v1/asset/asset.service.ts b/server/src/immich/api-v1/asset/asset.service.ts index 6d59647cbf..6a96bf531b 100644 --- a/server/src/immich/api-v1/asset/asset.service.ts +++ b/server/src/immich/api-v1/asset/asset.service.ts @@ -18,10 +18,16 @@ import { } from '@app/domain'; import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType, LibraryType } from '@app/infra/entities'; import { ImmichLogger } from '@app/infra/logger'; -import { Inject, Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common'; +import { + BadRequestException, + Inject, + Injectable, + InternalServerErrorException, + NotFoundException, +} from '@nestjs/common'; +import { parse } from 'node:path'; import { QueryFailedError } from 'typeorm'; import { IAssetRepositoryV1 } from './asset-repository'; -import { AssetCore } from './asset.core'; import { AssetBulkUploadCheckDto } from './dto/asset-check.dto'; import { AssetSearchDto } from './dto/asset-search.dto'; import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto'; @@ -41,7 +47,6 @@ import { CuratedObjectsResponseDto } from './response-dto/curated-objects-respon @Injectable() export class AssetService { readonly logger = new ImmichLogger(AssetService.name); - private assetCore: AssetCore; private access: AccessCore; constructor( @@ -52,7 +57,6 @@ export class AssetService { @Inject(ILibraryRepository) private libraryRepository: ILibraryRepository, @Inject(IUserRepository) private userRepository: IUserRepository, ) { - this.assetCore = new AssetCore(assetRepositoryV1, jobRepository); this.access = AccessCore.create(accessRepository); } @@ -75,19 +79,13 @@ export class AssetService { try { const libraryId = await this.getLibraryId(auth, dto.libraryId); await this.access.requirePermission(auth, Permission.ASSET_UPLOAD, libraryId); - AssetCore.requireQuota(auth, file.size); + this.requireQuota(auth, file.size); if (livePhotoFile) { const livePhotoDto = { ...dto, assetType: AssetType.VIDEO, isVisible: false, libraryId }; - livePhotoAsset = await this.assetCore.create(auth, livePhotoDto, livePhotoFile); + livePhotoAsset = await this.create(auth, livePhotoDto, livePhotoFile); } - const asset = await this.assetCore.create( - auth, - { ...dto, libraryId }, - file, - livePhotoAsset?.id, - sidecarFile?.originalPath, - ); + const asset = await this.create(auth, { ...dto, libraryId }, file, livePhotoAsset?.id, sidecarFile?.originalPath); await this.userRepository.updateUsage(auth.user.id, (livePhotoFile?.size || 0) + file.size); @@ -317,4 +315,58 @@ export class AssetService { return library.id; } + + private async create( + auth: AuthDto, + dto: CreateAssetDto & { libraryId: string }, + file: UploadFile, + livePhotoAssetId?: string, + sidecarPath?: string, + ): Promise { + const asset = await this.assetRepository.create({ + ownerId: auth.user.id, + libraryId: dto.libraryId, + + checksum: file.checksum, + originalPath: file.originalPath, + + deviceAssetId: dto.deviceAssetId, + deviceId: dto.deviceId, + + fileCreatedAt: dto.fileCreatedAt, + fileModifiedAt: dto.fileModifiedAt, + localDateTime: dto.fileCreatedAt, + deletedAt: null, + + type: mimeTypes.assetType(file.originalPath), + isFavorite: dto.isFavorite, + isArchived: dto.isArchived ?? false, + duration: dto.duration || null, + isVisible: dto.isVisible ?? true, + livePhotoVideo: livePhotoAssetId === null ? null : ({ id: livePhotoAssetId } as AssetEntity), + resizePath: null, + webpPath: null, + thumbhash: null, + encodedVideoPath: null, + tags: [], + sharedLinks: [], + originalFileName: parse(file.originalName).name, + faces: [], + sidecarPath: sidecarPath || null, + isReadOnly: dto.isReadOnly ?? false, + isExternal: dto.isExternal ?? false, + isOffline: dto.isOffline ?? false, + }); + + await this.assetRepository.upsertExif({ assetId: asset.id, fileSizeInByte: file.size }); + await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: { id: asset.id, source: 'upload' } }); + + return asset; + } + + private requireQuota(auth: AuthDto, size: number) { + if (auth.user.quotaSizeInBytes && auth.user.quotaSizeInBytes < auth.user.quotaUsageInBytes + size) { + throw new BadRequestException('Quota has been exceeded!'); + } + } }