From 79ca7ac4448e82d19a7057f560f05ccb07d0c29a Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 27 May 2026 10:00:55 -0400 Subject: [PATCH] refactor: asset create event --- server/src/repositories/event.repository.ts | 4 +- server/src/services/asset-media.service.ts | 141 +++++++++----------- server/src/services/user.service.ts | 8 +- 3 files changed, 75 insertions(+), 78 deletions(-) diff --git a/server/src/repositories/event.repository.ts b/server/src/repositories/event.repository.ts index 713828cd95..fa92a6b0b7 100644 --- a/server/src/repositories/event.repository.ts +++ b/server/src/repositories/event.repository.ts @@ -9,7 +9,7 @@ import { AuthDto } from 'src/dtos/auth.dto'; import { ImmichWorker, JobStatus, MetadataKey, QueueName, UserAvatarColor, UserStatus } from 'src/enum'; import { ConfigRepository } from 'src/repositories/config.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; -import { JobItem, JobSource } from 'src/types'; +import { JobItem, JobSource, UploadFile } from 'src/types'; type EmitHandlers = Partial<{ [T in EmitEvent]: Array> }>; @@ -42,7 +42,7 @@ type EventMap = { AlbumInvite: [{ id: string; userId: string; senderName: string }]; // asset events - AssetCreate: [{ asset: Asset }]; + AssetCreate: [{ asset: Asset; file: UploadFile }]; AssetTag: [{ assetId: string }]; AssetUntag: [{ assetId: string }]; AssetHide: [{ assetId: string; userId: string }]; diff --git a/server/src/services/asset-media.service.ts b/server/src/services/asset-media.service.ts index 6b0d73b77b..e82ed29ac7 100644 --- a/server/src/services/asset-media.service.ts +++ b/server/src/services/asset-media.service.ts @@ -146,17 +146,79 @@ export class AssetMediaService extends BaseService { { userId: auth.user.id, livePhotoVideoId: dto.livePhotoVideoId }, ); } - const asset = await this.create(auth.user.id, dto, file, sidecarFile); + + const asset = await this.assetRepository.create({ + ownerId: auth.user.id, + libraryId: null, + + checksum: file.checksum, + checksumAlgorithm: ChecksumAlgorithm.sha1File, + originalPath: file.originalPath, + + fileCreatedAt: dto.fileCreatedAt, + fileModifiedAt: dto.fileModifiedAt, + localDateTime: dto.fileCreatedAt, + + type: mimeTypes.assetType(file.originalPath), + isFavorite: dto.isFavorite, + duration: dto.duration || null, + visibility: dto.visibility ?? AssetVisibility.Timeline, + livePhotoVideoId: dto.livePhotoVideoId, + originalFileName: dto.filename || file.originalName, + }); + + if (dto.metadata?.length) { + await this.assetRepository.upsertMetadata(asset.id, dto.metadata); + } + + if (sidecarFile) { + await this.assetRepository.upsertFile({ + assetId: asset.id, + path: sidecarFile.originalPath, + type: AssetFileType.Sidecar, + }); + await this.storageRepository.utimes(sidecarFile.originalPath, new Date(), new Date(dto.fileModifiedAt)); + } + await this.storageRepository.utimes(file.originalPath, new Date(), new Date(dto.fileModifiedAt)); + await this.assetRepository.upsertExif({ + exif: { assetId: asset.id, fileSizeInByte: file.size }, + lockedPropertiesBehavior: 'override', + }); + + await this.jobRepository.queue({ name: JobName.AssetExtractMetadata, data: { id: asset.id, source: 'upload' } }); if (auth.sharedLink) { await this.addToSharedLink(auth.sharedLink, asset.id); } - await this.userRepository.updateUsage(auth.user.id, file.size); + await this.eventRepository.emit('AssetCreate', { asset, file }); return { id: asset.id, status: AssetMediaStatus.CREATED }; } catch (error: any) { - return this.handleUploadError(error, auth, file, sidecarFile); + // clean up files + await this.jobRepository.queue({ + name: JobName.FileDelete, + data: { files: [file.originalPath, sidecarFile?.originalPath] }, + }); + + // handle duplicates with a success response + if (isAssetChecksumConstraint(error)) { + const duplicateId = await this.assetRepository.getUploadAssetIdByChecksum(auth.user.id, file.checksum); + if (!duplicateId) { + this.logger.error(`Error locating duplicate for checksum constraint`); + throw new InternalServerErrorException(); + } + + if (auth.sharedLink) { + await this.addToSharedLink(auth.sharedLink, duplicateId); + } + + this.logger.debug(`Duplicate asset upload rejected: existing asset ${duplicateId}`); + return { status: AssetMediaStatus.DUPLICATE, id: duplicateId }; + } + + this.logger.error(`Error uploading file ${error}`, error?.stack); + throw error; } } @@ -290,78 +352,7 @@ export class AssetMediaService extends BaseService { auth: AuthDto, file: UploadFile, sidecarFile?: UploadFile, - ): Promise { - // clean up files - await this.jobRepository.queue({ - name: JobName.FileDelete, - data: { files: [file.originalPath, sidecarFile?.originalPath] }, - }); - - // handle duplicates with a success response - if (isAssetChecksumConstraint(error)) { - const duplicateId = await this.assetRepository.getUploadAssetIdByChecksum(auth.user.id, file.checksum); - if (!duplicateId) { - this.logger.error(`Error locating duplicate for checksum constraint`); - throw new InternalServerErrorException(); - } - - if (auth.sharedLink) { - await this.addToSharedLink(auth.sharedLink, duplicateId); - } - - this.logger.debug(`Duplicate asset upload rejected: existing asset ${duplicateId}`); - return { status: AssetMediaStatus.DUPLICATE, id: duplicateId }; - } - - this.logger.error(`Error uploading file ${error}`, error?.stack); - throw error; - } - - private async create(ownerId: string, dto: AssetMediaCreateDto, file: UploadFile, sidecarFile?: UploadFile) { - const asset = await this.assetRepository.create({ - ownerId, - libraryId: null, - - checksum: file.checksum, - checksumAlgorithm: ChecksumAlgorithm.sha1File, - originalPath: file.originalPath, - - fileCreatedAt: dto.fileCreatedAt, - fileModifiedAt: dto.fileModifiedAt, - localDateTime: dto.fileCreatedAt, - - type: mimeTypes.assetType(file.originalPath), - isFavorite: dto.isFavorite, - duration: dto.duration || null, - visibility: dto.visibility ?? AssetVisibility.Timeline, - livePhotoVideoId: dto.livePhotoVideoId, - originalFileName: dto.filename || file.originalName, - }); - - if (dto.metadata?.length) { - await this.assetRepository.upsertMetadata(asset.id, dto.metadata); - } - - if (sidecarFile) { - await this.assetRepository.upsertFile({ - assetId: asset.id, - path: sidecarFile.originalPath, - type: AssetFileType.Sidecar, - }); - await this.storageRepository.utimes(sidecarFile.originalPath, new Date(), new Date(dto.fileModifiedAt)); - } - await this.storageRepository.utimes(file.originalPath, new Date(), new Date(dto.fileModifiedAt)); - await this.assetRepository.upsertExif({ - exif: { assetId: asset.id, fileSizeInByte: file.size }, - lockedPropertiesBehavior: 'override', - }); - - await this.eventRepository.emit('AssetCreate', { asset }); - - await this.jobRepository.queue({ name: JobName.AssetExtractMetadata, data: { id: asset.id, source: 'upload' } }); - - return asset; - } + ): Promise {} private requireQuota(auth: AuthDto, size: number) { if (auth.user.quotaSizeInBytes !== null && auth.user.quotaSizeInBytes < auth.user.quotaUsageInBytes + size) { diff --git a/server/src/services/user.service.ts b/server/src/services/user.service.ts index 82ab90a590..27084eb3b4 100644 --- a/server/src/services/user.service.ts +++ b/server/src/services/user.service.ts @@ -3,7 +3,7 @@ import { Updateable } from 'kysely'; import { DateTime } from 'luxon'; import { SALT_ROUNDS } from 'src/constants'; import { StorageCore } from 'src/cores/storage.core'; -import { OnJob } from 'src/decorators'; +import { OnEvent, OnJob } from 'src/decorators'; import { AuthDto } from 'src/dtos/auth.dto'; import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto'; import { OnboardingDto, OnboardingResponseDto } from 'src/dtos/onboarding.dto'; @@ -11,6 +11,7 @@ import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } import { CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto'; import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto, mapUser, mapUserAdmin } from 'src/dtos/user.dto'; import { CacheControl, JobName, JobStatus, QueueName, StorageFolder, UserMetadataKey } from 'src/enum'; +import { ArgOf } from 'src/repositories/event.repository'; import { UserFindOptions } from 'src/repositories/user.repository'; import { UserTable } from 'src/schema/tables/user.table'; import { BaseService } from 'src/services/base.service'; @@ -230,6 +231,11 @@ export class UserService extends BaseService { }; } + @OnEvent({ name: 'AssetCreate' }) + async onAssetCreate({ asset, file }: ArgOf<'AssetCreate'>) { + await this.userRepository.updateUsage(asset.ownerId, file.size); + } + @OnJob({ name: JobName.UserSyncUsage, queue: QueueName.BackgroundTask }) async handleUserSyncUsage(): Promise { await this.userRepository.syncUsage();