mirror of
https://github.com/immich-app/immich.git
synced 2024-11-28 09:33:27 +02:00
refactor(server): move checkExistingAssets(), checkBulkUpdate() remove getAllAssets() (#9715)
* Refactor controller methods, non-breaking change * Remove getAllAssets * used imports * sync:sql * missing mock * Removing remaining references * chore: remove unused code --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
parent
95012dc19b
commit
d5cf8e4bfe
@ -699,27 +699,6 @@ describe('/asset', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /asset', () => {
|
||||
it('should return stack data', async () => {
|
||||
const { status, body } = await request(app).get('/asset').set('Authorization', `Bearer ${stackUser.accessToken}`);
|
||||
|
||||
const stack = body.find((asset: AssetResponseDto) => asset.id === stackAssets[0].id);
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(stack).toEqual(
|
||||
expect.objectContaining({
|
||||
stackCount: 3,
|
||||
stack:
|
||||
// Response includes children at the root level
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: stackAssets[1].id }),
|
||||
expect.objectContaining({ id: stackAssets[2].id }),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /asset', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).put('/asset');
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { LoginResponseDto, getAllAssets } from '@immich/sdk';
|
||||
import { LoginResponseDto, getAssetInfo, getAssetStatistics } from '@immich/sdk';
|
||||
import { Socket } from 'socket.io-client';
|
||||
import { errorDto } from 'src/responses';
|
||||
import { app, asBearerAuth, utils } from 'src/utils';
|
||||
@ -31,16 +31,16 @@ describe('/trash', () => {
|
||||
const { id: assetId } = await utils.createAsset(admin.accessToken);
|
||||
await utils.deleteAssets(admin.accessToken, [assetId]);
|
||||
|
||||
const before = await getAllAssets({}, { headers: asBearerAuth(admin.accessToken) });
|
||||
expect(before).toStrictEqual([expect.objectContaining({ id: assetId, isTrashed: true })]);
|
||||
const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
|
||||
expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: true }));
|
||||
|
||||
const { status } = await request(app).post('/trash/empty').set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(204);
|
||||
|
||||
await utils.waitForWebsocketEvent({ event: 'assetDelete', id: assetId });
|
||||
|
||||
const after = await getAllAssets({}, { headers: asBearerAuth(admin.accessToken) });
|
||||
expect(after.length).toBe(0);
|
||||
const after = await getAssetStatistics({ isTrashed: true }, { headers: asBearerAuth(admin.accessToken) });
|
||||
expect(after.total).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@ -56,14 +56,14 @@ describe('/trash', () => {
|
||||
const { id: assetId } = await utils.createAsset(admin.accessToken);
|
||||
await utils.deleteAssets(admin.accessToken, [assetId]);
|
||||
|
||||
const before = await getAllAssets({}, { headers: asBearerAuth(admin.accessToken) });
|
||||
expect(before).toStrictEqual([expect.objectContaining({ id: assetId, isTrashed: true })]);
|
||||
const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
|
||||
expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: true }));
|
||||
|
||||
const { status } = await request(app).post('/trash/restore').set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(204);
|
||||
|
||||
const after = await getAllAssets({}, { headers: asBearerAuth(admin.accessToken) });
|
||||
expect(after).toStrictEqual([expect.objectContaining({ id: assetId, isTrashed: false })]);
|
||||
const after = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
|
||||
expect(after).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: false }));
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { LoginResponseDto, getAllAlbums, getAllAssets } from '@immich/sdk';
|
||||
import { LoginResponseDto, getAllAlbums, getAssetStatistics } from '@immich/sdk';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { mkdir, readdir, rm, symlink } from 'node:fs/promises';
|
||||
import { asKeyAuth, immichCli, testAssetDir, utils } from 'src/utils';
|
||||
@ -28,8 +28,8 @@ describe(`immich upload`, () => {
|
||||
);
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.length).toBe(1);
|
||||
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.total).toBe(1);
|
||||
});
|
||||
|
||||
it('should skip a duplicate file', async () => {
|
||||
@ -40,8 +40,8 @@ describe(`immich upload`, () => {
|
||||
);
|
||||
expect(first.exitCode).toBe(0);
|
||||
|
||||
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.length).toBe(1);
|
||||
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.total).toBe(1);
|
||||
|
||||
const second = await immichCli(['upload', `${testAssetDir}/albums/nature/silver_fir.jpg`]);
|
||||
expect(second.stderr).toBe('');
|
||||
@ -60,8 +60,8 @@ describe(`immich upload`, () => {
|
||||
expect(stdout.split('\n')).toEqual(expect.arrayContaining([expect.stringContaining('No files found, exiting')]));
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.length).toBe(0);
|
||||
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.total).toBe(0);
|
||||
});
|
||||
|
||||
it('should have accurate dry run', async () => {
|
||||
@ -76,8 +76,8 @@ describe(`immich upload`, () => {
|
||||
);
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.length).toBe(0);
|
||||
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.total).toBe(0);
|
||||
});
|
||||
|
||||
it('dry run should handle duplicates', async () => {
|
||||
@ -88,8 +88,8 @@ describe(`immich upload`, () => {
|
||||
);
|
||||
expect(first.exitCode).toBe(0);
|
||||
|
||||
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.length).toBe(1);
|
||||
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.total).toBe(1);
|
||||
|
||||
const second = await immichCli(['upload', `${testAssetDir}/albums/nature/`, '--dry-run']);
|
||||
expect(second.stderr).toBe('');
|
||||
@ -112,8 +112,8 @@ describe(`immich upload`, () => {
|
||||
);
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.length).toBe(9);
|
||||
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.total).toBe(9);
|
||||
});
|
||||
});
|
||||
|
||||
@ -135,8 +135,8 @@ describe(`immich upload`, () => {
|
||||
expect(stderr).toBe('');
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.length).toBe(9);
|
||||
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.total).toBe(9);
|
||||
|
||||
const albums = await getAllAlbums({}, { headers: asKeyAuth(key) });
|
||||
expect(albums.length).toBe(1);
|
||||
@ -151,8 +151,8 @@ describe(`immich upload`, () => {
|
||||
expect(response1.stderr).toBe('');
|
||||
expect(response1.exitCode).toBe(0);
|
||||
|
||||
const assets1 = await getAllAssets({}, { headers: asKeyAuth(key) });
|
||||
expect(assets1.length).toBe(9);
|
||||
const assets1 = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||
expect(assets1.total).toBe(9);
|
||||
|
||||
const albums1 = await getAllAlbums({}, { headers: asKeyAuth(key) });
|
||||
expect(albums1.length).toBe(0);
|
||||
@ -167,8 +167,8 @@ describe(`immich upload`, () => {
|
||||
expect(response2.stderr).toBe('');
|
||||
expect(response2.exitCode).toBe(0);
|
||||
|
||||
const assets2 = await getAllAssets({}, { headers: asKeyAuth(key) });
|
||||
expect(assets2.length).toBe(9);
|
||||
const assets2 = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||
expect(assets2.total).toBe(9);
|
||||
|
||||
const albums2 = await getAllAlbums({}, { headers: asKeyAuth(key) });
|
||||
expect(albums2.length).toBe(1);
|
||||
@ -193,8 +193,8 @@ describe(`immich upload`, () => {
|
||||
expect(stderr).toBe('');
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.length).toBe(0);
|
||||
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.total).toBe(0);
|
||||
|
||||
const albums = await getAllAlbums({}, { headers: asKeyAuth(key) });
|
||||
expect(albums.length).toBe(0);
|
||||
@ -219,8 +219,8 @@ describe(`immich upload`, () => {
|
||||
expect(stderr).toBe('');
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.length).toBe(9);
|
||||
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.total).toBe(9);
|
||||
|
||||
const albums = await getAllAlbums({}, { headers: asKeyAuth(key) });
|
||||
expect(albums.length).toBe(1);
|
||||
@ -245,8 +245,8 @@ describe(`immich upload`, () => {
|
||||
expect(stderr).toBe('');
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.length).toBe(0);
|
||||
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.total).toBe(0);
|
||||
|
||||
const albums = await getAllAlbums({}, { headers: asKeyAuth(key) });
|
||||
expect(albums.length).toBe(0);
|
||||
@ -276,8 +276,8 @@ describe(`immich upload`, () => {
|
||||
expect(stderr).toBe('');
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.length).toBe(9);
|
||||
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.total).toBe(9);
|
||||
});
|
||||
|
||||
it('should have accurate dry run', async () => {
|
||||
@ -302,8 +302,8 @@ describe(`immich upload`, () => {
|
||||
expect(stderr).toBe('');
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.length).toBe(0);
|
||||
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.total).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@ -328,8 +328,8 @@ describe(`immich upload`, () => {
|
||||
);
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.length).toBe(1);
|
||||
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.total).toBe(1);
|
||||
});
|
||||
|
||||
it('should throw an error if attempting dry run', async () => {
|
||||
@ -344,8 +344,8 @@ describe(`immich upload`, () => {
|
||||
expect(stderr).toEqual(`error: option '-n, --dry-run' cannot be used with option '-h, --skip-hash'`);
|
||||
expect(exitCode).not.toBe(0);
|
||||
|
||||
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.length).toBe(0);
|
||||
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.total).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@ -367,8 +367,8 @@ describe(`immich upload`, () => {
|
||||
);
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.length).toBe(9);
|
||||
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.total).toBe(9);
|
||||
});
|
||||
|
||||
it('should reject string argument', async () => {
|
||||
@ -408,8 +408,8 @@ describe(`immich upload`, () => {
|
||||
);
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.length).toBe(8);
|
||||
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.total).toBe(8);
|
||||
});
|
||||
|
||||
it('should ignore assets matching glob pattern', async () => {
|
||||
@ -429,8 +429,8 @@ describe(`immich upload`, () => {
|
||||
);
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.length).toBe(1);
|
||||
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.total).toBe(1);
|
||||
});
|
||||
|
||||
it('should have accurate dry run', async () => {
|
||||
@ -451,8 +451,8 @@ describe(`immich upload`, () => {
|
||||
);
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.length).toBe(0);
|
||||
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.total).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -17,7 +17,6 @@ import {
|
||||
createSharedLink,
|
||||
createUser,
|
||||
deleteAssets,
|
||||
getAllAssets,
|
||||
getAllJobsStatus,
|
||||
getAssetInfo,
|
||||
getConfigDefaults,
|
||||
@ -340,8 +339,6 @@ export const utils = {
|
||||
|
||||
getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }),
|
||||
|
||||
getAllAssets: (accessToken: string) => getAllAssets({}, { headers: asBearerAuth(accessToken) }),
|
||||
|
||||
metadataSearch: async (accessToken: string, dto: MetadataSearchDto) => {
|
||||
return searchMetadata({ metadataSearchDto: dto }, { headers: asBearerAuth(accessToken) });
|
||||
},
|
||||
|
BIN
mobile/openapi/README.md
generated
BIN
mobile/openapi/README.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.
@ -969,109 +969,6 @@
|
||||
"Asset"
|
||||
]
|
||||
},
|
||||
"get": {
|
||||
"description": "Get all AssetEntity belong to the user",
|
||||
"operationId": "getAllAssets",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "if-none-match",
|
||||
"in": "header",
|
||||
"description": "ETag of data already cached on the client",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "isArchived",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "isFavorite",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "skip",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "take",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "updatedAfter",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "updatedBefore",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "userId",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/AssetResponseDto"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Asset"
|
||||
]
|
||||
},
|
||||
"put": {
|
||||
"operationId": "updateAssets",
|
||||
"parameters": [],
|
||||
|
@ -13,15 +13,14 @@ npm i --save @immich/sdk
|
||||
For a more detailed example, check out the [`@immich/cli`](https://github.com/immich-app/immich/tree/main/cli).
|
||||
|
||||
```typescript
|
||||
import { getAllAlbums, getAllAssets, getMyUserInfo, init } from "@immich/sdk";
|
||||
import { getAllAlbums, getMyUserInfo, init } from "@immich/sdk";
|
||||
|
||||
const API_KEY = "<API_KEY>"; // process.env.IMMICH_API_KEY
|
||||
|
||||
init({ baseUrl: "https://demo.immich.app/api", apiKey: API_KEY });
|
||||
|
||||
const user = await getMyUserInfo();
|
||||
const assets = await getAllAssets({ take: 1000 });
|
||||
const albums = await getAllAlbums({});
|
||||
|
||||
console.log({ user, assets, albums });
|
||||
console.log({ user, albums });
|
||||
```
|
||||
|
@ -1338,37 +1338,6 @@ export function deleteAssets({ assetBulkDeleteDto }: {
|
||||
body: assetBulkDeleteDto
|
||||
})));
|
||||
}
|
||||
/**
|
||||
* Get all AssetEntity belong to the user
|
||||
*/
|
||||
export function getAllAssets({ ifNoneMatch, isArchived, isFavorite, skip, take, updatedAfter, updatedBefore, userId }: {
|
||||
ifNoneMatch?: string;
|
||||
isArchived?: boolean;
|
||||
isFavorite?: boolean;
|
||||
skip?: number;
|
||||
take?: number;
|
||||
updatedAfter?: string;
|
||||
updatedBefore?: string;
|
||||
userId?: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: AssetResponseDto[];
|
||||
}>(`/asset${QS.query(QS.explode({
|
||||
isArchived,
|
||||
isFavorite,
|
||||
skip,
|
||||
take,
|
||||
updatedAfter,
|
||||
updatedBefore,
|
||||
userId
|
||||
}))}`, {
|
||||
...opts,
|
||||
headers: oazapfts.mergeHeaders(opts?.headers, {
|
||||
"if-none-match": ifNoneMatch
|
||||
})
|
||||
}));
|
||||
}
|
||||
export function updateAssets({ assetBulkUpdateDto }: {
|
||||
assetBulkUpdateDto: AssetBulkUpdateDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
|
@ -1,10 +1,12 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Inject,
|
||||
Param,
|
||||
ParseFilePipe,
|
||||
Post,
|
||||
Put,
|
||||
Res,
|
||||
UploadedFiles,
|
||||
@ -13,8 +15,18 @@ import {
|
||||
import { ApiConsumes, ApiTags } from '@nestjs/swagger';
|
||||
import { Response } from 'express';
|
||||
import { EndpointLifecycle } from 'src/decorators';
|
||||
import { AssetMediaResponseDto, AssetMediaStatusEnum } from 'src/dtos/asset-media-response.dto';
|
||||
import { AssetMediaReplaceDto, UploadFieldName } from 'src/dtos/asset-media.dto';
|
||||
import {
|
||||
AssetBulkUploadCheckResponseDto,
|
||||
AssetMediaResponseDto,
|
||||
AssetMediaStatusEnum,
|
||||
CheckExistingAssetsResponseDto,
|
||||
} from 'src/dtos/asset-media-response.dto';
|
||||
import {
|
||||
AssetBulkUploadCheckDto,
|
||||
AssetMediaReplaceDto,
|
||||
CheckExistingAssetsDto,
|
||||
UploadFieldName,
|
||||
} from 'src/dtos/asset-media.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||
@ -53,4 +65,30 @@ export class AssetMediaController {
|
||||
}
|
||||
return responseDto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if multiple assets exist on the server and returns all existing - used by background backup
|
||||
*/
|
||||
@Post('exist')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Authenticated()
|
||||
checkExistingAssets(
|
||||
@Auth() auth: AuthDto,
|
||||
@Body() dto: CheckExistingAssetsDto,
|
||||
): Promise<CheckExistingAssetsResponseDto> {
|
||||
return this.service.checkExistingAssets(auth, dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if assets exist by checksums
|
||||
*/
|
||||
@Post('bulk-upload-check')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Authenticated()
|
||||
checkBulkUpload(
|
||||
@Auth() auth: AuthDto,
|
||||
@Body() dto: AssetBulkUploadCheckDto,
|
||||
): Promise<AssetBulkUploadCheckResponseDto> {
|
||||
return this.service.bulkUploadCheck(auth, dto);
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Inject,
|
||||
Next,
|
||||
@ -16,20 +15,8 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { ApiBody, ApiConsumes, ApiHeader, ApiTags } from '@nestjs/swagger';
|
||||
import { NextFunction, Response } from 'express';
|
||||
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||
import {
|
||||
AssetBulkUploadCheckResponseDto,
|
||||
AssetFileUploadResponseDto,
|
||||
CheckExistingAssetsResponseDto,
|
||||
} from 'src/dtos/asset-v1-response.dto';
|
||||
import {
|
||||
AssetBulkUploadCheckDto,
|
||||
AssetSearchDto,
|
||||
CheckExistingAssetsDto,
|
||||
CreateAssetDto,
|
||||
GetAssetThumbnailDto,
|
||||
ServeFileDto,
|
||||
} from 'src/dtos/asset-v1.dto';
|
||||
import { AssetFileUploadResponseDto } from 'src/dtos/asset-v1-response.dto';
|
||||
import { CreateAssetDto, GetAssetThumbnailDto, ServeFileDto } from 'src/dtos/asset-v1.dto';
|
||||
import { AuthDto, ImmichHeader } from 'src/dtos/auth.dto';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { AssetUploadInterceptor } from 'src/middleware/asset-upload.interceptor';
|
||||
@ -109,45 +96,4 @@ export class AssetControllerV1 {
|
||||
) {
|
||||
await sendFile(res, next, () => this.service.serveThumbnail(auth, id, dto), this.logger);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all AssetEntity belong to the user
|
||||
*/
|
||||
@Get('/')
|
||||
@ApiHeader({
|
||||
name: 'if-none-match',
|
||||
description: 'ETag of data already cached on the client',
|
||||
required: false,
|
||||
schema: { type: 'string' },
|
||||
})
|
||||
@Authenticated()
|
||||
getAllAssets(@Auth() auth: AuthDto, @Query() dto: AssetSearchDto): Promise<AssetResponseDto[]> {
|
||||
return this.service.getAllAssets(auth, dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if multiple assets exist on the server and returns all existing - used by background backup
|
||||
*/
|
||||
@Post('/exist')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Authenticated()
|
||||
checkExistingAssets(
|
||||
@Auth() auth: AuthDto,
|
||||
@Body() dto: CheckExistingAssetsDto,
|
||||
): Promise<CheckExistingAssetsResponseDto> {
|
||||
return this.service.checkExistingAssets(auth, dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if assets exist by checksums
|
||||
*/
|
||||
@Post('/bulk-upload-check')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Authenticated()
|
||||
checkBulkUpload(
|
||||
@Auth() auth: AuthDto,
|
||||
@Body() dto: AssetBulkUploadCheckDto,
|
||||
): Promise<AssetBulkUploadCheckResponseDto> {
|
||||
return this.service.bulkUploadCheck(auth, dto);
|
||||
}
|
||||
}
|
||||
|
@ -9,3 +9,28 @@ export class AssetMediaResponseDto {
|
||||
status!: AssetMediaStatusEnum;
|
||||
id!: string;
|
||||
}
|
||||
|
||||
export enum AssetUploadAction {
|
||||
ACCEPT = 'accept',
|
||||
REJECT = 'reject',
|
||||
}
|
||||
|
||||
export enum AssetRejectReason {
|
||||
DUPLICATE = 'duplicate',
|
||||
UNSUPPORTED_FORMAT = 'unsupported-format',
|
||||
}
|
||||
|
||||
export class AssetBulkUploadCheckResult {
|
||||
id!: string;
|
||||
action!: AssetUploadAction;
|
||||
reason?: AssetRejectReason;
|
||||
assetId?: string;
|
||||
}
|
||||
|
||||
export class AssetBulkUploadCheckResponseDto {
|
||||
results!: AssetBulkUploadCheckResult[];
|
||||
}
|
||||
|
||||
export class CheckExistingAssetsResponseDto {
|
||||
existingIds!: string[];
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ArrayNotEmpty, IsArray, IsNotEmpty, IsString, ValidateNested } from 'class-validator';
|
||||
import { Optional, ValidateDate } from 'src/validation';
|
||||
|
||||
export enum UploadFieldName {
|
||||
@ -33,3 +34,31 @@ export class AssetMediaReplaceDto {
|
||||
@ApiProperty({ type: 'string', format: 'binary' })
|
||||
[UploadFieldName.ASSET_DATA]!: any;
|
||||
}
|
||||
|
||||
export class AssetBulkUploadCheckItem {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
id!: string;
|
||||
|
||||
/** base64 or hex encoded sha1 hash */
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
checksum!: string;
|
||||
}
|
||||
|
||||
export class AssetBulkUploadCheckDto {
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => AssetBulkUploadCheckItem)
|
||||
assets!: AssetBulkUploadCheckItem[];
|
||||
}
|
||||
|
||||
export class CheckExistingAssetsDto {
|
||||
@ArrayNotEmpty()
|
||||
@IsString({ each: true })
|
||||
@IsNotEmpty({ each: true })
|
||||
deviceAssetIds!: string[];
|
||||
|
||||
@IsNotEmpty()
|
||||
deviceId!: string;
|
||||
}
|
||||
|
@ -1,29 +1,4 @@
|
||||
export class AssetBulkUploadCheckResult {
|
||||
id!: string;
|
||||
action!: AssetUploadAction;
|
||||
reason?: AssetRejectReason;
|
||||
assetId?: string;
|
||||
}
|
||||
|
||||
export class AssetBulkUploadCheckResponseDto {
|
||||
results!: AssetBulkUploadCheckResult[];
|
||||
}
|
||||
|
||||
export enum AssetUploadAction {
|
||||
ACCEPT = 'accept',
|
||||
REJECT = 'reject',
|
||||
}
|
||||
|
||||
export enum AssetRejectReason {
|
||||
DUPLICATE = 'duplicate',
|
||||
UNSUPPORTED_FORMAT = 'unsupported-format',
|
||||
}
|
||||
|
||||
export class AssetFileUploadResponseDto {
|
||||
id!: string;
|
||||
duplicate!: boolean;
|
||||
}
|
||||
|
||||
export class CheckExistingAssetsResponseDto {
|
||||
existingIds!: string[];
|
||||
}
|
||||
|
@ -1,68 +1,8 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ArrayNotEmpty, IsArray, IsEnum, IsInt, IsNotEmpty, IsString, IsUUID, ValidateNested } from 'class-validator';
|
||||
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
|
||||
import { UploadFieldName } from 'src/dtos/asset.dto';
|
||||
import { Optional, ValidateBoolean, ValidateDate } from 'src/validation';
|
||||
|
||||
export class AssetBulkUploadCheckItem {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
id!: string;
|
||||
|
||||
/** base64 or hex encoded sha1 hash */
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
checksum!: string;
|
||||
}
|
||||
|
||||
export class AssetBulkUploadCheckDto {
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => AssetBulkUploadCheckItem)
|
||||
assets!: AssetBulkUploadCheckItem[];
|
||||
}
|
||||
|
||||
export class AssetSearchDto {
|
||||
@ValidateBoolean({ optional: true })
|
||||
isFavorite?: boolean;
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
isArchived?: boolean;
|
||||
|
||||
@Optional()
|
||||
@IsInt()
|
||||
@Type(() => Number)
|
||||
@ApiProperty({ type: 'integer' })
|
||||
skip?: number;
|
||||
|
||||
@Optional()
|
||||
@IsInt()
|
||||
@Type(() => Number)
|
||||
@ApiProperty({ type: 'integer' })
|
||||
take?: number;
|
||||
|
||||
@Optional()
|
||||
@IsUUID('4')
|
||||
@ApiProperty({ format: 'uuid' })
|
||||
userId?: string;
|
||||
|
||||
@ValidateDate({ optional: true })
|
||||
updatedAfter?: Date;
|
||||
|
||||
@ValidateDate({ optional: true })
|
||||
updatedBefore?: Date;
|
||||
}
|
||||
|
||||
export class CheckExistingAssetsDto {
|
||||
@ArrayNotEmpty()
|
||||
@IsString({ each: true })
|
||||
@IsNotEmpty({ each: true })
|
||||
deviceAssetIds!: string[];
|
||||
|
||||
@IsNotEmpty()
|
||||
deviceId!: string;
|
||||
}
|
||||
|
||||
export class CreateAssetDto {
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { AssetSearchDto, CheckExistingAssetsDto } from 'src/dtos/asset-v1.dto';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
|
||||
export interface AssetCheck {
|
||||
@ -12,10 +11,7 @@ export interface AssetOwnerCheck extends AssetCheck {
|
||||
|
||||
export interface IAssetRepositoryV1 {
|
||||
get(id: string): Promise<AssetEntity | null>;
|
||||
getAllByUserId(userId: string, dto: AssetSearchDto): Promise<AssetEntity[]>;
|
||||
getAssetsByChecksums(userId: string, checksums: Buffer[]): Promise<AssetCheck[]>;
|
||||
getExistingAssets(userId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise<string[]>;
|
||||
getByOriginalPath(originalPath: string): Promise<AssetOwnerCheck | null>;
|
||||
}
|
||||
|
||||
export const IAssetRepositoryV1 = 'IAssetRepositoryV1';
|
||||
|
@ -168,8 +168,10 @@ export interface IAssetRepository {
|
||||
getByIdsWithAllRelations(ids: string[]): Promise<AssetEntity[]>;
|
||||
getByDayOfYear(ownerIds: string[], monthDay: MonthDay): Promise<AssetEntity[]>;
|
||||
getByChecksum(libraryId: string | null, checksum: Buffer): Promise<AssetEntity | null>;
|
||||
getByChecksums(userId: string, checksums: Buffer[]): Promise<AssetEntity[]>;
|
||||
getUploadAssetIdByChecksum(ownerId: string, checksum: Buffer): Promise<string | undefined>;
|
||||
getByAlbumId(pagination: PaginationOptions, albumId: string): Paginated<AssetEntity>;
|
||||
getByDeviceIds(ownerId: string, deviceId: string, deviceAssetIds: string[]): Promise<AssetEntity[]>;
|
||||
getByUserId(pagination: PaginationOptions, userId: string, options?: AssetSearchOptions): Paginated<AssetEntity>;
|
||||
getById(
|
||||
id: string,
|
||||
|
@ -482,6 +482,20 @@ WHERE
|
||||
LIMIT
|
||||
1
|
||||
|
||||
-- AssetRepository.getByChecksums
|
||||
SELECT
|
||||
"AssetEntity"."id" AS "AssetEntity_id",
|
||||
"AssetEntity"."checksum" AS "AssetEntity_checksum"
|
||||
FROM
|
||||
"assets" "AssetEntity"
|
||||
WHERE
|
||||
(
|
||||
("AssetEntity"."ownerId" = $1)
|
||||
AND (
|
||||
"AssetEntity"."checksum" IN ($2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
)
|
||||
)
|
||||
|
||||
-- AssetRepository.getUploadAssetIdByChecksum
|
||||
SELECT
|
||||
"AssetEntity"."id" AS "AssetEntity_id"
|
||||
|
@ -74,43 +74,6 @@ WHERE
|
||||
((("LibraryEntity"."ownerId" = $1)))
|
||||
AND ("LibraryEntity"."deletedAt" IS NULL)
|
||||
|
||||
-- LibraryRepository.getAllByUserId
|
||||
SELECT
|
||||
"LibraryEntity"."id" AS "LibraryEntity_id",
|
||||
"LibraryEntity"."name" AS "LibraryEntity_name",
|
||||
"LibraryEntity"."ownerId" AS "LibraryEntity_ownerId",
|
||||
"LibraryEntity"."importPaths" AS "LibraryEntity_importPaths",
|
||||
"LibraryEntity"."exclusionPatterns" AS "LibraryEntity_exclusionPatterns",
|
||||
"LibraryEntity"."createdAt" AS "LibraryEntity_createdAt",
|
||||
"LibraryEntity"."updatedAt" AS "LibraryEntity_updatedAt",
|
||||
"LibraryEntity"."deletedAt" AS "LibraryEntity_deletedAt",
|
||||
"LibraryEntity"."refreshedAt" AS "LibraryEntity_refreshedAt",
|
||||
"LibraryEntity__LibraryEntity_owner"."id" AS "LibraryEntity__LibraryEntity_owner_id",
|
||||
"LibraryEntity__LibraryEntity_owner"."name" AS "LibraryEntity__LibraryEntity_owner_name",
|
||||
"LibraryEntity__LibraryEntity_owner"."isAdmin" AS "LibraryEntity__LibraryEntity_owner_isAdmin",
|
||||
"LibraryEntity__LibraryEntity_owner"."email" AS "LibraryEntity__LibraryEntity_owner_email",
|
||||
"LibraryEntity__LibraryEntity_owner"."storageLabel" AS "LibraryEntity__LibraryEntity_owner_storageLabel",
|
||||
"LibraryEntity__LibraryEntity_owner"."oauthId" AS "LibraryEntity__LibraryEntity_owner_oauthId",
|
||||
"LibraryEntity__LibraryEntity_owner"."profileImagePath" AS "LibraryEntity__LibraryEntity_owner_profileImagePath",
|
||||
"LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword",
|
||||
"LibraryEntity__LibraryEntity_owner"."createdAt" AS "LibraryEntity__LibraryEntity_owner_createdAt",
|
||||
"LibraryEntity__LibraryEntity_owner"."deletedAt" AS "LibraryEntity__LibraryEntity_owner_deletedAt",
|
||||
"LibraryEntity__LibraryEntity_owner"."status" AS "LibraryEntity__LibraryEntity_owner_status",
|
||||
"LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt",
|
||||
"LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes",
|
||||
"LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes"
|
||||
FROM
|
||||
"libraries" "LibraryEntity"
|
||||
LEFT JOIN "users" "LibraryEntity__LibraryEntity_owner" ON "LibraryEntity__LibraryEntity_owner"."id" = "LibraryEntity"."ownerId"
|
||||
AND (
|
||||
"LibraryEntity__LibraryEntity_owner"."deletedAt" IS NULL
|
||||
)
|
||||
WHERE
|
||||
((("LibraryEntity"."ownerId" = $1)))
|
||||
AND ("LibraryEntity"."deletedAt" IS NULL)
|
||||
ORDER BY
|
||||
"LibraryEntity"."createdAt" ASC
|
||||
|
||||
-- LibraryRepository.getAll
|
||||
SELECT
|
||||
"LibraryEntity"."id" AS "LibraryEntity_id",
|
||||
|
@ -1,9 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { AssetSearchDto, CheckExistingAssetsDto } from 'src/dtos/asset-v1.dto';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { AssetCheck, AssetOwnerCheck, IAssetRepositoryV1 } from 'src/interfaces/asset-v1.interface';
|
||||
import { OptionalBetween } from 'src/utils/database';
|
||||
import { AssetCheck, IAssetRepositoryV1 } from 'src/interfaces/asset-v1.interface';
|
||||
import { In } from 'typeorm/find-options/operator/In.js';
|
||||
import { Repository } from 'typeorm/repository/Repository.js';
|
||||
|
||||
@ -11,36 +9,6 @@ import { Repository } from 'typeorm/repository/Repository.js';
|
||||
export class AssetRepositoryV1 implements IAssetRepositoryV1 {
|
||||
constructor(@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>) {}
|
||||
|
||||
/**
|
||||
* Retrieves all assets by user ID.
|
||||
*
|
||||
* @param ownerId - The ID of the owner.
|
||||
* @param dto - The AssetSearchDto object containing search criteria.
|
||||
* @returns A Promise that resolves to an array of AssetEntity objects.
|
||||
*/
|
||||
getAllByUserId(ownerId: string, dto: AssetSearchDto): Promise<AssetEntity[]> {
|
||||
return this.assetRepository.find({
|
||||
where: {
|
||||
ownerId,
|
||||
isVisible: true,
|
||||
isFavorite: dto.isFavorite,
|
||||
isArchived: dto.isArchived,
|
||||
updatedAt: OptionalBetween(dto.updatedAfter, dto.updatedBefore),
|
||||
},
|
||||
relations: {
|
||||
exifInfo: true,
|
||||
tags: true,
|
||||
stack: { assets: true },
|
||||
},
|
||||
skip: dto.skip || 0,
|
||||
take: dto.take,
|
||||
order: {
|
||||
fileCreatedAt: 'DESC',
|
||||
},
|
||||
withDeleted: true,
|
||||
});
|
||||
}
|
||||
|
||||
get(id: string): Promise<AssetEntity | null> {
|
||||
return this.assetRepository.findOne({
|
||||
where: { id },
|
||||
@ -73,30 +41,4 @@ export class AssetRepositoryV1 implements IAssetRepositoryV1 {
|
||||
withDeleted: true,
|
||||
});
|
||||
}
|
||||
|
||||
async getExistingAssets(ownerId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise<string[]> {
|
||||
const assets = await this.assetRepository.find({
|
||||
select: { deviceAssetId: true },
|
||||
where: {
|
||||
deviceAssetId: In(checkDuplicateAssetDto.deviceAssetIds),
|
||||
deviceId: checkDuplicateAssetDto.deviceId,
|
||||
ownerId,
|
||||
},
|
||||
withDeleted: true,
|
||||
});
|
||||
return assets.map((asset) => asset.deviceAssetId);
|
||||
}
|
||||
|
||||
getByOriginalPath(originalPath: string): Promise<AssetOwnerCheck | null> {
|
||||
return this.assetRepository.findOne({
|
||||
select: {
|
||||
id: true,
|
||||
ownerId: true,
|
||||
checksum: true,
|
||||
},
|
||||
where: {
|
||||
originalPath,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -157,6 +157,18 @@ export class AssetRepository implements IAssetRepository {
|
||||
});
|
||||
}
|
||||
|
||||
getByDeviceIds(ownerId: string, deviceId: string, deviceAssetIds: string[]): Promise<AssetEntity[]> {
|
||||
return this.repository.find({
|
||||
select: { deviceAssetId: true },
|
||||
where: {
|
||||
deviceAssetId: In(deviceAssetIds),
|
||||
deviceId,
|
||||
ownerId,
|
||||
},
|
||||
withDeleted: true,
|
||||
});
|
||||
}
|
||||
|
||||
getByUserId(
|
||||
pagination: PaginationOptions,
|
||||
userId: string,
|
||||
@ -300,6 +312,21 @@ export class AssetRepository implements IAssetRepository {
|
||||
});
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.BUFFER] })
|
||||
getByChecksums(ownerId: string, checksums: Buffer[]): Promise<AssetEntity[]> {
|
||||
return this.repository.find({
|
||||
select: {
|
||||
id: true,
|
||||
checksum: true,
|
||||
},
|
||||
where: {
|
||||
ownerId,
|
||||
checksum: In(checksums),
|
||||
},
|
||||
withDeleted: true,
|
||||
});
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.BUFFER] })
|
||||
async getUploadAssetIdByChecksum(ownerId: string, checksum: Buffer): Promise<string | undefined> {
|
||||
const asset = await this.repository.findOne({
|
||||
|
@ -39,21 +39,6 @@ export class LibraryRepository implements ILibraryRepository {
|
||||
return this.repository.countBy({ ownerId });
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getAllByUserId(ownerId: string): Promise<LibraryEntity[]> {
|
||||
return this.repository.find({
|
||||
where: {
|
||||
ownerId,
|
||||
},
|
||||
relations: {
|
||||
owner: true,
|
||||
},
|
||||
order: {
|
||||
createdAt: 'ASC',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [] })
|
||||
getAll(withDeleted = false): Promise<LibraryEntity[]> {
|
||||
return this.repository.find({
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Stats } from 'node:fs';
|
||||
import { AssetMediaStatusEnum } from 'src/dtos/asset-media-response.dto';
|
||||
import { AssetMediaStatusEnum, AssetRejectReason, AssetUploadAction } from 'src/dtos/asset-media-response.dto';
|
||||
import { AssetMediaReplaceDto } from 'src/dtos/asset-media.dto';
|
||||
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType } from 'src/entities/asset.entity';
|
||||
import { ExifEntity } from 'src/entities/exif.entity';
|
||||
@ -277,4 +277,31 @@ describe('AssetMediaService', () => {
|
||||
expect(userMock.updateUsage).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('bulkUploadCheck', () => {
|
||||
it('should accept hex and base64 checksums', async () => {
|
||||
const file1 = Buffer.from('d2947b871a706081be194569951b7db246907957', 'hex');
|
||||
const file2 = Buffer.from('53be335e99f18a66ff12e9a901c7a6171dd76573', 'hex');
|
||||
|
||||
assetMock.getByChecksums.mockResolvedValue([
|
||||
{ id: 'asset-1', checksum: file1 } as AssetEntity,
|
||||
{ id: 'asset-2', checksum: file2 } as AssetEntity,
|
||||
]);
|
||||
|
||||
await expect(
|
||||
sut.bulkUploadCheck(authStub.admin, {
|
||||
assets: [
|
||||
{ id: '1', checksum: file1.toString('hex') },
|
||||
{ id: '2', checksum: file2.toString('base64') },
|
||||
],
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
results: [
|
||||
{ id: '1', assetId: 'asset-1', action: AssetUploadAction.REJECT, reason: AssetRejectReason.DUPLICATE },
|
||||
{ id: '2', assetId: 'asset-2', action: AssetUploadAction.REJECT, reason: AssetRejectReason.DUPLICATE },
|
||||
],
|
||||
});
|
||||
|
||||
expect(assetMock.getByChecksums).toHaveBeenCalledWith(authStub.admin.user.id, [file1, file2]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,7 +1,19 @@
|
||||
import { BadRequestException, Inject, Injectable, InternalServerErrorException } from '@nestjs/common';
|
||||
import { AccessCore, Permission } from 'src/cores/access.core';
|
||||
import { AssetMediaResponseDto, AssetMediaStatusEnum } from 'src/dtos/asset-media-response.dto';
|
||||
import { AssetMediaReplaceDto, UploadFieldName } from 'src/dtos/asset-media.dto';
|
||||
import {
|
||||
AssetBulkUploadCheckResponseDto,
|
||||
AssetMediaResponseDto,
|
||||
AssetMediaStatusEnum,
|
||||
AssetRejectReason,
|
||||
AssetUploadAction,
|
||||
CheckExistingAssetsResponseDto,
|
||||
} from 'src/dtos/asset-media-response.dto';
|
||||
import {
|
||||
AssetBulkUploadCheckDto,
|
||||
AssetMediaReplaceDto,
|
||||
CheckExistingAssetsDto,
|
||||
UploadFieldName,
|
||||
} from 'src/dtos/asset-media.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity';
|
||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||
@ -12,8 +24,8 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { mimeTypes } from 'src/utils/mime-types';
|
||||
import { fromChecksum } from 'src/utils/request';
|
||||
import { QueryFailedError } from 'typeorm';
|
||||
|
||||
export interface UploadRequest {
|
||||
auth: AuthDto | null;
|
||||
fieldName: UploadFieldName;
|
||||
@ -174,4 +186,49 @@ export class AssetMediaService {
|
||||
throw new BadRequestException('Quota has been exceeded!');
|
||||
}
|
||||
}
|
||||
|
||||
async checkExistingAssets(
|
||||
auth: AuthDto,
|
||||
checkExistingAssetsDto: CheckExistingAssetsDto,
|
||||
): Promise<CheckExistingAssetsResponseDto> {
|
||||
const assets = await this.assetRepository.getByDeviceIds(
|
||||
auth.user.id,
|
||||
checkExistingAssetsDto.deviceId,
|
||||
checkExistingAssetsDto.deviceAssetIds,
|
||||
);
|
||||
return {
|
||||
existingIds: assets.map((asset) => asset.id),
|
||||
};
|
||||
}
|
||||
|
||||
async bulkUploadCheck(auth: AuthDto, dto: AssetBulkUploadCheckDto): Promise<AssetBulkUploadCheckResponseDto> {
|
||||
const checksums: Buffer[] = dto.assets.map((asset) => fromChecksum(asset.checksum));
|
||||
const results = await this.assetRepository.getByChecksums(auth.user.id, checksums);
|
||||
const checksumMap: Record<string, string> = {};
|
||||
|
||||
for (const { id, checksum } of results) {
|
||||
checksumMap[checksum.toString('hex')] = id;
|
||||
}
|
||||
|
||||
return {
|
||||
results: dto.assets.map(({ id, checksum }) => {
|
||||
const duplicate = checksumMap[fromChecksum(checksum).toString('hex')];
|
||||
if (duplicate) {
|
||||
return {
|
||||
id,
|
||||
assetId: duplicate,
|
||||
action: AssetUploadAction.REJECT,
|
||||
reason: AssetRejectReason.DUPLICATE,
|
||||
};
|
||||
}
|
||||
|
||||
// TODO mime-check
|
||||
|
||||
return {
|
||||
id,
|
||||
action: AssetUploadAction.ACCEPT,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { AssetRejectReason, AssetUploadAction } from 'src/dtos/asset-v1-response.dto';
|
||||
import { CreateAssetDto } from 'src/dtos/asset-v1.dto';
|
||||
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType } from 'src/entities/asset.entity';
|
||||
import { ExifEntity } from 'src/entities/exif.entity';
|
||||
@ -74,10 +73,7 @@ describe('AssetService', () => {
|
||||
beforeEach(() => {
|
||||
assetRepositoryMockV1 = {
|
||||
get: vitest.fn(),
|
||||
getAllByUserId: vitest.fn(),
|
||||
getAssetsByChecksums: vitest.fn(),
|
||||
getExistingAssets: vitest.fn(),
|
||||
getByOriginalPath: vitest.fn(),
|
||||
};
|
||||
|
||||
accessMock = newAccessRepositoryMock();
|
||||
@ -194,32 +190,4 @@ describe('AssetService', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('bulkUploadCheck', () => {
|
||||
it('should accept hex and base64 checksums', async () => {
|
||||
const file1 = Buffer.from('d2947b871a706081be194569951b7db246907957', 'hex');
|
||||
const file2 = Buffer.from('53be335e99f18a66ff12e9a901c7a6171dd76573', 'hex');
|
||||
|
||||
assetRepositoryMockV1.getAssetsByChecksums.mockResolvedValue([
|
||||
{ id: 'asset-1', checksum: file1 },
|
||||
{ id: 'asset-2', checksum: file2 },
|
||||
]);
|
||||
|
||||
await expect(
|
||||
sut.bulkUploadCheck(authStub.admin, {
|
||||
assets: [
|
||||
{ id: '1', checksum: file1.toString('hex') },
|
||||
{ id: '2', checksum: file2.toString('base64') },
|
||||
],
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
results: [
|
||||
{ id: '1', assetId: 'asset-1', action: AssetUploadAction.REJECT, reason: AssetRejectReason.DUPLICATE },
|
||||
{ id: '2', assetId: 'asset-2', action: AssetUploadAction.REJECT, reason: AssetRejectReason.DUPLICATE },
|
||||
],
|
||||
});
|
||||
|
||||
expect(assetRepositoryMockV1.getAssetsByChecksums).toHaveBeenCalledWith(authStub.admin.user.id, [file1, file2]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -6,23 +6,8 @@ import {
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { AccessCore, Permission } from 'src/cores/access.core';
|
||||
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import {
|
||||
AssetBulkUploadCheckResponseDto,
|
||||
AssetFileUploadResponseDto,
|
||||
AssetRejectReason,
|
||||
AssetUploadAction,
|
||||
CheckExistingAssetsResponseDto,
|
||||
} from 'src/dtos/asset-v1-response.dto';
|
||||
import {
|
||||
AssetBulkUploadCheckDto,
|
||||
AssetSearchDto,
|
||||
CheckExistingAssetsDto,
|
||||
CreateAssetDto,
|
||||
GetAssetThumbnailDto,
|
||||
GetAssetThumbnailFormatEnum,
|
||||
ServeFileDto,
|
||||
} from 'src/dtos/asset-v1.dto';
|
||||
import { AssetFileUploadResponseDto } from 'src/dtos/asset-v1-response.dto';
|
||||
import { CreateAssetDto, GetAssetThumbnailDto, GetAssetThumbnailFormatEnum, ServeFileDto } from 'src/dtos/asset-v1.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType } from 'src/entities/asset.entity';
|
||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||
@ -36,7 +21,6 @@ import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { UploadFile } from 'src/services/asset-media.service';
|
||||
import { CacheControl, ImmichFileResponse, getLivePhotoMotionFilename } from 'src/utils/file';
|
||||
import { mimeTypes } from 'src/utils/mime-types';
|
||||
import { fromChecksum } from 'src/utils/request';
|
||||
import { QueryFailedError } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
@ -112,13 +96,6 @@ export class AssetServiceV1 {
|
||||
}
|
||||
}
|
||||
|
||||
public async getAllAssets(auth: AuthDto, dto: AssetSearchDto): Promise<AssetResponseDto[]> {
|
||||
const userId = dto.userId || auth.user.id;
|
||||
await this.access.requirePermission(auth, Permission.TIMELINE_READ, userId);
|
||||
const assets = await this.assetRepositoryV1.getAllByUserId(userId, dto);
|
||||
return assets.map((asset) => mapAsset(asset, { withStack: true, auth }));
|
||||
}
|
||||
|
||||
async serveThumbnail(auth: AuthDto, assetId: string, dto: GetAssetThumbnailDto): Promise<ImmichFileResponse> {
|
||||
await this.access.requirePermission(auth, Permission.ASSET_VIEW, assetId);
|
||||
|
||||
@ -159,46 +136,6 @@ export class AssetServiceV1 {
|
||||
});
|
||||
}
|
||||
|
||||
async checkExistingAssets(
|
||||
auth: AuthDto,
|
||||
checkExistingAssetsDto: CheckExistingAssetsDto,
|
||||
): Promise<CheckExistingAssetsResponseDto> {
|
||||
return {
|
||||
existingIds: await this.assetRepositoryV1.getExistingAssets(auth.user.id, checkExistingAssetsDto),
|
||||
};
|
||||
}
|
||||
|
||||
async bulkUploadCheck(auth: AuthDto, dto: AssetBulkUploadCheckDto): Promise<AssetBulkUploadCheckResponseDto> {
|
||||
const checksums: Buffer[] = dto.assets.map((asset) => fromChecksum(asset.checksum));
|
||||
const results = await this.assetRepositoryV1.getAssetsByChecksums(auth.user.id, checksums);
|
||||
const checksumMap: Record<string, string> = {};
|
||||
|
||||
for (const { id, checksum } of results) {
|
||||
checksumMap[checksum.toString('hex')] = id;
|
||||
}
|
||||
|
||||
return {
|
||||
results: dto.assets.map(({ id, checksum }) => {
|
||||
const duplicate = checksumMap[fromChecksum(checksum).toString('hex')];
|
||||
if (duplicate) {
|
||||
return {
|
||||
id,
|
||||
assetId: duplicate,
|
||||
action: AssetUploadAction.REJECT,
|
||||
reason: AssetRejectReason.DUPLICATE,
|
||||
};
|
||||
}
|
||||
|
||||
// TODO mime-check
|
||||
|
||||
return {
|
||||
id,
|
||||
action: AssetUploadAction.ACCEPT,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
private getThumbnailPath(asset: AssetEntity, format: GetAssetThumbnailFormatEnum) {
|
||||
switch (format) {
|
||||
case GetAssetThumbnailFormatEnum.WEBP: {
|
||||
|
@ -10,10 +10,12 @@ export const newAssetRepositoryMock = (): Mocked<IAssetRepository> => {
|
||||
getByIds: vitest.fn().mockResolvedValue([]),
|
||||
getByIdsWithAllRelations: vitest.fn().mockResolvedValue([]),
|
||||
getByAlbumId: vitest.fn(),
|
||||
getByDeviceIds: vitest.fn(),
|
||||
getByUserId: vitest.fn(),
|
||||
getById: vitest.fn(),
|
||||
getWithout: vitest.fn(),
|
||||
getByChecksum: vitest.fn(),
|
||||
getByChecksums: vitest.fn(),
|
||||
getUploadAssetIdByChecksum: vitest.fn(),
|
||||
getWith: vitest.fn(),
|
||||
getRandom: vitest.fn(),
|
||||
|
Loading…
Reference in New Issue
Block a user