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:
parent
19d4c5e9f7
commit
b306cf564e
BIN
mobile/openapi/README.md
generated
BIN
mobile/openapi/README.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/AssetApi.md
generated
BIN
mobile/openapi/doc/AssetApi.md
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/asset_api.dart
generated
BIN
mobile/openapi/lib/api/asset_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/asset_api_test.dart
generated
BIN
mobile/openapi/test/asset_api_test.dart
generated
Binary file not shown.
@ -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": [
|
||||||
|
103
open-api/typescript-sdk/client/api.ts
generated
103
open-api/typescript-sdk/client/api.ts
generated
@ -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.
|
||||||
|
@ -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)
|
||||||
|
@ -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(
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
@ -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();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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) {
|
||||||
|
@ -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,
|
||||||
|
@ -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);
|
||||||
|
@ -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}
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
@ -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);
|
||||||
};
|
};
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user