1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-22 01:47:08 +02:00

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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 421 additions and 174 deletions

BIN
mobile/openapi/README.md generated

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1057,6 +1057,7 @@
}, },
"/asset/assetById/{id}": { "/asset/assetById/{id}": {
"get": { "get": {
"deprecated": true,
"description": "Get a single asset's information", "description": "Get a single asset's information",
"operationId": "getAssetById", "operationId": "getAssetById",
"parameters": [ "parameters": [
@ -2319,6 +2320,54 @@
} }
}, },
"/asset/{id}": { "/asset/{id}": {
"get": {
"operationId": "getAssetInfo",
"parameters": [
{
"name": "id",
"required": true,
"in": "path",
"schema": {
"format": "uuid",
"type": "string"
}
},
{
"name": "key",
"required": false,
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AssetResponseDto"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Asset"
]
},
"put": { "put": {
"operationId": "updateAsset", "operationId": "updateAsset",
"parameters": [ "parameters": [

View File

@ -7147,6 +7147,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
* @param {string} id * @param {string} id
* @param {string} [key] * @param {string} [key]
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @deprecated
* @throws {RequiredError} * @throws {RequiredError}
*/ */
getAssetById: async (id: string, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { getAssetById: async (id: string, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
@ -7180,6 +7181,53 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {string} id
* @param {string} [key]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getAssetInfo: async (id: string, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('getAssetInfo', 'id', id)
const localVarPath = `/asset/{id}`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication cookie required
// authentication api_key required
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
if (key !== undefined) {
localVarQueryParameter['key'] = key;
}
setSearchParams(localVarUrlObj, localVarQueryParameter); setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
@ -8609,12 +8657,24 @@ export const AssetApiFp = function(configuration?: Configuration) {
* @param {string} id * @param {string} id
* @param {string} [key] * @param {string} [key]
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @deprecated
* @throws {RequiredError} * @throws {RequiredError}
*/ */
async getAssetById(id: string, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AssetResponseDto>> { async getAssetById(id: string, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AssetResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetById(id, key, options); const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetById(id, key, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
/**
*
* @param {string} id
* @param {string} [key]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getAssetInfo(id: string, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AssetResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetInfo(id, key, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/** /**
* *
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
@ -8982,11 +9042,21 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
* Get a single asset\'s information * Get a single asset\'s information
* @param {AssetApiGetAssetByIdRequest} requestParameters Request parameters. * @param {AssetApiGetAssetByIdRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @deprecated
* @throws {RequiredError} * @throws {RequiredError}
*/ */
getAssetById(requestParameters: AssetApiGetAssetByIdRequest, options?: AxiosRequestConfig): AxiosPromise<AssetResponseDto> { getAssetById(requestParameters: AssetApiGetAssetByIdRequest, options?: AxiosRequestConfig): AxiosPromise<AssetResponseDto> {
return localVarFp.getAssetById(requestParameters.id, requestParameters.key, options).then((request) => request(axios, basePath)); return localVarFp.getAssetById(requestParameters.id, requestParameters.key, options).then((request) => request(axios, basePath));
}, },
/**
*
* @param {AssetApiGetAssetInfoRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getAssetInfo(requestParameters: AssetApiGetAssetInfoRequest, options?: AxiosRequestConfig): AxiosPromise<AssetResponseDto> {
return localVarFp.getAssetInfo(requestParameters.id, requestParameters.key, options).then((request) => request(axios, basePath));
},
/** /**
* *
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
@ -9348,6 +9418,27 @@ export interface AssetApiGetAssetByIdRequest {
readonly key?: string readonly key?: string
} }
/**
* Request parameters for getAssetInfo operation in AssetApi.
* @export
* @interface AssetApiGetAssetInfoRequest
*/
export interface AssetApiGetAssetInfoRequest {
/**
*
* @type {string}
* @memberof AssetApiGetAssetInfo
*/
readonly id: string
/**
*
* @type {string}
* @memberof AssetApiGetAssetInfo
*/
readonly key?: string
}
/** /**
* Request parameters for getAssetStatistics operation in AssetApi. * Request parameters for getAssetStatistics operation in AssetApi.
* @export * @export
@ -10272,6 +10363,7 @@ export class AssetApi extends BaseAPI {
* Get a single asset\'s information * Get a single asset\'s information
* @param {AssetApiGetAssetByIdRequest} requestParameters Request parameters. * @param {AssetApiGetAssetByIdRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @deprecated
* @throws {RequiredError} * @throws {RequiredError}
* @memberof AssetApi * @memberof AssetApi
*/ */
@ -10279,6 +10371,17 @@ export class AssetApi extends BaseAPI {
return AssetApiFp(this.configuration).getAssetById(requestParameters.id, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); return AssetApiFp(this.configuration).getAssetById(requestParameters.id, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
} }
/**
*
* @param {AssetApiGetAssetInfoRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof AssetApi
*/
public getAssetInfo(requestParameters: AssetApiGetAssetInfoRequest, options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).getAssetInfo(requestParameters.id, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
}
/** /**
* *
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.

View File

@ -542,6 +542,109 @@ describe(`${AssetController.name} (e2e)`, () => {
} }
}); });
// TODO remove with deprecated endpoint
describe('GET /asset/assetById/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(server).get(`/asset/assetById/${uuidStub.notFound}`);
expect(body).toEqual(errorStub.unauthorized);
expect(status).toBe(401);
});
it('should require a valid id', async () => {
const { status, body } = await request(server)
.get(`/asset/assetById/${uuidStub.invalid}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorStub.badRequest(['id must be a UUID']));
});
it('should require access', async () => {
const { status, body } = await request(server)
.get(`/asset/assetById/${asset4.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorStub.noPermission);
});
it('should get the asset info', async () => {
const { status, body } = await request(server)
.get(`/asset/assetById/${asset1.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({
id: asset1.id,
stack: [],
stackCount: 0,
});
});
it('should work with a shared link', async () => {
const sharedLink = await api.sharedLinkApi.create(server, user1.accessToken, {
type: SharedLinkType.INDIVIDUAL,
assetIds: [asset1.id],
});
const { status, body } = await request(server).get(`/asset/assetById/${asset1.id}?key=${sharedLink.key}`);
expect(status).toBe(200);
expect(body).toMatchObject({
id: asset1.id,
stack: [],
stackCount: 0,
});
});
});
describe('GET /asset/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(server).get(`/asset/${uuidStub.notFound}`);
expect(body).toEqual(errorStub.unauthorized);
expect(status).toBe(401);
});
it('should require a valid id', async () => {
const { status, body } = await request(server)
.get(`/asset/${uuidStub.invalid}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorStub.badRequest(['id must be a UUID']));
});
it('should require access', async () => {
const { status, body } = await request(server)
.get(`/asset/${asset4.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorStub.noPermission);
});
it('should get the asset info', async () => {
const { status, body } = await request(server)
.get(`/asset/${asset1.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({
id: asset1.id,
stack: [],
stackCount: 0,
});
});
it('should work with a shared link', async () => {
const sharedLink = await api.sharedLinkApi.create(server, user1.accessToken, {
type: SharedLinkType.INDIVIDUAL,
assetIds: [asset1.id],
});
const { status, body } = await request(server).get(`/asset/${asset1.id}?key=${sharedLink.key}`);
expect(status).toBe(200);
expect(body).toMatchObject({
id: asset1.id,
stack: [],
stackCount: 0,
});
});
});
describe('POST /asset/upload', () => { describe('POST /asset/upload', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(server) const { status, body } = await request(server)

View File

@ -8,7 +8,6 @@ import {
newAccessRepositoryMock, newAccessRepositoryMock,
newAssetRepositoryMock, newAssetRepositoryMock,
newCommunicationRepositoryMock, newCommunicationRepositoryMock,
newCryptoRepositoryMock,
newJobRepositoryMock, newJobRepositoryMock,
newPartnerRepositoryMock, newPartnerRepositoryMock,
newStorageRepositoryMock, newStorageRepositoryMock,
@ -24,7 +23,6 @@ import {
ClientEvent, ClientEvent,
IAssetRepository, IAssetRepository,
ICommunicationRepository, ICommunicationRepository,
ICryptoRepository,
IJobRepository, IJobRepository,
IPartnerRepository, IPartnerRepository,
IStorageRepository, IStorageRepository,
@ -168,7 +166,6 @@ describe(AssetService.name, () => {
let sut: AssetService; let sut: AssetService;
let accessMock: IAccessRepositoryMock; let accessMock: IAccessRepositoryMock;
let assetMock: jest.Mocked<IAssetRepository>; let assetMock: jest.Mocked<IAssetRepository>;
let cryptoMock: jest.Mocked<ICryptoRepository>;
let jobMock: jest.Mocked<IJobRepository>; let jobMock: jest.Mocked<IJobRepository>;
let storageMock: jest.Mocked<IStorageRepository>; let storageMock: jest.Mocked<IStorageRepository>;
let userMock: jest.Mocked<IUserRepository>; let userMock: jest.Mocked<IUserRepository>;
@ -184,7 +181,6 @@ describe(AssetService.name, () => {
accessMock = newAccessRepositoryMock(); accessMock = newAccessRepositoryMock();
assetMock = newAssetRepositoryMock(); assetMock = newAssetRepositoryMock();
communicationMock = newCommunicationRepositoryMock(); communicationMock = newCommunicationRepositoryMock();
cryptoMock = newCryptoRepositoryMock();
jobMock = newJobRepositoryMock(); jobMock = newJobRepositoryMock();
storageMock = newStorageRepositoryMock(); storageMock = newStorageRepositoryMock();
userMock = newUserRepositoryMock(); userMock = newUserRepositoryMock();
@ -194,7 +190,6 @@ describe(AssetService.name, () => {
sut = new AssetService( sut = new AssetService(
accessMock, accessMock,
assetMock, assetMock,
cryptoMock,
jobMock, jobMock,
configMock, configMock,
storageMock, 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', () => { describe('update', () => {
it('should require asset write access for the id', async () => { it('should require asset write access for the id', async () => {
await expect(sut.update(authStub.admin, 'asset-1', { isArchived: false })).rejects.toBeInstanceOf( await expect(sut.update(authStub.admin, 'asset-1', { isArchived: false })).rejects.toBeInstanceOf(

View File

@ -15,7 +15,6 @@ import {
IAccessRepository, IAccessRepository,
IAssetRepository, IAssetRepository,
ICommunicationRepository, ICommunicationRepository,
ICryptoRepository,
IJobRepository, IJobRepository,
IPartnerRepository, IPartnerRepository,
IStorageRepository, IStorageRepository,
@ -87,7 +86,6 @@ export class AssetService {
constructor( constructor(
@Inject(IAccessRepository) accessRepository: IAccessRepository, @Inject(IAccessRepository) accessRepository: IAccessRepository,
@Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository, @Inject(IJobRepository) private jobRepository: IJobRepository,
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository, @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
@Inject(IStorageRepository) private storageRepository: IStorageRepository, @Inject(IStorageRepository) private storageRepository: IStorageRepository,
@ -400,6 +398,44 @@ export class AssetService {
return this.assetRepository.getAllByDeviceId(auth.user.id, deviceId); 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> { async update(auth: AuthDto, id: string, dto: UpdateAssetDto): Promise<AssetResponseDto> {
await this.access.requirePermission(auth, Permission.ASSET_UPDATE, id); await this.access.requirePermission(auth, Permission.ASSET_UPDATE, id);

View File

@ -20,12 +20,11 @@ export interface AssetOwnerCheck extends AssetCheck {
ownerId: string; ownerId: string;
} }
export interface IAssetRepository { export interface IAssetRepositoryV1 {
get(id: string): Promise<AssetEntity | null>; get(id: string): Promise<AssetEntity | null>;
create(asset: AssetCreate): Promise<AssetEntity>; create(asset: AssetCreate): Promise<AssetEntity>;
upsertExif(exif: Partial<ExifEntity>): Promise<void>; upsertExif(exif: Partial<ExifEntity>): Promise<void>;
getAllByUserId(userId: string, dto: AssetSearchDto): Promise<AssetEntity[]>; getAllByUserId(userId: string, dto: AssetSearchDto): Promise<AssetEntity[]>;
getById(assetId: string): Promise<AssetEntity>;
getLocationsByUserId(userId: string): Promise<CuratedLocationsResponseDto[]>; getLocationsByUserId(userId: string): Promise<CuratedLocationsResponseDto[]>;
getDetectedObjectsByUserId(userId: string): Promise<CuratedObjectsResponseDto[]>; getDetectedObjectsByUserId(userId: string): Promise<CuratedObjectsResponseDto[]>;
getSearchPropertiesByUserId(userId: string): Promise<SearchPropertiesDto[]>; getSearchPropertiesByUserId(userId: string): Promise<SearchPropertiesDto[]>;
@ -34,10 +33,10 @@ export interface IAssetRepository {
getByOriginalPath(originalPath: string): Promise<AssetOwnerCheck | null>; getByOriginalPath(originalPath: string): Promise<AssetOwnerCheck | null>;
} }
export const IAssetRepository = 'IAssetRepository'; export const IAssetRepositoryV1 = 'IAssetRepositoryV1';
@Injectable() @Injectable()
export class AssetRepository implements IAssetRepository { export class AssetRepositoryV1 implements IAssetRepositoryV1 {
constructor( constructor(
@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>, @InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
@InjectRepository(ExifEntity) private exifRepository: Repository<ExifEntity>, @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 * Get all assets belong to the user on the database
* @param ownerId * @param ownerId

View File

@ -1,4 +1,4 @@
import { AssetResponseDto, AuthDto } from '@app/domain'; import { AssetResponseDto, AssetService, AuthDto } from '@app/domain';
import { import {
Body, Body,
Controller, Controller,
@ -18,11 +18,11 @@ import {
import { ApiBody, ApiConsumes, ApiHeader, ApiTags } from '@nestjs/swagger'; import { ApiBody, ApiConsumes, ApiHeader, ApiTags } from '@nestjs/swagger';
import { NextFunction, Response } from 'express'; import { NextFunction, Response } from 'express';
import { Auth, Authenticated, FileResponse, SharedLinkRoute } from '../../app.guard'; 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 { UUIDParamDto } from '../../controllers/dto/uuid-param.dto';
import { FileUploadInterceptor, ImmichFile, Route, mapToUploadFile } from '../../interceptors'; import { FileUploadInterceptor, ImmichFile, Route, mapToUploadFile } from '../../interceptors';
import FileNotEmptyValidator from '../validation/file-not-empty-validator'; 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 { 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';
@ -45,7 +45,10 @@ interface UploadFiles {
@Controller(Route.ASSET) @Controller(Route.ASSET)
@Authenticated() @Authenticated()
export class AssetController { export class AssetController {
constructor(private assetService: AssetService) {} constructor(
private serviceV1: AssetServiceV1,
private service: AssetService,
) {}
@SharedLinkRoute() @SharedLinkRoute()
@Post('upload') @Post('upload')
@ -74,7 +77,7 @@ export class AssetController {
sidecarFile = mapToUploadFile(_sidecarFile); 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) { if (responseDto.duplicate) {
res.status(HttpStatus.OK); res.status(HttpStatus.OK);
} }
@ -92,7 +95,7 @@ export class AssetController {
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@Query(new ValidationPipe({ transform: true })) dto: ServeFileDto, @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() @SharedLinkRoute()
@ -105,22 +108,22 @@ export class AssetController {
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@Query(new ValidationPipe({ transform: true })) dto: GetAssetThumbnailDto, @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') @Get('/curated-objects')
getCuratedObjects(@Auth() auth: AuthDto): Promise<CuratedObjectsResponseDto[]> { getCuratedObjects(@Auth() auth: AuthDto): Promise<CuratedObjectsResponseDto[]> {
return this.assetService.getCuratedObject(auth); return this.serviceV1.getCuratedObject(auth);
} }
@Get('/curated-locations') @Get('/curated-locations')
getCuratedLocations(@Auth() auth: AuthDto): Promise<CuratedLocationsResponseDto[]> { getCuratedLocations(@Auth() auth: AuthDto): Promise<CuratedLocationsResponseDto[]> {
return this.assetService.getCuratedLocation(auth); return this.serviceV1.getCuratedLocation(auth);
} }
@Get('/search-terms') @Get('/search-terms')
getAssetSearchTerms(@Auth() auth: AuthDto): Promise<string[]> { 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, @Auth() auth: AuthDto,
@Query(new ValidationPipe({ transform: true })) dto: AssetSearchDto, @Query(new ValidationPipe({ transform: true })) dto: AssetSearchDto,
): Promise<AssetResponseDto[]> { ): Promise<AssetResponseDto[]> {
return this.assetService.getAllAssets(auth, dto); return this.serviceV1.getAllAssets(auth, dto);
} }
/** /**
* Get a single asset's information * Get a single asset's information
* @deprecated Use `/asset/:id`
*/ */
@SharedLinkRoute() @SharedLinkRoute()
@UseValidation()
@Get('/assetById/:id') @Get('/assetById/:id')
getAssetById(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<AssetResponseDto> { 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, @Auth() auth: AuthDto,
@Body(ValidationPipe) dto: CheckExistingAssetsDto, @Body(ValidationPipe) dto: CheckExistingAssetsDto,
): Promise<CheckExistingAssetsResponseDto> { ): Promise<CheckExistingAssetsResponseDto> {
return this.assetService.checkExistingAssets(auth, dto); return this.serviceV1.checkExistingAssets(auth, dto);
} }
/** /**
@ -170,6 +175,6 @@ export class AssetController {
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Body(ValidationPipe) dto: AssetBulkUploadCheckDto, @Body(ValidationPipe) dto: AssetBulkUploadCheckDto,
): Promise<AssetBulkUploadCheckResponseDto> { ): 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 { AssetEntity } from '@app/infra/entities';
import { BadRequestException } from '@nestjs/common'; import { BadRequestException } from '@nestjs/common';
import { parse } from 'node:path'; import { parse } from 'node:path';
import { IAssetRepository } from './asset-repository'; import { IAssetRepositoryV1 } from './asset-repository';
import { CreateAssetDto } from './dto/create-asset.dto'; import { CreateAssetDto } from './dto/create-asset.dto';
export class AssetCore { export class AssetCore {
constructor( constructor(
private repository: IAssetRepository, private repository: IAssetRepositoryV1,
private jobRepository: IJobRepository, 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 { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType, ExifEntity } from '@app/infra/entities';
import { BadRequestException } from '@nestjs/common';
import { import {
IAccessRepositoryMock, IAccessRepositoryMock,
assetStub, assetStub,
authStub, authStub,
fileStub, fileStub,
newAccessRepositoryMock, newAccessRepositoryMock,
newAssetRepositoryMock,
newJobRepositoryMock, newJobRepositoryMock,
newLibraryRepositoryMock, newLibraryRepositoryMock,
newUserRepositoryMock, newUserRepositoryMock,
} from '@test'; } from '@test';
import { when } from 'jest-when'; import { when } from 'jest-when';
import { QueryFailedError } from 'typeorm'; import { QueryFailedError } from 'typeorm';
import { IAssetRepository } from './asset-repository'; import { IAssetRepositoryV1 } from './asset-repository';
import { AssetService } from './asset.service'; import { AssetService } from './asset.service';
import { CreateAssetDto } from './dto/create-asset.dto'; import { CreateAssetDto } from './dto/create-asset.dto';
import { AssetRejectReason, AssetUploadAction } from './response-dto/asset-check-response.dto'; import { AssetRejectReason, AssetUploadAction } from './response-dto/asset-check-response.dto';
@ -59,19 +59,19 @@ const _getAsset_1 = () => {
describe('AssetService', () => { describe('AssetService', () => {
let sut: AssetService; let sut: AssetService;
let accessMock: IAccessRepositoryMock; let accessMock: IAccessRepositoryMock;
let assetRepositoryMock: jest.Mocked<IAssetRepository>; let assetRepositoryMockV1: jest.Mocked<IAssetRepositoryV1>;
let assetMock: jest.Mocked<IAssetRepository>;
let jobMock: jest.Mocked<IJobRepository>; let jobMock: jest.Mocked<IJobRepository>;
let libraryMock: jest.Mocked<ILibraryRepository>; let libraryMock: jest.Mocked<ILibraryRepository>;
let userMock: jest.Mocked<IUserRepository>; let userMock: jest.Mocked<IUserRepository>;
beforeEach(() => { beforeEach(() => {
assetRepositoryMock = { assetRepositoryMockV1 = {
get: jest.fn(), get: jest.fn(),
create: jest.fn(), create: jest.fn(),
upsertExif: jest.fn(), upsertExif: jest.fn(),
getAllByUserId: jest.fn(), getAllByUserId: jest.fn(),
getById: jest.fn(),
getDetectedObjectsByUserId: jest.fn(), getDetectedObjectsByUserId: jest.fn(),
getLocationsByUserId: jest.fn(), getLocationsByUserId: jest.fn(),
getSearchPropertiesByUserId: jest.fn(), getSearchPropertiesByUserId: jest.fn(),
@ -81,16 +81,17 @@ describe('AssetService', () => {
}; };
accessMock = newAccessRepositoryMock(); accessMock = newAccessRepositoryMock();
assetMock = newAssetRepositoryMock();
jobMock = newJobRepositoryMock(); jobMock = newJobRepositoryMock();
libraryMock = newLibraryRepositoryMock(); libraryMock = newLibraryRepositoryMock();
userMock = newUserRepositoryMock(); 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) .calledWith(assetStub.livePhotoStillAsset.id)
.mockResolvedValue(assetStub.livePhotoStillAsset); .mockResolvedValue(assetStub.livePhotoStillAsset);
when(assetRepositoryMock.get) when(assetRepositoryMockV1.get)
.calledWith(assetStub.livePhotoMotionAsset.id) .calledWith(assetStub.livePhotoMotionAsset.id)
.mockResolvedValue(assetStub.livePhotoMotionAsset); .mockResolvedValue(assetStub.livePhotoMotionAsset);
}); });
@ -108,12 +109,12 @@ describe('AssetService', () => {
}; };
const dto = _getCreateAssetDto(); const dto = _getCreateAssetDto();
assetRepositoryMock.create.mockResolvedValue(assetEntity); assetRepositoryMockV1.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(assetRepositoryMock.create).toHaveBeenCalled(); expect(assetRepositoryMockV1.create).toHaveBeenCalled();
expect(userMock.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, file.size); 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')); const error = new QueryFailedError('', [], new Error('unique key violation'));
(error as any).constraint = ASSET_CHECKSUM_CONSTRAINT; (error as any).constraint = ASSET_CHECKSUM_CONSTRAINT;
assetRepositoryMock.create.mockRejectedValue(error); assetRepositoryMockV1.create.mockRejectedValue(error);
assetRepositoryMock.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!]));
await expect(sut.uploadFile(authStub.user1, dto, file)).resolves.toEqual({ duplicate: true, id: 'id_1' }); 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')); const error = new QueryFailedError('', [], new Error('unique key violation'));
(error as any).constraint = ASSET_CHECKSUM_CONSTRAINT; (error as any).constraint = ASSET_CHECKSUM_CONSTRAINT;
assetRepositoryMock.create.mockResolvedValueOnce(assetStub.livePhotoMotionAsset); assetRepositoryMockV1.create.mockResolvedValueOnce(assetStub.livePhotoMotionAsset);
assetRepositoryMock.create.mockResolvedValueOnce(assetStub.livePhotoStillAsset); assetRepositoryMockV1.create.mockResolvedValueOnce(assetStub.livePhotoStillAsset);
accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([dto.libraryId!])); accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([dto.libraryId!]));
await expect( await expect(
@ -177,7 +178,7 @@ describe('AssetService', () => {
const file1 = Buffer.from('d2947b871a706081be194569951b7db246907957', 'hex'); const file1 = Buffer.from('d2947b871a706081be194569951b7db246907957', 'hex');
const file2 = Buffer.from('53be335e99f18a66ff12e9a901c7a6171dd76573', 'hex'); const file2 = Buffer.from('53be335e99f18a66ff12e9a901c7a6171dd76573', 'hex');
assetRepositoryMock.getAssetsByChecksums.mockResolvedValue([ assetRepositoryMockV1.getAssetsByChecksums.mockResolvedValue([
{ id: 'asset-1', checksum: file1 }, { id: 'asset-1', checksum: file1 },
{ id: 'asset-2', checksum: file2 }, { id: 'asset-2', checksum: file2 },
]); ]);
@ -196,62 +197,7 @@ describe('AssetService', () => {
], ],
}); });
expect(assetRepositoryMock.getAssetsByChecksums).toHaveBeenCalledWith(authStub.admin.user.id, [file1, file2]); expect(assetRepositoryMockV1.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();
}); });
}); });
}); });

View File

@ -3,24 +3,24 @@ import {
AssetResponseDto, AssetResponseDto,
AuthDto, AuthDto,
CacheControl, CacheControl,
getLivePhotoMotionFilename,
IAccessRepository, IAccessRepository,
IAssetRepository,
IJobRepository, IJobRepository,
ILibraryRepository, ILibraryRepository,
ImmichFileResponse,
IUserRepository, IUserRepository,
ImmichFileResponse,
JobName, JobName,
Permission,
UploadFile,
getLivePhotoMotionFilename,
mapAsset, mapAsset,
mimeTypes, mimeTypes,
Permission,
SanitizedAssetResponseDto,
UploadFile,
} 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 { Inject, Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common';
import { QueryFailedError } from 'typeorm'; import { QueryFailedError } from 'typeorm';
import { IAssetRepository } from './asset-repository'; import { IAssetRepositoryV1 } from './asset-repository';
import { AssetCore } from './asset.core'; 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';
@ -47,12 +47,13 @@ export class AssetService {
constructor( constructor(
@Inject(IAccessRepository) accessRepository: IAccessRepository, @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(IJobRepository) private jobRepository: IJobRepository,
@Inject(ILibraryRepository) private libraryRepository: ILibraryRepository, @Inject(ILibraryRepository) private libraryRepository: ILibraryRepository,
@Inject(IUserRepository) private userRepository: IUserRepository, @Inject(IUserRepository) private userRepository: IUserRepository,
) { ) {
this.assetCore = new AssetCore(_assetRepository, jobRepository); this.assetCore = new AssetCore(assetRepositoryV1, jobRepository);
this.access = AccessCore.create(accessRepository); this.access = AccessCore.create(accessRepository);
} }
@ -102,7 +103,7 @@ export class AssetService {
// handle duplicates with a success response // handle duplicates with a success response
if (error instanceof QueryFailedError && (error as any).constraint === ASSET_CHECKSUM_CONSTRAINT) { if (error instanceof QueryFailedError && (error as any).constraint === ASSET_CHECKSUM_CONSTRAINT) {
const checksums = [file.checksum, livePhotoFile?.checksum].filter((checksum): checksum is Buffer => !!checksum); 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 }; return { id: duplicate.id, duplicate: true };
} }
@ -114,35 +115,14 @@ export class AssetService {
public async getAllAssets(auth: AuthDto, dto: AssetSearchDto): Promise<AssetResponseDto[]> { public async getAllAssets(auth: AuthDto, dto: AssetSearchDto): Promise<AssetResponseDto[]> {
const userId = dto.userId || auth.user.id; const userId = dto.userId || auth.user.id;
await this.access.requirePermission(auth, Permission.TIMELINE_READ, userId); 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)); 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> { async serveThumbnail(auth: AuthDto, assetId: string, dto: GetAssetThumbnailDto): Promise<ImmichFileResponse> {
await this.access.requirePermission(auth, Permission.ASSET_VIEW, assetId); 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) { if (!asset) {
throw new NotFoundException('Asset not found'); 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 // this is not quite right as sometimes this returns the original still
await this.access.requirePermission(auth, Permission.ASSET_VIEW, assetId); 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) { if (!asset) {
throw new NotFoundException('Asset does not exist'); throw new NotFoundException('Asset does not exist');
} }
@ -182,7 +162,7 @@ export class AssetService {
async getAssetSearchTerm(auth: AuthDto): Promise<string[]> { async getAssetSearchTerm(auth: AuthDto): Promise<string[]> {
const possibleSearchTerm = new Set<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) => { rows.forEach((row: SearchPropertiesDto) => {
// tags // tags
row.tags?.map((tag: string) => possibleSearchTerm.add(tag?.toLowerCase())); row.tags?.map((tag: string) => possibleSearchTerm.add(tag?.toLowerCase()));
@ -213,11 +193,11 @@ export class AssetService {
} }
async getCuratedLocation(auth: AuthDto): Promise<CuratedLocationsResponseDto[]> { 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[]> { async getCuratedObject(auth: AuthDto): Promise<CuratedObjectsResponseDto[]> {
return this._assetRepository.getDetectedObjectsByUserId(auth.user.id); return this.assetRepositoryV1.getDetectedObjectsByUserId(auth.user.id);
} }
async checkExistingAssets( async checkExistingAssets(
@ -225,7 +205,7 @@ export class AssetService {
checkExistingAssetsDto: CheckExistingAssetsDto, checkExistingAssetsDto: CheckExistingAssetsDto,
): Promise<CheckExistingAssetsResponseDto> { ): Promise<CheckExistingAssetsResponseDto> {
return { 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 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> = {}; const checksumMap: Record<string, string> = {};
for (const { id, checksum } of results) { 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 { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
import { ScheduleModule } from '@nestjs/schedule'; import { ScheduleModule } from '@nestjs/schedule';
import { TypeOrmModule } from '@nestjs/typeorm'; 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 { AssetController as AssetControllerV1 } from './api-v1/asset/asset.controller';
import { AssetService } from './api-v1/asset/asset.service'; import { AssetService } from './api-v1/asset/asset.service';
import { AppGuard } from './app.guard'; import { AppGuard } from './app.guard';
@ -45,8 +45,8 @@ import { ErrorInterceptor, FileUploadInterceptor } from './interceptors';
controllers: [ controllers: [
ActivityController, ActivityController,
AssetsController, AssetsController,
AssetController,
AssetControllerV1, AssetControllerV1,
AssetController,
AppController, AppController,
AlbumController, AlbumController,
APIKeyController, APIKeyController,
@ -68,7 +68,7 @@ import { ErrorInterceptor, FileUploadInterceptor } from './interceptors';
providers: [ providers: [
{ provide: APP_INTERCEPTOR, useClass: ErrorInterceptor }, { provide: APP_INTERCEPTOR, useClass: ErrorInterceptor },
{ provide: APP_GUARD, useClass: AppGuard }, { provide: APP_GUARD, useClass: AppGuard },
{ provide: IAssetRepository, useClass: AssetRepository }, { provide: IAssetRepositoryV1, useClass: AssetRepositoryV1 },
AppService, AppService,
AssetService, AssetService,
FileUploadInterceptor, FileUploadInterceptor,

View File

@ -176,6 +176,12 @@ export class AssetController {
return this.service.updateStackParent(auth, dto); 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') @Put(':id')
updateAsset(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: UpdateDto): Promise<AssetResponseDto> { updateAsset(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: UpdateDto): Promise<AssetResponseDto> {
return this.service.update(auth, id, dto); return this.service.update(auth, id, dto);

View File

@ -62,7 +62,7 @@
// Get latest description from server // Get latest description from server
if (newAsset.id && !api.isSharedLink) { if (newAsset.id && !api.isSharedLink) {
const { data } = await api.assetApi.getAssetById({ id: asset.id }); const { data } = await api.assetApi.getAssetInfo({ id: asset.id });
people = data?.people || []; people = data?.people || [];
description = data.exifInfo?.description || ''; description = data.exifInfo?.description || '';
@ -126,7 +126,7 @@
}; };
const handleRefreshPeople = async () => { const handleRefreshPeople = async () => {
await api.assetApi.getAssetById({ id: asset.id }).then((res) => { await api.assetApi.getAssetInfo({ id: asset.id }).then((res) => {
people = res.data?.people || []; people = res.data?.people || [];
textArea.value = res.data?.exifInfo?.description || ''; textArea.value = res.data?.exifInfo?.description || '';
}); });
@ -234,7 +234,7 @@
{/key} {/key}
</section> </section>
{:else if description} {:else if description}
<p class="break-words whitespace-pre-line w-full text-black dark:text-white text-base">{description}</p> <p class="px-4 break-words whitespace-pre-line w-full text-black dark:text-white text-base">{description}</p>
{/if} {/if}
{#if !api.isSharedLink && people.length > 0} {#if !api.isSharedLink && people.length > 0}

View File

@ -27,7 +27,7 @@
assetId = link.assets[0].id; assetId = link.assets[0].id;
} }
const { data } = await api.assetApi.getAssetById({ id: assetId }); const { data } = await api.assetApi.getAssetInfo({ id: assetId });
return data; return data;
}; };

View File

@ -6,7 +6,7 @@ function createAssetViewingStore() {
const viewState = writable<boolean>(false); const viewState = writable<boolean>(false);
const setAssetId = async (id: string) => { const setAssetId = async (id: string) => {
const { data } = await api.assetApi.getAssetById({ id, key: api.getKey() }); const { data } = await api.assetApi.getAssetInfo({ id, key: api.getKey() });
viewingAssetStoreState.set(data); viewingAssetStoreState.set(data);
viewState.set(true); viewState.set(true);
}; };

View File

@ -3,7 +3,7 @@ import type { PageLoad } from './$types';
export const load = (async ({ params }) => { export const load = (async ({ params }) => {
const { key, assetId } = params; const { key, assetId } = params;
const { data: asset } = await api.assetApi.getAssetById({ id: assetId, key }); const { data: asset } = await api.assetApi.getAssetInfo({ id: assetId, key });
return { return {
asset, asset,