1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2024-12-28 22:51:59 +00:00

refactor(server): asset core (#6985)

refactor: asset core
This commit is contained in:
Jason Rasmussen 2024-02-08 16:56:06 -05:00 committed by GitHub
parent 5088acda10
commit bd1fa9377b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 70 additions and 99 deletions

View file

@ -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<AssetEntity | null>;
create(asset: AssetCreate): Promise<AssetEntity>;
upsertExif(exif: Partial<ExifEntity>): Promise<void>;
getAllByUserId(userId: string, dto: AssetSearchDto): Promise<AssetEntity[]>;
getLocationsByUserId(userId: string): Promise<CuratedLocationsResponseDto[]>;
getDetectedObjectsByUserId(userId: string): Promise<CuratedObjectsResponseDto[]>;
@ -132,14 +129,6 @@ export class AssetRepositoryV1 implements IAssetRepositoryV1 {
});
}
create(asset: AssetCreate): Promise<AssetEntity> {
return this.assetRepository.save(asset);
}
async upsertExif(exif: Partial<ExifEntity>): Promise<void> {
await this.exifRepository.upsert(exif, { conflictPaths: ['assetId'] });
}
/**
* Get assets by checksums on the database
* @param ownerId

View file

@ -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<AssetEntity> {
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!');
}
}
}

View file

@ -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(

View file

@ -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<AssetEntity> {
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!');
}
}
}