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 { AssetEntity, ExifEntity } from '@app/infra/entities';
|
||||||
import { OptionalBetween } from '@app/infra/infra.utils';
|
import { OptionalBetween } from '@app/infra/infra.utils';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
@ -22,8 +21,6 @@ export interface AssetOwnerCheck extends AssetCheck {
|
||||||
|
|
||||||
export interface IAssetRepositoryV1 {
|
export interface IAssetRepositoryV1 {
|
||||||
get(id: string): Promise<AssetEntity | null>;
|
get(id: string): Promise<AssetEntity | null>;
|
||||||
create(asset: AssetCreate): Promise<AssetEntity>;
|
|
||||||
upsertExif(exif: Partial<ExifEntity>): Promise<void>;
|
|
||||||
getAllByUserId(userId: string, dto: AssetSearchDto): Promise<AssetEntity[]>;
|
getAllByUserId(userId: string, dto: AssetSearchDto): Promise<AssetEntity[]>;
|
||||||
getLocationsByUserId(userId: string): Promise<CuratedLocationsResponseDto[]>;
|
getLocationsByUserId(userId: string): Promise<CuratedLocationsResponseDto[]>;
|
||||||
getDetectedObjectsByUserId(userId: string): Promise<CuratedObjectsResponseDto[]>;
|
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
|
* Get assets by checksums on the database
|
||||||
* @param ownerId
|
* @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(() => {
|
beforeEach(() => {
|
||||||
assetRepositoryMockV1 = {
|
assetRepositoryMockV1 = {
|
||||||
get: jest.fn(),
|
get: jest.fn(),
|
||||||
create: jest.fn(),
|
|
||||||
upsertExif: jest.fn(),
|
|
||||||
|
|
||||||
getAllByUserId: jest.fn(),
|
getAllByUserId: jest.fn(),
|
||||||
getDetectedObjectsByUserId: jest.fn(),
|
getDetectedObjectsByUserId: jest.fn(),
|
||||||
getLocationsByUserId: jest.fn(),
|
getLocationsByUserId: jest.fn(),
|
||||||
|
@ -109,12 +106,12 @@ describe('AssetService', () => {
|
||||||
};
|
};
|
||||||
const dto = _getCreateAssetDto();
|
const dto = _getCreateAssetDto();
|
||||||
|
|
||||||
assetRepositoryMockV1.create.mockResolvedValue(assetEntity);
|
assetMock.create.mockResolvedValue(assetEntity);
|
||||||
accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([dto.libraryId!]));
|
accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([dto.libraryId!]));
|
||||||
|
|
||||||
await expect(sut.uploadFile(authStub.user1, dto, file)).resolves.toEqual({ duplicate: false, id: 'id_1' });
|
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);
|
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'));
|
const error = new QueryFailedError('', [], new Error('unique key violation'));
|
||||||
(error as any).constraint = ASSET_CHECKSUM_CONSTRAINT;
|
(error as any).constraint = ASSET_CHECKSUM_CONSTRAINT;
|
||||||
|
|
||||||
assetRepositoryMockV1.create.mockRejectedValue(error);
|
assetMock.create.mockRejectedValue(error);
|
||||||
assetRepositoryMockV1.getAssetsByChecksums.mockResolvedValue([_getAsset_1()]);
|
assetRepositoryMockV1.getAssetsByChecksums.mockResolvedValue([_getAsset_1()]);
|
||||||
accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([dto.libraryId!]));
|
accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([dto.libraryId!]));
|
||||||
|
|
||||||
|
@ -149,8 +146,8 @@ describe('AssetService', () => {
|
||||||
const error = new QueryFailedError('', [], new Error('unique key violation'));
|
const error = new QueryFailedError('', [], new Error('unique key violation'));
|
||||||
(error as any).constraint = ASSET_CHECKSUM_CONSTRAINT;
|
(error as any).constraint = ASSET_CHECKSUM_CONSTRAINT;
|
||||||
|
|
||||||
assetRepositoryMockV1.create.mockResolvedValueOnce(assetStub.livePhotoMotionAsset);
|
assetMock.create.mockResolvedValueOnce(assetStub.livePhotoMotionAsset);
|
||||||
assetRepositoryMockV1.create.mockResolvedValueOnce(assetStub.livePhotoStillAsset);
|
assetMock.create.mockResolvedValueOnce(assetStub.livePhotoStillAsset);
|
||||||
accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([dto.libraryId!]));
|
accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([dto.libraryId!]));
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
|
|
|
@ -18,10 +18,16 @@ import {
|
||||||
} from '@app/domain';
|
} from '@app/domain';
|
||||||
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType, LibraryType } from '@app/infra/entities';
|
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType, LibraryType } from '@app/infra/entities';
|
||||||
import { ImmichLogger } from '@app/infra/logger';
|
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 { QueryFailedError } from 'typeorm';
|
||||||
import { IAssetRepositoryV1 } from './asset-repository';
|
import { IAssetRepositoryV1 } from './asset-repository';
|
||||||
import { AssetCore } from './asset.core';
|
|
||||||
import { AssetBulkUploadCheckDto } from './dto/asset-check.dto';
|
import { AssetBulkUploadCheckDto } from './dto/asset-check.dto';
|
||||||
import { AssetSearchDto } from './dto/asset-search.dto';
|
import { AssetSearchDto } from './dto/asset-search.dto';
|
||||||
import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
|
import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
|
||||||
|
@ -41,7 +47,6 @@ import { CuratedObjectsResponseDto } from './response-dto/curated-objects-respon
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AssetService {
|
export class AssetService {
|
||||||
readonly logger = new ImmichLogger(AssetService.name);
|
readonly logger = new ImmichLogger(AssetService.name);
|
||||||
private assetCore: AssetCore;
|
|
||||||
private access: AccessCore;
|
private access: AccessCore;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -52,7 +57,6 @@ export class AssetService {
|
||||||
@Inject(ILibraryRepository) private libraryRepository: ILibraryRepository,
|
@Inject(ILibraryRepository) private libraryRepository: ILibraryRepository,
|
||||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||||
) {
|
) {
|
||||||
this.assetCore = new AssetCore(assetRepositoryV1, jobRepository);
|
|
||||||
this.access = AccessCore.create(accessRepository);
|
this.access = AccessCore.create(accessRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,19 +79,13 @@ export class AssetService {
|
||||||
try {
|
try {
|
||||||
const libraryId = await this.getLibraryId(auth, dto.libraryId);
|
const libraryId = await this.getLibraryId(auth, dto.libraryId);
|
||||||
await this.access.requirePermission(auth, Permission.ASSET_UPLOAD, libraryId);
|
await this.access.requirePermission(auth, Permission.ASSET_UPLOAD, libraryId);
|
||||||
AssetCore.requireQuota(auth, file.size);
|
this.requireQuota(auth, file.size);
|
||||||
if (livePhotoFile) {
|
if (livePhotoFile) {
|
||||||
const livePhotoDto = { ...dto, assetType: AssetType.VIDEO, isVisible: false, libraryId };
|
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(
|
const asset = await this.create(auth, { ...dto, libraryId }, file, livePhotoAsset?.id, sidecarFile?.originalPath);
|
||||||
auth,
|
|
||||||
{ ...dto, libraryId },
|
|
||||||
file,
|
|
||||||
livePhotoAsset?.id,
|
|
||||||
sidecarFile?.originalPath,
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.userRepository.updateUsage(auth.user.id, (livePhotoFile?.size || 0) + file.size);
|
await this.userRepository.updateUsage(auth.user.id, (livePhotoFile?.size || 0) + file.size);
|
||||||
|
|
||||||
|
@ -317,4 +315,58 @@ export class AssetService {
|
||||||
|
|
||||||
return library.id;
|
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