mirror of
https://github.com/immich-app/immich.git
synced 2024-12-29 15:11:58 +00:00
parent
5088acda10
commit
bd1fa9377b
4 changed files with 70 additions and 99 deletions
|
@ -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
|
||||
|
|
|
@ -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!');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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!');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue