mirror of
https://github.com/immich-app/immich.git
synced 2025-01-01 08:31:59 +00: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
27 changed files with 286 additions and 572 deletions
|
@ -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', () => {
|
describe('PUT /asset', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).put('/asset');
|
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 { Socket } from 'socket.io-client';
|
||||||
import { errorDto } from 'src/responses';
|
import { errorDto } from 'src/responses';
|
||||||
import { app, asBearerAuth, utils } from 'src/utils';
|
import { app, asBearerAuth, utils } from 'src/utils';
|
||||||
|
@ -31,16 +31,16 @@ describe('/trash', () => {
|
||||||
const { id: assetId } = await utils.createAsset(admin.accessToken);
|
const { id: assetId } = await utils.createAsset(admin.accessToken);
|
||||||
await utils.deleteAssets(admin.accessToken, [assetId]);
|
await utils.deleteAssets(admin.accessToken, [assetId]);
|
||||||
|
|
||||||
const before = await getAllAssets({}, { headers: asBearerAuth(admin.accessToken) });
|
const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
|
||||||
expect(before).toStrictEqual([expect.objectContaining({ id: assetId, isTrashed: true })]);
|
expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: true }));
|
||||||
|
|
||||||
const { status } = await request(app).post('/trash/empty').set('Authorization', `Bearer ${admin.accessToken}`);
|
const { status } = await request(app).post('/trash/empty').set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(status).toBe(204);
|
expect(status).toBe(204);
|
||||||
|
|
||||||
await utils.waitForWebsocketEvent({ event: 'assetDelete', id: assetId });
|
await utils.waitForWebsocketEvent({ event: 'assetDelete', id: assetId });
|
||||||
|
|
||||||
const after = await getAllAssets({}, { headers: asBearerAuth(admin.accessToken) });
|
const after = await getAssetStatistics({ isTrashed: true }, { headers: asBearerAuth(admin.accessToken) });
|
||||||
expect(after.length).toBe(0);
|
expect(after.total).toBe(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -56,14 +56,14 @@ describe('/trash', () => {
|
||||||
const { id: assetId } = await utils.createAsset(admin.accessToken);
|
const { id: assetId } = await utils.createAsset(admin.accessToken);
|
||||||
await utils.deleteAssets(admin.accessToken, [assetId]);
|
await utils.deleteAssets(admin.accessToken, [assetId]);
|
||||||
|
|
||||||
const before = await getAllAssets({}, { headers: asBearerAuth(admin.accessToken) });
|
const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
|
||||||
expect(before).toStrictEqual([expect.objectContaining({ id: assetId, isTrashed: true })]);
|
expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: true }));
|
||||||
|
|
||||||
const { status } = await request(app).post('/trash/restore').set('Authorization', `Bearer ${admin.accessToken}`);
|
const { status } = await request(app).post('/trash/restore').set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(status).toBe(204);
|
expect(status).toBe(204);
|
||||||
|
|
||||||
const after = await getAllAssets({}, { headers: asBearerAuth(admin.accessToken) });
|
const after = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
|
||||||
expect(after).toStrictEqual([expect.objectContaining({ id: assetId, isTrashed: false })]);
|
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 { readFileSync } from 'node:fs';
|
||||||
import { mkdir, readdir, rm, symlink } from 'node:fs/promises';
|
import { mkdir, readdir, rm, symlink } from 'node:fs/promises';
|
||||||
import { asKeyAuth, immichCli, testAssetDir, utils } from 'src/utils';
|
import { asKeyAuth, immichCli, testAssetDir, utils } from 'src/utils';
|
||||||
|
@ -28,8 +28,8 @@ describe(`immich upload`, () => {
|
||||||
);
|
);
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
|
|
||||||
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||||
expect(assets.length).toBe(1);
|
expect(assets.total).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should skip a duplicate file', async () => {
|
it('should skip a duplicate file', async () => {
|
||||||
|
@ -40,8 +40,8 @@ describe(`immich upload`, () => {
|
||||||
);
|
);
|
||||||
expect(first.exitCode).toBe(0);
|
expect(first.exitCode).toBe(0);
|
||||||
|
|
||||||
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||||
expect(assets.length).toBe(1);
|
expect(assets.total).toBe(1);
|
||||||
|
|
||||||
const second = await immichCli(['upload', `${testAssetDir}/albums/nature/silver_fir.jpg`]);
|
const second = await immichCli(['upload', `${testAssetDir}/albums/nature/silver_fir.jpg`]);
|
||||||
expect(second.stderr).toBe('');
|
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(stdout.split('\n')).toEqual(expect.arrayContaining([expect.stringContaining('No files found, exiting')]));
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
|
|
||||||
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||||
expect(assets.length).toBe(0);
|
expect(assets.total).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have accurate dry run', async () => {
|
it('should have accurate dry run', async () => {
|
||||||
|
@ -76,8 +76,8 @@ describe(`immich upload`, () => {
|
||||||
);
|
);
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
|
|
||||||
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||||
expect(assets.length).toBe(0);
|
expect(assets.total).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('dry run should handle duplicates', async () => {
|
it('dry run should handle duplicates', async () => {
|
||||||
|
@ -88,8 +88,8 @@ describe(`immich upload`, () => {
|
||||||
);
|
);
|
||||||
expect(first.exitCode).toBe(0);
|
expect(first.exitCode).toBe(0);
|
||||||
|
|
||||||
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||||
expect(assets.length).toBe(1);
|
expect(assets.total).toBe(1);
|
||||||
|
|
||||||
const second = await immichCli(['upload', `${testAssetDir}/albums/nature/`, '--dry-run']);
|
const second = await immichCli(['upload', `${testAssetDir}/albums/nature/`, '--dry-run']);
|
||||||
expect(second.stderr).toBe('');
|
expect(second.stderr).toBe('');
|
||||||
|
@ -112,8 +112,8 @@ describe(`immich upload`, () => {
|
||||||
);
|
);
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
|
|
||||||
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||||
expect(assets.length).toBe(9);
|
expect(assets.total).toBe(9);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -135,8 +135,8 @@ describe(`immich upload`, () => {
|
||||||
expect(stderr).toBe('');
|
expect(stderr).toBe('');
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
|
|
||||||
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||||
expect(assets.length).toBe(9);
|
expect(assets.total).toBe(9);
|
||||||
|
|
||||||
const albums = await getAllAlbums({}, { headers: asKeyAuth(key) });
|
const albums = await getAllAlbums({}, { headers: asKeyAuth(key) });
|
||||||
expect(albums.length).toBe(1);
|
expect(albums.length).toBe(1);
|
||||||
|
@ -151,8 +151,8 @@ describe(`immich upload`, () => {
|
||||||
expect(response1.stderr).toBe('');
|
expect(response1.stderr).toBe('');
|
||||||
expect(response1.exitCode).toBe(0);
|
expect(response1.exitCode).toBe(0);
|
||||||
|
|
||||||
const assets1 = await getAllAssets({}, { headers: asKeyAuth(key) });
|
const assets1 = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||||
expect(assets1.length).toBe(9);
|
expect(assets1.total).toBe(9);
|
||||||
|
|
||||||
const albums1 = await getAllAlbums({}, { headers: asKeyAuth(key) });
|
const albums1 = await getAllAlbums({}, { headers: asKeyAuth(key) });
|
||||||
expect(albums1.length).toBe(0);
|
expect(albums1.length).toBe(0);
|
||||||
|
@ -167,8 +167,8 @@ describe(`immich upload`, () => {
|
||||||
expect(response2.stderr).toBe('');
|
expect(response2.stderr).toBe('');
|
||||||
expect(response2.exitCode).toBe(0);
|
expect(response2.exitCode).toBe(0);
|
||||||
|
|
||||||
const assets2 = await getAllAssets({}, { headers: asKeyAuth(key) });
|
const assets2 = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||||
expect(assets2.length).toBe(9);
|
expect(assets2.total).toBe(9);
|
||||||
|
|
||||||
const albums2 = await getAllAlbums({}, { headers: asKeyAuth(key) });
|
const albums2 = await getAllAlbums({}, { headers: asKeyAuth(key) });
|
||||||
expect(albums2.length).toBe(1);
|
expect(albums2.length).toBe(1);
|
||||||
|
@ -193,8 +193,8 @@ describe(`immich upload`, () => {
|
||||||
expect(stderr).toBe('');
|
expect(stderr).toBe('');
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
|
|
||||||
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||||
expect(assets.length).toBe(0);
|
expect(assets.total).toBe(0);
|
||||||
|
|
||||||
const albums = await getAllAlbums({}, { headers: asKeyAuth(key) });
|
const albums = await getAllAlbums({}, { headers: asKeyAuth(key) });
|
||||||
expect(albums.length).toBe(0);
|
expect(albums.length).toBe(0);
|
||||||
|
@ -219,8 +219,8 @@ describe(`immich upload`, () => {
|
||||||
expect(stderr).toBe('');
|
expect(stderr).toBe('');
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
|
|
||||||
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||||
expect(assets.length).toBe(9);
|
expect(assets.total).toBe(9);
|
||||||
|
|
||||||
const albums = await getAllAlbums({}, { headers: asKeyAuth(key) });
|
const albums = await getAllAlbums({}, { headers: asKeyAuth(key) });
|
||||||
expect(albums.length).toBe(1);
|
expect(albums.length).toBe(1);
|
||||||
|
@ -245,8 +245,8 @@ describe(`immich upload`, () => {
|
||||||
expect(stderr).toBe('');
|
expect(stderr).toBe('');
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
|
|
||||||
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||||
expect(assets.length).toBe(0);
|
expect(assets.total).toBe(0);
|
||||||
|
|
||||||
const albums = await getAllAlbums({}, { headers: asKeyAuth(key) });
|
const albums = await getAllAlbums({}, { headers: asKeyAuth(key) });
|
||||||
expect(albums.length).toBe(0);
|
expect(albums.length).toBe(0);
|
||||||
|
@ -276,8 +276,8 @@ describe(`immich upload`, () => {
|
||||||
expect(stderr).toBe('');
|
expect(stderr).toBe('');
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
|
|
||||||
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||||
expect(assets.length).toBe(9);
|
expect(assets.total).toBe(9);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have accurate dry run', async () => {
|
it('should have accurate dry run', async () => {
|
||||||
|
@ -302,8 +302,8 @@ describe(`immich upload`, () => {
|
||||||
expect(stderr).toBe('');
|
expect(stderr).toBe('');
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
|
|
||||||
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||||
expect(assets.length).toBe(0);
|
expect(assets.total).toBe(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -328,8 +328,8 @@ describe(`immich upload`, () => {
|
||||||
);
|
);
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
|
|
||||||
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||||
expect(assets.length).toBe(1);
|
expect(assets.total).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error if attempting dry run', async () => {
|
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(stderr).toEqual(`error: option '-n, --dry-run' cannot be used with option '-h, --skip-hash'`);
|
||||||
expect(exitCode).not.toBe(0);
|
expect(exitCode).not.toBe(0);
|
||||||
|
|
||||||
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||||
expect(assets.length).toBe(0);
|
expect(assets.total).toBe(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -367,8 +367,8 @@ describe(`immich upload`, () => {
|
||||||
);
|
);
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
|
|
||||||
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||||
expect(assets.length).toBe(9);
|
expect(assets.total).toBe(9);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject string argument', async () => {
|
it('should reject string argument', async () => {
|
||||||
|
@ -408,8 +408,8 @@ describe(`immich upload`, () => {
|
||||||
);
|
);
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
|
|
||||||
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||||
expect(assets.length).toBe(8);
|
expect(assets.total).toBe(8);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should ignore assets matching glob pattern', async () => {
|
it('should ignore assets matching glob pattern', async () => {
|
||||||
|
@ -429,8 +429,8 @@ describe(`immich upload`, () => {
|
||||||
);
|
);
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
|
|
||||||
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||||
expect(assets.length).toBe(1);
|
expect(assets.total).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have accurate dry run', async () => {
|
it('should have accurate dry run', async () => {
|
||||||
|
@ -451,8 +451,8 @@ describe(`immich upload`, () => {
|
||||||
);
|
);
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
|
|
||||||
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||||
expect(assets.length).toBe(0);
|
expect(assets.total).toBe(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,7 +17,6 @@ import {
|
||||||
createSharedLink,
|
createSharedLink,
|
||||||
createUser,
|
createUser,
|
||||||
deleteAssets,
|
deleteAssets,
|
||||||
getAllAssets,
|
|
||||||
getAllJobsStatus,
|
getAllJobsStatus,
|
||||||
getAssetInfo,
|
getAssetInfo,
|
||||||
getConfigDefaults,
|
getConfigDefaults,
|
||||||
|
@ -340,8 +339,6 @@ export const utils = {
|
||||||
|
|
||||||
getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }),
|
getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }),
|
||||||
|
|
||||||
getAllAssets: (accessToken: string) => getAllAssets({}, { headers: asBearerAuth(accessToken) }),
|
|
||||||
|
|
||||||
metadataSearch: async (accessToken: string, dto: MetadataSearchDto) => {
|
metadataSearch: async (accessToken: string, dto: MetadataSearchDto) => {
|
||||||
return searchMetadata({ metadataSearchDto: dto }, { headers: asBearerAuth(accessToken) });
|
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"
|
"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": {
|
"put": {
|
||||||
"operationId": "updateAssets",
|
"operationId": "updateAssets",
|
||||||
"parameters": [],
|
"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).
|
For a more detailed example, check out the [`@immich/cli`](https://github.com/immich-app/immich/tree/main/cli).
|
||||||
|
|
||||||
```typescript
|
```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
|
const API_KEY = "<API_KEY>"; // process.env.IMMICH_API_KEY
|
||||||
|
|
||||||
init({ baseUrl: "https://demo.immich.app/api", apiKey: API_KEY });
|
init({ baseUrl: "https://demo.immich.app/api", apiKey: API_KEY });
|
||||||
|
|
||||||
const user = await getMyUserInfo();
|
const user = await getMyUserInfo();
|
||||||
const assets = await getAllAssets({ take: 1000 });
|
|
||||||
const albums = await getAllAlbums({});
|
const albums = await getAllAlbums({});
|
||||||
|
|
||||||
console.log({ user, assets, albums });
|
console.log({ user, albums });
|
||||||
```
|
```
|
||||||
|
|
|
@ -1338,37 +1338,6 @@ export function deleteAssets({ assetBulkDeleteDto }: {
|
||||||
body: 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 }: {
|
export function updateAssets({ assetBulkUpdateDto }: {
|
||||||
assetBulkUpdateDto: AssetBulkUpdateDto;
|
assetBulkUpdateDto: AssetBulkUpdateDto;
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
|
HttpCode,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
Inject,
|
Inject,
|
||||||
Param,
|
Param,
|
||||||
ParseFilePipe,
|
ParseFilePipe,
|
||||||
|
Post,
|
||||||
Put,
|
Put,
|
||||||
Res,
|
Res,
|
||||||
UploadedFiles,
|
UploadedFiles,
|
||||||
|
@ -13,8 +15,18 @@ import {
|
||||||
import { ApiConsumes, ApiTags } from '@nestjs/swagger';
|
import { ApiConsumes, ApiTags } from '@nestjs/swagger';
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import { EndpointLifecycle } from 'src/decorators';
|
import { EndpointLifecycle } from 'src/decorators';
|
||||||
import { AssetMediaResponseDto, AssetMediaStatusEnum } from 'src/dtos/asset-media-response.dto';
|
import {
|
||||||
import { AssetMediaReplaceDto, UploadFieldName } from 'src/dtos/asset-media.dto';
|
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 { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||||
|
@ -53,4 +65,30 @@ export class AssetMediaController {
|
||||||
}
|
}
|
||||||
return responseDto;
|
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,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
HttpCode,
|
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
Inject,
|
Inject,
|
||||||
Next,
|
Next,
|
||||||
|
@ -16,20 +15,8 @@ import {
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
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 { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
import { AssetFileUploadResponseDto } from 'src/dtos/asset-v1-response.dto';
|
||||||
import {
|
import { CreateAssetDto, GetAssetThumbnailDto, ServeFileDto } from 'src/dtos/asset-v1.dto';
|
||||||
AssetBulkUploadCheckResponseDto,
|
|
||||||
AssetFileUploadResponseDto,
|
|
||||||
CheckExistingAssetsResponseDto,
|
|
||||||
} from 'src/dtos/asset-v1-response.dto';
|
|
||||||
import {
|
|
||||||
AssetBulkUploadCheckDto,
|
|
||||||
AssetSearchDto,
|
|
||||||
CheckExistingAssetsDto,
|
|
||||||
CreateAssetDto,
|
|
||||||
GetAssetThumbnailDto,
|
|
||||||
ServeFileDto,
|
|
||||||
} from 'src/dtos/asset-v1.dto';
|
|
||||||
import { AuthDto, ImmichHeader } from 'src/dtos/auth.dto';
|
import { AuthDto, ImmichHeader } from 'src/dtos/auth.dto';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { AssetUploadInterceptor } from 'src/middleware/asset-upload.interceptor';
|
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);
|
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;
|
status!: AssetMediaStatusEnum;
|
||||||
id!: string;
|
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 { 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';
|
import { Optional, ValidateDate } from 'src/validation';
|
||||||
|
|
||||||
export enum UploadFieldName {
|
export enum UploadFieldName {
|
||||||
|
@ -33,3 +34,31 @@ export class AssetMediaReplaceDto {
|
||||||
@ApiProperty({ type: 'string', format: 'binary' })
|
@ApiProperty({ type: 'string', format: 'binary' })
|
||||||
[UploadFieldName.ASSET_DATA]!: any;
|
[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 {
|
export class AssetFileUploadResponseDto {
|
||||||
id!: string;
|
id!: string;
|
||||||
duplicate!: boolean;
|
duplicate!: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CheckExistingAssetsResponseDto {
|
|
||||||
existingIds!: string[];
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,68 +1,8 @@
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Type } from 'class-transformer';
|
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
|
||||||
import { ArrayNotEmpty, IsArray, IsEnum, IsInt, IsNotEmpty, IsString, IsUUID, ValidateNested } from 'class-validator';
|
|
||||||
import { UploadFieldName } from 'src/dtos/asset.dto';
|
import { UploadFieldName } from 'src/dtos/asset.dto';
|
||||||
import { Optional, ValidateBoolean, ValidateDate } from 'src/validation';
|
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 {
|
export class CreateAssetDto {
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsString()
|
@IsString()
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { AssetSearchDto, CheckExistingAssetsDto } from 'src/dtos/asset-v1.dto';
|
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
|
|
||||||
export interface AssetCheck {
|
export interface AssetCheck {
|
||||||
|
@ -12,10 +11,7 @@ export interface AssetOwnerCheck extends AssetCheck {
|
||||||
|
|
||||||
export interface IAssetRepositoryV1 {
|
export interface IAssetRepositoryV1 {
|
||||||
get(id: string): Promise<AssetEntity | null>;
|
get(id: string): Promise<AssetEntity | null>;
|
||||||
getAllByUserId(userId: string, dto: AssetSearchDto): Promise<AssetEntity[]>;
|
|
||||||
getAssetsByChecksums(userId: string, checksums: Buffer[]): Promise<AssetCheck[]>;
|
getAssetsByChecksums(userId: string, checksums: Buffer[]): Promise<AssetCheck[]>;
|
||||||
getExistingAssets(userId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise<string[]>;
|
|
||||||
getByOriginalPath(originalPath: string): Promise<AssetOwnerCheck | null>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IAssetRepositoryV1 = 'IAssetRepositoryV1';
|
export const IAssetRepositoryV1 = 'IAssetRepositoryV1';
|
||||||
|
|
|
@ -168,8 +168,10 @@ export interface IAssetRepository {
|
||||||
getByIdsWithAllRelations(ids: string[]): Promise<AssetEntity[]>;
|
getByIdsWithAllRelations(ids: string[]): Promise<AssetEntity[]>;
|
||||||
getByDayOfYear(ownerIds: string[], monthDay: MonthDay): Promise<AssetEntity[]>;
|
getByDayOfYear(ownerIds: string[], monthDay: MonthDay): Promise<AssetEntity[]>;
|
||||||
getByChecksum(libraryId: string | null, checksum: Buffer): Promise<AssetEntity | null>;
|
getByChecksum(libraryId: string | null, checksum: Buffer): Promise<AssetEntity | null>;
|
||||||
|
getByChecksums(userId: string, checksums: Buffer[]): Promise<AssetEntity[]>;
|
||||||
getUploadAssetIdByChecksum(ownerId: string, checksum: Buffer): Promise<string | undefined>;
|
getUploadAssetIdByChecksum(ownerId: string, checksum: Buffer): Promise<string | undefined>;
|
||||||
getByAlbumId(pagination: PaginationOptions, albumId: string): Paginated<AssetEntity>;
|
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>;
|
getByUserId(pagination: PaginationOptions, userId: string, options?: AssetSearchOptions): Paginated<AssetEntity>;
|
||||||
getById(
|
getById(
|
||||||
id: string,
|
id: string,
|
||||||
|
|
|
@ -482,6 +482,20 @@ WHERE
|
||||||
LIMIT
|
LIMIT
|
||||||
1
|
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
|
-- AssetRepository.getUploadAssetIdByChecksum
|
||||||
SELECT
|
SELECT
|
||||||
"AssetEntity"."id" AS "AssetEntity_id"
|
"AssetEntity"."id" AS "AssetEntity_id"
|
||||||
|
|
|
@ -74,43 +74,6 @@ WHERE
|
||||||
((("LibraryEntity"."ownerId" = $1)))
|
((("LibraryEntity"."ownerId" = $1)))
|
||||||
AND ("LibraryEntity"."deletedAt" IS NULL)
|
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
|
-- LibraryRepository.getAll
|
||||||
SELECT
|
SELECT
|
||||||
"LibraryEntity"."id" AS "LibraryEntity_id",
|
"LibraryEntity"."id" AS "LibraryEntity_id",
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { AssetSearchDto, CheckExistingAssetsDto } from 'src/dtos/asset-v1.dto';
|
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { AssetCheck, AssetOwnerCheck, IAssetRepositoryV1 } from 'src/interfaces/asset-v1.interface';
|
import { AssetCheck, IAssetRepositoryV1 } from 'src/interfaces/asset-v1.interface';
|
||||||
import { OptionalBetween } from 'src/utils/database';
|
|
||||||
import { In } from 'typeorm/find-options/operator/In.js';
|
import { In } from 'typeorm/find-options/operator/In.js';
|
||||||
import { Repository } from 'typeorm/repository/Repository.js';
|
import { Repository } from 'typeorm/repository/Repository.js';
|
||||||
|
|
||||||
|
@ -11,36 +9,6 @@ import { Repository } from 'typeorm/repository/Repository.js';
|
||||||
export class AssetRepositoryV1 implements IAssetRepositoryV1 {
|
export class AssetRepositoryV1 implements IAssetRepositoryV1 {
|
||||||
constructor(@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>) {}
|
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> {
|
get(id: string): Promise<AssetEntity | null> {
|
||||||
return this.assetRepository.findOne({
|
return this.assetRepository.findOne({
|
||||||
where: { id },
|
where: { id },
|
||||||
|
@ -73,30 +41,4 @@ export class AssetRepositoryV1 implements IAssetRepositoryV1 {
|
||||||
withDeleted: true,
|
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(
|
getByUserId(
|
||||||
pagination: PaginationOptions,
|
pagination: PaginationOptions,
|
||||||
userId: string,
|
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] })
|
@GenerateSql({ params: [DummyValue.UUID, DummyValue.BUFFER] })
|
||||||
async getUploadAssetIdByChecksum(ownerId: string, checksum: Buffer): Promise<string | undefined> {
|
async getUploadAssetIdByChecksum(ownerId: string, checksum: Buffer): Promise<string | undefined> {
|
||||||
const asset = await this.repository.findOne({
|
const asset = await this.repository.findOne({
|
||||||
|
|
|
@ -39,21 +39,6 @@ export class LibraryRepository implements ILibraryRepository {
|
||||||
return this.repository.countBy({ ownerId });
|
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: [] })
|
@GenerateSql({ params: [] })
|
||||||
getAll(withDeleted = false): Promise<LibraryEntity[]> {
|
getAll(withDeleted = false): Promise<LibraryEntity[]> {
|
||||||
return this.repository.find({
|
return this.repository.find({
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Stats } from 'node:fs';
|
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 { AssetMediaReplaceDto } from 'src/dtos/asset-media.dto';
|
||||||
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType } from 'src/entities/asset.entity';
|
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType } from 'src/entities/asset.entity';
|
||||||
import { ExifEntity } from 'src/entities/exif.entity';
|
import { ExifEntity } from 'src/entities/exif.entity';
|
||||||
|
@ -277,4 +277,31 @@ describe('AssetMediaService', () => {
|
||||||
expect(userMock.updateUsage).not.toHaveBeenCalled();
|
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 { BadRequestException, Inject, Injectable, InternalServerErrorException } from '@nestjs/common';
|
||||||
import { AccessCore, Permission } from 'src/cores/access.core';
|
import { AccessCore, Permission } from 'src/cores/access.core';
|
||||||
import { AssetMediaResponseDto, AssetMediaStatusEnum } from 'src/dtos/asset-media-response.dto';
|
import {
|
||||||
import { AssetMediaReplaceDto, UploadFieldName } from 'src/dtos/asset-media.dto';
|
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 { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity';
|
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
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 { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||||
import { mimeTypes } from 'src/utils/mime-types';
|
import { mimeTypes } from 'src/utils/mime-types';
|
||||||
|
import { fromChecksum } from 'src/utils/request';
|
||||||
import { QueryFailedError } from 'typeorm';
|
import { QueryFailedError } from 'typeorm';
|
||||||
|
|
||||||
export interface UploadRequest {
|
export interface UploadRequest {
|
||||||
auth: AuthDto | null;
|
auth: AuthDto | null;
|
||||||
fieldName: UploadFieldName;
|
fieldName: UploadFieldName;
|
||||||
|
@ -174,4 +186,49 @@ export class AssetMediaService {
|
||||||
throw new BadRequestException('Quota has been exceeded!');
|
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 { CreateAssetDto } from 'src/dtos/asset-v1.dto';
|
||||||
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType } from 'src/entities/asset.entity';
|
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType } from 'src/entities/asset.entity';
|
||||||
import { ExifEntity } from 'src/entities/exif.entity';
|
import { ExifEntity } from 'src/entities/exif.entity';
|
||||||
|
@ -74,10 +73,7 @@ describe('AssetService', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
assetRepositoryMockV1 = {
|
assetRepositoryMockV1 = {
|
||||||
get: vitest.fn(),
|
get: vitest.fn(),
|
||||||
getAllByUserId: vitest.fn(),
|
|
||||||
getAssetsByChecksums: vitest.fn(),
|
getAssetsByChecksums: vitest.fn(),
|
||||||
getExistingAssets: vitest.fn(),
|
|
||||||
getByOriginalPath: vitest.fn(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
accessMock = newAccessRepositoryMock();
|
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,
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { AccessCore, Permission } from 'src/cores/access.core';
|
import { AccessCore, Permission } from 'src/cores/access.core';
|
||||||
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
|
import { AssetFileUploadResponseDto } from 'src/dtos/asset-v1-response.dto';
|
||||||
import {
|
import { CreateAssetDto, GetAssetThumbnailDto, GetAssetThumbnailFormatEnum, ServeFileDto } from 'src/dtos/asset-v1.dto';
|
||||||
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 { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType } from 'src/entities/asset.entity';
|
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType } from 'src/entities/asset.entity';
|
||||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
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 { UploadFile } from 'src/services/asset-media.service';
|
||||||
import { CacheControl, ImmichFileResponse, getLivePhotoMotionFilename } from 'src/utils/file';
|
import { CacheControl, ImmichFileResponse, getLivePhotoMotionFilename } from 'src/utils/file';
|
||||||
import { mimeTypes } from 'src/utils/mime-types';
|
import { mimeTypes } from 'src/utils/mime-types';
|
||||||
import { fromChecksum } from 'src/utils/request';
|
|
||||||
import { QueryFailedError } from 'typeorm';
|
import { QueryFailedError } from 'typeorm';
|
||||||
|
|
||||||
@Injectable()
|
@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> {
|
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);
|
||||||
|
|
||||||
|
@ -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) {
|
private getThumbnailPath(asset: AssetEntity, format: GetAssetThumbnailFormatEnum) {
|
||||||
switch (format) {
|
switch (format) {
|
||||||
case GetAssetThumbnailFormatEnum.WEBP: {
|
case GetAssetThumbnailFormatEnum.WEBP: {
|
||||||
|
|
|
@ -10,10 +10,12 @@ export const newAssetRepositoryMock = (): Mocked<IAssetRepository> => {
|
||||||
getByIds: vitest.fn().mockResolvedValue([]),
|
getByIds: vitest.fn().mockResolvedValue([]),
|
||||||
getByIdsWithAllRelations: vitest.fn().mockResolvedValue([]),
|
getByIdsWithAllRelations: vitest.fn().mockResolvedValue([]),
|
||||||
getByAlbumId: vitest.fn(),
|
getByAlbumId: vitest.fn(),
|
||||||
|
getByDeviceIds: vitest.fn(),
|
||||||
getByUserId: vitest.fn(),
|
getByUserId: vitest.fn(),
|
||||||
getById: vitest.fn(),
|
getById: vitest.fn(),
|
||||||
getWithout: vitest.fn(),
|
getWithout: vitest.fn(),
|
||||||
getByChecksum: vitest.fn(),
|
getByChecksum: vitest.fn(),
|
||||||
|
getByChecksums: vitest.fn(),
|
||||||
getUploadAssetIdByChecksum: vitest.fn(),
|
getUploadAssetIdByChecksum: vitest.fn(),
|
||||||
getWith: vitest.fn(),
|
getWith: vitest.fn(),
|
||||||
getRandom: vitest.fn(),
|
getRandom: vitest.fn(),
|
||||||
|
|
Loading…
Reference in a new issue