refactor(server): move asset detail endpoint to new controller (#6636)

* refactor(server): move asset by id to new controller

* chore: open api

* refactor: more consolidation

* refactor: asset service
This commit is contained in:
Jason Rasmussen
2024-01-25 12:52:21 -05:00
committed by GitHub
parent 19d4c5e9f7
commit b306cf564e
20 changed files with 541 additions and 174 deletions

View File

@@ -8,7 +8,6 @@ import {
newAccessRepositoryMock,
newAssetRepositoryMock,
newCommunicationRepositoryMock,
newCryptoRepositoryMock,
newJobRepositoryMock,
newPartnerRepositoryMock,
newStorageRepositoryMock,
@@ -24,7 +23,6 @@ import {
ClientEvent,
IAssetRepository,
ICommunicationRepository,
ICryptoRepository,
IJobRepository,
IPartnerRepository,
IStorageRepository,
@@ -168,7 +166,6 @@ describe(AssetService.name, () => {
let sut: AssetService;
let accessMock: IAccessRepositoryMock;
let assetMock: jest.Mocked<IAssetRepository>;
let cryptoMock: jest.Mocked<ICryptoRepository>;
let jobMock: jest.Mocked<IJobRepository>;
let storageMock: jest.Mocked<IStorageRepository>;
let userMock: jest.Mocked<IUserRepository>;
@@ -184,7 +181,6 @@ describe(AssetService.name, () => {
accessMock = newAccessRepositoryMock();
assetMock = newAssetRepositoryMock();
communicationMock = newCommunicationRepositoryMock();
cryptoMock = newCryptoRepositoryMock();
jobMock = newJobRepositoryMock();
storageMock = newStorageRepositoryMock();
userMock = newUserRepositoryMock();
@@ -194,7 +190,6 @@ describe(AssetService.name, () => {
sut = new AssetService(
accessMock,
assetMock,
cryptoMock,
jobMock,
configMock,
storageMock,
@@ -657,6 +652,59 @@ describe(AssetService.name, () => {
});
});
describe('get', () => {
it('should allow owner access', async () => {
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
assetMock.getById.mockResolvedValue(assetStub.image);
await sut.get(authStub.admin, assetStub.image.id);
expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(
authStub.admin.user.id,
new Set([assetStub.image.id]),
);
});
it('should allow shared link access', async () => {
accessMock.asset.checkSharedLinkAccess.mockResolvedValue(new Set([assetStub.image.id]));
assetMock.getById.mockResolvedValue(assetStub.image);
await sut.get(authStub.adminSharedLink, assetStub.image.id);
expect(accessMock.asset.checkSharedLinkAccess).toHaveBeenCalledWith(
authStub.adminSharedLink.sharedLink?.id,
new Set([assetStub.image.id]),
);
});
it('should allow partner sharing access', async () => {
accessMock.asset.checkPartnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
assetMock.getById.mockResolvedValue(assetStub.image);
await sut.get(authStub.admin, assetStub.image.id);
expect(accessMock.asset.checkPartnerAccess).toHaveBeenCalledWith(
authStub.admin.user.id,
new Set([assetStub.image.id]),
);
});
it('should allow shared album access', async () => {
accessMock.asset.checkAlbumAccess.mockResolvedValue(new Set([assetStub.image.id]));
assetMock.getById.mockResolvedValue(assetStub.image);
await sut.get(authStub.admin, assetStub.image.id);
expect(accessMock.asset.checkAlbumAccess).toHaveBeenCalledWith(
authStub.admin.user.id,
new Set([assetStub.image.id]),
);
});
it('should throw an error for no access', async () => {
await expect(sut.get(authStub.admin, assetStub.image.id)).rejects.toBeInstanceOf(BadRequestException);
expect(assetMock.getById).not.toHaveBeenCalled();
});
it('should throw an error for an invalid shared link', async () => {
await expect(sut.get(authStub.adminSharedLink, assetStub.image.id)).rejects.toBeInstanceOf(BadRequestException);
expect(accessMock.asset.checkOwnerAccess).not.toHaveBeenCalled();
expect(assetMock.getById).not.toHaveBeenCalled();
});
});
describe('update', () => {
it('should require asset write access for the id', async () => {
await expect(sut.update(authStub.admin, 'asset-1', { isArchived: false })).rejects.toBeInstanceOf(

View File

@@ -15,7 +15,6 @@ import {
IAccessRepository,
IAssetRepository,
ICommunicationRepository,
ICryptoRepository,
IJobRepository,
IPartnerRepository,
IStorageRepository,
@@ -87,7 +86,6 @@ export class AssetService {
constructor(
@Inject(IAccessRepository) accessRepository: IAccessRepository,
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository,
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
@@ -400,6 +398,44 @@ export class AssetService {
return this.assetRepository.getAllByDeviceId(auth.user.id, deviceId);
}
async get(auth: AuthDto, id: string): Promise<AssetResponseDto | SanitizedAssetResponseDto> {
await this.access.requirePermission(auth, Permission.ASSET_READ, id);
const asset = await this.assetRepository.getById(id, {
exifInfo: true,
tags: true,
sharedLinks: true,
smartInfo: true,
owner: true,
faces: {
person: true,
},
stack: {
exifInfo: true,
},
});
if (!asset) {
throw new BadRequestException('Asset not found');
}
if (auth.sharedLink && !auth.sharedLink.showExif) {
return mapAsset(asset, { stripMetadata: true, withStack: true });
}
const data = mapAsset(asset, { withStack: true });
if (auth.sharedLink) {
delete data.owner;
}
if (data.ownerId !== auth.user.id) {
data.people = [];
}
return data;
}
async update(auth: AuthDto, id: string, dto: UpdateAssetDto): Promise<AssetResponseDto> {
await this.access.requirePermission(auth, Permission.ASSET_UPDATE, id);

View File

@@ -20,12 +20,11 @@ export interface AssetOwnerCheck extends AssetCheck {
ownerId: string;
}
export interface IAssetRepository {
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[]>;
getById(assetId: string): Promise<AssetEntity>;
getLocationsByUserId(userId: string): Promise<CuratedLocationsResponseDto[]>;
getDetectedObjectsByUserId(userId: string): Promise<CuratedObjectsResponseDto[]>;
getSearchPropertiesByUserId(userId: string): Promise<SearchPropertiesDto[]>;
@@ -34,10 +33,10 @@ export interface IAssetRepository {
getByOriginalPath(originalPath: string): Promise<AssetOwnerCheck | null>;
}
export const IAssetRepository = 'IAssetRepository';
export const IAssetRepositoryV1 = 'IAssetRepositoryV1';
@Injectable()
export class AssetRepository implements IAssetRepository {
export class AssetRepositoryV1 implements IAssetRepositoryV1 {
constructor(
@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
@InjectRepository(ExifEntity) private exifRepository: Repository<ExifEntity>,
@@ -93,34 +92,6 @@ export class AssetRepository implements IAssetRepository {
);
}
/**
* Get a single asset information by its ID
* - include exif info
* @param assetId
*/
getById(assetId: string): Promise<AssetEntity> {
return this.assetRepository.findOneOrFail({
where: {
id: assetId,
},
relations: {
exifInfo: true,
tags: true,
sharedLinks: true,
smartInfo: true,
owner: true,
faces: {
person: true,
},
stack: {
exifInfo: true,
},
},
// We are specifically asking for this asset. Return it even if it is soft deleted
withDeleted: true,
});
}
/**
* Get all assets belong to the user on the database
* @param ownerId

View File

@@ -1,4 +1,4 @@
import { AssetResponseDto, AuthDto } from '@app/domain';
import { AssetResponseDto, AssetService, AuthDto } from '@app/domain';
import {
Body,
Controller,
@@ -18,11 +18,11 @@ import {
import { ApiBody, ApiConsumes, ApiHeader, ApiTags } from '@nestjs/swagger';
import { NextFunction, Response } from 'express';
import { Auth, Authenticated, FileResponse, SharedLinkRoute } from '../../app.guard';
import { sendFile } from '../../app.utils';
import { UseValidation, sendFile } from '../../app.utils';
import { UUIDParamDto } from '../../controllers/dto/uuid-param.dto';
import { FileUploadInterceptor, ImmichFile, Route, mapToUploadFile } from '../../interceptors';
import FileNotEmptyValidator from '../validation/file-not-empty-validator';
import { AssetService } from './asset.service';
import { AssetService as AssetServiceV1 } from './asset.service';
import { AssetBulkUploadCheckDto } from './dto/asset-check.dto';
import { AssetSearchDto } from './dto/asset-search.dto';
import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
@@ -45,7 +45,10 @@ interface UploadFiles {
@Controller(Route.ASSET)
@Authenticated()
export class AssetController {
constructor(private assetService: AssetService) {}
constructor(
private serviceV1: AssetServiceV1,
private service: AssetService,
) {}
@SharedLinkRoute()
@Post('upload')
@@ -74,7 +77,7 @@ export class AssetController {
sidecarFile = mapToUploadFile(_sidecarFile);
}
const responseDto = await this.assetService.uploadFile(auth, dto, file, livePhotoFile, sidecarFile);
const responseDto = await this.serviceV1.uploadFile(auth, dto, file, livePhotoFile, sidecarFile);
if (responseDto.duplicate) {
res.status(HttpStatus.OK);
}
@@ -92,7 +95,7 @@ export class AssetController {
@Param() { id }: UUIDParamDto,
@Query(new ValidationPipe({ transform: true })) dto: ServeFileDto,
) {
await sendFile(res, next, () => this.assetService.serveFile(auth, id, dto));
await sendFile(res, next, () => this.serviceV1.serveFile(auth, id, dto));
}
@SharedLinkRoute()
@@ -105,22 +108,22 @@ export class AssetController {
@Param() { id }: UUIDParamDto,
@Query(new ValidationPipe({ transform: true })) dto: GetAssetThumbnailDto,
) {
await sendFile(res, next, () => this.assetService.serveThumbnail(auth, id, dto));
await sendFile(res, next, () => this.serviceV1.serveThumbnail(auth, id, dto));
}
@Get('/curated-objects')
getCuratedObjects(@Auth() auth: AuthDto): Promise<CuratedObjectsResponseDto[]> {
return this.assetService.getCuratedObject(auth);
return this.serviceV1.getCuratedObject(auth);
}
@Get('/curated-locations')
getCuratedLocations(@Auth() auth: AuthDto): Promise<CuratedLocationsResponseDto[]> {
return this.assetService.getCuratedLocation(auth);
return this.serviceV1.getCuratedLocation(auth);
}
@Get('/search-terms')
getAssetSearchTerms(@Auth() auth: AuthDto): Promise<string[]> {
return this.assetService.getAssetSearchTerm(auth);
return this.serviceV1.getAssetSearchTerm(auth);
}
/**
@@ -137,16 +140,18 @@ export class AssetController {
@Auth() auth: AuthDto,
@Query(new ValidationPipe({ transform: true })) dto: AssetSearchDto,
): Promise<AssetResponseDto[]> {
return this.assetService.getAllAssets(auth, dto);
return this.serviceV1.getAllAssets(auth, dto);
}
/**
* Get a single asset's information
* @deprecated Use `/asset/:id`
*/
@SharedLinkRoute()
@UseValidation()
@Get('/assetById/:id')
getAssetById(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<AssetResponseDto> {
return this.assetService.getAssetById(auth, id) as Promise<AssetResponseDto>;
return this.service.get(auth, id) as Promise<AssetResponseDto>;
}
/**
@@ -158,7 +163,7 @@ export class AssetController {
@Auth() auth: AuthDto,
@Body(ValidationPipe) dto: CheckExistingAssetsDto,
): Promise<CheckExistingAssetsResponseDto> {
return this.assetService.checkExistingAssets(auth, dto);
return this.serviceV1.checkExistingAssets(auth, dto);
}
/**
@@ -170,6 +175,6 @@ export class AssetController {
@Auth() auth: AuthDto,
@Body(ValidationPipe) dto: AssetBulkUploadCheckDto,
): Promise<AssetBulkUploadCheckResponseDto> {
return this.assetService.bulkUploadCheck(auth, dto);
return this.serviceV1.bulkUploadCheck(auth, dto);
}
}

View File

@@ -2,12 +2,12 @@ import { AuthDto, IJobRepository, JobName, mimeTypes, UploadFile } from '@app/do
import { AssetEntity } from '@app/infra/entities';
import { BadRequestException } from '@nestjs/common';
import { parse } from 'node:path';
import { IAssetRepository } from './asset-repository';
import { IAssetRepositoryV1 } from './asset-repository';
import { CreateAssetDto } from './dto/create-asset.dto';
export class AssetCore {
constructor(
private repository: IAssetRepository,
private repository: IAssetRepositoryV1,
private jobRepository: IJobRepository,
) {}

View File

@@ -1,19 +1,19 @@
import { IJobRepository, ILibraryRepository, IUserRepository, JobName } from '@app/domain';
import { IAssetRepository, IJobRepository, ILibraryRepository, IUserRepository, JobName } from '@app/domain';
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType, ExifEntity } from '@app/infra/entities';
import { BadRequestException } from '@nestjs/common';
import {
IAccessRepositoryMock,
assetStub,
authStub,
fileStub,
newAccessRepositoryMock,
newAssetRepositoryMock,
newJobRepositoryMock,
newLibraryRepositoryMock,
newUserRepositoryMock,
} from '@test';
import { when } from 'jest-when';
import { QueryFailedError } from 'typeorm';
import { IAssetRepository } from './asset-repository';
import { IAssetRepositoryV1 } from './asset-repository';
import { AssetService } from './asset.service';
import { CreateAssetDto } from './dto/create-asset.dto';
import { AssetRejectReason, AssetUploadAction } from './response-dto/asset-check-response.dto';
@@ -59,19 +59,19 @@ const _getAsset_1 = () => {
describe('AssetService', () => {
let sut: AssetService;
let accessMock: IAccessRepositoryMock;
let assetRepositoryMock: jest.Mocked<IAssetRepository>;
let assetRepositoryMockV1: jest.Mocked<IAssetRepositoryV1>;
let assetMock: jest.Mocked<IAssetRepository>;
let jobMock: jest.Mocked<IJobRepository>;
let libraryMock: jest.Mocked<ILibraryRepository>;
let userMock: jest.Mocked<IUserRepository>;
beforeEach(() => {
assetRepositoryMock = {
assetRepositoryMockV1 = {
get: jest.fn(),
create: jest.fn(),
upsertExif: jest.fn(),
getAllByUserId: jest.fn(),
getById: jest.fn(),
getDetectedObjectsByUserId: jest.fn(),
getLocationsByUserId: jest.fn(),
getSearchPropertiesByUserId: jest.fn(),
@@ -81,16 +81,17 @@ describe('AssetService', () => {
};
accessMock = newAccessRepositoryMock();
assetMock = newAssetRepositoryMock();
jobMock = newJobRepositoryMock();
libraryMock = newLibraryRepositoryMock();
userMock = newUserRepositoryMock();
sut = new AssetService(accessMock, assetRepositoryMock, jobMock, libraryMock, userMock);
sut = new AssetService(accessMock, assetRepositoryMockV1, assetMock, jobMock, libraryMock, userMock);
when(assetRepositoryMock.get)
when(assetRepositoryMockV1.get)
.calledWith(assetStub.livePhotoStillAsset.id)
.mockResolvedValue(assetStub.livePhotoStillAsset);
when(assetRepositoryMock.get)
when(assetRepositoryMockV1.get)
.calledWith(assetStub.livePhotoMotionAsset.id)
.mockResolvedValue(assetStub.livePhotoMotionAsset);
});
@@ -108,12 +109,12 @@ describe('AssetService', () => {
};
const dto = _getCreateAssetDto();
assetRepositoryMock.create.mockResolvedValue(assetEntity);
assetRepositoryMockV1.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(assetRepositoryMock.create).toHaveBeenCalled();
expect(assetRepositoryMockV1.create).toHaveBeenCalled();
expect(userMock.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, file.size);
});
@@ -130,8 +131,8 @@ describe('AssetService', () => {
const error = new QueryFailedError('', [], new Error('unique key violation'));
(error as any).constraint = ASSET_CHECKSUM_CONSTRAINT;
assetRepositoryMock.create.mockRejectedValue(error);
assetRepositoryMock.getAssetsByChecksums.mockResolvedValue([_getAsset_1()]);
assetRepositoryMockV1.create.mockRejectedValue(error);
assetRepositoryMockV1.getAssetsByChecksums.mockResolvedValue([_getAsset_1()]);
accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([dto.libraryId!]));
await expect(sut.uploadFile(authStub.user1, dto, file)).resolves.toEqual({ duplicate: true, id: 'id_1' });
@@ -148,8 +149,8 @@ describe('AssetService', () => {
const error = new QueryFailedError('', [], new Error('unique key violation'));
(error as any).constraint = ASSET_CHECKSUM_CONSTRAINT;
assetRepositoryMock.create.mockResolvedValueOnce(assetStub.livePhotoMotionAsset);
assetRepositoryMock.create.mockResolvedValueOnce(assetStub.livePhotoStillAsset);
assetRepositoryMockV1.create.mockResolvedValueOnce(assetStub.livePhotoMotionAsset);
assetRepositoryMockV1.create.mockResolvedValueOnce(assetStub.livePhotoStillAsset);
accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([dto.libraryId!]));
await expect(
@@ -177,7 +178,7 @@ describe('AssetService', () => {
const file1 = Buffer.from('d2947b871a706081be194569951b7db246907957', 'hex');
const file2 = Buffer.from('53be335e99f18a66ff12e9a901c7a6171dd76573', 'hex');
assetRepositoryMock.getAssetsByChecksums.mockResolvedValue([
assetRepositoryMockV1.getAssetsByChecksums.mockResolvedValue([
{ id: 'asset-1', checksum: file1 },
{ id: 'asset-2', checksum: file2 },
]);
@@ -196,62 +197,7 @@ describe('AssetService', () => {
],
});
expect(assetRepositoryMock.getAssetsByChecksums).toHaveBeenCalledWith(authStub.admin.user.id, [file1, file2]);
});
});
describe('getAssetById', () => {
it('should allow owner access', async () => {
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
assetRepositoryMock.getById.mockResolvedValue(assetStub.image);
await sut.getAssetById(authStub.admin, assetStub.image.id);
expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(
authStub.admin.user.id,
new Set([assetStub.image.id]),
);
});
it('should allow shared link access', async () => {
accessMock.asset.checkSharedLinkAccess.mockResolvedValue(new Set([assetStub.image.id]));
assetRepositoryMock.getById.mockResolvedValue(assetStub.image);
await sut.getAssetById(authStub.adminSharedLink, assetStub.image.id);
expect(accessMock.asset.checkSharedLinkAccess).toHaveBeenCalledWith(
authStub.adminSharedLink.sharedLink?.id,
new Set([assetStub.image.id]),
);
});
it('should allow partner sharing access', async () => {
accessMock.asset.checkPartnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
assetRepositoryMock.getById.mockResolvedValue(assetStub.image);
await sut.getAssetById(authStub.admin, assetStub.image.id);
expect(accessMock.asset.checkPartnerAccess).toHaveBeenCalledWith(
authStub.admin.user.id,
new Set([assetStub.image.id]),
);
});
it('should allow shared album access', async () => {
accessMock.asset.checkAlbumAccess.mockResolvedValue(new Set([assetStub.image.id]));
assetRepositoryMock.getById.mockResolvedValue(assetStub.image);
await sut.getAssetById(authStub.admin, assetStub.image.id);
expect(accessMock.asset.checkAlbumAccess).toHaveBeenCalledWith(
authStub.admin.user.id,
new Set([assetStub.image.id]),
);
});
it('should throw an error for no access', async () => {
await expect(sut.getAssetById(authStub.admin, assetStub.image.id)).rejects.toBeInstanceOf(BadRequestException);
expect(assetRepositoryMock.getById).not.toHaveBeenCalled();
});
it('should throw an error for an invalid shared link', async () => {
await expect(sut.getAssetById(authStub.adminSharedLink, assetStub.image.id)).rejects.toBeInstanceOf(
BadRequestException,
);
expect(accessMock.asset.checkOwnerAccess).not.toHaveBeenCalled();
expect(assetRepositoryMock.getById).not.toHaveBeenCalled();
expect(assetRepositoryMockV1.getAssetsByChecksums).toHaveBeenCalledWith(authStub.admin.user.id, [file1, file2]);
});
});
});

View File

@@ -3,24 +3,24 @@ import {
AssetResponseDto,
AuthDto,
CacheControl,
getLivePhotoMotionFilename,
IAccessRepository,
IAssetRepository,
IJobRepository,
ILibraryRepository,
ImmichFileResponse,
IUserRepository,
ImmichFileResponse,
JobName,
Permission,
UploadFile,
getLivePhotoMotionFilename,
mapAsset,
mimeTypes,
Permission,
SanitizedAssetResponseDto,
UploadFile,
} 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 { QueryFailedError } from 'typeorm';
import { IAssetRepository } from './asset-repository';
import { IAssetRepositoryV1 } from './asset-repository';
import { AssetCore } from './asset.core';
import { AssetBulkUploadCheckDto } from './dto/asset-check.dto';
import { AssetSearchDto } from './dto/asset-search.dto';
@@ -47,12 +47,13 @@ export class AssetService {
constructor(
@Inject(IAccessRepository) accessRepository: IAccessRepository,
@Inject(IAssetRepository) private _assetRepository: IAssetRepository,
@Inject(IAssetRepositoryV1) private assetRepositoryV1: IAssetRepositoryV1,
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository,
@Inject(ILibraryRepository) private libraryRepository: ILibraryRepository,
@Inject(IUserRepository) private userRepository: IUserRepository,
) {
this.assetCore = new AssetCore(_assetRepository, jobRepository);
this.assetCore = new AssetCore(assetRepositoryV1, jobRepository);
this.access = AccessCore.create(accessRepository);
}
@@ -102,7 +103,7 @@ export class AssetService {
// handle duplicates with a success response
if (error instanceof QueryFailedError && (error as any).constraint === ASSET_CHECKSUM_CONSTRAINT) {
const checksums = [file.checksum, livePhotoFile?.checksum].filter((checksum): checksum is Buffer => !!checksum);
const [duplicate] = await this._assetRepository.getAssetsByChecksums(auth.user.id, checksums);
const [duplicate] = await this.assetRepositoryV1.getAssetsByChecksums(auth.user.id, checksums);
return { id: duplicate.id, duplicate: true };
}
@@ -114,35 +115,14 @@ export class AssetService {
public async getAllAssets(auth: AuthDto, dto: AssetSearchDto): Promise<AssetResponseDto[]> {
const userId = dto.userId || auth.user.id;
await this.access.requirePermission(auth, Permission.TIMELINE_READ, userId);
const assets = await this._assetRepository.getAllByUserId(userId, dto);
const assets = await this.assetRepositoryV1.getAllByUserId(userId, dto);
return assets.map((asset) => mapAsset(asset));
}
public async getAssetById(auth: AuthDto, assetId: string): Promise<AssetResponseDto | SanitizedAssetResponseDto> {
await this.access.requirePermission(auth, Permission.ASSET_READ, assetId);
const asset = await this._assetRepository.getById(assetId);
if (!auth.sharedLink || auth.sharedLink?.showExif) {
const data = mapAsset(asset, { withStack: true });
if (data.ownerId !== auth.user.id) {
data.people = [];
}
if (auth.sharedLink) {
delete data.owner;
}
return data;
} else {
return mapAsset(asset, { stripMetadata: true, withStack: true });
}
}
async serveThumbnail(auth: AuthDto, assetId: string, dto: GetAssetThumbnailDto): Promise<ImmichFileResponse> {
await this.access.requirePermission(auth, Permission.ASSET_VIEW, assetId);
const asset = await this._assetRepository.get(assetId);
const asset = await this.assetRepositoryV1.get(assetId);
if (!asset) {
throw new NotFoundException('Asset not found');
}
@@ -160,7 +140,7 @@ export class AssetService {
// this is not quite right as sometimes this returns the original still
await this.access.requirePermission(auth, Permission.ASSET_VIEW, assetId);
const asset = await this._assetRepository.getById(assetId);
const asset = await this.assetRepository.getById(assetId);
if (!asset) {
throw new NotFoundException('Asset does not exist');
}
@@ -182,7 +162,7 @@ export class AssetService {
async getAssetSearchTerm(auth: AuthDto): Promise<string[]> {
const possibleSearchTerm = new Set<string>();
const rows = await this._assetRepository.getSearchPropertiesByUserId(auth.user.id);
const rows = await this.assetRepositoryV1.getSearchPropertiesByUserId(auth.user.id);
rows.forEach((row: SearchPropertiesDto) => {
// tags
row.tags?.map((tag: string) => possibleSearchTerm.add(tag?.toLowerCase()));
@@ -213,11 +193,11 @@ export class AssetService {
}
async getCuratedLocation(auth: AuthDto): Promise<CuratedLocationsResponseDto[]> {
return this._assetRepository.getLocationsByUserId(auth.user.id);
return this.assetRepositoryV1.getLocationsByUserId(auth.user.id);
}
async getCuratedObject(auth: AuthDto): Promise<CuratedObjectsResponseDto[]> {
return this._assetRepository.getDetectedObjectsByUserId(auth.user.id);
return this.assetRepositoryV1.getDetectedObjectsByUserId(auth.user.id);
}
async checkExistingAssets(
@@ -225,7 +205,7 @@ export class AssetService {
checkExistingAssetsDto: CheckExistingAssetsDto,
): Promise<CheckExistingAssetsResponseDto> {
return {
existingIds: await this._assetRepository.getExistingAssets(auth.user.id, checkExistingAssetsDto),
existingIds: await this.assetRepositoryV1.getExistingAssets(auth.user.id, checkExistingAssetsDto),
};
}
@@ -238,7 +218,7 @@ export class AssetService {
}
const checksums: Buffer[] = dto.assets.map((asset) => Buffer.from(asset.checksum, 'hex'));
const results = await this._assetRepository.getAssetsByChecksums(auth.user.id, checksums);
const results = await this.assetRepositoryV1.getAssetsByChecksums(auth.user.id, checksums);
const checksumMap: Record<string, string> = {};
for (const { id, checksum } of results) {

View File

@@ -5,7 +5,7 @@ import { Module, OnModuleInit } from '@nestjs/common';
import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
import { ScheduleModule } from '@nestjs/schedule';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AssetRepository, IAssetRepository } from './api-v1/asset/asset-repository';
import { AssetRepositoryV1, IAssetRepositoryV1 } from './api-v1/asset/asset-repository';
import { AssetController as AssetControllerV1 } from './api-v1/asset/asset.controller';
import { AssetService } from './api-v1/asset/asset.service';
import { AppGuard } from './app.guard';
@@ -45,8 +45,8 @@ import { ErrorInterceptor, FileUploadInterceptor } from './interceptors';
controllers: [
ActivityController,
AssetsController,
AssetController,
AssetControllerV1,
AssetController,
AppController,
AlbumController,
APIKeyController,
@@ -68,7 +68,7 @@ import { ErrorInterceptor, FileUploadInterceptor } from './interceptors';
providers: [
{ provide: APP_INTERCEPTOR, useClass: ErrorInterceptor },
{ provide: APP_GUARD, useClass: AppGuard },
{ provide: IAssetRepository, useClass: AssetRepository },
{ provide: IAssetRepositoryV1, useClass: AssetRepositoryV1 },
AppService,
AssetService,
FileUploadInterceptor,

View File

@@ -176,6 +176,12 @@ export class AssetController {
return this.service.updateStackParent(auth, dto);
}
@SharedLinkRoute()
@Get(':id')
getAssetInfo(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<AssetResponseDto> {
return this.service.get(auth, id) as Promise<AssetResponseDto>;
}
@Put(':id')
updateAsset(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: UpdateDto): Promise<AssetResponseDto> {
return this.service.update(auth, id, dto);