mirror of
https://github.com/immich-app/immich.git
synced 2024-12-29 15:11:58 +00:00
refactor(server): library service (#8050)
* refactor: library service * chore: open api * fix: checks
This commit is contained in:
parent
761e7fdd2d
commit
40262c30cb
21 changed files with 189 additions and 318 deletions
|
@ -27,7 +27,7 @@ describe('/library', () => {
|
||||||
await utils.resetDatabase();
|
await utils.resetDatabase();
|
||||||
admin = await utils.adminSetup();
|
admin = await utils.adminSetup();
|
||||||
user = await utils.userSetup(admin.accessToken, userDto.user1);
|
user = await utils.userSetup(admin.accessToken, userDto.user1);
|
||||||
library = await utils.createLibrary(admin.accessToken, { type: LibraryType.External });
|
library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId, type: LibraryType.External });
|
||||||
websocket = await utils.connectWebsocket(admin.accessToken);
|
websocket = await utils.connectWebsocket(admin.accessToken);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -82,7 +82,7 @@ describe('/library', () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.post('/library')
|
.post('/library')
|
||||||
.set('Authorization', `Bearer ${user.accessToken}`)
|
.set('Authorization', `Bearer ${user.accessToken}`)
|
||||||
.send({ type: LibraryType.External });
|
.send({ ownerId: admin.userId, type: LibraryType.External });
|
||||||
|
|
||||||
expect(status).toBe(403);
|
expect(status).toBe(403);
|
||||||
expect(body).toEqual(errorDto.forbidden);
|
expect(body).toEqual(errorDto.forbidden);
|
||||||
|
@ -92,7 +92,7 @@ describe('/library', () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.post('/library')
|
.post('/library')
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({ type: LibraryType.External });
|
.send({ ownerId: admin.userId, type: LibraryType.External });
|
||||||
|
|
||||||
expect(status).toBe(201);
|
expect(status).toBe(201);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
|
@ -113,6 +113,7 @@ describe('/library', () => {
|
||||||
.post('/library')
|
.post('/library')
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({
|
.send({
|
||||||
|
ownerId: admin.userId,
|
||||||
type: LibraryType.External,
|
type: LibraryType.External,
|
||||||
name: 'My Awesome Library',
|
name: 'My Awesome Library',
|
||||||
importPaths: ['/path/to/import'],
|
importPaths: ['/path/to/import'],
|
||||||
|
@ -133,6 +134,7 @@ describe('/library', () => {
|
||||||
.post('/library')
|
.post('/library')
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({
|
.send({
|
||||||
|
ownerId: admin.userId,
|
||||||
type: LibraryType.External,
|
type: LibraryType.External,
|
||||||
name: 'My Awesome Library',
|
name: 'My Awesome Library',
|
||||||
importPaths: ['/path', '/path'],
|
importPaths: ['/path', '/path'],
|
||||||
|
@ -148,6 +150,7 @@ describe('/library', () => {
|
||||||
.post('/library')
|
.post('/library')
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({
|
.send({
|
||||||
|
ownerId: admin.userId,
|
||||||
type: LibraryType.External,
|
type: LibraryType.External,
|
||||||
name: 'My Awesome Library',
|
name: 'My Awesome Library',
|
||||||
importPaths: ['/path/to/import'],
|
importPaths: ['/path/to/import'],
|
||||||
|
@ -162,7 +165,7 @@ describe('/library', () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.post('/library')
|
.post('/library')
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({ type: LibraryType.Upload });
|
.send({ ownerId: admin.userId, type: LibraryType.Upload });
|
||||||
|
|
||||||
expect(status).toBe(201);
|
expect(status).toBe(201);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
|
@ -182,7 +185,7 @@ describe('/library', () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.post('/library')
|
.post('/library')
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({ type: LibraryType.Upload, name: 'My Awesome Library' });
|
.send({ ownerId: admin.userId, type: LibraryType.Upload, name: 'My Awesome Library' });
|
||||||
|
|
||||||
expect(status).toBe(201);
|
expect(status).toBe(201);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
|
@ -196,7 +199,7 @@ describe('/library', () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.post('/library')
|
.post('/library')
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({ type: LibraryType.Upload, importPaths: ['/path/to/import'] });
|
.send({ ownerId: admin.userId, type: LibraryType.Upload, importPaths: ['/path/to/import'] });
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest('Upload libraries cannot have import paths'));
|
expect(body).toEqual(errorDto.badRequest('Upload libraries cannot have import paths'));
|
||||||
|
@ -206,7 +209,7 @@ describe('/library', () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.post('/library')
|
.post('/library')
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({ type: LibraryType.Upload, exclusionPatterns: ['**/Raw/**'] });
|
.send({ ownerId: admin.userId, type: LibraryType.Upload, exclusionPatterns: ['**/Raw/**'] });
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest('Upload libraries cannot have exclusion patterns'));
|
expect(body).toEqual(errorDto.badRequest('Upload libraries cannot have exclusion patterns'));
|
||||||
|
@ -330,7 +333,10 @@ describe('/library', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get library by id', async () => {
|
it('should get library by id', async () => {
|
||||||
const library = await utils.createLibrary(admin.accessToken, { type: LibraryType.External });
|
const library = await utils.createLibrary(admin.accessToken, {
|
||||||
|
ownerId: admin.userId,
|
||||||
|
type: LibraryType.External,
|
||||||
|
});
|
||||||
|
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get(`/library/${library.id}`)
|
.get(`/library/${library.id}`)
|
||||||
|
@ -386,7 +392,10 @@ describe('/library', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should delete an external library', async () => {
|
it('should delete an external library', async () => {
|
||||||
const library = await utils.createLibrary(admin.accessToken, { type: LibraryType.External });
|
const library = await utils.createLibrary(admin.accessToken, {
|
||||||
|
ownerId: admin.userId,
|
||||||
|
type: LibraryType.External,
|
||||||
|
});
|
||||||
|
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.delete(`/library/${library.id}`)
|
.delete(`/library/${library.id}`)
|
||||||
|
@ -407,6 +416,7 @@ describe('/library', () => {
|
||||||
|
|
||||||
it('should delete an external library with assets', async () => {
|
it('should delete an external library with assets', async () => {
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
const library = await utils.createLibrary(admin.accessToken, {
|
||||||
|
ownerId: admin.userId,
|
||||||
type: LibraryType.External,
|
type: LibraryType.External,
|
||||||
importPaths: [`${testAssetDirInternal}/temp`],
|
importPaths: [`${testAssetDirInternal}/temp`],
|
||||||
});
|
});
|
||||||
|
@ -455,6 +465,7 @@ describe('/library', () => {
|
||||||
|
|
||||||
it('should not scan an upload library', async () => {
|
it('should not scan an upload library', async () => {
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
const library = await utils.createLibrary(admin.accessToken, {
|
||||||
|
ownerId: admin.userId,
|
||||||
type: LibraryType.Upload,
|
type: LibraryType.Upload,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -468,6 +479,7 @@ describe('/library', () => {
|
||||||
|
|
||||||
it('should scan external library', async () => {
|
it('should scan external library', async () => {
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
const library = await utils.createLibrary(admin.accessToken, {
|
||||||
|
ownerId: admin.userId,
|
||||||
type: LibraryType.External,
|
type: LibraryType.External,
|
||||||
importPaths: [`${testAssetDirInternal}/temp/directoryA`],
|
importPaths: [`${testAssetDirInternal}/temp/directoryA`],
|
||||||
});
|
});
|
||||||
|
@ -483,6 +495,7 @@ describe('/library', () => {
|
||||||
|
|
||||||
it('should scan external library with exclusion pattern', async () => {
|
it('should scan external library with exclusion pattern', async () => {
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
const library = await utils.createLibrary(admin.accessToken, {
|
||||||
|
ownerId: admin.userId,
|
||||||
type: LibraryType.External,
|
type: LibraryType.External,
|
||||||
importPaths: [`${testAssetDirInternal}/temp`],
|
importPaths: [`${testAssetDirInternal}/temp`],
|
||||||
exclusionPatterns: ['**/directoryA'],
|
exclusionPatterns: ['**/directoryA'],
|
||||||
|
@ -499,6 +512,7 @@ describe('/library', () => {
|
||||||
|
|
||||||
it('should scan multiple import paths', async () => {
|
it('should scan multiple import paths', async () => {
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
const library = await utils.createLibrary(admin.accessToken, {
|
||||||
|
ownerId: admin.userId,
|
||||||
type: LibraryType.External,
|
type: LibraryType.External,
|
||||||
importPaths: [`${testAssetDirInternal}/temp/directoryA`, `${testAssetDirInternal}/temp/directoryB`],
|
importPaths: [`${testAssetDirInternal}/temp/directoryA`, `${testAssetDirInternal}/temp/directoryB`],
|
||||||
});
|
});
|
||||||
|
@ -515,6 +529,7 @@ describe('/library', () => {
|
||||||
|
|
||||||
it('should pick up new files', async () => {
|
it('should pick up new files', async () => {
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
const library = await utils.createLibrary(admin.accessToken, {
|
||||||
|
ownerId: admin.userId,
|
||||||
type: LibraryType.External,
|
type: LibraryType.External,
|
||||||
importPaths: [`${testAssetDirInternal}/temp`],
|
importPaths: [`${testAssetDirInternal}/temp`],
|
||||||
});
|
});
|
||||||
|
|
BIN
mobile/openapi/doc/CreateLibraryDto.md
generated
BIN
mobile/openapi/doc/CreateLibraryDto.md
generated
Binary file not shown.
Binary file not shown.
BIN
mobile/openapi/lib/model/create_library_dto.dart
generated
BIN
mobile/openapi/lib/model/create_library_dto.dart
generated
Binary file not shown.
Binary file not shown.
|
@ -7646,6 +7646,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
"ownerId",
|
||||||
"type"
|
"type"
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
@ -10689,7 +10690,8 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"importPath"
|
"importPath",
|
||||||
|
"isValid"
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
|
|
@ -466,7 +466,7 @@ export type CreateLibraryDto = {
|
||||||
isVisible?: boolean;
|
isVisible?: boolean;
|
||||||
isWatched?: boolean;
|
isWatched?: boolean;
|
||||||
name?: string;
|
name?: string;
|
||||||
ownerId?: string;
|
ownerId: string;
|
||||||
"type": LibraryType;
|
"type": LibraryType;
|
||||||
};
|
};
|
||||||
export type UpdateLibraryDto = {
|
export type UpdateLibraryDto = {
|
||||||
|
@ -491,7 +491,7 @@ export type ValidateLibraryDto = {
|
||||||
};
|
};
|
||||||
export type ValidateLibraryImportPathResponseDto = {
|
export type ValidateLibraryImportPathResponseDto = {
|
||||||
importPath: string;
|
importPath: string;
|
||||||
isValid?: boolean;
|
isValid: boolean;
|
||||||
message?: string;
|
message?: string;
|
||||||
};
|
};
|
||||||
export type ValidateLibraryResponseDto = {
|
export type ValidateLibraryResponseDto = {
|
||||||
|
|
|
@ -46,6 +46,7 @@ describe(`Library watcher (e2e)`, () => {
|
||||||
describe('Single import path', () => {
|
describe('Single import path', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await api.libraryApi.create(server, admin.accessToken, {
|
await api.libraryApi.create(server, admin.accessToken, {
|
||||||
|
ownerId: admin.userId,
|
||||||
type: LibraryType.EXTERNAL,
|
type: LibraryType.EXTERNAL,
|
||||||
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
|
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
|
||||||
});
|
});
|
||||||
|
@ -133,6 +134,7 @@ describe(`Library watcher (e2e)`, () => {
|
||||||
await fs.mkdir(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir3`, { recursive: true });
|
await fs.mkdir(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir3`, { recursive: true });
|
||||||
|
|
||||||
await api.libraryApi.create(server, admin.accessToken, {
|
await api.libraryApi.create(server, admin.accessToken, {
|
||||||
|
ownerId: admin.userId,
|
||||||
type: LibraryType.EXTERNAL,
|
type: LibraryType.EXTERNAL,
|
||||||
importPaths: [
|
importPaths: [
|
||||||
`${IMMICH_TEST_ASSET_TEMP_PATH}/dir1`,
|
`${IMMICH_TEST_ASSET_TEMP_PATH}/dir1`,
|
||||||
|
@ -190,6 +192,7 @@ describe(`Library watcher (e2e)`, () => {
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
library = await api.libraryApi.create(server, admin.accessToken, {
|
library = await api.libraryApi.create(server, admin.accessToken, {
|
||||||
|
ownerId: admin.userId,
|
||||||
type: LibraryType.EXTERNAL,
|
type: LibraryType.EXTERNAL,
|
||||||
importPaths: [
|
importPaths: [
|
||||||
`${IMMICH_TEST_ASSET_TEMP_PATH}/dir1`,
|
`${IMMICH_TEST_ASSET_TEMP_PATH}/dir1`,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { LibraryResponseDto, LoginResponseDto } from '@app/domain';
|
import { LoginResponseDto } from '@app/domain';
|
||||||
import { LibraryController } from '@app/immich';
|
import { LibraryController } from '@app/immich';
|
||||||
import { AssetType, LibraryType } from '@app/infra/entities';
|
import { LibraryType } from '@app/infra/entities';
|
||||||
import { errorStub, uuidStub } from '@test/fixtures';
|
import { errorStub, uuidStub } from '@test/fixtures';
|
||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
|
@ -41,6 +41,7 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const library = await api.libraryApi.create(server, admin.accessToken, {
|
const library = await api.libraryApi.create(server, admin.accessToken, {
|
||||||
|
ownerId: admin.userId,
|
||||||
type: LibraryType.EXTERNAL,
|
type: LibraryType.EXTERNAL,
|
||||||
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
|
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
|
||||||
});
|
});
|
||||||
|
@ -72,6 +73,7 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
||||||
|
|
||||||
it('should scan new files', async () => {
|
it('should scan new files', async () => {
|
||||||
const library = await api.libraryApi.create(server, admin.accessToken, {
|
const library = await api.libraryApi.create(server, admin.accessToken, {
|
||||||
|
ownerId: admin.userId,
|
||||||
type: LibraryType.EXTERNAL,
|
type: LibraryType.EXTERNAL,
|
||||||
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
|
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
|
||||||
});
|
});
|
||||||
|
@ -107,6 +109,7 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
||||||
describe('with refreshModifiedFiles=true', () => {
|
describe('with refreshModifiedFiles=true', () => {
|
||||||
it('should reimport modified files', async () => {
|
it('should reimport modified files', async () => {
|
||||||
const library = await api.libraryApi.create(server, admin.accessToken, {
|
const library = await api.libraryApi.create(server, admin.accessToken, {
|
||||||
|
ownerId: admin.userId,
|
||||||
type: LibraryType.EXTERNAL,
|
type: LibraryType.EXTERNAL,
|
||||||
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
|
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
|
||||||
});
|
});
|
||||||
|
@ -153,6 +156,7 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
||||||
|
|
||||||
it('should not reimport unmodified files', async () => {
|
it('should not reimport unmodified files', async () => {
|
||||||
const library = await api.libraryApi.create(server, admin.accessToken, {
|
const library = await api.libraryApi.create(server, admin.accessToken, {
|
||||||
|
ownerId: admin.userId,
|
||||||
type: LibraryType.EXTERNAL,
|
type: LibraryType.EXTERNAL,
|
||||||
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
|
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
|
||||||
});
|
});
|
||||||
|
@ -192,6 +196,7 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
||||||
describe('with refreshAllFiles=true', () => {
|
describe('with refreshAllFiles=true', () => {
|
||||||
it('should reimport all files', async () => {
|
it('should reimport all files', async () => {
|
||||||
const library = await api.libraryApi.create(server, admin.accessToken, {
|
const library = await api.libraryApi.create(server, admin.accessToken, {
|
||||||
|
ownerId: admin.userId,
|
||||||
type: LibraryType.EXTERNAL,
|
type: LibraryType.EXTERNAL,
|
||||||
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
|
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
|
||||||
});
|
});
|
||||||
|
@ -251,6 +256,7 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const library = await api.libraryApi.create(server, admin.accessToken, {
|
const library = await api.libraryApi.create(server, admin.accessToken, {
|
||||||
|
ownerId: admin.userId,
|
||||||
type: LibraryType.EXTERNAL,
|
type: LibraryType.EXTERNAL,
|
||||||
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
|
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
|
||||||
});
|
});
|
||||||
|
@ -277,6 +283,7 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
||||||
|
|
||||||
it('should not remove online files', async () => {
|
it('should not remove online files', async () => {
|
||||||
const library = await api.libraryApi.create(server, admin.accessToken, {
|
const library = await api.libraryApi.create(server, admin.accessToken, {
|
||||||
|
ownerId: admin.userId,
|
||||||
type: LibraryType.EXTERNAL,
|
type: LibraryType.EXTERNAL,
|
||||||
importPaths: [`${IMMICH_TEST_ASSET_PATH}/albums/nature`],
|
importPaths: [`${IMMICH_TEST_ASSET_PATH}/albums/nature`],
|
||||||
});
|
});
|
||||||
|
|
|
@ -153,7 +153,7 @@
|
||||||
"coverageDirectory": "./coverage",
|
"coverageDirectory": "./coverage",
|
||||||
"coverageThreshold": {
|
"coverageThreshold": {
|
||||||
"./src/domain/": {
|
"./src/domain/": {
|
||||||
"branches": 79,
|
"branches": 75,
|
||||||
"functions": 80,
|
"functions": 80,
|
||||||
"lines": 90,
|
"lines": 90,
|
||||||
"statements": 90
|
"statements": 90
|
||||||
|
|
|
@ -33,12 +33,6 @@ export enum Permission {
|
||||||
TIMELINE_READ = 'timeline.read',
|
TIMELINE_READ = 'timeline.read',
|
||||||
TIMELINE_DOWNLOAD = 'timeline.download',
|
TIMELINE_DOWNLOAD = 'timeline.download',
|
||||||
|
|
||||||
LIBRARY_CREATE = 'library.create',
|
|
||||||
LIBRARY_READ = 'library.read',
|
|
||||||
LIBRARY_UPDATE = 'library.update',
|
|
||||||
LIBRARY_DELETE = 'library.delete',
|
|
||||||
LIBRARY_DOWNLOAD = 'library.download',
|
|
||||||
|
|
||||||
PERSON_READ = 'person.read',
|
PERSON_READ = 'person.read',
|
||||||
PERSON_WRITE = 'person.write',
|
PERSON_WRITE = 'person.write',
|
||||||
PERSON_MERGE = 'person.merge',
|
PERSON_MERGE = 'person.merge',
|
||||||
|
@ -261,29 +255,6 @@ export class AccessCore {
|
||||||
return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set();
|
return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set();
|
||||||
}
|
}
|
||||||
|
|
||||||
case Permission.LIBRARY_READ: {
|
|
||||||
if (auth.user.isAdmin) {
|
|
||||||
return new Set(ids);
|
|
||||||
}
|
|
||||||
const isOwner = await this.repository.library.checkOwnerAccess(auth.user.id, ids);
|
|
||||||
const isPartner = await this.repository.library.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner));
|
|
||||||
return setUnion(isOwner, isPartner);
|
|
||||||
}
|
|
||||||
|
|
||||||
case Permission.LIBRARY_UPDATE: {
|
|
||||||
if (auth.user.isAdmin) {
|
|
||||||
return new Set(ids);
|
|
||||||
}
|
|
||||||
return await this.repository.library.checkOwnerAccess(auth.user.id, ids);
|
|
||||||
}
|
|
||||||
|
|
||||||
case Permission.LIBRARY_DELETE: {
|
|
||||||
if (auth.user.isAdmin) {
|
|
||||||
return new Set(ids);
|
|
||||||
}
|
|
||||||
return await this.repository.library.checkOwnerAccess(auth.user.id, ids);
|
|
||||||
}
|
|
||||||
|
|
||||||
case Permission.PERSON_READ: {
|
case Permission.PERSON_READ: {
|
||||||
return await this.repository.person.checkOwnerAccess(auth.user.id, ids);
|
return await this.repository.person.checkOwnerAccess(auth.user.id, ids);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,8 @@ export class CreateLibraryDto {
|
||||||
@ApiProperty({ enumName: 'LibraryType', enum: LibraryType })
|
@ApiProperty({ enumName: 'LibraryType', enum: LibraryType })
|
||||||
type!: LibraryType;
|
type!: LibraryType;
|
||||||
|
|
||||||
@ValidateUUID({ optional: true })
|
@ValidateUUID()
|
||||||
ownerId?: string;
|
ownerId!: string;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@Optional()
|
@Optional()
|
||||||
|
|
|
@ -706,7 +706,7 @@ describe(LibraryService.name, () => {
|
||||||
libraryMock.getUploadLibraryCount.mockResolvedValue(2);
|
libraryMock.getUploadLibraryCount.mockResolvedValue(2);
|
||||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
||||||
|
|
||||||
await sut.delete(authStub.admin, libraryStub.externalLibrary1.id);
|
await sut.delete(libraryStub.externalLibrary1.id);
|
||||||
|
|
||||||
expect(jobMock.queue).toHaveBeenCalledWith({
|
expect(jobMock.queue).toHaveBeenCalledWith({
|
||||||
name: JobName.LIBRARY_DELETE,
|
name: JobName.LIBRARY_DELETE,
|
||||||
|
@ -721,9 +721,7 @@ describe(LibraryService.name, () => {
|
||||||
libraryMock.getUploadLibraryCount.mockResolvedValue(1);
|
libraryMock.getUploadLibraryCount.mockResolvedValue(1);
|
||||||
libraryMock.get.mockResolvedValue(libraryStub.uploadLibrary1);
|
libraryMock.get.mockResolvedValue(libraryStub.uploadLibrary1);
|
||||||
|
|
||||||
await expect(sut.delete(authStub.admin, libraryStub.uploadLibrary1.id)).rejects.toBeInstanceOf(
|
await expect(sut.delete(libraryStub.uploadLibrary1.id)).rejects.toBeInstanceOf(BadRequestException);
|
||||||
BadRequestException,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(jobMock.queue).not.toHaveBeenCalled();
|
expect(jobMock.queue).not.toHaveBeenCalled();
|
||||||
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
||||||
|
@ -735,7 +733,7 @@ describe(LibraryService.name, () => {
|
||||||
libraryMock.getUploadLibraryCount.mockResolvedValue(1);
|
libraryMock.getUploadLibraryCount.mockResolvedValue(1);
|
||||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
||||||
|
|
||||||
await sut.delete(authStub.admin, libraryStub.externalLibrary1.id);
|
await sut.delete(libraryStub.externalLibrary1.id);
|
||||||
|
|
||||||
expect(jobMock.queue).toHaveBeenCalledWith({
|
expect(jobMock.queue).toHaveBeenCalledWith({
|
||||||
name: JobName.LIBRARY_DELETE,
|
name: JobName.LIBRARY_DELETE,
|
||||||
|
@ -757,26 +755,16 @@ describe(LibraryService.name, () => {
|
||||||
storageMock.watch.mockImplementation(makeMockWatcher({ close: mockClose }));
|
storageMock.watch.mockImplementation(makeMockWatcher({ close: mockClose }));
|
||||||
|
|
||||||
await sut.init();
|
await sut.init();
|
||||||
await sut.delete(authStub.admin, libraryStub.externalLibraryWithImportPaths1.id);
|
await sut.delete(libraryStub.externalLibraryWithImportPaths1.id);
|
||||||
|
|
||||||
expect(mockClose).toHaveBeenCalled();
|
expect(mockClose).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getCount', () => {
|
|
||||||
it('should call the repository', async () => {
|
|
||||||
libraryMock.getCountForUser.mockResolvedValue(17);
|
|
||||||
|
|
||||||
await expect(sut.getCount(authStub.admin)).resolves.toBe(17);
|
|
||||||
|
|
||||||
expect(libraryMock.getCountForUser).toHaveBeenCalledWith(authStub.admin.user.id);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('get', () => {
|
describe('get', () => {
|
||||||
it('should return a library', async () => {
|
it('should return a library', async () => {
|
||||||
libraryMock.get.mockResolvedValue(libraryStub.uploadLibrary1);
|
libraryMock.get.mockResolvedValue(libraryStub.uploadLibrary1);
|
||||||
await expect(sut.get(authStub.admin, libraryStub.uploadLibrary1.id)).resolves.toEqual(
|
await expect(sut.get(libraryStub.uploadLibrary1.id)).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: libraryStub.uploadLibrary1.id,
|
id: libraryStub.uploadLibrary1.id,
|
||||||
name: libraryStub.uploadLibrary1.name,
|
name: libraryStub.uploadLibrary1.name,
|
||||||
|
@ -789,15 +777,16 @@ describe(LibraryService.name, () => {
|
||||||
|
|
||||||
it('should throw an error when a library is not found', async () => {
|
it('should throw an error when a library is not found', async () => {
|
||||||
libraryMock.get.mockResolvedValue(null);
|
libraryMock.get.mockResolvedValue(null);
|
||||||
await expect(sut.get(authStub.admin, libraryStub.uploadLibrary1.id)).rejects.toBeInstanceOf(BadRequestException);
|
await expect(sut.get(libraryStub.uploadLibrary1.id)).rejects.toBeInstanceOf(BadRequestException);
|
||||||
expect(libraryMock.get).toHaveBeenCalledWith(libraryStub.uploadLibrary1.id);
|
expect(libraryMock.get).toHaveBeenCalledWith(libraryStub.uploadLibrary1.id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getStatistics', () => {
|
describe('getStatistics', () => {
|
||||||
it('should return library statistics', async () => {
|
it('should return library statistics', async () => {
|
||||||
|
libraryMock.get.mockResolvedValue(libraryStub.uploadLibrary1);
|
||||||
libraryMock.getStatistics.mockResolvedValue({ photos: 10, videos: 0, total: 10, usage: 1337 });
|
libraryMock.getStatistics.mockResolvedValue({ photos: 10, videos: 0, total: 10, usage: 1337 });
|
||||||
await expect(sut.getStatistics(authStub.admin, libraryStub.uploadLibrary1.id)).resolves.toEqual({
|
await expect(sut.getStatistics(libraryStub.uploadLibrary1.id)).resolves.toEqual({
|
||||||
photos: 10,
|
photos: 10,
|
||||||
videos: 0,
|
videos: 0,
|
||||||
total: 10,
|
total: 10,
|
||||||
|
@ -812,11 +801,7 @@ describe(LibraryService.name, () => {
|
||||||
describe('external library', () => {
|
describe('external library', () => {
|
||||||
it('should create with default settings', async () => {
|
it('should create with default settings', async () => {
|
||||||
libraryMock.create.mockResolvedValue(libraryStub.externalLibrary1);
|
libraryMock.create.mockResolvedValue(libraryStub.externalLibrary1);
|
||||||
await expect(
|
await expect(sut.create({ ownerId: authStub.admin.user.id, type: LibraryType.EXTERNAL })).resolves.toEqual(
|
||||||
sut.create(authStub.admin, {
|
|
||||||
type: LibraryType.EXTERNAL,
|
|
||||||
}),
|
|
||||||
).resolves.toEqual(
|
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: libraryStub.externalLibrary1.id,
|
id: libraryStub.externalLibrary1.id,
|
||||||
type: LibraryType.EXTERNAL,
|
type: LibraryType.EXTERNAL,
|
||||||
|
@ -845,10 +830,7 @@ describe(LibraryService.name, () => {
|
||||||
it('should create with name', async () => {
|
it('should create with name', async () => {
|
||||||
libraryMock.create.mockResolvedValue(libraryStub.externalLibrary1);
|
libraryMock.create.mockResolvedValue(libraryStub.externalLibrary1);
|
||||||
await expect(
|
await expect(
|
||||||
sut.create(authStub.admin, {
|
sut.create({ ownerId: authStub.admin.user.id, type: LibraryType.EXTERNAL, name: 'My Awesome Library' }),
|
||||||
type: LibraryType.EXTERNAL,
|
|
||||||
name: 'My Awesome Library',
|
|
||||||
}),
|
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: libraryStub.externalLibrary1.id,
|
id: libraryStub.externalLibrary1.id,
|
||||||
|
@ -878,10 +860,7 @@ describe(LibraryService.name, () => {
|
||||||
it('should create invisible', async () => {
|
it('should create invisible', async () => {
|
||||||
libraryMock.create.mockResolvedValue(libraryStub.externalLibrary1);
|
libraryMock.create.mockResolvedValue(libraryStub.externalLibrary1);
|
||||||
await expect(
|
await expect(
|
||||||
sut.create(authStub.admin, {
|
sut.create({ ownerId: authStub.admin.user.id, type: LibraryType.EXTERNAL, isVisible: false }),
|
||||||
type: LibraryType.EXTERNAL,
|
|
||||||
isVisible: false,
|
|
||||||
}),
|
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: libraryStub.externalLibrary1.id,
|
id: libraryStub.externalLibrary1.id,
|
||||||
|
@ -911,7 +890,8 @@ describe(LibraryService.name, () => {
|
||||||
it('should create with import paths', async () => {
|
it('should create with import paths', async () => {
|
||||||
libraryMock.create.mockResolvedValue(libraryStub.externalLibrary1);
|
libraryMock.create.mockResolvedValue(libraryStub.externalLibrary1);
|
||||||
await expect(
|
await expect(
|
||||||
sut.create(authStub.admin, {
|
sut.create({
|
||||||
|
ownerId: authStub.admin.user.id,
|
||||||
type: LibraryType.EXTERNAL,
|
type: LibraryType.EXTERNAL,
|
||||||
importPaths: ['/data/images', '/data/videos'],
|
importPaths: ['/data/images', '/data/videos'],
|
||||||
}),
|
}),
|
||||||
|
@ -948,7 +928,8 @@ describe(LibraryService.name, () => {
|
||||||
libraryMock.getAll.mockResolvedValue([]);
|
libraryMock.getAll.mockResolvedValue([]);
|
||||||
|
|
||||||
await sut.init();
|
await sut.init();
|
||||||
await sut.create(authStub.admin, {
|
await sut.create({
|
||||||
|
ownerId: authStub.admin.user.id,
|
||||||
type: LibraryType.EXTERNAL,
|
type: LibraryType.EXTERNAL,
|
||||||
importPaths: libraryStub.externalLibraryWithImportPaths1.importPaths,
|
importPaths: libraryStub.externalLibraryWithImportPaths1.importPaths,
|
||||||
});
|
});
|
||||||
|
@ -963,7 +944,8 @@ describe(LibraryService.name, () => {
|
||||||
it('should create with exclusion patterns', async () => {
|
it('should create with exclusion patterns', async () => {
|
||||||
libraryMock.create.mockResolvedValue(libraryStub.externalLibrary1);
|
libraryMock.create.mockResolvedValue(libraryStub.externalLibrary1);
|
||||||
await expect(
|
await expect(
|
||||||
sut.create(authStub.admin, {
|
sut.create({
|
||||||
|
ownerId: authStub.admin.user.id,
|
||||||
type: LibraryType.EXTERNAL,
|
type: LibraryType.EXTERNAL,
|
||||||
exclusionPatterns: ['*.tmp', '*.bak'],
|
exclusionPatterns: ['*.tmp', '*.bak'],
|
||||||
}),
|
}),
|
||||||
|
@ -997,11 +979,7 @@ describe(LibraryService.name, () => {
|
||||||
describe('upload library', () => {
|
describe('upload library', () => {
|
||||||
it('should create with default settings', async () => {
|
it('should create with default settings', async () => {
|
||||||
libraryMock.create.mockResolvedValue(libraryStub.uploadLibrary1);
|
libraryMock.create.mockResolvedValue(libraryStub.uploadLibrary1);
|
||||||
await expect(
|
await expect(sut.create({ ownerId: authStub.admin.user.id, type: LibraryType.UPLOAD })).resolves.toEqual(
|
||||||
sut.create(authStub.admin, {
|
|
||||||
type: LibraryType.UPLOAD,
|
|
||||||
}),
|
|
||||||
).resolves.toEqual(
|
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: libraryStub.uploadLibrary1.id,
|
id: libraryStub.uploadLibrary1.id,
|
||||||
type: LibraryType.UPLOAD,
|
type: LibraryType.UPLOAD,
|
||||||
|
@ -1030,10 +1008,7 @@ describe(LibraryService.name, () => {
|
||||||
it('should create with name', async () => {
|
it('should create with name', async () => {
|
||||||
libraryMock.create.mockResolvedValue(libraryStub.uploadLibrary1);
|
libraryMock.create.mockResolvedValue(libraryStub.uploadLibrary1);
|
||||||
await expect(
|
await expect(
|
||||||
sut.create(authStub.admin, {
|
sut.create({ ownerId: authStub.admin.user.id, type: LibraryType.UPLOAD, name: 'My Awesome Library' }),
|
||||||
type: LibraryType.UPLOAD,
|
|
||||||
name: 'My Awesome Library',
|
|
||||||
}),
|
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: libraryStub.uploadLibrary1.id,
|
id: libraryStub.uploadLibrary1.id,
|
||||||
|
@ -1062,7 +1037,8 @@ describe(LibraryService.name, () => {
|
||||||
|
|
||||||
it('should not create with import paths', async () => {
|
it('should not create with import paths', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
sut.create(authStub.admin, {
|
sut.create({
|
||||||
|
ownerId: authStub.admin.user.id,
|
||||||
type: LibraryType.UPLOAD,
|
type: LibraryType.UPLOAD,
|
||||||
importPaths: ['/data/images', '/data/videos'],
|
importPaths: ['/data/images', '/data/videos'],
|
||||||
}),
|
}),
|
||||||
|
@ -1073,7 +1049,8 @@ describe(LibraryService.name, () => {
|
||||||
|
|
||||||
it('should not create with exclusion patterns', async () => {
|
it('should not create with exclusion patterns', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
sut.create(authStub.admin, {
|
sut.create({
|
||||||
|
ownerId: authStub.admin.user.id,
|
||||||
type: LibraryType.UPLOAD,
|
type: LibraryType.UPLOAD,
|
||||||
exclusionPatterns: ['*.tmp', '*.bak'],
|
exclusionPatterns: ['*.tmp', '*.bak'],
|
||||||
}),
|
}),
|
||||||
|
@ -1084,10 +1061,7 @@ describe(LibraryService.name, () => {
|
||||||
|
|
||||||
it('should not create watched', async () => {
|
it('should not create watched', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
sut.create(authStub.admin, {
|
sut.create({ ownerId: authStub.admin.user.id, type: LibraryType.UPLOAD, isWatched: true }),
|
||||||
type: LibraryType.UPLOAD,
|
|
||||||
isWatched: true,
|
|
||||||
}),
|
|
||||||
).rejects.toBeInstanceOf(BadRequestException);
|
).rejects.toBeInstanceOf(BadRequestException);
|
||||||
|
|
||||||
expect(storageMock.watch).not.toHaveBeenCalled();
|
expect(storageMock.watch).not.toHaveBeenCalled();
|
||||||
|
@ -1117,14 +1091,9 @@ describe(LibraryService.name, () => {
|
||||||
|
|
||||||
it('should update library', async () => {
|
it('should update library', async () => {
|
||||||
libraryMock.update.mockResolvedValue(libraryStub.uploadLibrary1);
|
libraryMock.update.mockResolvedValue(libraryStub.uploadLibrary1);
|
||||||
await expect(sut.update(authStub.admin, authStub.admin.user.id, {})).resolves.toEqual(
|
libraryMock.get.mockResolvedValue(libraryStub.uploadLibrary1);
|
||||||
mapLibrary(libraryStub.uploadLibrary1),
|
await expect(sut.update('library-id', {})).resolves.toEqual(mapLibrary(libraryStub.uploadLibrary1));
|
||||||
);
|
expect(libraryMock.update).toHaveBeenCalledWith(expect.objectContaining({ id: 'library-id' }));
|
||||||
expect(libraryMock.update).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
id: authStub.admin.user.id,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should re-watch library when updating import paths', async () => {
|
it('should re-watch library when updating import paths', async () => {
|
||||||
|
@ -1137,15 +1106,11 @@ describe(LibraryService.name, () => {
|
||||||
|
|
||||||
storageMock.checkFileExists.mockResolvedValue(true);
|
storageMock.checkFileExists.mockResolvedValue(true);
|
||||||
|
|
||||||
await expect(
|
await expect(sut.update('library-id', { importPaths: ['/data/user1/foo'] })).resolves.toEqual(
|
||||||
sut.update(authStub.admin, authStub.admin.user.id, { importPaths: ['/data/user1/foo'] }),
|
mapLibrary(libraryStub.externalLibraryWithImportPaths1),
|
||||||
).resolves.toEqual(mapLibrary(libraryStub.externalLibraryWithImportPaths1));
|
|
||||||
|
|
||||||
expect(libraryMock.update).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
id: authStub.admin.user.id,
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
expect(libraryMock.update).toHaveBeenCalledWith(expect.objectContaining({ id: 'library-id' }));
|
||||||
expect(storageMock.watch).toHaveBeenCalledWith(
|
expect(storageMock.watch).toHaveBeenCalledWith(
|
||||||
libraryStub.externalLibraryWithImportPaths1.importPaths,
|
libraryStub.externalLibraryWithImportPaths1.importPaths,
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
|
@ -1158,15 +1123,11 @@ describe(LibraryService.name, () => {
|
||||||
configMock.load.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
|
configMock.load.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
|
||||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
||||||
|
|
||||||
await expect(sut.update(authStub.admin, authStub.admin.user.id, { exclusionPatterns: ['bar'] })).resolves.toEqual(
|
await expect(sut.update('library-id', { exclusionPatterns: ['bar'] })).resolves.toEqual(
|
||||||
mapLibrary(libraryStub.externalLibraryWithImportPaths1),
|
mapLibrary(libraryStub.externalLibraryWithImportPaths1),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(libraryMock.update).toHaveBeenCalledWith(
|
expect(libraryMock.update).toHaveBeenCalledWith(expect.objectContaining({ id: 'library-id' }));
|
||||||
expect.objectContaining({
|
|
||||||
id: authStub.admin.user.id,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(storageMock.watch).toHaveBeenCalledWith(
|
expect(storageMock.watch).toHaveBeenCalledWith(
|
||||||
expect.arrayContaining([expect.any(String)]),
|
expect.arrayContaining([expect.any(String)]),
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
|
@ -1411,7 +1372,7 @@ describe(LibraryService.name, () => {
|
||||||
it('should queue a library scan of external library', async () => {
|
it('should queue a library scan of external library', async () => {
|
||||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
||||||
|
|
||||||
await sut.queueScan(authStub.admin, libraryStub.externalLibrary1.id, {});
|
await sut.queueScan(libraryStub.externalLibrary1.id, {});
|
||||||
|
|
||||||
expect(jobMock.queue.mock.calls).toEqual([
|
expect(jobMock.queue.mock.calls).toEqual([
|
||||||
[
|
[
|
||||||
|
@ -1430,9 +1391,7 @@ describe(LibraryService.name, () => {
|
||||||
it('should not queue a library scan of upload library', async () => {
|
it('should not queue a library scan of upload library', async () => {
|
||||||
libraryMock.get.mockResolvedValue(libraryStub.uploadLibrary1);
|
libraryMock.get.mockResolvedValue(libraryStub.uploadLibrary1);
|
||||||
|
|
||||||
await expect(sut.queueScan(authStub.admin, libraryStub.uploadLibrary1.id, {})).rejects.toBeInstanceOf(
|
await expect(sut.queueScan(libraryStub.uploadLibrary1.id, {})).rejects.toBeInstanceOf(BadRequestException);
|
||||||
BadRequestException,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(jobMock.queue).not.toBeCalled();
|
expect(jobMock.queue).not.toBeCalled();
|
||||||
});
|
});
|
||||||
|
@ -1440,7 +1399,7 @@ describe(LibraryService.name, () => {
|
||||||
it('should queue a library scan of all modified assets', async () => {
|
it('should queue a library scan of all modified assets', async () => {
|
||||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
||||||
|
|
||||||
await sut.queueScan(authStub.admin, libraryStub.externalLibrary1.id, { refreshModifiedFiles: true });
|
await sut.queueScan(libraryStub.externalLibrary1.id, { refreshModifiedFiles: true });
|
||||||
|
|
||||||
expect(jobMock.queue.mock.calls).toEqual([
|
expect(jobMock.queue.mock.calls).toEqual([
|
||||||
[
|
[
|
||||||
|
@ -1459,7 +1418,7 @@ describe(LibraryService.name, () => {
|
||||||
it('should queue a forced library scan', async () => {
|
it('should queue a forced library scan', async () => {
|
||||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
||||||
|
|
||||||
await sut.queueScan(authStub.admin, libraryStub.externalLibrary1.id, { refreshAllFiles: true });
|
await sut.queueScan(libraryStub.externalLibrary1.id, { refreshAllFiles: true });
|
||||||
|
|
||||||
expect(jobMock.queue.mock.calls).toEqual([
|
expect(jobMock.queue.mock.calls).toEqual([
|
||||||
[
|
[
|
||||||
|
@ -1478,7 +1437,7 @@ describe(LibraryService.name, () => {
|
||||||
|
|
||||||
describe('queueEmptyTrash', () => {
|
describe('queueEmptyTrash', () => {
|
||||||
it('should queue the trash job', async () => {
|
it('should queue the trash job', async () => {
|
||||||
await sut.queueRemoveOffline(authStub.admin, libraryStub.externalLibrary1.id);
|
await sut.queueRemoveOffline(libraryStub.externalLibrary1.id);
|
||||||
|
|
||||||
expect(jobMock.queue.mock.calls).toEqual([
|
expect(jobMock.queue.mock.calls).toEqual([
|
||||||
[
|
[
|
||||||
|
@ -1566,17 +1525,15 @@ describe(LibraryService.name, () => {
|
||||||
|
|
||||||
storageMock.checkFileExists.mockResolvedValue(true);
|
storageMock.checkFileExists.mockResolvedValue(true);
|
||||||
|
|
||||||
const result = await sut.validate(authStub.external1, libraryStub.externalLibraryWithImportPaths1.id, {
|
await expect(sut.validate('library-id', { importPaths: ['/data/user1/'] })).resolves.toEqual({
|
||||||
importPaths: ['/data/user1/'],
|
importPaths: [
|
||||||
|
{
|
||||||
|
importPath: '/data/user1/',
|
||||||
|
isValid: true,
|
||||||
|
message: undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.importPaths).toEqual([
|
|
||||||
{
|
|
||||||
importPath: '/data/user1/',
|
|
||||||
isValid: true,
|
|
||||||
message: undefined,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should detect when path does not exist', async () => {
|
it('should detect when path does not exist', async () => {
|
||||||
|
@ -1585,17 +1542,15 @@ describe(LibraryService.name, () => {
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await sut.validate(authStub.external1, libraryStub.externalLibraryWithImportPaths1.id, {
|
await expect(sut.validate('library-id', { importPaths: ['/data/user1/'] })).resolves.toEqual({
|
||||||
importPaths: ['/data/user1/'],
|
importPaths: [
|
||||||
|
{
|
||||||
|
importPath: '/data/user1/',
|
||||||
|
isValid: false,
|
||||||
|
message: 'Path does not exist (ENOENT)',
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.importPaths).toEqual([
|
|
||||||
{
|
|
||||||
importPath: '/data/user1/',
|
|
||||||
isValid: false,
|
|
||||||
message: 'Path does not exist (ENOENT)',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should detect when path is not a directory', async () => {
|
it('should detect when path is not a directory', async () => {
|
||||||
|
@ -1603,17 +1558,15 @@ describe(LibraryService.name, () => {
|
||||||
isDirectory: () => false,
|
isDirectory: () => false,
|
||||||
} as Stats);
|
} as Stats);
|
||||||
|
|
||||||
const result = await sut.validate(authStub.external1, libraryStub.externalLibraryWithImportPaths1.id, {
|
await expect(sut.validate('library-id', { importPaths: ['/data/user1/file'] })).resolves.toEqual({
|
||||||
importPaths: ['/data/user1/file'],
|
importPaths: [
|
||||||
|
{
|
||||||
|
importPath: '/data/user1/file',
|
||||||
|
isValid: false,
|
||||||
|
message: 'Not a directory',
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.importPaths).toEqual([
|
|
||||||
{
|
|
||||||
importPath: '/data/user1/file',
|
|
||||||
isValid: false,
|
|
||||||
message: 'Not a directory',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return an unknown exception from stat', async () => {
|
it('should return an unknown exception from stat', async () => {
|
||||||
|
@ -1621,17 +1574,15 @@ describe(LibraryService.name, () => {
|
||||||
throw new Error('Unknown error');
|
throw new Error('Unknown error');
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await sut.validate(authStub.external1, libraryStub.externalLibraryWithImportPaths1.id, {
|
await expect(sut.validate('library-id', { importPaths: ['/data/user1/'] })).resolves.toEqual({
|
||||||
importPaths: ['/data/user1/'],
|
importPaths: [
|
||||||
|
{
|
||||||
|
importPath: '/data/user1/',
|
||||||
|
isValid: false,
|
||||||
|
message: 'Error: Unknown error',
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.importPaths).toEqual([
|
|
||||||
{
|
|
||||||
importPath: '/data/user1/',
|
|
||||||
isValid: false,
|
|
||||||
message: 'Error: Unknown error',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should detect when access rights are missing', async () => {
|
it('should detect when access rights are missing', async () => {
|
||||||
|
@ -1641,17 +1592,15 @@ describe(LibraryService.name, () => {
|
||||||
|
|
||||||
storageMock.checkFileExists.mockResolvedValue(false);
|
storageMock.checkFileExists.mockResolvedValue(false);
|
||||||
|
|
||||||
const result = await sut.validate(authStub.external1, libraryStub.externalLibraryWithImportPaths1.id, {
|
await expect(sut.validate('library-id', { importPaths: ['/data/user1/'] })).resolves.toEqual({
|
||||||
importPaths: ['/data/user1/'],
|
importPaths: [
|
||||||
|
{
|
||||||
|
importPath: '/data/user1/',
|
||||||
|
isValid: false,
|
||||||
|
message: 'Lacking read permission for folder',
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.importPaths).toEqual([
|
|
||||||
{
|
|
||||||
importPath: '/data/user1/',
|
|
||||||
isValid: false,
|
|
||||||
message: 'Lacking read permission for folder',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should detect when import path is in immich media folder', async () => {
|
it('should detect when import path is in immich media folder', async () => {
|
||||||
|
@ -1659,26 +1608,26 @@ describe(LibraryService.name, () => {
|
||||||
const validImport = libraryStub.hasImmichPaths.importPaths[1];
|
const validImport = libraryStub.hasImmichPaths.importPaths[1];
|
||||||
when(storageMock.checkFileExists).calledWith(validImport, R_OK).mockResolvedValue(true);
|
when(storageMock.checkFileExists).calledWith(validImport, R_OK).mockResolvedValue(true);
|
||||||
|
|
||||||
const result = await sut.validate(authStub.external1, libraryStub.hasImmichPaths.id, {
|
await expect(
|
||||||
importPaths: libraryStub.hasImmichPaths.importPaths,
|
sut.validate('library-id', { importPaths: libraryStub.hasImmichPaths.importPaths }),
|
||||||
|
).resolves.toEqual({
|
||||||
|
importPaths: [
|
||||||
|
{
|
||||||
|
importPath: libraryStub.hasImmichPaths.importPaths[0],
|
||||||
|
isValid: false,
|
||||||
|
message: 'Cannot use media upload folder for external libraries',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
importPath: validImport,
|
||||||
|
isValid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
importPath: libraryStub.hasImmichPaths.importPaths[2],
|
||||||
|
isValid: false,
|
||||||
|
message: 'Cannot use media upload folder for external libraries',
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.importPaths).toEqual([
|
|
||||||
{
|
|
||||||
importPath: libraryStub.hasImmichPaths.importPaths[0],
|
|
||||||
isValid: false,
|
|
||||||
message: 'Cannot use media upload folder for external libraries',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
importPath: validImport,
|
|
||||||
isValid: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
importPath: libraryStub.hasImmichPaths.importPaths[2],
|
|
||||||
isValid: false,
|
|
||||||
message: 'Cannot use media upload folder for external libraries',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,8 +8,7 @@ import { EventEmitter } from 'node:events';
|
||||||
import { Stats } from 'node:fs';
|
import { Stats } from 'node:fs';
|
||||||
import path, { basename, parse } from 'node:path';
|
import path, { basename, parse } from 'node:path';
|
||||||
import picomatch from 'picomatch';
|
import picomatch from 'picomatch';
|
||||||
import { AccessCore, Permission } from '../access';
|
import { AccessCore } from '../access';
|
||||||
import { AuthDto } from '../auth';
|
|
||||||
import { mimeTypes } from '../domain.constant';
|
import { mimeTypes } from '../domain.constant';
|
||||||
import { handlePromiseError, usePagination, validateCronExpression } from '../domain.util';
|
import { handlePromiseError, usePagination, validateCronExpression } from '../domain.util';
|
||||||
import { IBaseJob, IEntityJob, ILibraryFileJob, ILibraryRefreshJob, JOBS_ASSET_PAGINATION_SIZE, JobName } from '../job';
|
import { IBaseJob, IEntityJob, ILibraryFileJob, ILibraryRefreshJob, JOBS_ASSET_PAGINATION_SIZE, JobName } from '../job';
|
||||||
|
@ -226,24 +225,17 @@ export class LibraryService extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getStatistics(auth: AuthDto, id: string): Promise<LibraryStatsResponseDto> {
|
async getStatistics(id: string): Promise<LibraryStatsResponseDto> {
|
||||||
await this.access.requirePermission(auth, Permission.LIBRARY_READ, id);
|
await this.findOrFail(id);
|
||||||
|
|
||||||
return this.repository.getStatistics(id);
|
return this.repository.getStatistics(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCount(auth: AuthDto): Promise<number> {
|
async get(id: string): Promise<LibraryResponseDto> {
|
||||||
return this.repository.getCountForUser(auth.user.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
async get(auth: AuthDto, id: string): Promise<LibraryResponseDto> {
|
|
||||||
await this.access.requirePermission(auth, Permission.LIBRARY_READ, id);
|
|
||||||
|
|
||||||
const library = await this.findOrFail(id);
|
const library = await this.findOrFail(id);
|
||||||
return mapLibrary(library);
|
return mapLibrary(library);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAll(auth: AuthDto, dto: SearchLibraryDto): Promise<LibraryResponseDto[]> {
|
async getAll(dto: SearchLibraryDto): Promise<LibraryResponseDto[]> {
|
||||||
const libraries = await this.repository.getAll(false, dto.type);
|
const libraries = await this.repository.getAll(false, dto.type);
|
||||||
return libraries.map((library) => mapLibrary(library));
|
return libraries.map((library) => mapLibrary(library));
|
||||||
}
|
}
|
||||||
|
@ -257,7 +249,7 @@ export class LibraryService extends EventEmitter {
|
||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(auth: AuthDto, dto: CreateLibraryDto): Promise<LibraryResponseDto> {
|
async create(dto: CreateLibraryDto): Promise<LibraryResponseDto> {
|
||||||
switch (dto.type) {
|
switch (dto.type) {
|
||||||
case LibraryType.EXTERNAL: {
|
case LibraryType.EXTERNAL: {
|
||||||
if (!dto.name) {
|
if (!dto.name) {
|
||||||
|
@ -282,14 +274,8 @@ export class LibraryService extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let ownerId = auth.user.id;
|
|
||||||
|
|
||||||
if (dto.ownerId) {
|
|
||||||
ownerId = dto.ownerId;
|
|
||||||
}
|
|
||||||
|
|
||||||
const library = await this.repository.create({
|
const library = await this.repository.create({
|
||||||
ownerId,
|
ownerId: dto.ownerId,
|
||||||
name: dto.name,
|
name: dto.name,
|
||||||
type: dto.type,
|
type: dto.type,
|
||||||
importPaths: dto.importPaths ?? [],
|
importPaths: dto.importPaths ?? [],
|
||||||
|
@ -297,7 +283,7 @@ export class LibraryService extends EventEmitter {
|
||||||
isVisible: dto.isVisible ?? true,
|
isVisible: dto.isVisible ?? true,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logger.log(`Creating ${dto.type} library for user ${auth.user.name}`);
|
this.logger.log(`Creating ${dto.type} library for ${dto.ownerId}}`);
|
||||||
|
|
||||||
if (dto.type === LibraryType.EXTERNAL) {
|
if (dto.type === LibraryType.EXTERNAL) {
|
||||||
await this.watch(library.id);
|
await this.watch(library.id);
|
||||||
|
@ -364,29 +350,19 @@ export class LibraryService extends EventEmitter {
|
||||||
return validation;
|
return validation;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async validate(auth: AuthDto, id: string, dto: ValidateLibraryDto): Promise<ValidateLibraryResponseDto> {
|
async validate(id: string, dto: ValidateLibraryDto): Promise<ValidateLibraryResponseDto> {
|
||||||
await this.access.requirePermission(auth, Permission.LIBRARY_UPDATE, id);
|
const importPaths = await Promise.all(
|
||||||
|
(dto.importPaths || []).map((importPath) => this.validateImportPath(importPath)),
|
||||||
const response = new ValidateLibraryResponseDto();
|
);
|
||||||
|
return { importPaths };
|
||||||
if (dto.importPaths) {
|
|
||||||
response.importPaths = await Promise.all(
|
|
||||||
dto.importPaths.map(async (importPath) => {
|
|
||||||
return await this.validateImportPath(importPath);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(auth: AuthDto, id: string, dto: UpdateLibraryDto): Promise<LibraryResponseDto> {
|
async update(id: string, dto: UpdateLibraryDto): Promise<LibraryResponseDto> {
|
||||||
await this.access.requirePermission(auth, Permission.LIBRARY_UPDATE, id);
|
await this.findOrFail(id);
|
||||||
|
|
||||||
const library = await this.repository.update({ id, ...dto });
|
const library = await this.repository.update({ id, ...dto });
|
||||||
|
|
||||||
if (dto.importPaths) {
|
if (dto.importPaths) {
|
||||||
const validation = await this.validate(auth, id, { importPaths: dto.importPaths });
|
const validation = await this.validate(id, { importPaths: dto.importPaths });
|
||||||
if (validation.importPaths) {
|
if (validation.importPaths) {
|
||||||
for (const path of validation.importPaths) {
|
for (const path of validation.importPaths) {
|
||||||
if (!path.isValid) {
|
if (!path.isValid) {
|
||||||
|
@ -404,11 +380,9 @@ export class LibraryService extends EventEmitter {
|
||||||
return mapLibrary(library);
|
return mapLibrary(library);
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(auth: AuthDto, id: string) {
|
async delete(id: string) {
|
||||||
await this.access.requirePermission(auth, Permission.LIBRARY_DELETE, id);
|
|
||||||
|
|
||||||
const library = await this.findOrFail(id);
|
const library = await this.findOrFail(id);
|
||||||
const uploadCount = await this.repository.getUploadLibraryCount(auth.user.id);
|
const uploadCount = await this.repository.getUploadLibraryCount(library.ownerId);
|
||||||
if (library.type === LibraryType.UPLOAD && uploadCount <= 1) {
|
if (library.type === LibraryType.UPLOAD && uploadCount <= 1) {
|
||||||
throw new BadRequestException('Cannot delete the last upload library');
|
throw new BadRequestException('Cannot delete the last upload library');
|
||||||
}
|
}
|
||||||
|
@ -565,11 +539,9 @@ export class LibraryService extends EventEmitter {
|
||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
async queueScan(auth: AuthDto, id: string, dto: ScanLibraryDto) {
|
async queueScan(id: string, dto: ScanLibraryDto) {
|
||||||
await this.access.requirePermission(auth, Permission.LIBRARY_UPDATE, id);
|
const library = await this.findOrFail(id);
|
||||||
|
if (library.type !== LibraryType.EXTERNAL) {
|
||||||
const library = await this.repository.get(id);
|
|
||||||
if (!library || library.type !== LibraryType.EXTERNAL) {
|
|
||||||
throw new BadRequestException('Can only refresh external libraries');
|
throw new BadRequestException('Can only refresh external libraries');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -583,16 +555,9 @@ export class LibraryService extends EventEmitter {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async queueRemoveOffline(auth: AuthDto, id: string) {
|
async queueRemoveOffline(id: string) {
|
||||||
this.logger.verbose(`Removing offline files from library: ${id}`);
|
this.logger.verbose(`Removing offline files from library: ${id}`);
|
||||||
await this.access.requirePermission(auth, Permission.LIBRARY_UPDATE, id);
|
await this.jobRepository.queue({ name: JobName.LIBRARY_REMOVE_OFFLINE, data: { id } });
|
||||||
|
|
||||||
await this.jobRepository.queue({
|
|
||||||
name: JobName.LIBRARY_REMOVE_OFFLINE,
|
|
||||||
data: {
|
|
||||||
id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleQueueAllScan(job: IBaseJob): Promise<JobStatus> {
|
async handleQueueAllScan(job: IBaseJob): Promise<JobStatus> {
|
||||||
|
|
|
@ -26,7 +26,6 @@ export interface IAccessRepository {
|
||||||
|
|
||||||
library: {
|
library: {
|
||||||
checkOwnerAccess(userId: string, libraryIds: Set<string>): Promise<Set<string>>;
|
checkOwnerAccess(userId: string, libraryIds: Set<string>): Promise<Set<string>>;
|
||||||
checkPartnerAccess(userId: string, partnerIds: Set<string>): Promise<Set<string>>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
timeline: {
|
timeline: {
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import {
|
import {
|
||||||
AuthDto,
|
|
||||||
CreateLibraryDto as CreateDto,
|
CreateLibraryDto as CreateDto,
|
||||||
LibraryService,
|
LibraryService,
|
||||||
LibraryStatsResponseDto,
|
LibraryStatsResponseDto,
|
||||||
|
@ -12,7 +11,7 @@ import {
|
||||||
} from '@app/domain';
|
} from '@app/domain';
|
||||||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
|
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { AdminRoute, Auth, Authenticated } from '../app.guard';
|
import { AdminRoute, Authenticated } from '../app.guard';
|
||||||
import { UUIDParamDto } from './dto/uuid-param.dto';
|
import { UUIDParamDto } from './dto/uuid-param.dto';
|
||||||
|
|
||||||
@ApiTags('Library')
|
@ApiTags('Library')
|
||||||
|
@ -23,55 +22,52 @@ export class LibraryController {
|
||||||
constructor(private service: LibraryService) {}
|
constructor(private service: LibraryService) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
getAllLibraries(@Auth() auth: AuthDto, @Query() dto: SearchLibraryDto): Promise<ResponseDto[]> {
|
getAllLibraries(@Query() dto: SearchLibraryDto): Promise<ResponseDto[]> {
|
||||||
return this.service.getAll(auth, dto);
|
return this.service.getAll(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
createLibrary(@Auth() auth: AuthDto, @Body() dto: CreateDto): Promise<ResponseDto> {
|
createLibrary(@Body() dto: CreateDto): Promise<ResponseDto> {
|
||||||
return this.service.create(auth, dto);
|
return this.service.create(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put(':id')
|
@Put(':id')
|
||||||
updateLibrary(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: UpdateDto): Promise<ResponseDto> {
|
updateLibrary(@Param() { id }: UUIDParamDto, @Body() dto: UpdateDto): Promise<ResponseDto> {
|
||||||
return this.service.update(auth, id, dto);
|
return this.service.update(id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
getLibrary(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<ResponseDto> {
|
getLibrary(@Param() { id }: UUIDParamDto): Promise<ResponseDto> {
|
||||||
return this.service.get(auth, id);
|
return this.service.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post(':id/validate')
|
@Post(':id/validate')
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
validate(
|
// TODO: change endpoint to validate current settings instead
|
||||||
@Auth() auth: AuthDto,
|
validate(@Param() { id }: UUIDParamDto, @Body() dto: ValidateLibraryDto): Promise<ValidateLibraryResponseDto> {
|
||||||
@Param() { id }: UUIDParamDto,
|
return this.service.validate(id, dto);
|
||||||
@Body() dto: ValidateLibraryDto,
|
|
||||||
): Promise<ValidateLibraryResponseDto> {
|
|
||||||
return this.service.validate(auth, id, dto);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
@HttpCode(HttpStatus.NO_CONTENT)
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
deleteLibrary(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
deleteLibrary(@Param() { id }: UUIDParamDto): Promise<void> {
|
||||||
return this.service.delete(auth, id);
|
return this.service.delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id/statistics')
|
@Get(':id/statistics')
|
||||||
getLibraryStatistics(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<LibraryStatsResponseDto> {
|
getLibraryStatistics(@Param() { id }: UUIDParamDto): Promise<LibraryStatsResponseDto> {
|
||||||
return this.service.getStatistics(auth, id);
|
return this.service.getStatistics(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post(':id/scan')
|
@Post(':id/scan')
|
||||||
@HttpCode(HttpStatus.NO_CONTENT)
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
scanLibrary(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: ScanLibraryDto) {
|
scanLibrary(@Param() { id }: UUIDParamDto, @Body() dto: ScanLibraryDto) {
|
||||||
return this.service.queueScan(auth, id, dto);
|
return this.service.queueScan(id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post(':id/removeOffline')
|
@Post(':id/removeOffline')
|
||||||
@HttpCode(HttpStatus.NO_CONTENT)
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
removeOfflineFiles(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto) {
|
removeOfflineFiles(@Param() { id }: UUIDParamDto) {
|
||||||
return this.service.queueRemoveOffline(auth, id);
|
return this.service.queueRemoveOffline(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -307,10 +307,7 @@ class AuthDeviceAccess implements IAuthDeviceAccess {
|
||||||
}
|
}
|
||||||
|
|
||||||
class LibraryAccess implements ILibraryAccess {
|
class LibraryAccess implements ILibraryAccess {
|
||||||
constructor(
|
constructor(private libraryRepository: Repository<LibraryEntity>) {}
|
||||||
private libraryRepository: Repository<LibraryEntity>,
|
|
||||||
private partnerRepository: Repository<PartnerEntity>,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||||
@ChunkedSet({ paramIndex: 1 })
|
@ChunkedSet({ paramIndex: 1 })
|
||||||
|
@ -329,22 +326,6 @@ class LibraryAccess implements ILibraryAccess {
|
||||||
})
|
})
|
||||||
.then((libraries) => new Set(libraries.map((library) => library.id)));
|
.then((libraries) => new Set(libraries.map((library) => library.id)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
|
||||||
@ChunkedSet({ paramIndex: 1 })
|
|
||||||
async checkPartnerAccess(userId: string, partnerIds: Set<string>): Promise<Set<string>> {
|
|
||||||
if (partnerIds.size === 0) {
|
|
||||||
return new Set();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.partnerRepository
|
|
||||||
.createQueryBuilder('partner')
|
|
||||||
.select('partner.sharedById')
|
|
||||||
.where('partner.sharedById IN (:...partnerIds)', { partnerIds: [...partnerIds] })
|
|
||||||
.andWhere('partner.sharedWithId = :userId', { userId })
|
|
||||||
.getMany()
|
|
||||||
.then((partners) => new Set(partners.map((partner) => partner.sharedById)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class TimelineAccess implements ITimelineAccess {
|
class TimelineAccess implements ITimelineAccess {
|
||||||
|
@ -457,7 +438,7 @@ export class AccessRepository implements IAccessRepository {
|
||||||
this.album = new AlbumAccess(albumRepository, sharedLinkRepository);
|
this.album = new AlbumAccess(albumRepository, sharedLinkRepository);
|
||||||
this.asset = new AssetAccess(albumRepository, assetRepository, partnerRepository, sharedLinkRepository);
|
this.asset = new AssetAccess(albumRepository, assetRepository, partnerRepository, sharedLinkRepository);
|
||||||
this.authDevice = new AuthDeviceAccess(tokenRepository);
|
this.authDevice = new AuthDeviceAccess(tokenRepository);
|
||||||
this.library = new LibraryAccess(libraryRepository, partnerRepository);
|
this.library = new LibraryAccess(libraryRepository);
|
||||||
this.person = new PersonAccess(assetFaceRepository, personRepository);
|
this.person = new PersonAccess(assetFaceRepository, personRepository);
|
||||||
this.partner = new PartnerAccess(partnerRepository);
|
this.partner = new PartnerAccess(partnerRepository);
|
||||||
this.timeline = new TimelineAccess(partnerRepository);
|
this.timeline = new TimelineAccess(partnerRepository);
|
||||||
|
|
|
@ -196,16 +196,6 @@ WHERE
|
||||||
)
|
)
|
||||||
AND ("LibraryEntity"."deletedAt" IS NULL)
|
AND ("LibraryEntity"."deletedAt" IS NULL)
|
||||||
|
|
||||||
-- AccessRepository.library.checkPartnerAccess
|
|
||||||
SELECT
|
|
||||||
"partner"."sharedById" AS "partner_sharedById",
|
|
||||||
"partner"."sharedWithId" AS "partner_sharedWithId"
|
|
||||||
FROM
|
|
||||||
"partners" "partner"
|
|
||||||
WHERE
|
|
||||||
"partner"."sharedById" IN ($1)
|
|
||||||
AND "partner"."sharedWithId" = $2
|
|
||||||
|
|
||||||
-- AccessRepository.person.checkOwnerAccess
|
-- AccessRepository.person.checkOwnerAccess
|
||||||
SELECT
|
SELECT
|
||||||
"PersonEntity"."id" AS "PersonEntity_id"
|
"PersonEntity"."id" AS "PersonEntity_id"
|
||||||
|
|
|
@ -42,7 +42,6 @@ export const newAccessRepositoryMock = (reset = true): IAccessRepositoryMock =>
|
||||||
|
|
||||||
library: {
|
library: {
|
||||||
checkOwnerAccess: jest.fn().mockResolvedValue(new Set()),
|
checkOwnerAccess: jest.fn().mockResolvedValue(new Set()),
|
||||||
checkPartnerAccess: jest.fn().mockResolvedValue(new Set()),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
timeline: {
|
timeline: {
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{
|
const dispatch = createEventDispatcher<{
|
||||||
cancel: void;
|
cancel: void;
|
||||||
submit: { ownerId: string | null };
|
submit: { ownerId: string };
|
||||||
delete: void;
|
delete: void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,6 @@
|
||||||
removeOfflineFiles,
|
removeOfflineFiles,
|
||||||
scanLibrary,
|
scanLibrary,
|
||||||
updateLibrary,
|
updateLibrary,
|
||||||
type CreateLibraryDto,
|
|
||||||
type LibraryResponseDto,
|
type LibraryResponseDto,
|
||||||
type LibraryStatsResponseDto,
|
type LibraryStatsResponseDto,
|
||||||
type UserResponseDto,
|
type UserResponseDto,
|
||||||
|
@ -117,14 +116,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCreate = async (ownerId: string | null) => {
|
const handleCreate = async (ownerId: string) => {
|
||||||
try {
|
try {
|
||||||
let createLibraryDto: CreateLibraryDto = { type: LibraryType.External };
|
const createdLibrary = await createLibrary({ createLibraryDto: { ownerId, type: LibraryType.External } });
|
||||||
if (ownerId) {
|
|
||||||
createLibraryDto = { ...createLibraryDto, ownerId };
|
|
||||||
}
|
|
||||||
|
|
||||||
const createdLibrary = await createLibrary({ createLibraryDto });
|
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: `Created library: ${createdLibrary.name}`,
|
message: `Created library: ${createdLibrary.name}`,
|
||||||
|
|
Loading…
Reference in a new issue