mirror of
https://github.com/immich-app/immich.git
synced 2024-12-02 09:52:47 +02:00
feat(server,web): remove external path nonsense and make libraries admin-only (#7237)
* remove external path * open-api * make sql * move library settings to admin panel * Add documentation * show external libraries only * fix library list * make user library settings look good * fix test * fix tests * fix tests * can pick user for library * fix tests * fix e2e * chore: make sql * Use unauth exception * delete user library list * cleanup * fix e2e * fix await lint * chore: remove unused code * chore: cleanup * revert docs * fix: is admin stuff * table alignment --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
369acc7bea
commit
efa6efd200
@ -44,7 +44,6 @@ export const userDto = {
|
|||||||
email: signupDto.admin.email,
|
email: signupDto.admin.email,
|
||||||
password: signupDto.admin.password,
|
password: signupDto.admin.password,
|
||||||
storageLabel: 'admin',
|
storageLabel: 'admin',
|
||||||
externalPath: null,
|
|
||||||
oauthId: '',
|
oauthId: '',
|
||||||
shouldChangePassword: false,
|
shouldChangePassword: false,
|
||||||
profileImagePath: '',
|
profileImagePath: '',
|
||||||
@ -63,7 +62,6 @@ export const userDto = {
|
|||||||
email: createUserDto.user1.email,
|
email: createUserDto.user1.email,
|
||||||
password: createUserDto.user1.password,
|
password: createUserDto.user1.password,
|
||||||
storageLabel: null,
|
storageLabel: null,
|
||||||
externalPath: null,
|
|
||||||
oauthId: '',
|
oauthId: '',
|
||||||
shouldChangePassword: false,
|
shouldChangePassword: false,
|
||||||
profileImagePath: '',
|
profileImagePath: '',
|
||||||
|
@ -65,7 +65,6 @@ export const signupResponseDto = {
|
|||||||
name: 'Immich Admin',
|
name: 'Immich Admin',
|
||||||
email: 'admin@immich.cloud',
|
email: 'admin@immich.cloud',
|
||||||
storageLabel: 'admin',
|
storageLabel: 'admin',
|
||||||
externalPath: null,
|
|
||||||
profileImagePath: '',
|
profileImagePath: '',
|
||||||
// why? lol
|
// why? lol
|
||||||
shouldChangePassword: true,
|
shouldChangePassword: true,
|
||||||
|
BIN
mobile/openapi/README.md
generated
BIN
mobile/openapi/README.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/CreateLibraryDto.md
generated
BIN
mobile/openapi/doc/CreateLibraryDto.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/CreateUserDto.md
generated
BIN
mobile/openapi/doc/CreateUserDto.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/LibraryApi.md
generated
BIN
mobile/openapi/doc/LibraryApi.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/PartnerResponseDto.md
generated
BIN
mobile/openapi/doc/PartnerResponseDto.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/UpdateUserDto.md
generated
BIN
mobile/openapi/doc/UpdateUserDto.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/UserResponseDto.md
generated
BIN
mobile/openapi/doc/UserResponseDto.md
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/library_api.dart
generated
BIN
mobile/openapi/lib/api/library_api.dart
generated
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.
BIN
mobile/openapi/lib/model/create_user_dto.dart
generated
BIN
mobile/openapi/lib/model/create_user_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/partner_response_dto.dart
generated
BIN
mobile/openapi/lib/model/partner_response_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/update_user_dto.dart
generated
BIN
mobile/openapi/lib/model/update_user_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/user_response_dto.dart
generated
BIN
mobile/openapi/lib/model/user_response_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/create_library_dto_test.dart
generated
BIN
mobile/openapi/test/create_library_dto_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/create_user_dto_test.dart
generated
BIN
mobile/openapi/test/create_user_dto_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/library_api_test.dart
generated
BIN
mobile/openapi/test/library_api_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/partner_response_dto_test.dart
generated
BIN
mobile/openapi/test/partner_response_dto_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/update_user_dto_test.dart
generated
BIN
mobile/openapi/test/update_user_dto_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/user_response_dto_test.dart
generated
BIN
mobile/openapi/test/user_response_dto_test.dart
generated
Binary file not shown.
@ -3299,8 +3299,17 @@
|
|||||||
},
|
},
|
||||||
"/library": {
|
"/library": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "getLibraries",
|
"operationId": "getAllLibraries",
|
||||||
"parameters": [],
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "type",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/LibraryType"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"content": {
|
"content": {
|
||||||
@ -3407,7 +3416,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "getLibraryInfo",
|
"operationId": "getLibrary",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "id",
|
"name": "id",
|
||||||
@ -7592,6 +7601,10 @@
|
|||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"ownerId": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"$ref": "#/components/schemas/LibraryType"
|
"$ref": "#/components/schemas/LibraryType"
|
||||||
}
|
}
|
||||||
@ -7648,10 +7661,6 @@
|
|||||||
"email": {
|
"email": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"externalPath": {
|
|
||||||
"nullable": true,
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"memoriesEnabled": {
|
"memoriesEnabled": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
@ -8549,10 +8558,6 @@
|
|||||||
"email": {
|
"email": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"externalPath": {
|
|
||||||
"nullable": true,
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -8601,7 +8606,6 @@
|
|||||||
"createdAt",
|
"createdAt",
|
||||||
"deletedAt",
|
"deletedAt",
|
||||||
"email",
|
"email",
|
||||||
"externalPath",
|
|
||||||
"id",
|
"id",
|
||||||
"isAdmin",
|
"isAdmin",
|
||||||
"name",
|
"name",
|
||||||
@ -10326,9 +10330,6 @@
|
|||||||
"email": {
|
"email": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"externalPath": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"id": {
|
"id": {
|
||||||
"format": "uuid",
|
"format": "uuid",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@ -10455,10 +10456,6 @@
|
|||||||
"email": {
|
"email": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"externalPath": {
|
|
||||||
"nullable": true,
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -10504,7 +10501,6 @@
|
|||||||
"createdAt",
|
"createdAt",
|
||||||
"deletedAt",
|
"deletedAt",
|
||||||
"email",
|
"email",
|
||||||
"externalPath",
|
|
||||||
"id",
|
"id",
|
||||||
"isAdmin",
|
"isAdmin",
|
||||||
"name",
|
"name",
|
||||||
|
@ -66,7 +66,6 @@ export type UserResponseDto = {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
deletedAt: string | null;
|
deletedAt: string | null;
|
||||||
email: string;
|
email: string;
|
||||||
externalPath: string | null;
|
|
||||||
id: string;
|
id: string;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
memoriesEnabled?: boolean;
|
memoriesEnabled?: boolean;
|
||||||
@ -462,6 +461,7 @@ export type CreateLibraryDto = {
|
|||||||
isVisible?: boolean;
|
isVisible?: boolean;
|
||||||
isWatched?: boolean;
|
isWatched?: boolean;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
ownerId?: string;
|
||||||
"type": LibraryType;
|
"type": LibraryType;
|
||||||
};
|
};
|
||||||
export type UpdateLibraryDto = {
|
export type UpdateLibraryDto = {
|
||||||
@ -506,7 +506,6 @@ export type PartnerResponseDto = {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
deletedAt: string | null;
|
deletedAt: string | null;
|
||||||
email: string;
|
email: string;
|
||||||
externalPath: string | null;
|
|
||||||
id: string;
|
id: string;
|
||||||
inTimeline?: boolean;
|
inTimeline?: boolean;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
@ -950,7 +949,6 @@ export type UpdateTagDto = {
|
|||||||
};
|
};
|
||||||
export type CreateUserDto = {
|
export type CreateUserDto = {
|
||||||
email: string;
|
email: string;
|
||||||
externalPath?: string | null;
|
|
||||||
memoriesEnabled?: boolean;
|
memoriesEnabled?: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
password: string;
|
password: string;
|
||||||
@ -960,7 +958,6 @@ export type CreateUserDto = {
|
|||||||
export type UpdateUserDto = {
|
export type UpdateUserDto = {
|
||||||
avatarColor?: UserAvatarColor;
|
avatarColor?: UserAvatarColor;
|
||||||
email?: string;
|
email?: string;
|
||||||
externalPath?: string;
|
|
||||||
id: string;
|
id: string;
|
||||||
isAdmin?: boolean;
|
isAdmin?: boolean;
|
||||||
memoriesEnabled?: boolean;
|
memoriesEnabled?: boolean;
|
||||||
@ -1841,11 +1838,15 @@ export function sendJobCommand({ id, jobCommandDto }: {
|
|||||||
body: jobCommandDto
|
body: jobCommandDto
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
export function getLibraries(opts?: Oazapfts.RequestOpts) {
|
export function getAllLibraries({ $type }: {
|
||||||
|
$type?: LibraryType;
|
||||||
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
status: 200;
|
status: 200;
|
||||||
data: LibraryResponseDto[];
|
data: LibraryResponseDto[];
|
||||||
}>("/library", {
|
}>(`/library${QS.query(QS.explode({
|
||||||
|
"type": $type
|
||||||
|
}))}`, {
|
||||||
...opts
|
...opts
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@ -1869,7 +1870,7 @@ export function deleteLibrary({ id }: {
|
|||||||
method: "DELETE"
|
method: "DELETE"
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
export function getLibraryInfo({ id }: {
|
export function getLibrary({ id }: {
|
||||||
id: string;
|
id: string;
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
|
@ -41,6 +41,7 @@ describe(`${AssetController.name} (e2e)`, () => {
|
|||||||
let app: INestApplication;
|
let app: INestApplication;
|
||||||
let server: any;
|
let server: any;
|
||||||
let assetRepository: IAssetRepository;
|
let assetRepository: IAssetRepository;
|
||||||
|
let admin: LoginResponseDto;
|
||||||
let user1: LoginResponseDto;
|
let user1: LoginResponseDto;
|
||||||
let user2: LoginResponseDto;
|
let user2: LoginResponseDto;
|
||||||
let userWithQuota: LoginResponseDto;
|
let userWithQuota: LoginResponseDto;
|
||||||
@ -72,7 +73,7 @@ describe(`${AssetController.name} (e2e)`, () => {
|
|||||||
await testApp.reset();
|
await testApp.reset();
|
||||||
|
|
||||||
await api.authApi.adminSignUp(server);
|
await api.authApi.adminSignUp(server);
|
||||||
const admin = await api.authApi.adminLogin(server);
|
admin = await api.authApi.adminLogin(server);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
api.userApi.create(server, admin.accessToken, userDto.user1),
|
api.userApi.create(server, admin.accessToken, userDto.user1),
|
||||||
@ -86,12 +87,7 @@ describe(`${AssetController.name} (e2e)`, () => {
|
|||||||
api.authApi.login(server, userDto.userWithQuota),
|
api.authApi.login(server, userDto.userWithQuota),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const [user1Libraries, user2Libraries] = await Promise.all([
|
libraries = await api.libraryApi.getAll(server, admin.accessToken);
|
||||||
api.libraryApi.getAll(server, user1.accessToken),
|
|
||||||
api.libraryApi.getAll(server, user2.accessToken),
|
|
||||||
]);
|
|
||||||
|
|
||||||
libraries = [...user1Libraries, ...user2Libraries];
|
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@ -615,7 +611,7 @@ describe(`${AssetController.name} (e2e)`, () => {
|
|||||||
|
|
||||||
it("should not upload to another user's library", async () => {
|
it("should not upload to another user's library", async () => {
|
||||||
const content = randomBytes(32);
|
const content = randomBytes(32);
|
||||||
const [library] = await api.libraryApi.getAll(server, user2.accessToken);
|
const [library] = await api.libraryApi.getAll(server, admin.accessToken);
|
||||||
await api.assetApi.upload(server, user1.accessToken, 'example-image', { content });
|
await api.assetApi.upload(server, user1.accessToken, 'example-image', { content });
|
||||||
|
|
||||||
const { body, status } = await request(server)
|
const { body, status } = await request(server)
|
||||||
|
@ -10,6 +10,7 @@ import { testApp } from '../utils';
|
|||||||
describe(`${LibraryController.name} (e2e)`, () => {
|
describe(`${LibraryController.name} (e2e)`, () => {
|
||||||
let server: any;
|
let server: any;
|
||||||
let admin: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
|
let user: LoginResponseDto;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const app = await testApp.create();
|
const app = await testApp.create();
|
||||||
@ -25,6 +26,9 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
|||||||
await testApp.reset();
|
await testApp.reset();
|
||||||
await api.authApi.adminSignUp(server);
|
await api.authApi.adminSignUp(server);
|
||||||
admin = await api.authApi.adminLogin(server);
|
admin = await api.authApi.adminLogin(server);
|
||||||
|
|
||||||
|
await api.userApi.create(server, admin.accessToken, userDto.user1);
|
||||||
|
user = await api.authApi.login(server, userDto.user1);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /library', () => {
|
describe('GET /library', () => {
|
||||||
@ -39,18 +43,19 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
|||||||
.get('/library')
|
.get('/library')
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toHaveLength(1);
|
expect(body).toEqual(
|
||||||
expect(body).toEqual([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
ownerId: admin.userId,
|
ownerId: admin.userId,
|
||||||
type: LibraryType.UPLOAD,
|
type: LibraryType.UPLOAD,
|
||||||
name: 'Default Library',
|
name: 'Default Library',
|
||||||
refreshedAt: null,
|
refreshedAt: null,
|
||||||
assetCount: 0,
|
assetCount: 0,
|
||||||
importPaths: [],
|
importPaths: [],
|
||||||
exclusionPatterns: [],
|
exclusionPatterns: [],
|
||||||
}),
|
}),
|
||||||
]);
|
]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -61,6 +66,16 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
|||||||
expect(body).toEqual(errorStub.unauthorized);
|
expect(body).toEqual(errorStub.unauthorized);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should require admin authentication', async () => {
|
||||||
|
const { status, body } = await request(server)
|
||||||
|
.post('/library')
|
||||||
|
.set('Authorization', `Bearer ${user.accessToken}`)
|
||||||
|
.send({ type: LibraryType.EXTERNAL });
|
||||||
|
|
||||||
|
expect(status).toBe(403);
|
||||||
|
expect(body).toEqual(errorStub.forbidden);
|
||||||
|
});
|
||||||
|
|
||||||
it('should create an external library with defaults', async () => {
|
it('should create an external library with defaults', async () => {
|
||||||
const { status, body } = await request(server)
|
const { status, body } = await request(server)
|
||||||
.post('/library')
|
.post('/library')
|
||||||
@ -184,29 +199,6 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
|||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorStub.badRequest('Upload libraries cannot have exclusion patterns'));
|
expect(body).toEqual(errorStub.badRequest('Upload libraries cannot have exclusion patterns'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow a non-admin to create a library', async () => {
|
|
||||||
await api.userApi.create(server, admin.accessToken, userDto.user1);
|
|
||||||
const user1 = await api.authApi.login(server, userDto.user1);
|
|
||||||
|
|
||||||
const { status, body } = await request(server)
|
|
||||||
.post('/library')
|
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
|
||||||
.send({ type: LibraryType.EXTERNAL });
|
|
||||||
|
|
||||||
expect(status).toBe(201);
|
|
||||||
expect(body).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
ownerId: user1.userId,
|
|
||||||
type: LibraryType.EXTERNAL,
|
|
||||||
name: 'New External Library',
|
|
||||||
refreshedAt: null,
|
|
||||||
assetCount: 0,
|
|
||||||
importPaths: [],
|
|
||||||
exclusionPatterns: [],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('PUT /library/:id', () => {
|
describe('PUT /library/:id', () => {
|
||||||
@ -249,7 +241,6 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should change the import paths', async () => {
|
it('should change the import paths', async () => {
|
||||||
await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, IMMICH_TEST_ASSET_TEMP_PATH);
|
|
||||||
const { status, body } = await request(server)
|
const { status, body } = await request(server)
|
||||||
.put(`/library/${library.id}`)
|
.put(`/library/${library.id}`)
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
@ -327,6 +318,14 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
|||||||
expect(body).toEqual(errorStub.unauthorized);
|
expect(body).toEqual(errorStub.unauthorized);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should require admin access', async () => {
|
||||||
|
const { status, body } = await request(server)
|
||||||
|
.get(`/library/${uuidStub.notFound}`)
|
||||||
|
.set('Authorization', `Bearer ${user.accessToken}`);
|
||||||
|
expect(status).toBe(403);
|
||||||
|
expect(body).toEqual(errorStub.forbidden);
|
||||||
|
});
|
||||||
|
|
||||||
it('should get library by id', async () => {
|
it('should get library by id', async () => {
|
||||||
const library = await api.libraryApi.create(server, admin.accessToken, { type: LibraryType.EXTERNAL });
|
const library = await api.libraryApi.create(server, admin.accessToken, { type: LibraryType.EXTERNAL });
|
||||||
|
|
||||||
@ -347,27 +346,6 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not allow getting another user's library", async () => {
|
|
||||||
await Promise.all([
|
|
||||||
api.userApi.create(server, admin.accessToken, userDto.user1),
|
|
||||||
api.userApi.create(server, admin.accessToken, userDto.user2),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const [user1, user2] = await Promise.all([
|
|
||||||
api.authApi.login(server, userDto.user1),
|
|
||||||
api.authApi.login(server, userDto.user2),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const library = await api.libraryApi.create(server, user1.accessToken, { type: LibraryType.EXTERNAL });
|
|
||||||
|
|
||||||
const { status, body } = await request(server)
|
|
||||||
.get(`/library/${library.id}`)
|
|
||||||
.set('Authorization', `Bearer ${user2.accessToken}`);
|
|
||||||
|
|
||||||
expect(status).toBe(400);
|
|
||||||
expect(body).toEqual(errorStub.badRequest('Not found or no library.read access'));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('DELETE /library/:id', () => {
|
describe('DELETE /library/:id', () => {
|
||||||
@ -390,7 +368,7 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
|||||||
expect(body).toEqual(errorStub.noDeleteUploadLibrary);
|
expect(body).toEqual(errorStub.noDeleteUploadLibrary);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should delete an empty library', async () => {
|
it('should delete an external library', async () => {
|
||||||
const library = await api.libraryApi.create(server, admin.accessToken, { type: LibraryType.EXTERNAL });
|
const library = await api.libraryApi.create(server, admin.accessToken, { type: LibraryType.EXTERNAL });
|
||||||
|
|
||||||
const { status, body } = await request(server)
|
const { status, body } = await request(server)
|
||||||
@ -401,7 +379,6 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
|||||||
expect(body).toEqual({});
|
expect(body).toEqual({});
|
||||||
|
|
||||||
const libraries = await api.libraryApi.getAll(server, admin.accessToken);
|
const libraries = await api.libraryApi.getAll(server, admin.accessToken);
|
||||||
expect(libraries).toHaveLength(1);
|
|
||||||
expect(libraries).not.toEqual(
|
expect(libraries).not.toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@ -455,74 +432,42 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
|||||||
library = await api.libraryApi.create(server, admin.accessToken, { type: LibraryType.EXTERNAL });
|
library = await api.libraryApi.create(server, admin.accessToken, { type: LibraryType.EXTERNAL });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail with no external path set', async () => {
|
it('should pass with no import paths', async () => {
|
||||||
const { status, body } = await request(server)
|
const response = await api.libraryApi.validate(server, admin.accessToken, library.id, { importPaths: [] });
|
||||||
.post(`/library/${library.id}/validate`)
|
expect(response.importPaths).toEqual([]);
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
|
|
||||||
.send({ importPaths: [] });
|
|
||||||
|
|
||||||
expect(status).toBe(400);
|
|
||||||
expect(body).toEqual(errorStub.badRequest('User has no external path set'));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('With external path set', () => {
|
it('should fail if path does not exist', async () => {
|
||||||
beforeEach(async () => {
|
const pathToTest = `${IMMICH_TEST_ASSET_TEMP_PATH}/does/not/exist`;
|
||||||
await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, IMMICH_TEST_ASSET_TEMP_PATH);
|
|
||||||
|
const response = await api.libraryApi.validate(server, admin.accessToken, library.id, {
|
||||||
|
importPaths: [pathToTest],
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should pass with no import paths', async () => {
|
expect(response.importPaths?.length).toEqual(1);
|
||||||
const response = await api.libraryApi.validate(server, admin.accessToken, library.id, { importPaths: [] });
|
const pathResponse = response?.importPaths?.at(0);
|
||||||
expect(response.importPaths).toEqual([]);
|
|
||||||
|
expect(pathResponse).toEqual({
|
||||||
|
importPath: pathToTest,
|
||||||
|
isValid: false,
|
||||||
|
message: `Path does not exist (ENOENT)`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail if path is a file', async () => {
|
||||||
|
const pathToTest = `${IMMICH_TEST_ASSET_TEMP_PATH}/does/not/exist`;
|
||||||
|
|
||||||
|
const response = await api.libraryApi.validate(server, admin.accessToken, library.id, {
|
||||||
|
importPaths: [pathToTest],
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not allow paths outside of the external path', async () => {
|
expect(response.importPaths?.length).toEqual(1);
|
||||||
const pathToTest = `${IMMICH_TEST_ASSET_TEMP_PATH}/../`;
|
const pathResponse = response?.importPaths?.at(0);
|
||||||
const response = await api.libraryApi.validate(server, admin.accessToken, library.id, {
|
|
||||||
importPaths: [pathToTest],
|
|
||||||
});
|
|
||||||
expect(response.importPaths?.length).toEqual(1);
|
|
||||||
const pathResponse = response?.importPaths?.at(0);
|
|
||||||
|
|
||||||
expect(pathResponse).toEqual({
|
expect(pathResponse).toEqual({
|
||||||
importPath: pathToTest,
|
importPath: pathToTest,
|
||||||
isValid: false,
|
isValid: false,
|
||||||
message: `Not contained in user's external path`,
|
message: `Path does not exist (ENOENT)`,
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should fail if path does not exist', async () => {
|
|
||||||
const pathToTest = `${IMMICH_TEST_ASSET_TEMP_PATH}/does/not/exist`;
|
|
||||||
|
|
||||||
const response = await api.libraryApi.validate(server, admin.accessToken, library.id, {
|
|
||||||
importPaths: [pathToTest],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(response.importPaths?.length).toEqual(1);
|
|
||||||
const pathResponse = response?.importPaths?.at(0);
|
|
||||||
|
|
||||||
expect(pathResponse).toEqual({
|
|
||||||
importPath: pathToTest,
|
|
||||||
isValid: false,
|
|
||||||
message: `Path does not exist (ENOENT)`,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should fail if path is a file', async () => {
|
|
||||||
const pathToTest = `${IMMICH_TEST_ASSET_TEMP_PATH}/does/not/exist`;
|
|
||||||
|
|
||||||
const response = await api.libraryApi.validate(server, admin.accessToken, library.id, {
|
|
||||||
importPaths: [pathToTest],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(response.importPaths?.length).toEqual(1);
|
|
||||||
const pathResponse = response?.importPaths?.at(0);
|
|
||||||
|
|
||||||
expect(pathResponse).toEqual({
|
|
||||||
importPath: pathToTest,
|
|
||||||
isValid: false,
|
|
||||||
message: `Path does not exist (ENOENT)`,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -26,7 +26,12 @@ export const userApi = {
|
|||||||
|
|
||||||
return body as UserResponseDto;
|
return body as UserResponseDto;
|
||||||
},
|
},
|
||||||
setExternalPath: async (server: any, accessToken: string, id: string, externalPath: string) => {
|
delete: async (server: any, accessToken: string, id: string) => {
|
||||||
return await userApi.update(server, accessToken, { id, externalPath });
|
const { status, body } = await request(server).delete(`/user/${id}`).set('Authorization', `Bearer ${accessToken}`);
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toMatchObject({ id, deletedAt: expect.any(String) });
|
||||||
|
|
||||||
|
return body as UserResponseDto;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -30,8 +30,6 @@ describe(`Library watcher (e2e)`, () => {
|
|||||||
await restoreTempFolder();
|
await restoreTempFolder();
|
||||||
await api.authApi.adminSignUp(server);
|
await api.authApi.adminSignUp(server);
|
||||||
admin = await api.authApi.adminLogin(server);
|
admin = await api.authApi.adminLogin(server);
|
||||||
|
|
||||||
await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
@ -205,8 +203,6 @@ describe(`Library watcher (e2e)`, () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/');
|
|
||||||
|
|
||||||
await fs.mkdir(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir1`, { recursive: true });
|
await fs.mkdir(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir1`, { recursive: true });
|
||||||
await fs.mkdir(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir2`, { recursive: true });
|
await fs.mkdir(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir2`, { recursive: true });
|
||||||
await fs.mkdir(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir3`, { recursive: true });
|
await fs.mkdir(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir3`, { recursive: true });
|
||||||
|
@ -40,8 +40,6 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
|||||||
type: LibraryType.EXTERNAL,
|
type: LibraryType.EXTERNAL,
|
||||||
importPaths: [`${IMMICH_TEST_ASSET_PATH}/albums/nature`],
|
importPaths: [`${IMMICH_TEST_ASSET_PATH}/albums/nature`],
|
||||||
});
|
});
|
||||||
await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/');
|
|
||||||
|
|
||||||
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
|
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
|
||||||
|
|
||||||
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
||||||
@ -79,8 +77,6 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
|||||||
type: LibraryType.EXTERNAL,
|
type: LibraryType.EXTERNAL,
|
||||||
importPaths: [`${IMMICH_TEST_ASSET_PATH}/albums/nature`],
|
importPaths: [`${IMMICH_TEST_ASSET_PATH}/albums/nature`],
|
||||||
});
|
});
|
||||||
await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/');
|
|
||||||
|
|
||||||
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
|
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
|
||||||
|
|
||||||
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
||||||
@ -118,16 +114,12 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should scan external library with exclusion pattern', async () => {
|
it('should scan external library with exclusion pattern', async () => {
|
||||||
await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/not/a/real/path');
|
|
||||||
|
|
||||||
const library = await api.libraryApi.create(server, admin.accessToken, {
|
const library = await api.libraryApi.create(server, admin.accessToken, {
|
||||||
type: LibraryType.EXTERNAL,
|
type: LibraryType.EXTERNAL,
|
||||||
importPaths: [`${IMMICH_TEST_ASSET_PATH}/albums/nature`],
|
importPaths: [`${IMMICH_TEST_ASSET_PATH}/albums/nature`],
|
||||||
exclusionPatterns: ['**/el_corcal*'],
|
exclusionPatterns: ['**/el_corcal*'],
|
||||||
});
|
});
|
||||||
|
|
||||||
await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/');
|
|
||||||
|
|
||||||
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
|
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
|
||||||
|
|
||||||
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
||||||
@ -163,7 +155,6 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
|||||||
type: LibraryType.EXTERNAL,
|
type: LibraryType.EXTERNAL,
|
||||||
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
|
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
|
||||||
});
|
});
|
||||||
await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/');
|
|
||||||
|
|
||||||
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
|
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
|
||||||
|
|
||||||
@ -190,39 +181,11 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should offline files outside of changed external path', async () => {
|
|
||||||
const library = await api.libraryApi.create(server, admin.accessToken, {
|
|
||||||
type: LibraryType.EXTERNAL,
|
|
||||||
importPaths: [`${IMMICH_TEST_ASSET_PATH}/albums/nature`],
|
|
||||||
});
|
|
||||||
await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/');
|
|
||||||
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
|
|
||||||
|
|
||||||
await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/some/other/path');
|
|
||||||
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
|
|
||||||
|
|
||||||
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
|
||||||
|
|
||||||
expect(assets).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
isOffline: true,
|
|
||||||
originalFileName: 'el_torcal_rocks',
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
isOffline: true,
|
|
||||||
originalFileName: 'tanners_ridge',
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
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, {
|
||||||
type: LibraryType.EXTERNAL,
|
type: LibraryType.EXTERNAL,
|
||||||
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
|
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
|
||||||
});
|
});
|
||||||
await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/');
|
|
||||||
|
|
||||||
await fs.promises.cp(
|
await fs.promises.cp(
|
||||||
`${IMMICH_TEST_ASSET_PATH}/albums/nature/silver_fir.jpg`,
|
`${IMMICH_TEST_ASSET_PATH}/albums/nature/silver_fir.jpg`,
|
||||||
@ -258,7 +221,6 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
|||||||
type: LibraryType.EXTERNAL,
|
type: LibraryType.EXTERNAL,
|
||||||
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
|
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
|
||||||
});
|
});
|
||||||
await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/');
|
|
||||||
|
|
||||||
await fs.promises.cp(
|
await fs.promises.cp(
|
||||||
`${IMMICH_TEST_ASSET_PATH}/albums/nature/el_torcal_rocks.jpg`,
|
`${IMMICH_TEST_ASSET_PATH}/albums/nature/el_torcal_rocks.jpg`,
|
||||||
@ -305,7 +267,6 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
|||||||
type: LibraryType.EXTERNAL,
|
type: LibraryType.EXTERNAL,
|
||||||
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
|
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
|
||||||
});
|
});
|
||||||
await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/');
|
|
||||||
|
|
||||||
await fs.promises.cp(
|
await fs.promises.cp(
|
||||||
`${IMMICH_TEST_ASSET_PATH}/albums/nature/el_torcal_rocks.jpg`,
|
`${IMMICH_TEST_ASSET_PATH}/albums/nature/el_torcal_rocks.jpg`,
|
||||||
@ -345,7 +306,6 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
|||||||
type: LibraryType.EXTERNAL,
|
type: LibraryType.EXTERNAL,
|
||||||
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
|
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
|
||||||
});
|
});
|
||||||
await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/');
|
|
||||||
|
|
||||||
await fs.promises.cp(
|
await fs.promises.cp(
|
||||||
`${IMMICH_TEST_ASSET_PATH}/albums/nature/el_torcal_rocks.jpg`,
|
`${IMMICH_TEST_ASSET_PATH}/albums/nature/el_torcal_rocks.jpg`,
|
||||||
@ -387,72 +347,6 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('External path', () => {
|
|
||||||
let library: LibraryResponseDto;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
library = await api.libraryApi.create(server, admin.accessToken, {
|
|
||||||
type: LibraryType.EXTERNAL,
|
|
||||||
importPaths: [`${IMMICH_TEST_ASSET_PATH}/albums/nature`],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not scan assets for user without external path', async () => {
|
|
||||||
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
|
|
||||||
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
|
||||||
|
|
||||||
expect(assets).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not import assets outside of user's external path", async () => {
|
|
||||||
await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/not/a/real/path');
|
|
||||||
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
|
|
||||||
|
|
||||||
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
|
||||||
expect(assets).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it.each([`${IMMICH_TEST_ASSET_PATH}/albums/nature`, `${IMMICH_TEST_ASSET_PATH}/albums/nature/`])(
|
|
||||||
'should scan external library with external path %s',
|
|
||||||
async (externalPath: string) => {
|
|
||||||
await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, externalPath);
|
|
||||||
|
|
||||||
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
|
|
||||||
|
|
||||||
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
|
||||||
|
|
||||||
expect(assets).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
type: AssetType.IMAGE,
|
|
||||||
originalFileName: 'el_torcal_rocks',
|
|
||||||
libraryId: library.id,
|
|
||||||
resized: true,
|
|
||||||
exifInfo: expect.objectContaining({
|
|
||||||
exifImageWidth: 512,
|
|
||||||
exifImageHeight: 341,
|
|
||||||
latitude: null,
|
|
||||||
longitude: null,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
type: AssetType.IMAGE,
|
|
||||||
originalFileName: 'silver_fir',
|
|
||||||
libraryId: library.id,
|
|
||||||
resized: true,
|
|
||||||
exifInfo: expect.objectContaining({
|
|
||||||
exifImageWidth: 511,
|
|
||||||
exifImageHeight: 323,
|
|
||||||
latitude: null,
|
|
||||||
longitude: null,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not scan an upload library', async () => {
|
it('should not scan an upload library', async () => {
|
||||||
const library = await api.libraryApi.create(server, admin.accessToken, {
|
const library = await api.libraryApi.create(server, admin.accessToken, {
|
||||||
type: LibraryType.UPLOAD,
|
type: LibraryType.UPLOAD,
|
||||||
@ -484,7 +378,6 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
|||||||
type: LibraryType.EXTERNAL,
|
type: LibraryType.EXTERNAL,
|
||||||
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
|
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
|
||||||
});
|
});
|
||||||
await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/');
|
|
||||||
|
|
||||||
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
|
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
|
||||||
|
|
||||||
@ -506,12 +399,11 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
|||||||
expect(assets).toEqual([]);
|
expect(assets).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not remvove 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, {
|
||||||
type: LibraryType.EXTERNAL,
|
type: LibraryType.EXTERNAL,
|
||||||
importPaths: [`${IMMICH_TEST_ASSET_PATH}/albums/nature`],
|
importPaths: [`${IMMICH_TEST_ASSET_PATH}/albums/nature`],
|
||||||
});
|
});
|
||||||
await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/');
|
|
||||||
|
|
||||||
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
|
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
|
||||||
|
|
||||||
|
@ -1,59 +1,62 @@
|
|||||||
import { LibraryEntity, LibraryType } from '@app/infra/entities';
|
import { LibraryEntity, LibraryType } from '@app/infra/entities';
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { ArrayMaxSize, ArrayUnique, IsBoolean, IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
import { ArrayMaxSize, ArrayUnique, IsBoolean, IsEnum, IsNotEmpty, IsString } from 'class-validator';
|
||||||
import { ValidateUUID } from '../domain.util';
|
import { Optional, ValidateUUID } from '../domain.util';
|
||||||
|
|
||||||
export class CreateLibraryDto {
|
export class CreateLibraryDto {
|
||||||
@IsEnum(LibraryType)
|
@IsEnum(LibraryType)
|
||||||
@ApiProperty({ enumName: 'LibraryType', enum: LibraryType })
|
@ApiProperty({ enumName: 'LibraryType', enum: LibraryType })
|
||||||
type!: LibraryType;
|
type!: LibraryType;
|
||||||
|
|
||||||
|
@ValidateUUID({ optional: true })
|
||||||
|
ownerId?: string;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
@Optional()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@Optional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
isVisible?: boolean;
|
isVisible?: boolean;
|
||||||
|
|
||||||
@IsOptional()
|
@Optional()
|
||||||
@IsString({ each: true })
|
@IsString({ each: true })
|
||||||
@IsNotEmpty({ each: true })
|
@IsNotEmpty({ each: true })
|
||||||
@ArrayUnique()
|
@ArrayUnique()
|
||||||
@ArrayMaxSize(128)
|
@ArrayMaxSize(128)
|
||||||
importPaths?: string[];
|
importPaths?: string[];
|
||||||
|
|
||||||
@IsOptional()
|
@Optional()
|
||||||
@IsString({ each: true })
|
@IsString({ each: true })
|
||||||
@IsNotEmpty({ each: true })
|
@IsNotEmpty({ each: true })
|
||||||
@ArrayUnique()
|
@ArrayUnique()
|
||||||
@ArrayMaxSize(128)
|
@ArrayMaxSize(128)
|
||||||
exclusionPatterns?: string[];
|
exclusionPatterns?: string[];
|
||||||
|
|
||||||
@IsOptional()
|
@Optional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
isWatched?: boolean;
|
isWatched?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpdateLibraryDto {
|
export class UpdateLibraryDto {
|
||||||
@IsOptional()
|
@Optional()
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@Optional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
isVisible?: boolean;
|
isVisible?: boolean;
|
||||||
|
|
||||||
@IsOptional()
|
@Optional()
|
||||||
@IsString({ each: true })
|
@IsString({ each: true })
|
||||||
@IsNotEmpty({ each: true })
|
@IsNotEmpty({ each: true })
|
||||||
@ArrayUnique()
|
@ArrayUnique()
|
||||||
@ArrayMaxSize(128)
|
@ArrayMaxSize(128)
|
||||||
importPaths?: string[];
|
importPaths?: string[];
|
||||||
|
|
||||||
@IsOptional()
|
@Optional()
|
||||||
@IsNotEmpty({ each: true })
|
@IsNotEmpty({ each: true })
|
||||||
@IsString({ each: true })
|
@IsString({ each: true })
|
||||||
@ArrayUnique()
|
@ArrayUnique()
|
||||||
@ -68,14 +71,14 @@ export class CrawlOptionsDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ValidateLibraryDto {
|
export class ValidateLibraryDto {
|
||||||
@IsOptional()
|
@Optional()
|
||||||
@IsString({ each: true })
|
@IsString({ each: true })
|
||||||
@IsNotEmpty({ each: true })
|
@IsNotEmpty({ each: true })
|
||||||
@ArrayUnique()
|
@ArrayUnique()
|
||||||
@ArrayMaxSize(128)
|
@ArrayMaxSize(128)
|
||||||
importPaths?: string[];
|
importPaths?: string[];
|
||||||
|
|
||||||
@IsOptional()
|
@Optional()
|
||||||
@IsNotEmpty({ each: true })
|
@IsNotEmpty({ each: true })
|
||||||
@IsString({ each: true })
|
@IsString({ each: true })
|
||||||
@ArrayUnique()
|
@ArrayUnique()
|
||||||
@ -100,14 +103,21 @@ export class LibrarySearchDto {
|
|||||||
|
|
||||||
export class ScanLibraryDto {
|
export class ScanLibraryDto {
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
@IsOptional()
|
@Optional()
|
||||||
refreshModifiedFiles?: boolean;
|
refreshModifiedFiles?: boolean;
|
||||||
|
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
@IsOptional()
|
@Optional()
|
||||||
refreshAllFiles?: boolean = false;
|
refreshAllFiles?: boolean = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class SearchLibraryDto {
|
||||||
|
@IsEnum(LibraryType)
|
||||||
|
@ApiProperty({ enumName: 'LibraryType', enum: LibraryType })
|
||||||
|
@Optional()
|
||||||
|
type?: LibraryType;
|
||||||
|
}
|
||||||
|
|
||||||
export class LibraryResponseDto {
|
export class LibraryResponseDto {
|
||||||
id!: string;
|
id!: string;
|
||||||
ownerId!: string;
|
ownerId!: string;
|
||||||
|
@ -140,24 +140,6 @@ describe(LibraryService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('handleQueueAssetRefresh', () => {
|
describe('handleQueueAssetRefresh', () => {
|
||||||
it("should not queue assets outside of user's external path", async () => {
|
|
||||||
const mockLibraryJob: ILibraryRefreshJob = {
|
|
||||||
id: libraryStub.externalLibrary1.id,
|
|
||||||
refreshModifiedFiles: false,
|
|
||||||
refreshAllFiles: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
|
||||||
storageMock.crawl.mockResolvedValue(['/data/user2/photo.jpg']);
|
|
||||||
assetMock.getByLibraryId.mockResolvedValue([]);
|
|
||||||
libraryMock.getOnlineAssetPaths.mockResolvedValue([]);
|
|
||||||
userMock.get.mockResolvedValue(userStub.externalPath1);
|
|
||||||
|
|
||||||
await sut.handleQueueAssetRefresh(mockLibraryJob);
|
|
||||||
|
|
||||||
expect(jobMock.queue.mock.calls).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should queue new assets', async () => {
|
it('should queue new assets', async () => {
|
||||||
const mockLibraryJob: ILibraryRefreshJob = {
|
const mockLibraryJob: ILibraryRefreshJob = {
|
||||||
id: libraryStub.externalLibrary1.id,
|
id: libraryStub.externalLibrary1.id,
|
||||||
@ -168,8 +150,7 @@ describe(LibraryService.name, () => {
|
|||||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
||||||
storageMock.crawl.mockResolvedValue(['/data/user1/photo.jpg']);
|
storageMock.crawl.mockResolvedValue(['/data/user1/photo.jpg']);
|
||||||
assetMock.getByLibraryId.mockResolvedValue([]);
|
assetMock.getByLibraryId.mockResolvedValue([]);
|
||||||
libraryMock.getOnlineAssetPaths.mockResolvedValue([]);
|
userMock.get.mockResolvedValue(userStub.admin);
|
||||||
userMock.get.mockResolvedValue(userStub.externalPath1);
|
|
||||||
|
|
||||||
await sut.handleQueueAssetRefresh(mockLibraryJob);
|
await sut.handleQueueAssetRefresh(mockLibraryJob);
|
||||||
|
|
||||||
@ -196,8 +177,7 @@ describe(LibraryService.name, () => {
|
|||||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
||||||
storageMock.crawl.mockResolvedValue(['/data/user1/photo.jpg']);
|
storageMock.crawl.mockResolvedValue(['/data/user1/photo.jpg']);
|
||||||
assetMock.getByLibraryId.mockResolvedValue([]);
|
assetMock.getByLibraryId.mockResolvedValue([]);
|
||||||
libraryMock.getOnlineAssetPaths.mockResolvedValue([]);
|
userMock.get.mockResolvedValue(userStub.admin);
|
||||||
userMock.get.mockResolvedValue(userStub.externalPath1);
|
|
||||||
|
|
||||||
await sut.handleQueueAssetRefresh(mockLibraryJob);
|
await sut.handleQueueAssetRefresh(mockLibraryJob);
|
||||||
|
|
||||||
@ -214,45 +194,6 @@ describe(LibraryService.name, () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should mark assets outside of the user's external path as offline", async () => {
|
|
||||||
const mockLibraryJob: ILibraryRefreshJob = {
|
|
||||||
id: libraryStub.externalLibrary1.id,
|
|
||||||
refreshModifiedFiles: false,
|
|
||||||
refreshAllFiles: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
|
||||||
storageMock.crawl.mockResolvedValue(['/data/user1/photo.jpg']);
|
|
||||||
assetMock.getByLibraryId.mockResolvedValue([assetStub.external]);
|
|
||||||
libraryMock.getOnlineAssetPaths.mockResolvedValue([]);
|
|
||||||
userMock.get.mockResolvedValue(userStub.externalPath2);
|
|
||||||
|
|
||||||
await sut.handleQueueAssetRefresh(mockLibraryJob);
|
|
||||||
|
|
||||||
expect(assetMock.updateAll.mock.calls).toEqual([
|
|
||||||
[
|
|
||||||
[assetStub.external.id],
|
|
||||||
{
|
|
||||||
isOffline: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not scan libraries owned by user without external path', async () => {
|
|
||||||
const mockLibraryJob: ILibraryRefreshJob = {
|
|
||||||
id: libraryStub.externalLibrary1.id,
|
|
||||||
refreshModifiedFiles: false,
|
|
||||||
refreshAllFiles: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
|
||||||
|
|
||||||
userMock.get.mockResolvedValue(userStub.user1);
|
|
||||||
|
|
||||||
await expect(sut.handleQueueAssetRefresh(mockLibraryJob)).resolves.toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not scan upload libraries', async () => {
|
it('should not scan upload libraries', async () => {
|
||||||
const mockLibraryJob: ILibraryRefreshJob = {
|
const mockLibraryJob: ILibraryRefreshJob = {
|
||||||
id: libraryStub.externalLibrary1.id,
|
id: libraryStub.externalLibrary1.id,
|
||||||
@ -287,7 +228,6 @@ describe(LibraryService.name, () => {
|
|||||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
||||||
storageMock.crawl.mockResolvedValue([]);
|
storageMock.crawl.mockResolvedValue([]);
|
||||||
assetMock.getByLibraryId.mockResolvedValue([]);
|
assetMock.getByLibraryId.mockResolvedValue([]);
|
||||||
libraryMock.getOnlineAssetPaths.mockResolvedValue([]);
|
|
||||||
userMock.get.mockResolvedValue(userStub.externalPathRoot);
|
userMock.get.mockResolvedValue(userStub.externalPathRoot);
|
||||||
|
|
||||||
await sut.handleQueueAssetRefresh(mockLibraryJob);
|
await sut.handleQueueAssetRefresh(mockLibraryJob);
|
||||||
@ -303,7 +243,7 @@ describe(LibraryService.name, () => {
|
|||||||
let mockUser: UserEntity;
|
let mockUser: UserEntity;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockUser = userStub.externalPath1;
|
mockUser = userStub.admin;
|
||||||
userMock.get.mockResolvedValue(mockUser);
|
userMock.get.mockResolvedValue(mockUser);
|
||||||
|
|
||||||
storageMock.stat.mockResolvedValue({
|
storageMock.stat.mockResolvedValue({
|
||||||
@ -780,26 +720,6 @@ describe(LibraryService.name, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getAllForUser', () => {
|
|
||||||
it('should return all libraries for user', async () => {
|
|
||||||
libraryMock.getAllByUserId.mockResolvedValue([libraryStub.uploadLibrary1, libraryStub.externalLibrary1]);
|
|
||||||
await expect(sut.getAllForUser(authStub.admin)).resolves.toEqual([
|
|
||||||
expect.objectContaining({
|
|
||||||
id: libraryStub.uploadLibrary1.id,
|
|
||||||
name: libraryStub.uploadLibrary1.name,
|
|
||||||
ownerId: libraryStub.uploadLibrary1.ownerId,
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
id: libraryStub.externalLibrary1.id,
|
|
||||||
name: libraryStub.externalLibrary1.name,
|
|
||||||
ownerId: libraryStub.externalLibrary1.ownerId,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(libraryMock.getAllByUserId).toHaveBeenCalledWith(authStub.admin.user.id);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getStatistics', () => {
|
describe('getStatistics', () => {
|
||||||
it('should return library statistics', async () => {
|
it('should return library statistics', async () => {
|
||||||
libraryMock.getStatistics.mockResolvedValue({ photos: 10, videos: 0, total: 10, usage: 1337 });
|
libraryMock.getStatistics.mockResolvedValue({ photos: 10, videos: 0, total: 10, usage: 1337 });
|
||||||
@ -1144,12 +1064,12 @@ describe(LibraryService.name, () => {
|
|||||||
storageMock.checkFileExists.mockResolvedValue(true);
|
storageMock.checkFileExists.mockResolvedValue(true);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
sut.update(authStub.external1, authStub.external1.user.id, { importPaths: ['/data/user1/foo'] }),
|
sut.update(authStub.admin, authStub.admin.user.id, { importPaths: ['/data/user1/foo'] }),
|
||||||
).resolves.toEqual(mapLibrary(libraryStub.externalLibraryWithImportPaths1));
|
).resolves.toEqual(mapLibrary(libraryStub.externalLibraryWithImportPaths1));
|
||||||
|
|
||||||
expect(libraryMock.update).toHaveBeenCalledWith(
|
expect(libraryMock.update).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: authStub.external1.user.id,
|
id: authStub.admin.user.id,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(storageMock.watch).toHaveBeenCalledWith(
|
expect(storageMock.watch).toHaveBeenCalledWith(
|
||||||
@ -1584,26 +1504,6 @@ describe(LibraryService.name, () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should error when no external path is set', async () => {
|
|
||||||
await expect(
|
|
||||||
sut.validate(authStub.admin, libraryStub.externalLibrary1.id, { importPaths: ['/photos'] }),
|
|
||||||
).rejects.toBeInstanceOf(BadRequestException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should detect when path is outside external path', async () => {
|
|
||||||
const result = await sut.validate(authStub.external1, libraryStub.externalLibraryWithImportPaths1.id, {
|
|
||||||
importPaths: ['/data/user2'],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.importPaths).toEqual([
|
|
||||||
{
|
|
||||||
importPath: '/data/user2',
|
|
||||||
isValid: false,
|
|
||||||
message: "Not contained in user's external path",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should detect when path does not exist', async () => {
|
it('should detect when path does not exist', async () => {
|
||||||
storageMock.stat.mockImplementation(() => {
|
storageMock.stat.mockImplementation(() => {
|
||||||
const error = { code: 'ENOENT' } as any;
|
const error = { code: 'ENOENT' } as any;
|
||||||
|
@ -29,6 +29,7 @@ import {
|
|||||||
LibraryResponseDto,
|
LibraryResponseDto,
|
||||||
LibraryStatsResponseDto,
|
LibraryStatsResponseDto,
|
||||||
ScanLibraryDto,
|
ScanLibraryDto,
|
||||||
|
SearchLibraryDto,
|
||||||
UpdateLibraryDto,
|
UpdateLibraryDto,
|
||||||
ValidateLibraryDto,
|
ValidateLibraryDto,
|
||||||
ValidateLibraryImportPathResponseDto,
|
ValidateLibraryImportPathResponseDto,
|
||||||
@ -182,6 +183,7 @@ export class LibraryService extends EventEmitter {
|
|||||||
|
|
||||||
async getStatistics(auth: AuthDto, id: string): Promise<LibraryStatsResponseDto> {
|
async getStatistics(auth: AuthDto, id: string): Promise<LibraryStatsResponseDto> {
|
||||||
await this.access.requirePermission(auth, Permission.LIBRARY_READ, id);
|
await this.access.requirePermission(auth, Permission.LIBRARY_READ, id);
|
||||||
|
|
||||||
return this.repository.getStatistics(id);
|
return this.repository.getStatistics(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -189,17 +191,18 @@ export class LibraryService extends EventEmitter {
|
|||||||
return this.repository.getCountForUser(auth.user.id);
|
return this.repository.getCountForUser(auth.user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllForUser(auth: AuthDto): Promise<LibraryResponseDto[]> {
|
|
||||||
const libraries = await this.repository.getAllByUserId(auth.user.id);
|
|
||||||
return libraries.map((library) => mapLibrary(library));
|
|
||||||
}
|
|
||||||
|
|
||||||
async get(auth: AuthDto, id: string): Promise<LibraryResponseDto> {
|
async get(auth: AuthDto, id: string): Promise<LibraryResponseDto> {
|
||||||
await this.access.requirePermission(auth, Permission.LIBRARY_READ, id);
|
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[]> {
|
||||||
|
const libraries = await this.repository.getAll(false, dto.type);
|
||||||
|
return libraries.map((library) => mapLibrary(library));
|
||||||
|
}
|
||||||
|
|
||||||
async handleQueueCleanup(): Promise<boolean> {
|
async handleQueueCleanup(): Promise<boolean> {
|
||||||
this.logger.debug('Cleaning up any pending library deletions');
|
this.logger.debug('Cleaning up any pending library deletions');
|
||||||
const pendingDeletion = await this.repository.getAllDeleted();
|
const pendingDeletion = await this.repository.getAllDeleted();
|
||||||
@ -234,8 +237,14 @@ 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: auth.user.id,
|
ownerId,
|
||||||
name: dto.name,
|
name: dto.name,
|
||||||
type: dto.type,
|
type: dto.type,
|
||||||
importPaths: dto.importPaths ?? [],
|
importPaths: dto.importPaths ?? [],
|
||||||
@ -300,24 +309,11 @@ export class LibraryService extends EventEmitter {
|
|||||||
public async validate(auth: AuthDto, id: string, dto: ValidateLibraryDto): Promise<ValidateLibraryResponseDto> {
|
public async validate(auth: AuthDto, id: string, dto: ValidateLibraryDto): Promise<ValidateLibraryResponseDto> {
|
||||||
await this.access.requirePermission(auth, Permission.LIBRARY_UPDATE, id);
|
await this.access.requirePermission(auth, Permission.LIBRARY_UPDATE, id);
|
||||||
|
|
||||||
if (!auth.user.externalPath) {
|
|
||||||
throw new BadRequestException('User has no external path set');
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = new ValidateLibraryResponseDto();
|
const response = new ValidateLibraryResponseDto();
|
||||||
|
|
||||||
if (dto.importPaths) {
|
if (dto.importPaths) {
|
||||||
response.importPaths = await Promise.all(
|
response.importPaths = await Promise.all(
|
||||||
dto.importPaths.map(async (importPath) => {
|
dto.importPaths.map(async (importPath) => {
|
||||||
const normalizedPath = path.normalize(importPath);
|
|
||||||
|
|
||||||
if (!this.isInExternalPath(normalizedPath, auth.user.externalPath)) {
|
|
||||||
const validation = new ValidateLibraryImportPathResponseDto();
|
|
||||||
validation.importPath = importPath;
|
|
||||||
validation.message = `Not contained in user's external path`;
|
|
||||||
return validation;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await this.validateImportPath(importPath);
|
return await this.validateImportPath(importPath);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -328,6 +324,7 @@ export class LibraryService extends EventEmitter {
|
|||||||
|
|
||||||
async update(auth: AuthDto, id: string, dto: UpdateLibraryDto): Promise<LibraryResponseDto> {
|
async update(auth: AuthDto, id: string, dto: UpdateLibraryDto): Promise<LibraryResponseDto> {
|
||||||
await this.access.requirePermission(auth, Permission.LIBRARY_UPDATE, id);
|
await this.access.requirePermission(auth, Permission.LIBRARY_UPDATE, id);
|
||||||
|
|
||||||
const library = await this.repository.update({ id, ...dto });
|
const library = await this.repository.update({ id, ...dto });
|
||||||
|
|
||||||
if (dto.importPaths) {
|
if (dto.importPaths) {
|
||||||
@ -404,7 +401,7 @@ export class LibraryService extends EventEmitter {
|
|||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
// File can't be accessed and does not already exist in db
|
// File can't be accessed and does not already exist in db
|
||||||
throw new BadRequestException("Can't access file", { cause: error });
|
throw new BadRequestException('Cannot access file', { cause: error });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -591,12 +588,6 @@ export class LibraryService extends EventEmitter {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await this.userRepository.get(library.ownerId, {});
|
|
||||||
if (!user?.externalPath) {
|
|
||||||
this.logger.warn('User has no external path set, cannot refresh library');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.verbose(`Refreshing library: ${job.id}`);
|
this.logger.verbose(`Refreshing library: ${job.id}`);
|
||||||
|
|
||||||
const pathValidation = await Promise.all(
|
const pathValidation = await Promise.all(
|
||||||
@ -618,11 +609,7 @@ export class LibraryService extends EventEmitter {
|
|||||||
exclusionPatterns: library.exclusionPatterns,
|
exclusionPatterns: library.exclusionPatterns,
|
||||||
});
|
});
|
||||||
|
|
||||||
const crawledAssetPaths = rawPaths
|
const crawledAssetPaths = rawPaths.map((filePath) => path.normalize(filePath));
|
||||||
// Normalize file paths. This is important to prevent security issues like path traversal
|
|
||||||
.map((filePath) => path.normalize(filePath))
|
|
||||||
// Filter out paths that are not within the user's external path
|
|
||||||
.filter((assetPath) => this.isInExternalPath(assetPath, user.externalPath)) as string[];
|
|
||||||
|
|
||||||
this.logger.debug(`Found ${crawledAssetPaths.length} asset(s) when crawling import paths ${library.importPaths}`);
|
this.logger.debug(`Found ${crawledAssetPaths.length} asset(s) when crawling import paths ${library.importPaths}`);
|
||||||
const assetsInLibrary = await this.assetRepository.getByLibraryId([job.id]);
|
const assetsInLibrary = await this.assetRepository.getByLibraryId([job.id]);
|
||||||
|
@ -18,7 +18,6 @@ const responseDto = {
|
|||||||
createdAt: new Date('2021-01-01'),
|
createdAt: new Date('2021-01-01'),
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
updatedAt: new Date('2021-01-01'),
|
updatedAt: new Date('2021-01-01'),
|
||||||
externalPath: null,
|
|
||||||
memoriesEnabled: true,
|
memoriesEnabled: true,
|
||||||
avatarColor: UserAvatarColor.PRIMARY,
|
avatarColor: UserAvatarColor.PRIMARY,
|
||||||
quotaSizeInBytes: null,
|
quotaSizeInBytes: null,
|
||||||
@ -37,7 +36,6 @@ const responseDto = {
|
|||||||
createdAt: new Date('2021-01-01'),
|
createdAt: new Date('2021-01-01'),
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
updatedAt: new Date('2021-01-01'),
|
updatedAt: new Date('2021-01-01'),
|
||||||
externalPath: null,
|
|
||||||
memoriesEnabled: true,
|
memoriesEnabled: true,
|
||||||
avatarColor: UserAvatarColor.PRIMARY,
|
avatarColor: UserAvatarColor.PRIMARY,
|
||||||
inTimeline: true,
|
inTimeline: true,
|
||||||
|
@ -5,7 +5,6 @@ export const ILibraryRepository = 'ILibraryRepository';
|
|||||||
|
|
||||||
export interface ILibraryRepository {
|
export interface ILibraryRepository {
|
||||||
getCountForUser(ownerId: string): Promise<number>;
|
getCountForUser(ownerId: string): Promise<number>;
|
||||||
getAllByUserId(userId: string, type?: LibraryType): Promise<LibraryEntity[]>;
|
|
||||||
getAll(withDeleted?: boolean, type?: LibraryType): Promise<LibraryEntity[]>;
|
getAll(withDeleted?: boolean, type?: LibraryType): Promise<LibraryEntity[]>;
|
||||||
getAllDeleted(): Promise<LibraryEntity[]>;
|
getAllDeleted(): Promise<LibraryEntity[]>;
|
||||||
get(id: string, withDeleted?: boolean): Promise<LibraryEntity | null>;
|
get(id: string, withDeleted?: boolean): Promise<LibraryEntity | null>;
|
||||||
@ -16,7 +15,5 @@ export interface ILibraryRepository {
|
|||||||
getUploadLibraryCount(ownerId: string): Promise<number>;
|
getUploadLibraryCount(ownerId: string): Promise<number>;
|
||||||
update(library: Partial<LibraryEntity>): Promise<LibraryEntity>;
|
update(library: Partial<LibraryEntity>): Promise<LibraryEntity>;
|
||||||
getStatistics(id: string): Promise<LibraryStatsResponseDto>;
|
getStatistics(id: string): Promise<LibraryStatsResponseDto>;
|
||||||
getOnlineAssetPaths(id: string): Promise<string[]>;
|
|
||||||
getAssetIds(id: string, withDeleted?: boolean): Promise<string[]>;
|
getAssetIds(id: string, withDeleted?: boolean): Promise<string[]>;
|
||||||
existsByName(name: string, withDeleted?: boolean): Promise<boolean>;
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
import { Optional } from '../../domain.util';
|
||||||
|
|
||||||
export enum SearchSuggestionType {
|
export enum SearchSuggestionType {
|
||||||
COUNTRY = 'country',
|
COUNTRY = 'country',
|
||||||
@ -16,18 +17,18 @@ export class SearchSuggestionRequestDto {
|
|||||||
type!: SearchSuggestionType;
|
type!: SearchSuggestionType;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
@Optional()
|
||||||
country?: string;
|
country?: string;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
@Optional()
|
||||||
state?: string;
|
state?: string;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
@Optional()
|
||||||
make?: string;
|
make?: string;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
@Optional()
|
||||||
model?: string;
|
model?: string;
|
||||||
}
|
}
|
||||||
|
@ -21,10 +21,6 @@ export class CreateUserDto {
|
|||||||
@Transform(toSanitized)
|
@Transform(toSanitized)
|
||||||
storageLabel?: string | null;
|
storageLabel?: string | null;
|
||||||
|
|
||||||
@Optional({ nullable: true })
|
|
||||||
@IsString()
|
|
||||||
externalPath?: string | null;
|
|
||||||
|
|
||||||
@Optional()
|
@Optional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
memoriesEnabled?: boolean;
|
memoriesEnabled?: boolean;
|
||||||
|
@ -25,10 +25,6 @@ export class UpdateUserDto {
|
|||||||
@Transform(toSanitized)
|
@Transform(toSanitized)
|
||||||
storageLabel?: string;
|
storageLabel?: string;
|
||||||
|
|
||||||
@Optional()
|
|
||||||
@IsString()
|
|
||||||
externalPath?: string;
|
|
||||||
|
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsUUID('4')
|
@IsUUID('4')
|
||||||
@ApiProperty({ format: 'uuid' })
|
@ApiProperty({ format: 'uuid' })
|
||||||
|
@ -22,7 +22,6 @@ export class UserDto {
|
|||||||
|
|
||||||
export class UserResponseDto extends UserDto {
|
export class UserResponseDto extends UserDto {
|
||||||
storageLabel!: string | null;
|
storageLabel!: string | null;
|
||||||
externalPath!: string | null;
|
|
||||||
shouldChangePassword!: boolean;
|
shouldChangePassword!: boolean;
|
||||||
isAdmin!: boolean;
|
isAdmin!: boolean;
|
||||||
createdAt!: Date;
|
createdAt!: Date;
|
||||||
@ -50,7 +49,6 @@ export function mapUser(entity: UserEntity): UserResponseDto {
|
|||||||
return {
|
return {
|
||||||
...mapSimpleUser(entity),
|
...mapSimpleUser(entity),
|
||||||
storageLabel: entity.storageLabel,
|
storageLabel: entity.storageLabel,
|
||||||
externalPath: entity.externalPath,
|
|
||||||
shouldChangePassword: entity.shouldChangePassword,
|
shouldChangePassword: entity.shouldChangePassword,
|
||||||
isAdmin: entity.isAdmin,
|
isAdmin: entity.isAdmin,
|
||||||
createdAt: entity.createdAt,
|
createdAt: entity.createdAt,
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { LibraryType, UserEntity } from '@app/infra/entities';
|
import { LibraryType, UserEntity } from '@app/infra/entities';
|
||||||
import { BadRequestException, ForbiddenException } from '@nestjs/common';
|
import { BadRequestException, ForbiddenException } from '@nestjs/common';
|
||||||
import path from 'node:path';
|
|
||||||
import sanitize from 'sanitize-filename';
|
import sanitize from 'sanitize-filename';
|
||||||
import { ICryptoRepository, ILibraryRepository, IUserRepository } from '../repositories';
|
import { ICryptoRepository, ILibraryRepository, IUserRepository } from '../repositories';
|
||||||
import { UserResponseDto } from './response-dto';
|
import { UserResponseDto } from './response-dto';
|
||||||
@ -42,7 +41,6 @@ export class UserCore {
|
|||||||
// Users can never update the isAdmin property.
|
// Users can never update the isAdmin property.
|
||||||
delete dto.isAdmin;
|
delete dto.isAdmin;
|
||||||
delete dto.storageLabel;
|
delete dto.storageLabel;
|
||||||
delete dto.externalPath;
|
|
||||||
} else if (dto.isAdmin && user.id !== id) {
|
} else if (dto.isAdmin && user.id !== id) {
|
||||||
// Admin cannot create another admin.
|
// Admin cannot create another admin.
|
||||||
throw new BadRequestException('The server already has an admin');
|
throw new BadRequestException('The server already has an admin');
|
||||||
@ -70,12 +68,6 @@ export class UserCore {
|
|||||||
dto.storageLabel = null;
|
dto.storageLabel = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dto.externalPath === '') {
|
|
||||||
dto.externalPath = null;
|
|
||||||
} else if (dto.externalPath) {
|
|
||||||
dto.externalPath = path.normalize(dto.externalPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.userRepository.update(id, dto);
|
return this.userRepository.update(id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,13 +5,14 @@ import {
|
|||||||
LibraryStatsResponseDto,
|
LibraryStatsResponseDto,
|
||||||
LibraryResponseDto as ResponseDto,
|
LibraryResponseDto as ResponseDto,
|
||||||
ScanLibraryDto,
|
ScanLibraryDto,
|
||||||
|
SearchLibraryDto,
|
||||||
UpdateLibraryDto as UpdateDto,
|
UpdateLibraryDto as UpdateDto,
|
||||||
ValidateLibraryDto,
|
ValidateLibraryDto,
|
||||||
ValidateLibraryResponseDto,
|
ValidateLibraryResponseDto,
|
||||||
} from '@app/domain';
|
} from '@app/domain';
|
||||||
import { Body, Controller, Delete, Get, HttpCode, Param, Post, Put } from '@nestjs/common';
|
import { Body, Controller, Delete, Get, HttpCode, Param, Post, Put, Query } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { Auth, Authenticated } from '../app.guard';
|
import { AdminRoute, Auth, Authenticated } from '../app.guard';
|
||||||
import { UseValidation } from '../app.utils';
|
import { UseValidation } from '../app.utils';
|
||||||
import { UUIDParamDto } from './dto/uuid-param.dto';
|
import { UUIDParamDto } from './dto/uuid-param.dto';
|
||||||
|
|
||||||
@ -19,12 +20,13 @@ import { UUIDParamDto } from './dto/uuid-param.dto';
|
|||||||
@Controller('library')
|
@Controller('library')
|
||||||
@Authenticated()
|
@Authenticated()
|
||||||
@UseValidation()
|
@UseValidation()
|
||||||
|
@AdminRoute()
|
||||||
export class LibraryController {
|
export class LibraryController {
|
||||||
constructor(private service: LibraryService) {}
|
constructor(private service: LibraryService) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
getLibraries(@Auth() auth: AuthDto): Promise<ResponseDto[]> {
|
getAllLibraries(@Auth() auth: AuthDto, @Query() dto: SearchLibraryDto): Promise<ResponseDto[]> {
|
||||||
return this.service.getAllForUser(auth);
|
return this.service.getAll(auth, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@ -38,7 +40,7 @@ export class LibraryController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
getLibraryInfo(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<ResponseDto> {
|
getLibrary(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<ResponseDto> {
|
||||||
return this.service.get(auth, id);
|
return this.service.get(auth, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,9 +43,6 @@ export class UserEntity {
|
|||||||
@Column({ type: 'varchar', unique: true, default: null })
|
@Column({ type: 'varchar', unique: true, default: null })
|
||||||
storageLabel!: string | null;
|
storageLabel!: string | null;
|
||||||
|
|
||||||
@Column({ type: 'varchar', default: null })
|
|
||||||
externalPath!: string | null;
|
|
||||||
|
|
||||||
@Column({ default: '', select: false })
|
@Column({ default: '', select: false })
|
||||||
password?: string;
|
password?: string;
|
||||||
|
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class RemoveExternalPath1708425975121 implements MigrationInterface {
|
||||||
|
name = 'RemoveExternalPath1708425975121';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "externalPath"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "users" ADD "externalPath" character varying`);
|
||||||
|
}
|
||||||
|
}
|
@ -21,7 +21,6 @@ FROM
|
|||||||
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
|
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
|
||||||
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
|
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
|
||||||
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
|
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
|
||||||
"AlbumEntity__AlbumEntity_owner"."externalPath" AS "AlbumEntity__AlbumEntity_owner_externalPath",
|
|
||||||
"AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId",
|
"AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId",
|
||||||
"AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath",
|
"AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath",
|
||||||
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
|
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
|
||||||
@ -37,7 +36,6 @@ FROM
|
|||||||
"AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin",
|
"AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin",
|
||||||
"AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email",
|
"AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email",
|
||||||
"AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel",
|
"AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel",
|
||||||
"AlbumEntity__AlbumEntity_sharedUsers"."externalPath" AS "AlbumEntity__AlbumEntity_sharedUsers_externalPath",
|
|
||||||
"AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId",
|
"AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId",
|
||||||
"AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath",
|
"AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath",
|
||||||
"AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
|
"AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
|
||||||
@ -97,7 +95,6 @@ SELECT
|
|||||||
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
|
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
|
||||||
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
|
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
|
||||||
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
|
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
|
||||||
"AlbumEntity__AlbumEntity_owner"."externalPath" AS "AlbumEntity__AlbumEntity_owner_externalPath",
|
|
||||||
"AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId",
|
"AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId",
|
||||||
"AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath",
|
"AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath",
|
||||||
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
|
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
|
||||||
@ -113,7 +110,6 @@ SELECT
|
|||||||
"AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin",
|
"AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin",
|
||||||
"AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email",
|
"AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email",
|
||||||
"AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel",
|
"AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel",
|
||||||
"AlbumEntity__AlbumEntity_sharedUsers"."externalPath" AS "AlbumEntity__AlbumEntity_sharedUsers_externalPath",
|
|
||||||
"AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId",
|
"AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId",
|
||||||
"AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath",
|
"AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath",
|
||||||
"AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
|
"AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
|
||||||
@ -155,7 +151,6 @@ SELECT
|
|||||||
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
|
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
|
||||||
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
|
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
|
||||||
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
|
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
|
||||||
"AlbumEntity__AlbumEntity_owner"."externalPath" AS "AlbumEntity__AlbumEntity_owner_externalPath",
|
|
||||||
"AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId",
|
"AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId",
|
||||||
"AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath",
|
"AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath",
|
||||||
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
|
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
|
||||||
@ -171,7 +166,6 @@ SELECT
|
|||||||
"AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin",
|
"AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin",
|
||||||
"AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email",
|
"AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email",
|
||||||
"AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel",
|
"AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel",
|
||||||
"AlbumEntity__AlbumEntity_sharedUsers"."externalPath" AS "AlbumEntity__AlbumEntity_sharedUsers_externalPath",
|
|
||||||
"AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId",
|
"AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId",
|
||||||
"AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath",
|
"AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath",
|
||||||
"AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
|
"AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
|
||||||
@ -285,7 +279,6 @@ SELECT
|
|||||||
"AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin",
|
"AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin",
|
||||||
"AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email",
|
"AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email",
|
||||||
"AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel",
|
"AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel",
|
||||||
"AlbumEntity__AlbumEntity_sharedUsers"."externalPath" AS "AlbumEntity__AlbumEntity_sharedUsers_externalPath",
|
|
||||||
"AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId",
|
"AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId",
|
||||||
"AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath",
|
"AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath",
|
||||||
"AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
|
"AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
|
||||||
@ -313,7 +306,6 @@ SELECT
|
|||||||
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
|
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
|
||||||
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
|
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
|
||||||
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
|
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
|
||||||
"AlbumEntity__AlbumEntity_owner"."externalPath" AS "AlbumEntity__AlbumEntity_owner_externalPath",
|
|
||||||
"AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId",
|
"AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId",
|
||||||
"AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath",
|
"AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath",
|
||||||
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
|
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
|
||||||
@ -358,7 +350,6 @@ SELECT
|
|||||||
"AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin",
|
"AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin",
|
||||||
"AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email",
|
"AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email",
|
||||||
"AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel",
|
"AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel",
|
||||||
"AlbumEntity__AlbumEntity_sharedUsers"."externalPath" AS "AlbumEntity__AlbumEntity_sharedUsers_externalPath",
|
|
||||||
"AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId",
|
"AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId",
|
||||||
"AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath",
|
"AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath",
|
||||||
"AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
|
"AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
|
||||||
@ -386,7 +377,6 @@ SELECT
|
|||||||
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
|
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
|
||||||
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
|
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
|
||||||
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
|
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
|
||||||
"AlbumEntity__AlbumEntity_owner"."externalPath" AS "AlbumEntity__AlbumEntity_owner_externalPath",
|
|
||||||
"AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId",
|
"AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId",
|
||||||
"AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath",
|
"AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath",
|
||||||
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
|
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
|
||||||
@ -468,7 +458,6 @@ SELECT
|
|||||||
"AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin",
|
"AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin",
|
||||||
"AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email",
|
"AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email",
|
||||||
"AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel",
|
"AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel",
|
||||||
"AlbumEntity__AlbumEntity_sharedUsers"."externalPath" AS "AlbumEntity__AlbumEntity_sharedUsers_externalPath",
|
|
||||||
"AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId",
|
"AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId",
|
||||||
"AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath",
|
"AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath",
|
||||||
"AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
|
"AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
|
||||||
@ -496,7 +485,6 @@ SELECT
|
|||||||
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
|
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
|
||||||
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
|
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
|
||||||
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
|
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
|
||||||
"AlbumEntity__AlbumEntity_owner"."externalPath" AS "AlbumEntity__AlbumEntity_owner_externalPath",
|
|
||||||
"AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId",
|
"AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId",
|
||||||
"AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath",
|
"AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath",
|
||||||
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
|
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
|
||||||
@ -559,7 +547,6 @@ SELECT
|
|||||||
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
|
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
|
||||||
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
|
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
|
||||||
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
|
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
|
||||||
"AlbumEntity__AlbumEntity_owner"."externalPath" AS "AlbumEntity__AlbumEntity_owner_externalPath",
|
|
||||||
"AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId",
|
"AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId",
|
||||||
"AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath",
|
"AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath",
|
||||||
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
|
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
|
||||||
|
@ -15,7 +15,6 @@ FROM
|
|||||||
"APIKeyEntity__APIKeyEntity_user"."isAdmin" AS "APIKeyEntity__APIKeyEntity_user_isAdmin",
|
"APIKeyEntity__APIKeyEntity_user"."isAdmin" AS "APIKeyEntity__APIKeyEntity_user_isAdmin",
|
||||||
"APIKeyEntity__APIKeyEntity_user"."email" AS "APIKeyEntity__APIKeyEntity_user_email",
|
"APIKeyEntity__APIKeyEntity_user"."email" AS "APIKeyEntity__APIKeyEntity_user_email",
|
||||||
"APIKeyEntity__APIKeyEntity_user"."storageLabel" AS "APIKeyEntity__APIKeyEntity_user_storageLabel",
|
"APIKeyEntity__APIKeyEntity_user"."storageLabel" AS "APIKeyEntity__APIKeyEntity_user_storageLabel",
|
||||||
"APIKeyEntity__APIKeyEntity_user"."externalPath" AS "APIKeyEntity__APIKeyEntity_user_externalPath",
|
|
||||||
"APIKeyEntity__APIKeyEntity_user"."oauthId" AS "APIKeyEntity__APIKeyEntity_user_oauthId",
|
"APIKeyEntity__APIKeyEntity_user"."oauthId" AS "APIKeyEntity__APIKeyEntity_user_oauthId",
|
||||||
"APIKeyEntity__APIKeyEntity_user"."profileImagePath" AS "APIKeyEntity__APIKeyEntity_user_profileImagePath",
|
"APIKeyEntity__APIKeyEntity_user"."profileImagePath" AS "APIKeyEntity__APIKeyEntity_user_profileImagePath",
|
||||||
"APIKeyEntity__APIKeyEntity_user"."shouldChangePassword" AS "APIKeyEntity__APIKeyEntity_user_shouldChangePassword",
|
"APIKeyEntity__APIKeyEntity_user"."shouldChangePassword" AS "APIKeyEntity__APIKeyEntity_user_shouldChangePassword",
|
||||||
|
@ -23,7 +23,6 @@ FROM
|
|||||||
"LibraryEntity__LibraryEntity_owner"."isAdmin" AS "LibraryEntity__LibraryEntity_owner_isAdmin",
|
"LibraryEntity__LibraryEntity_owner"."isAdmin" AS "LibraryEntity__LibraryEntity_owner_isAdmin",
|
||||||
"LibraryEntity__LibraryEntity_owner"."email" AS "LibraryEntity__LibraryEntity_owner_email",
|
"LibraryEntity__LibraryEntity_owner"."email" AS "LibraryEntity__LibraryEntity_owner_email",
|
||||||
"LibraryEntity__LibraryEntity_owner"."storageLabel" AS "LibraryEntity__LibraryEntity_owner_storageLabel",
|
"LibraryEntity__LibraryEntity_owner"."storageLabel" AS "LibraryEntity__LibraryEntity_owner_storageLabel",
|
||||||
"LibraryEntity__LibraryEntity_owner"."externalPath" AS "LibraryEntity__LibraryEntity_owner_externalPath",
|
|
||||||
"LibraryEntity__LibraryEntity_owner"."oauthId" AS "LibraryEntity__LibraryEntity_owner_oauthId",
|
"LibraryEntity__LibraryEntity_owner"."oauthId" AS "LibraryEntity__LibraryEntity_owner_oauthId",
|
||||||
"LibraryEntity__LibraryEntity_owner"."profileImagePath" AS "LibraryEntity__LibraryEntity_owner_profileImagePath",
|
"LibraryEntity__LibraryEntity_owner"."profileImagePath" AS "LibraryEntity__LibraryEntity_owner_profileImagePath",
|
||||||
"LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword",
|
"LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword",
|
||||||
@ -139,7 +138,6 @@ SELECT
|
|||||||
"LibraryEntity__LibraryEntity_owner"."isAdmin" AS "LibraryEntity__LibraryEntity_owner_isAdmin",
|
"LibraryEntity__LibraryEntity_owner"."isAdmin" AS "LibraryEntity__LibraryEntity_owner_isAdmin",
|
||||||
"LibraryEntity__LibraryEntity_owner"."email" AS "LibraryEntity__LibraryEntity_owner_email",
|
"LibraryEntity__LibraryEntity_owner"."email" AS "LibraryEntity__LibraryEntity_owner_email",
|
||||||
"LibraryEntity__LibraryEntity_owner"."storageLabel" AS "LibraryEntity__LibraryEntity_owner_storageLabel",
|
"LibraryEntity__LibraryEntity_owner"."storageLabel" AS "LibraryEntity__LibraryEntity_owner_storageLabel",
|
||||||
"LibraryEntity__LibraryEntity_owner"."externalPath" AS "LibraryEntity__LibraryEntity_owner_externalPath",
|
|
||||||
"LibraryEntity__LibraryEntity_owner"."oauthId" AS "LibraryEntity__LibraryEntity_owner_oauthId",
|
"LibraryEntity__LibraryEntity_owner"."oauthId" AS "LibraryEntity__LibraryEntity_owner_oauthId",
|
||||||
"LibraryEntity__LibraryEntity_owner"."profileImagePath" AS "LibraryEntity__LibraryEntity_owner_profileImagePath",
|
"LibraryEntity__LibraryEntity_owner"."profileImagePath" AS "LibraryEntity__LibraryEntity_owner_profileImagePath",
|
||||||
"LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword",
|
"LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword",
|
||||||
@ -185,7 +183,6 @@ SELECT
|
|||||||
"LibraryEntity__LibraryEntity_owner"."isAdmin" AS "LibraryEntity__LibraryEntity_owner_isAdmin",
|
"LibraryEntity__LibraryEntity_owner"."isAdmin" AS "LibraryEntity__LibraryEntity_owner_isAdmin",
|
||||||
"LibraryEntity__LibraryEntity_owner"."email" AS "LibraryEntity__LibraryEntity_owner_email",
|
"LibraryEntity__LibraryEntity_owner"."email" AS "LibraryEntity__LibraryEntity_owner_email",
|
||||||
"LibraryEntity__LibraryEntity_owner"."storageLabel" AS "LibraryEntity__LibraryEntity_owner_storageLabel",
|
"LibraryEntity__LibraryEntity_owner"."storageLabel" AS "LibraryEntity__LibraryEntity_owner_storageLabel",
|
||||||
"LibraryEntity__LibraryEntity_owner"."externalPath" AS "LibraryEntity__LibraryEntity_owner_externalPath",
|
|
||||||
"LibraryEntity__LibraryEntity_owner"."oauthId" AS "LibraryEntity__LibraryEntity_owner_oauthId",
|
"LibraryEntity__LibraryEntity_owner"."oauthId" AS "LibraryEntity__LibraryEntity_owner_oauthId",
|
||||||
"LibraryEntity__LibraryEntity_owner"."profileImagePath" AS "LibraryEntity__LibraryEntity_owner_profileImagePath",
|
"LibraryEntity__LibraryEntity_owner"."profileImagePath" AS "LibraryEntity__LibraryEntity_owner_profileImagePath",
|
||||||
"LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword",
|
"LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword",
|
||||||
@ -225,7 +222,6 @@ SELECT
|
|||||||
"LibraryEntity__LibraryEntity_owner"."isAdmin" AS "LibraryEntity__LibraryEntity_owner_isAdmin",
|
"LibraryEntity__LibraryEntity_owner"."isAdmin" AS "LibraryEntity__LibraryEntity_owner_isAdmin",
|
||||||
"LibraryEntity__LibraryEntity_owner"."email" AS "LibraryEntity__LibraryEntity_owner_email",
|
"LibraryEntity__LibraryEntity_owner"."email" AS "LibraryEntity__LibraryEntity_owner_email",
|
||||||
"LibraryEntity__LibraryEntity_owner"."storageLabel" AS "LibraryEntity__LibraryEntity_owner_storageLabel",
|
"LibraryEntity__LibraryEntity_owner"."storageLabel" AS "LibraryEntity__LibraryEntity_owner_storageLabel",
|
||||||
"LibraryEntity__LibraryEntity_owner"."externalPath" AS "LibraryEntity__LibraryEntity_owner_externalPath",
|
|
||||||
"LibraryEntity__LibraryEntity_owner"."oauthId" AS "LibraryEntity__LibraryEntity_owner_oauthId",
|
"LibraryEntity__LibraryEntity_owner"."oauthId" AS "LibraryEntity__LibraryEntity_owner_oauthId",
|
||||||
"LibraryEntity__LibraryEntity_owner"."profileImagePath" AS "LibraryEntity__LibraryEntity_owner_profileImagePath",
|
"LibraryEntity__LibraryEntity_owner"."profileImagePath" AS "LibraryEntity__LibraryEntity_owner_profileImagePath",
|
||||||
"LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword",
|
"LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword",
|
||||||
|
@ -150,7 +150,6 @@ FROM
|
|||||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."isAdmin" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_isAdmin",
|
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."isAdmin" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_isAdmin",
|
||||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."email" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_email",
|
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."email" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_email",
|
||||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."storageLabel" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_storageLabel",
|
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."storageLabel" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_storageLabel",
|
||||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."externalPath" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_externalPath",
|
|
||||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."oauthId" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_oauthId",
|
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."oauthId" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_oauthId",
|
||||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."profileImagePath" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_profileImagePath",
|
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."profileImagePath" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_profileImagePath",
|
||||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."shouldChangePassword" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_shouldChangePassword",
|
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."shouldChangePassword" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_shouldChangePassword",
|
||||||
@ -254,7 +253,6 @@ SELECT
|
|||||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."isAdmin" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_isAdmin",
|
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."isAdmin" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_isAdmin",
|
||||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."email" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_email",
|
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."email" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_email",
|
||||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."storageLabel" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_storageLabel",
|
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."storageLabel" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_storageLabel",
|
||||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."externalPath" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_externalPath",
|
|
||||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."oauthId" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_oauthId",
|
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."oauthId" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_oauthId",
|
||||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."profileImagePath" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_profileImagePath",
|
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."profileImagePath" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_profileImagePath",
|
||||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."shouldChangePassword" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_shouldChangePassword",
|
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."shouldChangePassword" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_shouldChangePassword",
|
||||||
@ -308,7 +306,6 @@ FROM
|
|||||||
"SharedLinkEntity__SharedLinkEntity_user"."isAdmin" AS "SharedLinkEntity__SharedLinkEntity_user_isAdmin",
|
"SharedLinkEntity__SharedLinkEntity_user"."isAdmin" AS "SharedLinkEntity__SharedLinkEntity_user_isAdmin",
|
||||||
"SharedLinkEntity__SharedLinkEntity_user"."email" AS "SharedLinkEntity__SharedLinkEntity_user_email",
|
"SharedLinkEntity__SharedLinkEntity_user"."email" AS "SharedLinkEntity__SharedLinkEntity_user_email",
|
||||||
"SharedLinkEntity__SharedLinkEntity_user"."storageLabel" AS "SharedLinkEntity__SharedLinkEntity_user_storageLabel",
|
"SharedLinkEntity__SharedLinkEntity_user"."storageLabel" AS "SharedLinkEntity__SharedLinkEntity_user_storageLabel",
|
||||||
"SharedLinkEntity__SharedLinkEntity_user"."externalPath" AS "SharedLinkEntity__SharedLinkEntity_user_externalPath",
|
|
||||||
"SharedLinkEntity__SharedLinkEntity_user"."oauthId" AS "SharedLinkEntity__SharedLinkEntity_user_oauthId",
|
"SharedLinkEntity__SharedLinkEntity_user"."oauthId" AS "SharedLinkEntity__SharedLinkEntity_user_oauthId",
|
||||||
"SharedLinkEntity__SharedLinkEntity_user"."profileImagePath" AS "SharedLinkEntity__SharedLinkEntity_user_profileImagePath",
|
"SharedLinkEntity__SharedLinkEntity_user"."profileImagePath" AS "SharedLinkEntity__SharedLinkEntity_user_profileImagePath",
|
||||||
"SharedLinkEntity__SharedLinkEntity_user"."shouldChangePassword" AS "SharedLinkEntity__SharedLinkEntity_user_shouldChangePassword",
|
"SharedLinkEntity__SharedLinkEntity_user"."shouldChangePassword" AS "SharedLinkEntity__SharedLinkEntity_user_shouldChangePassword",
|
||||||
|
@ -8,7 +8,6 @@ SELECT
|
|||||||
"UserEntity"."isAdmin" AS "UserEntity_isAdmin",
|
"UserEntity"."isAdmin" AS "UserEntity_isAdmin",
|
||||||
"UserEntity"."email" AS "UserEntity_email",
|
"UserEntity"."email" AS "UserEntity_email",
|
||||||
"UserEntity"."storageLabel" AS "UserEntity_storageLabel",
|
"UserEntity"."storageLabel" AS "UserEntity_storageLabel",
|
||||||
"UserEntity"."externalPath" AS "UserEntity_externalPath",
|
|
||||||
"UserEntity"."oauthId" AS "UserEntity_oauthId",
|
"UserEntity"."oauthId" AS "UserEntity_oauthId",
|
||||||
"UserEntity"."profileImagePath" AS "UserEntity_profileImagePath",
|
"UserEntity"."profileImagePath" AS "UserEntity_profileImagePath",
|
||||||
"UserEntity"."shouldChangePassword" AS "UserEntity_shouldChangePassword",
|
"UserEntity"."shouldChangePassword" AS "UserEntity_shouldChangePassword",
|
||||||
@ -55,7 +54,6 @@ SELECT
|
|||||||
"user"."isAdmin" AS "user_isAdmin",
|
"user"."isAdmin" AS "user_isAdmin",
|
||||||
"user"."email" AS "user_email",
|
"user"."email" AS "user_email",
|
||||||
"user"."storageLabel" AS "user_storageLabel",
|
"user"."storageLabel" AS "user_storageLabel",
|
||||||
"user"."externalPath" AS "user_externalPath",
|
|
||||||
"user"."oauthId" AS "user_oauthId",
|
"user"."oauthId" AS "user_oauthId",
|
||||||
"user"."profileImagePath" AS "user_profileImagePath",
|
"user"."profileImagePath" AS "user_profileImagePath",
|
||||||
"user"."shouldChangePassword" AS "user_shouldChangePassword",
|
"user"."shouldChangePassword" AS "user_shouldChangePassword",
|
||||||
@ -79,7 +77,6 @@ SELECT
|
|||||||
"UserEntity"."isAdmin" AS "UserEntity_isAdmin",
|
"UserEntity"."isAdmin" AS "UserEntity_isAdmin",
|
||||||
"UserEntity"."email" AS "UserEntity_email",
|
"UserEntity"."email" AS "UserEntity_email",
|
||||||
"UserEntity"."storageLabel" AS "UserEntity_storageLabel",
|
"UserEntity"."storageLabel" AS "UserEntity_storageLabel",
|
||||||
"UserEntity"."externalPath" AS "UserEntity_externalPath",
|
|
||||||
"UserEntity"."oauthId" AS "UserEntity_oauthId",
|
"UserEntity"."oauthId" AS "UserEntity_oauthId",
|
||||||
"UserEntity"."profileImagePath" AS "UserEntity_profileImagePath",
|
"UserEntity"."profileImagePath" AS "UserEntity_profileImagePath",
|
||||||
"UserEntity"."shouldChangePassword" AS "UserEntity_shouldChangePassword",
|
"UserEntity"."shouldChangePassword" AS "UserEntity_shouldChangePassword",
|
||||||
@ -105,7 +102,6 @@ SELECT
|
|||||||
"UserEntity"."isAdmin" AS "UserEntity_isAdmin",
|
"UserEntity"."isAdmin" AS "UserEntity_isAdmin",
|
||||||
"UserEntity"."email" AS "UserEntity_email",
|
"UserEntity"."email" AS "UserEntity_email",
|
||||||
"UserEntity"."storageLabel" AS "UserEntity_storageLabel",
|
"UserEntity"."storageLabel" AS "UserEntity_storageLabel",
|
||||||
"UserEntity"."externalPath" AS "UserEntity_externalPath",
|
|
||||||
"UserEntity"."oauthId" AS "UserEntity_oauthId",
|
"UserEntity"."oauthId" AS "UserEntity_oauthId",
|
||||||
"UserEntity"."profileImagePath" AS "UserEntity_profileImagePath",
|
"UserEntity"."profileImagePath" AS "UserEntity_profileImagePath",
|
||||||
"UserEntity"."shouldChangePassword" AS "UserEntity_shouldChangePassword",
|
"UserEntity"."shouldChangePassword" AS "UserEntity_shouldChangePassword",
|
||||||
|
@ -18,7 +18,6 @@ FROM
|
|||||||
"UserTokenEntity__UserTokenEntity_user"."isAdmin" AS "UserTokenEntity__UserTokenEntity_user_isAdmin",
|
"UserTokenEntity__UserTokenEntity_user"."isAdmin" AS "UserTokenEntity__UserTokenEntity_user_isAdmin",
|
||||||
"UserTokenEntity__UserTokenEntity_user"."email" AS "UserTokenEntity__UserTokenEntity_user_email",
|
"UserTokenEntity__UserTokenEntity_user"."email" AS "UserTokenEntity__UserTokenEntity_user_email",
|
||||||
"UserTokenEntity__UserTokenEntity_user"."storageLabel" AS "UserTokenEntity__UserTokenEntity_user_storageLabel",
|
"UserTokenEntity__UserTokenEntity_user"."storageLabel" AS "UserTokenEntity__UserTokenEntity_user_storageLabel",
|
||||||
"UserTokenEntity__UserTokenEntity_user"."externalPath" AS "UserTokenEntity__UserTokenEntity_user_externalPath",
|
|
||||||
"UserTokenEntity__UserTokenEntity_user"."oauthId" AS "UserTokenEntity__UserTokenEntity_user_oauthId",
|
"UserTokenEntity__UserTokenEntity_user"."oauthId" AS "UserTokenEntity__UserTokenEntity_user_oauthId",
|
||||||
"UserTokenEntity__UserTokenEntity_user"."profileImagePath" AS "UserTokenEntity__UserTokenEntity_user_profileImagePath",
|
"UserTokenEntity__UserTokenEntity_user"."profileImagePath" AS "UserTokenEntity__UserTokenEntity_user_profileImagePath",
|
||||||
"UserTokenEntity__UserTokenEntity_user"."shouldChangePassword" AS "UserTokenEntity__UserTokenEntity_user_shouldChangePassword",
|
"UserTokenEntity__UserTokenEntity_user"."shouldChangePassword" AS "UserTokenEntity__UserTokenEntity_user_shouldChangePassword",
|
||||||
|
1
server/test/fixtures/auth.stub.ts
vendored
1
server/test/fixtures/auth.stub.ts
vendored
@ -52,7 +52,6 @@ export const authStub = {
|
|||||||
id: 'user-id',
|
id: 'user-id',
|
||||||
email: 'immich@test.com',
|
email: 'immich@test.com',
|
||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
externalPath: '/data/user1',
|
|
||||||
} as UserEntity,
|
} as UserEntity,
|
||||||
userToken: {
|
userToken: {
|
||||||
id: 'token-id',
|
id: 'token-id',
|
||||||
|
20
server/test/fixtures/library.stub.ts
vendored
20
server/test/fixtures/library.stub.ts
vendored
@ -20,8 +20,8 @@ export const libraryStub = {
|
|||||||
id: 'library-id',
|
id: 'library-id',
|
||||||
name: 'test_library',
|
name: 'test_library',
|
||||||
assets: [],
|
assets: [],
|
||||||
owner: userStub.externalPath1,
|
owner: userStub.admin,
|
||||||
ownerId: 'user-id',
|
ownerId: 'admin_id',
|
||||||
type: LibraryType.EXTERNAL,
|
type: LibraryType.EXTERNAL,
|
||||||
importPaths: [],
|
importPaths: [],
|
||||||
createdAt: new Date('2023-01-01'),
|
createdAt: new Date('2023-01-01'),
|
||||||
@ -34,8 +34,8 @@ export const libraryStub = {
|
|||||||
id: 'library-id2',
|
id: 'library-id2',
|
||||||
name: 'test_library2',
|
name: 'test_library2',
|
||||||
assets: [],
|
assets: [],
|
||||||
owner: userStub.externalPath1,
|
owner: userStub.admin,
|
||||||
ownerId: 'user-id',
|
ownerId: 'admin_id',
|
||||||
type: LibraryType.EXTERNAL,
|
type: LibraryType.EXTERNAL,
|
||||||
importPaths: [],
|
importPaths: [],
|
||||||
createdAt: new Date('2021-01-01'),
|
createdAt: new Date('2021-01-01'),
|
||||||
@ -48,8 +48,8 @@ export const libraryStub = {
|
|||||||
id: 'library-id-with-paths1',
|
id: 'library-id-with-paths1',
|
||||||
name: 'library-with-import-paths1',
|
name: 'library-with-import-paths1',
|
||||||
assets: [],
|
assets: [],
|
||||||
owner: userStub.externalPath1,
|
owner: userStub.admin,
|
||||||
ownerId: 'user-id',
|
ownerId: 'admin_id',
|
||||||
type: LibraryType.EXTERNAL,
|
type: LibraryType.EXTERNAL,
|
||||||
importPaths: ['/foo', '/bar'],
|
importPaths: ['/foo', '/bar'],
|
||||||
createdAt: new Date('2023-01-01'),
|
createdAt: new Date('2023-01-01'),
|
||||||
@ -62,8 +62,8 @@ export const libraryStub = {
|
|||||||
id: 'library-id-with-paths2',
|
id: 'library-id-with-paths2',
|
||||||
name: 'library-with-import-paths2',
|
name: 'library-with-import-paths2',
|
||||||
assets: [],
|
assets: [],
|
||||||
owner: userStub.externalPath1,
|
owner: userStub.admin,
|
||||||
ownerId: 'user-id',
|
ownerId: 'admin_id',
|
||||||
type: LibraryType.EXTERNAL,
|
type: LibraryType.EXTERNAL,
|
||||||
importPaths: ['/xyz', '/asdf'],
|
importPaths: ['/xyz', '/asdf'],
|
||||||
createdAt: new Date('2023-01-01'),
|
createdAt: new Date('2023-01-01'),
|
||||||
@ -76,7 +76,7 @@ export const libraryStub = {
|
|||||||
id: 'library-id',
|
id: 'library-id',
|
||||||
name: 'test_library',
|
name: 'test_library',
|
||||||
assets: [],
|
assets: [],
|
||||||
owner: userStub.externalPath1,
|
owner: userStub.admin,
|
||||||
ownerId: 'user-id',
|
ownerId: 'user-id',
|
||||||
type: LibraryType.EXTERNAL,
|
type: LibraryType.EXTERNAL,
|
||||||
importPaths: [],
|
importPaths: [],
|
||||||
@ -90,7 +90,7 @@ export const libraryStub = {
|
|||||||
id: 'library-id1337',
|
id: 'library-id1337',
|
||||||
name: 'importpath-exclusion-library1',
|
name: 'importpath-exclusion-library1',
|
||||||
assets: [],
|
assets: [],
|
||||||
owner: userStub.externalPath1,
|
owner: userStub.admin,
|
||||||
ownerId: 'user-id',
|
ownerId: 'user-id',
|
||||||
type: LibraryType.EXTERNAL,
|
type: LibraryType.EXTERNAL,
|
||||||
importPaths: ['/xyz', '/asdf'],
|
importPaths: ['/xyz', '/asdf'],
|
||||||
|
44
server/test/fixtures/user.stub.ts
vendored
44
server/test/fixtures/user.stub.ts
vendored
@ -31,7 +31,6 @@ export const userStub = {
|
|||||||
password: 'admin_password',
|
password: 'admin_password',
|
||||||
name: 'admin_name',
|
name: 'admin_name',
|
||||||
storageLabel: 'admin',
|
storageLabel: 'admin',
|
||||||
externalPath: null,
|
|
||||||
oauthId: '',
|
oauthId: '',
|
||||||
shouldChangePassword: false,
|
shouldChangePassword: false,
|
||||||
profileImagePath: '',
|
profileImagePath: '',
|
||||||
@ -50,7 +49,6 @@ export const userStub = {
|
|||||||
password: 'immich_password',
|
password: 'immich_password',
|
||||||
name: 'immich_name',
|
name: 'immich_name',
|
||||||
storageLabel: null,
|
storageLabel: null,
|
||||||
externalPath: null,
|
|
||||||
oauthId: '',
|
oauthId: '',
|
||||||
shouldChangePassword: false,
|
shouldChangePassword: false,
|
||||||
profileImagePath: '',
|
profileImagePath: '',
|
||||||
@ -69,7 +67,6 @@ export const userStub = {
|
|||||||
password: 'immich_password',
|
password: 'immich_password',
|
||||||
name: 'immich_name',
|
name: 'immich_name',
|
||||||
storageLabel: null,
|
storageLabel: null,
|
||||||
externalPath: null,
|
|
||||||
oauthId: '',
|
oauthId: '',
|
||||||
shouldChangePassword: false,
|
shouldChangePassword: false,
|
||||||
profileImagePath: '',
|
profileImagePath: '',
|
||||||
@ -88,45 +85,6 @@ export const userStub = {
|
|||||||
password: 'immich_password',
|
password: 'immich_password',
|
||||||
name: 'immich_name',
|
name: 'immich_name',
|
||||||
storageLabel: 'label-1',
|
storageLabel: 'label-1',
|
||||||
externalPath: null,
|
|
||||||
oauthId: '',
|
|
||||||
shouldChangePassword: false,
|
|
||||||
profileImagePath: '',
|
|
||||||
createdAt: new Date('2021-01-01'),
|
|
||||||
deletedAt: null,
|
|
||||||
updatedAt: new Date('2021-01-01'),
|
|
||||||
tags: [],
|
|
||||||
assets: [],
|
|
||||||
memoriesEnabled: true,
|
|
||||||
avatarColor: UserAvatarColor.PRIMARY,
|
|
||||||
quotaSizeInBytes: null,
|
|
||||||
quotaUsageInBytes: 0,
|
|
||||||
}),
|
|
||||||
externalPath1: Object.freeze<UserEntity>({
|
|
||||||
...authStub.user1.user,
|
|
||||||
password: 'immich_password',
|
|
||||||
name: 'immich_name',
|
|
||||||
storageLabel: 'label-1',
|
|
||||||
externalPath: '/data/user1',
|
|
||||||
oauthId: '',
|
|
||||||
shouldChangePassword: false,
|
|
||||||
profileImagePath: '',
|
|
||||||
createdAt: new Date('2021-01-01'),
|
|
||||||
deletedAt: null,
|
|
||||||
updatedAt: new Date('2021-01-01'),
|
|
||||||
tags: [],
|
|
||||||
assets: [],
|
|
||||||
memoriesEnabled: true,
|
|
||||||
avatarColor: UserAvatarColor.PRIMARY,
|
|
||||||
quotaSizeInBytes: null,
|
|
||||||
quotaUsageInBytes: 0,
|
|
||||||
}),
|
|
||||||
externalPath2: Object.freeze<UserEntity>({
|
|
||||||
...authStub.user1.user,
|
|
||||||
password: 'immich_password',
|
|
||||||
name: 'immich_name',
|
|
||||||
storageLabel: 'label-1',
|
|
||||||
externalPath: '/data/user2',
|
|
||||||
oauthId: '',
|
oauthId: '',
|
||||||
shouldChangePassword: false,
|
shouldChangePassword: false,
|
||||||
profileImagePath: '',
|
profileImagePath: '',
|
||||||
@ -145,7 +103,6 @@ export const userStub = {
|
|||||||
password: 'immich_password',
|
password: 'immich_password',
|
||||||
name: 'immich_name',
|
name: 'immich_name',
|
||||||
storageLabel: 'label-1',
|
storageLabel: 'label-1',
|
||||||
externalPath: '/',
|
|
||||||
oauthId: '',
|
oauthId: '',
|
||||||
shouldChangePassword: false,
|
shouldChangePassword: false,
|
||||||
profileImagePath: '',
|
profileImagePath: '',
|
||||||
@ -164,7 +121,6 @@ export const userStub = {
|
|||||||
password: 'immich_password',
|
password: 'immich_password',
|
||||||
name: 'immich_name',
|
name: 'immich_name',
|
||||||
storageLabel: 'label-1',
|
storageLabel: 'label-1',
|
||||||
externalPath: null,
|
|
||||||
oauthId: '',
|
oauthId: '',
|
||||||
shouldChangePassword: false,
|
shouldChangePassword: false,
|
||||||
profileImagePath: '/path/to/profile.jpg',
|
profileImagePath: '/path/to/profile.jpg',
|
||||||
|
@ -4,7 +4,6 @@ export const newLibraryRepositoryMock = (): jest.Mocked<ILibraryRepository> => {
|
|||||||
return {
|
return {
|
||||||
get: jest.fn(),
|
get: jest.fn(),
|
||||||
getCountForUser: jest.fn(),
|
getCountForUser: jest.fn(),
|
||||||
getAllByUserId: jest.fn(),
|
|
||||||
create: jest.fn(),
|
create: jest.fn(),
|
||||||
delete: jest.fn(),
|
delete: jest.fn(),
|
||||||
softDelete: jest.fn(),
|
softDelete: jest.fn(),
|
||||||
@ -12,9 +11,7 @@ export const newLibraryRepositoryMock = (): jest.Mocked<ILibraryRepository> => {
|
|||||||
getStatistics: jest.fn(),
|
getStatistics: jest.fn(),
|
||||||
getDefaultUploadLibrary: jest.fn(),
|
getDefaultUploadLibrary: jest.fn(),
|
||||||
getUploadLibraryCount: jest.fn(),
|
getUploadLibraryCount: jest.fn(),
|
||||||
getOnlineAssetPaths: jest.fn(),
|
|
||||||
getAssetIds: jest.fn(),
|
getAssetIds: jest.fn(),
|
||||||
existsByName: jest.fn(),
|
|
||||||
getAllDeleted: jest.fn(),
|
getAllDeleted: jest.fn(),
|
||||||
getAll: jest.fn(),
|
getAll: jest.fn(),
|
||||||
};
|
};
|
||||||
|
@ -34,14 +34,13 @@
|
|||||||
|
|
||||||
const editUser = async () => {
|
const editUser = async () => {
|
||||||
try {
|
try {
|
||||||
const { id, email, name, storageLabel, externalPath } = user;
|
const { id, email, name, storageLabel } = user;
|
||||||
await updateUser({
|
await updateUser({
|
||||||
updateUserDto: {
|
updateUserDto: {
|
||||||
id,
|
id,
|
||||||
email,
|
email,
|
||||||
name,
|
name,
|
||||||
storageLabel: storageLabel || '',
|
storageLabel: storageLabel || '',
|
||||||
externalPath: externalPath || '',
|
|
||||||
quotaSizeInBytes: quotaSize ? convertToBytes(Number(quotaSize), 'GiB') : null,
|
quotaSizeInBytes: quotaSize ? convertToBytes(Number(quotaSize), 'GiB') : null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -126,22 +125,6 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="m-4 flex flex-col gap-2">
|
|
||||||
<label class="immich-form-label" for="external-path">External Path</label>
|
|
||||||
<input
|
|
||||||
class="immich-form-input"
|
|
||||||
id="external-path"
|
|
||||||
name="external-path"
|
|
||||||
type="text"
|
|
||||||
bind:value={user.externalPath}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Note: Absolute path of parent import directory. A user can only import files if they exist at or under this
|
|
||||||
path.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if error}
|
{#if error}
|
||||||
<p class="ml-4 text-sm text-red-400">{error}</p>
|
<p class="ml-4 text-sm text-red-400">{error}</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -243,8 +243,8 @@
|
|||||||
addImportPath = true;
|
addImportPath = true;
|
||||||
}}>Add path</Button
|
}}>Add path</Button
|
||||||
></td
|
></td
|
||||||
></tr
|
>
|
||||||
>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div class="flex justify-between w-full">
|
<div class="flex justify-between w-full">
|
||||||
|
54
web/src/lib/components/forms/library-user-picker-form.svelte
Normal file
54
web/src/lib/components/forms/library-user-picker-form.svelte
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
import Icon from '$lib/components/elements/icon.svelte';
|
||||||
|
import Button from '../elements/buttons/button.svelte';
|
||||||
|
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
|
||||||
|
import { mdiFolderSync } from '@mdi/js';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { getAllUsers } from '@immich/sdk';
|
||||||
|
import { user } from '$lib/stores/user.store';
|
||||||
|
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
|
||||||
|
|
||||||
|
let ownerId: string = $user.id;
|
||||||
|
|
||||||
|
let userOptions: { value: string; text: string }[] = [];
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
const users = await getAllUsers({ isAll: true });
|
||||||
|
userOptions = users.map((user) => ({ value: user.id, text: user.name }));
|
||||||
|
});
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher<{
|
||||||
|
cancel: void;
|
||||||
|
submit: { ownerId: string | null };
|
||||||
|
delete: void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const handleCancel = () => dispatch('cancel');
|
||||||
|
const handleSubmit = () => dispatch('submit', { ownerId });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FullScreenModal on:clickOutside={() => handleCancel()}>
|
||||||
|
<div
|
||||||
|
class="w-[500px] max-w-[95vw] rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex flex-col place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
|
||||||
|
>
|
||||||
|
<Icon path={mdiFolderSync} size="4em" />
|
||||||
|
<h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">Select library owner</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off">
|
||||||
|
<p class="p-5 text-sm">NOTE: This cannot be changed later!</p>
|
||||||
|
|
||||||
|
<SettingSelect bind:value={ownerId} options={userOptions} name="user" />
|
||||||
|
|
||||||
|
<div class="mt-8 flex w-full gap-4 px-4">
|
||||||
|
<Button color="gray" fullwidth on:click={() => handleCancel()}>Cancel</Button>
|
||||||
|
|
||||||
|
<Button type="submit" fullwidth>Create</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</FullScreenModal>
|
@ -4,7 +4,7 @@
|
|||||||
import SideBarSection from '$lib/components/shared-components/side-bar/side-bar-section.svelte';
|
import SideBarSection from '$lib/components/shared-components/side-bar/side-bar-section.svelte';
|
||||||
import StatusBox from '$lib/components/shared-components/status-box.svelte';
|
import StatusBox from '$lib/components/shared-components/status-box.svelte';
|
||||||
import { AppRoute } from '$lib/constants';
|
import { AppRoute } from '$lib/constants';
|
||||||
import { mdiAccountMultipleOutline, mdiCog, mdiServer, mdiSync, mdiTools } from '@mdi/js';
|
import { mdiAccountMultipleOutline, mdiBookshelf, mdiCog, mdiServer, mdiSync, mdiTools } from '@mdi/js';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SideBarSection>
|
<SideBarSection>
|
||||||
@ -21,6 +21,13 @@
|
|||||||
<a data-sveltekit-preload-data="hover" href={AppRoute.ADMIN_SETTINGS} draggable="false">
|
<a data-sveltekit-preload-data="hover" href={AppRoute.ADMIN_SETTINGS} draggable="false">
|
||||||
<SideBarButton title="Settings" icon={mdiCog} isSelected={$page.route.id === AppRoute.ADMIN_SETTINGS} />
|
<SideBarButton title="Settings" icon={mdiCog} isSelected={$page.route.id === AppRoute.ADMIN_SETTINGS} />
|
||||||
</a>
|
</a>
|
||||||
|
<a data-sveltekit-preload-data="hover" href={AppRoute.ADMIN_LIBRARY_MANAGEMENT} draggable="false">
|
||||||
|
<SideBarButton
|
||||||
|
title="External Libraries"
|
||||||
|
icon={mdiBookshelf}
|
||||||
|
isSelected={$page.route.id === AppRoute.ADMIN_LIBRARY_MANAGEMENT}
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
<a data-sveltekit-preload-data="hover" href={AppRoute.ADMIN_STATS} draggable="false">
|
<a data-sveltekit-preload-data="hover" href={AppRoute.ADMIN_STATS} draggable="false">
|
||||||
<SideBarButton title="Server Stats" icon={mdiServer} isSelected={$page.route.id === AppRoute.ADMIN_STATS} />
|
<SideBarButton title="Server Stats" icon={mdiServer} isSelected={$page.route.id === AppRoute.ADMIN_STATS} />
|
||||||
</a>
|
</a>
|
||||||
|
@ -1,416 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import Icon from '$lib/components/elements/icon.svelte';
|
|
||||||
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
|
||||||
import { locale } from '$lib/stores/preferences.store';
|
|
||||||
import { getBytesWithUnit } from '$lib/utils/byte-units';
|
|
||||||
import { getContextMenuPosition } from '$lib/utils/context-menu';
|
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
|
||||||
import {
|
|
||||||
LibraryType,
|
|
||||||
createLibrary,
|
|
||||||
deleteLibrary,
|
|
||||||
getLibraries,
|
|
||||||
getLibraryStatistics,
|
|
||||||
removeOfflineFiles,
|
|
||||||
scanLibrary,
|
|
||||||
updateLibrary,
|
|
||||||
type LibraryResponseDto,
|
|
||||||
type LibraryStatsResponseDto,
|
|
||||||
} from '@immich/sdk';
|
|
||||||
import { mdiDatabase, mdiDotsVertical, mdiUpload } from '@mdi/js';
|
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import { fade, slide } from 'svelte/transition';
|
|
||||||
import Button from '../elements/buttons/button.svelte';
|
|
||||||
import LibraryImportPathsForm from '../forms/library-import-paths-form.svelte';
|
|
||||||
import LibraryRenameForm from '../forms/library-rename-form.svelte';
|
|
||||||
import LibraryScanSettingsForm from '../forms/library-scan-settings-form.svelte';
|
|
||||||
import ConfirmDialogue from '../shared-components/confirm-dialogue.svelte';
|
|
||||||
import ContextMenu from '../shared-components/context-menu/context-menu.svelte';
|
|
||||||
import MenuOption from '../shared-components/context-menu/menu-option.svelte';
|
|
||||||
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
|
||||||
import Portal from '../shared-components/portal/portal.svelte';
|
|
||||||
|
|
||||||
let libraries: LibraryResponseDto[] = [];
|
|
||||||
|
|
||||||
let stats: LibraryStatsResponseDto[] = [];
|
|
||||||
let photos: number[] = [];
|
|
||||||
let videos: number[] = [];
|
|
||||||
let totalCount: number[] = [];
|
|
||||||
let diskUsage: number[] = [];
|
|
||||||
let diskUsageUnit: string[] = [];
|
|
||||||
|
|
||||||
let confirmDeleteLibrary: LibraryResponseDto | null = null;
|
|
||||||
let deletedLibrary: LibraryResponseDto | null = null;
|
|
||||||
|
|
||||||
let editImportPaths: number | null;
|
|
||||||
let editScanSettings: number | null;
|
|
||||||
let renameLibrary: number | null;
|
|
||||||
|
|
||||||
let updateLibraryIndex: number | null;
|
|
||||||
|
|
||||||
let deleteAssetCount = 0;
|
|
||||||
|
|
||||||
let dropdownOpen: boolean[] = [];
|
|
||||||
let showContextMenu = false;
|
|
||||||
let contextMenuPosition = { x: 0, y: 0 };
|
|
||||||
let selectedLibraryIndex = 0;
|
|
||||||
let selectedLibrary: LibraryResponseDto | null = null;
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
await readLibraryList();
|
|
||||||
});
|
|
||||||
|
|
||||||
const closeAll = () => {
|
|
||||||
editImportPaths = null;
|
|
||||||
editScanSettings = null;
|
|
||||||
renameLibrary = null;
|
|
||||||
updateLibraryIndex = null;
|
|
||||||
showContextMenu = false;
|
|
||||||
|
|
||||||
for (let index = 0; index < dropdownOpen.length; index++) {
|
|
||||||
dropdownOpen[index] = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const showMenu = (event: MouseEvent, library: LibraryResponseDto, index: number) => {
|
|
||||||
contextMenuPosition = getContextMenuPosition(event);
|
|
||||||
showContextMenu = !showContextMenu;
|
|
||||||
|
|
||||||
selectedLibraryIndex = index;
|
|
||||||
selectedLibrary = library;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onMenuExit = () => {
|
|
||||||
showContextMenu = false;
|
|
||||||
};
|
|
||||||
const refreshStats = async (listIndex: number) => {
|
|
||||||
stats[listIndex] = await getLibraryStatistics({ id: libraries[listIndex].id });
|
|
||||||
photos[listIndex] = stats[listIndex].photos;
|
|
||||||
videos[listIndex] = stats[listIndex].videos;
|
|
||||||
totalCount[listIndex] = stats[listIndex].total;
|
|
||||||
[diskUsage[listIndex], diskUsageUnit[listIndex]] = getBytesWithUnit(stats[listIndex].usage, 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
async function readLibraryList() {
|
|
||||||
libraries = await getLibraries();
|
|
||||||
|
|
||||||
dropdownOpen.length = libraries.length;
|
|
||||||
|
|
||||||
for (let index = 0; index < libraries.length; index++) {
|
|
||||||
await refreshStats(index);
|
|
||||||
dropdownOpen[index] = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCreate = async (libraryType: LibraryType) => {
|
|
||||||
try {
|
|
||||||
const createdLibrary = await createLibrary({
|
|
||||||
createLibraryDto: { type: libraryType },
|
|
||||||
});
|
|
||||||
|
|
||||||
notificationController.show({
|
|
||||||
message: `Created library: ${createdLibrary.name}`,
|
|
||||||
type: NotificationType.Info,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, 'Unable to create library');
|
|
||||||
} finally {
|
|
||||||
await readLibraryList();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpdate = async (event: Partial<LibraryResponseDto>) => {
|
|
||||||
if (updateLibraryIndex === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const libraryId = libraries[updateLibraryIndex].id;
|
|
||||||
await updateLibrary({ id: libraryId, updateLibraryDto: { ...event } });
|
|
||||||
closeAll();
|
|
||||||
await readLibraryList();
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, 'Unable to update library');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = async () => {
|
|
||||||
if (confirmDeleteLibrary) {
|
|
||||||
deletedLibrary = confirmDeleteLibrary;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!deletedLibrary) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await deleteLibrary({ id: deletedLibrary.id });
|
|
||||||
notificationController.show({
|
|
||||||
message: `Library deleted`,
|
|
||||||
type: NotificationType.Info,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, 'Unable to remove library');
|
|
||||||
} finally {
|
|
||||||
confirmDeleteLibrary = null;
|
|
||||||
deletedLibrary = null;
|
|
||||||
await readLibraryList();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleScanAll = async () => {
|
|
||||||
try {
|
|
||||||
for (const library of libraries) {
|
|
||||||
if (library.type === LibraryType.External) {
|
|
||||||
await scanLibrary({ id: library.id, scanLibraryDto: {} });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
notificationController.show({
|
|
||||||
message: `Refreshing all libraries`,
|
|
||||||
type: NotificationType.Info,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, 'Unable to scan libraries');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleScan = async (libraryId: string) => {
|
|
||||||
try {
|
|
||||||
await scanLibrary({ id: libraryId, scanLibraryDto: {} });
|
|
||||||
notificationController.show({
|
|
||||||
message: `Scanning library for new files`,
|
|
||||||
type: NotificationType.Info,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, 'Unable to scan library');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleScanChanges = async (libraryId: string) => {
|
|
||||||
try {
|
|
||||||
await scanLibrary({ id: libraryId, scanLibraryDto: { refreshModifiedFiles: true } });
|
|
||||||
notificationController.show({
|
|
||||||
message: `Scanning library for changed files`,
|
|
||||||
type: NotificationType.Info,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, 'Unable to scan library');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleForceScan = async (libraryId: string) => {
|
|
||||||
try {
|
|
||||||
await scanLibrary({ id: libraryId, scanLibraryDto: { refreshAllFiles: true } });
|
|
||||||
notificationController.show({
|
|
||||||
message: `Forcing refresh of all library files`,
|
|
||||||
type: NotificationType.Info,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, 'Unable to scan library');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveOffline = async (libraryId: string) => {
|
|
||||||
try {
|
|
||||||
await removeOfflineFiles({ id: libraryId });
|
|
||||||
notificationController.show({
|
|
||||||
message: `Removing Offline Files`,
|
|
||||||
type: NotificationType.Info,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, 'Unable to remove offline files');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onRenameClicked = () => {
|
|
||||||
closeAll();
|
|
||||||
renameLibrary = selectedLibraryIndex;
|
|
||||||
updateLibraryIndex = selectedLibraryIndex;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onEditImportPathClicked = () => {
|
|
||||||
closeAll();
|
|
||||||
editImportPaths = selectedLibraryIndex;
|
|
||||||
updateLibraryIndex = selectedLibraryIndex;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onScanNewLibraryClicked = async () => {
|
|
||||||
closeAll();
|
|
||||||
|
|
||||||
if (selectedLibrary) {
|
|
||||||
await handleScan(selectedLibrary.id);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onScanSettingClicked = () => {
|
|
||||||
closeAll();
|
|
||||||
editScanSettings = selectedLibraryIndex;
|
|
||||||
updateLibraryIndex = selectedLibraryIndex;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onScanAllLibraryFilesClicked = async () => {
|
|
||||||
closeAll();
|
|
||||||
if (selectedLibrary) {
|
|
||||||
await handleScanChanges(selectedLibrary.id);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onForceScanAllLibraryFilesClicked = async () => {
|
|
||||||
closeAll();
|
|
||||||
if (selectedLibrary) {
|
|
||||||
await handleForceScan(selectedLibrary.id);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onRemoveOfflineFilesClicked = async () => {
|
|
||||||
closeAll();
|
|
||||||
if (selectedLibrary) {
|
|
||||||
await handleRemoveOffline(selectedLibrary.id);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDeleteLibraryClicked = async () => {
|
|
||||||
closeAll();
|
|
||||||
|
|
||||||
if (selectedLibrary && confirm(`Are you sure you want to delete ${selectedLibrary.name} library?`) == true) {
|
|
||||||
await refreshStats(selectedLibraryIndex);
|
|
||||||
if (totalCount[selectedLibraryIndex] > 0) {
|
|
||||||
deleteAssetCount = totalCount[selectedLibraryIndex];
|
|
||||||
confirmDeleteLibrary = selectedLibrary;
|
|
||||||
} else {
|
|
||||||
deletedLibrary = selectedLibrary;
|
|
||||||
await handleDelete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if confirmDeleteLibrary}
|
|
||||||
<ConfirmDialogue
|
|
||||||
title="Warning!"
|
|
||||||
prompt="Are you sure you want to delete this library? This will DELETE all {deleteAssetCount} contained assets and cannot be undone."
|
|
||||||
on:confirm={handleDelete}
|
|
||||||
on:cancel={() => (confirmDeleteLibrary = null)}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<section class="my-4">
|
|
||||||
<div class="flex flex-col gap-2" in:fade={{ duration: 500 }}>
|
|
||||||
{#if libraries.length > 0}
|
|
||||||
<table class="w-full text-left">
|
|
||||||
<thead
|
|
||||||
class="mb-4 flex h-12 w-full rounded-md border bg-gray-50 text-immich-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-primary"
|
|
||||||
>
|
|
||||||
<tr class="flex w-full place-items-center">
|
|
||||||
<th class="w-1/6 text-center text-sm font-medium">Type</th>
|
|
||||||
<th class="w-1/3 text-center text-sm font-medium">Name</th>
|
|
||||||
<th class="w-1/5 text-center text-sm font-medium">Assets</th>
|
|
||||||
<th class="w-1/6 text-center text-sm font-medium">Size</th>
|
|
||||||
<th class="w-1/6 text-center text-sm font-medium" />
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
|
|
||||||
{#each libraries as library, index (library.id)}
|
|
||||||
<tr
|
|
||||||
class={`flex h-[80px] w-full place-items-center text-center dark:text-immich-dark-fg ${
|
|
||||||
index % 2 == 0
|
|
||||||
? 'bg-immich-gray dark:bg-immich-dark-gray/75'
|
|
||||||
: 'bg-immich-bg dark:bg-immich-dark-gray/50'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<td class="w-1/6 px-10 text-sm">
|
|
||||||
{#if library.type === LibraryType.External}
|
|
||||||
<Icon path={mdiDatabase} size="40" title="External library (created on {library.createdAt})" />
|
|
||||||
{:else if library.type === LibraryType.Upload}
|
|
||||||
<Icon path={mdiUpload} size="40" title="Upload library (created on {library.createdAt})" />
|
|
||||||
{/if}</td
|
|
||||||
>
|
|
||||||
|
|
||||||
<td class="w-1/3 text-ellipsis px-4 text-sm">{library.name}</td>
|
|
||||||
{#if totalCount[index] == undefined}
|
|
||||||
<td colspan="2" class="flex w-1/3 items-center justify-center text-ellipsis px-4 text-sm">
|
|
||||||
<LoadingSpinner size="40" />
|
|
||||||
</td>
|
|
||||||
{:else}
|
|
||||||
<td class="w-1/6 text-ellipsis px-4 text-sm">
|
|
||||||
{totalCount[index].toLocaleString($locale)}
|
|
||||||
</td>
|
|
||||||
<td class="w-1/6 text-ellipsis px-4 text-sm">{diskUsage[index]} {diskUsageUnit[index]}</td>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<td class="w-1/6 text-ellipsis px-4 text-sm">
|
|
||||||
<button
|
|
||||||
class="rounded-full bg-immich-primary p-3 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700"
|
|
||||||
on:click|stopPropagation|preventDefault={(e) => showMenu(e, library, index)}
|
|
||||||
>
|
|
||||||
<Icon path={mdiDotsVertical} size="16" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{#if showContextMenu}
|
|
||||||
<Portal target="body">
|
|
||||||
<ContextMenu {...contextMenuPosition} on:outclick={onMenuExit}>
|
|
||||||
<MenuOption on:click={onRenameClicked} text={`Rename`} />
|
|
||||||
|
|
||||||
{#if selectedLibrary && selectedLibrary.type === LibraryType.External}
|
|
||||||
<MenuOption on:click={onEditImportPathClicked} text="Edit Import Paths" />
|
|
||||||
<MenuOption on:click={onScanSettingClicked} text="Scan Settings" />
|
|
||||||
<hr />
|
|
||||||
<MenuOption on:click={onScanNewLibraryClicked} text="Scan New Library Files" />
|
|
||||||
<MenuOption
|
|
||||||
on:click={onScanAllLibraryFilesClicked}
|
|
||||||
text="Re-scan All Library Files"
|
|
||||||
subtitle={'Only refreshes modified files'}
|
|
||||||
/>
|
|
||||||
<MenuOption
|
|
||||||
on:click={onForceScanAllLibraryFilesClicked}
|
|
||||||
text="Force Re-scan All Library Files"
|
|
||||||
subtitle={'Refreshes every file'}
|
|
||||||
/>
|
|
||||||
<hr />
|
|
||||||
<MenuOption on:click={onRemoveOfflineFilesClicked} text="Remove Offline Files" />
|
|
||||||
<MenuOption on:click={onDeleteLibraryClicked}>
|
|
||||||
<p class="text-red-600">Delete library</p>
|
|
||||||
</MenuOption>
|
|
||||||
{/if}
|
|
||||||
</ContextMenu>
|
|
||||||
</Portal>
|
|
||||||
{/if}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{#if renameLibrary === index}
|
|
||||||
<div transition:slide={{ duration: 250 }}>
|
|
||||||
<LibraryRenameForm
|
|
||||||
{library}
|
|
||||||
on:submit={({ detail }) => handleUpdate(detail)}
|
|
||||||
on:cancel={() => (renameLibrary = null)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if editImportPaths === index}
|
|
||||||
<div transition:slide={{ duration: 250 }}>
|
|
||||||
<LibraryImportPathsForm
|
|
||||||
{library}
|
|
||||||
on:submit={({ detail }) => handleUpdate(detail)}
|
|
||||||
on:cancel={() => (editImportPaths = null)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if editScanSettings === index}
|
|
||||||
<div transition:slide={{ duration: 250 }} class="mb-4 ml-4 mr-4">
|
|
||||||
<LibraryScanSettingsForm
|
|
||||||
{library}
|
|
||||||
on:submit={({ detail }) => handleUpdate(detail.library)}
|
|
||||||
on:cancel={() => (editScanSettings = null)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{/if}
|
|
||||||
<div class="my-2 flex justify-end gap-2">
|
|
||||||
<Button size="sm" on:click={() => handleScanAll()}>Scan All Libraries</Button>
|
|
||||||
<Button size="sm" on:click={() => handleCreate(LibraryType.External)}>Create External Library</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
@ -66,14 +66,6 @@
|
|||||||
required={false}
|
required={false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingInputField
|
|
||||||
inputType={SettingInputFieldType.TEXT}
|
|
||||||
label="EXTERNAL PATH"
|
|
||||||
disabled={true}
|
|
||||||
value={editedUser.externalPath || ''}
|
|
||||||
required={false}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
<Button type="submit" size="sm" on:click={() => handleSaveProfile()}>Save</Button>
|
<Button type="submit" size="sm" on:click={() => handleSaveProfile()}>Save</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -9,7 +9,6 @@
|
|||||||
import AppearanceSettings from './appearance-settings.svelte';
|
import AppearanceSettings from './appearance-settings.svelte';
|
||||||
import ChangePasswordSettings from './change-password-settings.svelte';
|
import ChangePasswordSettings from './change-password-settings.svelte';
|
||||||
import DeviceList from './device-list.svelte';
|
import DeviceList from './device-list.svelte';
|
||||||
import LibraryList from './library-list.svelte';
|
|
||||||
import MemoriesSettings from './memories-settings.svelte';
|
import MemoriesSettings from './memories-settings.svelte';
|
||||||
import OAuthSettings from './oauth-settings.svelte';
|
import OAuthSettings from './oauth-settings.svelte';
|
||||||
import PartnerSettings from './partner-settings.svelte';
|
import PartnerSettings from './partner-settings.svelte';
|
||||||
@ -43,10 +42,6 @@
|
|||||||
<DeviceList bind:devices />
|
<DeviceList bind:devices />
|
||||||
</SettingAccordion>
|
</SettingAccordion>
|
||||||
|
|
||||||
<SettingAccordion key="libraries" title="Libraries" subtitle="Manage your asset libraries">
|
|
||||||
<LibraryList />
|
|
||||||
</SettingAccordion>
|
|
||||||
|
|
||||||
<SettingAccordion key="memories" title="Memories" subtitle="Manage what you see in your memories.">
|
<SettingAccordion key="memories" title="Memories" subtitle="Manage what you see in your memories.">
|
||||||
<MemoriesSettings user={$user} />
|
<MemoriesSettings user={$user} />
|
||||||
</SettingAccordion>
|
</SettingAccordion>
|
||||||
|
@ -11,6 +11,7 @@ export enum AssetAction {
|
|||||||
|
|
||||||
export enum AppRoute {
|
export enum AppRoute {
|
||||||
ADMIN_USER_MANAGEMENT = '/admin/user-management',
|
ADMIN_USER_MANAGEMENT = '/admin/user-management',
|
||||||
|
ADMIN_LIBRARY_MANAGEMENT = '/admin/library-management',
|
||||||
ADMIN_SETTINGS = '/admin/system-settings',
|
ADMIN_SETTINGS = '/admin/system-settings',
|
||||||
ADMIN_STATS = '/admin/server-status',
|
ADMIN_STATS = '/admin/server-status',
|
||||||
ADMIN_JOBS = '/admin/jobs-status',
|
ADMIN_JOBS = '/admin/jobs-status',
|
||||||
|
450
web/src/routes/admin/library-management/+page.svelte
Normal file
450
web/src/routes/admin/library-management/+page.svelte
Normal file
@ -0,0 +1,450 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Button from '$lib/components/elements/buttons/button.svelte';
|
||||||
|
import Icon from '$lib/components/elements/icon.svelte';
|
||||||
|
import LibraryImportPathsForm from '$lib/components/forms/library-import-paths-form.svelte';
|
||||||
|
import LibraryRenameForm from '$lib/components/forms/library-rename-form.svelte';
|
||||||
|
import LibraryScanSettingsForm from '$lib/components/forms/library-scan-settings-form.svelte';
|
||||||
|
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||||
|
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
|
||||||
|
import ContextMenu from '$lib/components/shared-components/context-menu/context-menu.svelte';
|
||||||
|
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||||
|
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
||||||
|
import {
|
||||||
|
notificationController,
|
||||||
|
NotificationType,
|
||||||
|
} from '$lib/components/shared-components/notification/notification';
|
||||||
|
import Portal from '$lib/components/shared-components/portal/portal.svelte';
|
||||||
|
import { getBytesWithUnit } from '$lib/utils/byte-units';
|
||||||
|
import { getContextMenuPosition } from '$lib/utils/context-menu';
|
||||||
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
|
import {
|
||||||
|
LibraryType,
|
||||||
|
createLibrary,
|
||||||
|
deleteLibrary,
|
||||||
|
getLibraryStatistics,
|
||||||
|
removeOfflineFiles,
|
||||||
|
scanLibrary,
|
||||||
|
updateLibrary,
|
||||||
|
type LibraryResponseDto,
|
||||||
|
type LibraryStatsResponseDto,
|
||||||
|
getAllLibraries,
|
||||||
|
type UserResponseDto,
|
||||||
|
getUserById,
|
||||||
|
type CreateLibraryDto,
|
||||||
|
} from '@immich/sdk';
|
||||||
|
import { mdiDatabase, mdiDotsVertical, mdiUpload } from '@mdi/js';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { fade, slide } from 'svelte/transition';
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
import LibraryUserPickerForm from '$lib/components/forms/library-user-picker-form.svelte';
|
||||||
|
|
||||||
|
export let data: PageData;
|
||||||
|
|
||||||
|
let libraries: LibraryResponseDto[] = [];
|
||||||
|
|
||||||
|
let stats: LibraryStatsResponseDto[] = [];
|
||||||
|
let owner: UserResponseDto[] = [];
|
||||||
|
let photos: number[] = [];
|
||||||
|
let videos: number[] = [];
|
||||||
|
let totalCount: number[] = [];
|
||||||
|
let diskUsage: number[] = [];
|
||||||
|
let diskUsageUnit: string[] = [];
|
||||||
|
|
||||||
|
let confirmDeleteLibrary: LibraryResponseDto | null = null;
|
||||||
|
let deletedLibrary: LibraryResponseDto | null = null;
|
||||||
|
|
||||||
|
let editImportPaths: number | null;
|
||||||
|
let editScanSettings: number | null;
|
||||||
|
let renameLibrary: number | null;
|
||||||
|
|
||||||
|
let updateLibraryIndex: number | null;
|
||||||
|
|
||||||
|
let deleteAssetCount = 0;
|
||||||
|
|
||||||
|
let dropdownOpen: boolean[] = [];
|
||||||
|
let showContextMenu = false;
|
||||||
|
let contextMenuPosition = { x: 0, y: 0 };
|
||||||
|
let selectedLibraryIndex = 0;
|
||||||
|
let selectedLibrary: LibraryResponseDto | null = null;
|
||||||
|
|
||||||
|
let toCreateLibrary = false;
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await readLibraryList();
|
||||||
|
});
|
||||||
|
|
||||||
|
const closeAll = () => {
|
||||||
|
editImportPaths = null;
|
||||||
|
editScanSettings = null;
|
||||||
|
renameLibrary = null;
|
||||||
|
updateLibraryIndex = null;
|
||||||
|
showContextMenu = false;
|
||||||
|
|
||||||
|
for (let index = 0; index < dropdownOpen.length; index++) {
|
||||||
|
dropdownOpen[index] = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const showMenu = (event: MouseEvent, library: LibraryResponseDto, index: number) => {
|
||||||
|
contextMenuPosition = getContextMenuPosition(event);
|
||||||
|
showContextMenu = !showContextMenu;
|
||||||
|
|
||||||
|
selectedLibraryIndex = index;
|
||||||
|
selectedLibrary = library;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMenuExit = () => {
|
||||||
|
showContextMenu = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshStats = async (listIndex: number) => {
|
||||||
|
stats[listIndex] = await getLibraryStatistics({ id: libraries[listIndex].id });
|
||||||
|
owner[listIndex] = await getUserById({ id: libraries[listIndex].ownerId });
|
||||||
|
photos[listIndex] = stats[listIndex].photos;
|
||||||
|
videos[listIndex] = stats[listIndex].videos;
|
||||||
|
totalCount[listIndex] = stats[listIndex].total;
|
||||||
|
[diskUsage[listIndex], diskUsageUnit[listIndex]] = getBytesWithUnit(stats[listIndex].usage, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function readLibraryList() {
|
||||||
|
libraries = await getAllLibraries({ $type: LibraryType.External });
|
||||||
|
dropdownOpen.length = libraries.length;
|
||||||
|
|
||||||
|
for (let index = 0; index < libraries.length; index++) {
|
||||||
|
await refreshStats(index);
|
||||||
|
dropdownOpen[index] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCreate = async (ownerId: string | null) => {
|
||||||
|
try {
|
||||||
|
let createLibraryDto: CreateLibraryDto = { type: LibraryType.External };
|
||||||
|
if (ownerId) {
|
||||||
|
createLibraryDto = { ...createLibraryDto, ownerId };
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdLibrary = await createLibrary({ createLibraryDto });
|
||||||
|
|
||||||
|
notificationController.show({
|
||||||
|
message: `Created library: ${createdLibrary.name}`,
|
||||||
|
type: NotificationType.Info,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, 'Unable to create library');
|
||||||
|
} finally {
|
||||||
|
toCreateLibrary = false;
|
||||||
|
await readLibraryList();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdate = async (event: Partial<LibraryResponseDto>) => {
|
||||||
|
if (updateLibraryIndex === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const libraryId = libraries[updateLibraryIndex].id;
|
||||||
|
await updateLibrary({ id: libraryId, updateLibraryDto: { ...event } });
|
||||||
|
closeAll();
|
||||||
|
await readLibraryList();
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, 'Unable to update library');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
if (confirmDeleteLibrary) {
|
||||||
|
deletedLibrary = confirmDeleteLibrary;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!deletedLibrary) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteLibrary({ id: deletedLibrary.id });
|
||||||
|
notificationController.show({
|
||||||
|
message: `Library deleted`,
|
||||||
|
type: NotificationType.Info,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, 'Unable to remove library');
|
||||||
|
} finally {
|
||||||
|
confirmDeleteLibrary = null;
|
||||||
|
deletedLibrary = null;
|
||||||
|
await readLibraryList();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScanAll = async () => {
|
||||||
|
try {
|
||||||
|
for (const library of libraries) {
|
||||||
|
if (library.type === LibraryType.External) {
|
||||||
|
await scanLibrary({ id: library.id, scanLibraryDto: {} });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notificationController.show({
|
||||||
|
message: `Refreshing all libraries`,
|
||||||
|
type: NotificationType.Info,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, 'Unable to scan libraries');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScan = async (libraryId: string) => {
|
||||||
|
try {
|
||||||
|
await scanLibrary({ id: libraryId, scanLibraryDto: {} });
|
||||||
|
notificationController.show({
|
||||||
|
message: `Scanning library for new files`,
|
||||||
|
type: NotificationType.Info,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, 'Unable to scan library');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScanChanges = async (libraryId: string) => {
|
||||||
|
try {
|
||||||
|
await scanLibrary({ id: libraryId, scanLibraryDto: { refreshModifiedFiles: true } });
|
||||||
|
notificationController.show({
|
||||||
|
message: `Scanning library for changed files`,
|
||||||
|
type: NotificationType.Info,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, 'Unable to scan library');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleForceScan = async (libraryId: string) => {
|
||||||
|
try {
|
||||||
|
await scanLibrary({ id: libraryId, scanLibraryDto: { refreshAllFiles: true } });
|
||||||
|
notificationController.show({
|
||||||
|
message: `Forcing refresh of all library files`,
|
||||||
|
type: NotificationType.Info,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, 'Unable to scan library');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveOffline = async (libraryId: string) => {
|
||||||
|
try {
|
||||||
|
await removeOfflineFiles({ id: libraryId });
|
||||||
|
notificationController.show({
|
||||||
|
message: `Removing Offline Files`,
|
||||||
|
type: NotificationType.Info,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, 'Unable to remove offline files');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRenameClicked = () => {
|
||||||
|
closeAll();
|
||||||
|
renameLibrary = selectedLibraryIndex;
|
||||||
|
updateLibraryIndex = selectedLibraryIndex;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onEditImportPathClicked = () => {
|
||||||
|
closeAll();
|
||||||
|
editImportPaths = selectedLibraryIndex;
|
||||||
|
updateLibraryIndex = selectedLibraryIndex;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onScanNewLibraryClicked = async () => {
|
||||||
|
closeAll();
|
||||||
|
|
||||||
|
if (selectedLibrary) {
|
||||||
|
await handleScan(selectedLibrary.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onScanSettingClicked = () => {
|
||||||
|
closeAll();
|
||||||
|
editScanSettings = selectedLibraryIndex;
|
||||||
|
updateLibraryIndex = selectedLibraryIndex;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onScanAllLibraryFilesClicked = async () => {
|
||||||
|
closeAll();
|
||||||
|
if (selectedLibrary) {
|
||||||
|
await handleScanChanges(selectedLibrary.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onForceScanAllLibraryFilesClicked = async () => {
|
||||||
|
closeAll();
|
||||||
|
if (selectedLibrary) {
|
||||||
|
await handleForceScan(selectedLibrary.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRemoveOfflineFilesClicked = async () => {
|
||||||
|
closeAll();
|
||||||
|
if (selectedLibrary) {
|
||||||
|
await handleRemoveOffline(selectedLibrary.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDeleteLibraryClicked = async () => {
|
||||||
|
closeAll();
|
||||||
|
|
||||||
|
if (selectedLibrary && confirm(`Are you sure you want to delete ${selectedLibrary.name} library?`) == true) {
|
||||||
|
await refreshStats(selectedLibraryIndex);
|
||||||
|
if (totalCount[selectedLibraryIndex] > 0) {
|
||||||
|
deleteAssetCount = totalCount[selectedLibraryIndex];
|
||||||
|
confirmDeleteLibrary = selectedLibrary;
|
||||||
|
} else {
|
||||||
|
deletedLibrary = selectedLibrary;
|
||||||
|
await handleDelete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if confirmDeleteLibrary}
|
||||||
|
<ConfirmDialogue
|
||||||
|
title="Warning!"
|
||||||
|
prompt="Are you sure you want to delete this library? This will delete all {deleteAssetCount} contained assets from Immich and cannot be undone. Files will remain on disk."
|
||||||
|
on:confirm={handleDelete}
|
||||||
|
on:cancel={() => (confirmDeleteLibrary = null)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if toCreateLibrary}
|
||||||
|
<LibraryUserPickerForm
|
||||||
|
on:submit={({ detail }) => handleCreate(detail.ownerId)}
|
||||||
|
on:cancel={() => (toCreateLibrary = false)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<UserPageLayout title={data.meta.title} admin>
|
||||||
|
<section class="my-4">
|
||||||
|
<div class="flex flex-col gap-2" in:fade={{ duration: 500 }}>
|
||||||
|
{#if libraries.length > 0}
|
||||||
|
<table class="w-full text-left">
|
||||||
|
<thead
|
||||||
|
class="mb-4 flex h-12 w-full rounded-md border bg-gray-50 text-immich-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-primary"
|
||||||
|
>
|
||||||
|
<tr class="grid grid-cols-6 w-full place-items-center">
|
||||||
|
<th class="text-center text-sm font-medium">Type</th>
|
||||||
|
<th class="text-center text-sm font-medium">Name</th>
|
||||||
|
<th class="text-center text-sm font-medium">Owner</th>
|
||||||
|
<th class="text-center text-sm font-medium">Assets</th>
|
||||||
|
<th class="text-center text-sm font-medium">Size</th>
|
||||||
|
<th class="text-center text-sm font-medium" />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="block overflow-y-auto rounded-md border dark:border-immich-dark-gray">
|
||||||
|
{#each libraries as library, index (library.id)}
|
||||||
|
<tr
|
||||||
|
class={`grid grid-cols-6 h-[80px] w-full place-items-center text-center dark:text-immich-dark-fg ${
|
||||||
|
index % 2 == 0
|
||||||
|
? 'bg-immich-gray dark:bg-immich-dark-gray/75'
|
||||||
|
: 'bg-immich-bg dark:bg-immich-dark-gray/50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<td class=" px-10 text-sm">
|
||||||
|
{#if library.type === LibraryType.External}
|
||||||
|
<Icon path={mdiDatabase} size="40" title="External library (created on {library.createdAt})" />
|
||||||
|
{:else if library.type === LibraryType.Upload}
|
||||||
|
<Icon path={mdiUpload} size="40" title="Upload library (created on {library.createdAt})" />
|
||||||
|
{/if}</td
|
||||||
|
>
|
||||||
|
|
||||||
|
<td class=" text-ellipsis px-4 text-sm">{library.name}</td>
|
||||||
|
<td class=" text-ellipsis px-4 text-sm">
|
||||||
|
{#if owner[index] == undefined}
|
||||||
|
<LoadingSpinner size="40" />
|
||||||
|
{:else}{owner[index].name}{/if}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{#if totalCount[index] == undefined}
|
||||||
|
<td colspan="2" class="flex w-1/3 items-center justify-center text-ellipsis px-4 text-sm">
|
||||||
|
<LoadingSpinner size="40" />
|
||||||
|
</td>
|
||||||
|
{:else}
|
||||||
|
<td class=" text-ellipsis px-4 text-sm">
|
||||||
|
{totalCount[index]}
|
||||||
|
</td>
|
||||||
|
<td class=" text-ellipsis px-4 text-sm">{diskUsage[index]} {diskUsageUnit[index]}</td>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<td class=" text-ellipsis px-4 text-sm">
|
||||||
|
<button
|
||||||
|
class="rounded-full bg-immich-primary p-3 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700"
|
||||||
|
on:click|stopPropagation|preventDefault={(e) => showMenu(e, library, index)}
|
||||||
|
>
|
||||||
|
<Icon path={mdiDotsVertical} size="16" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{#if showContextMenu}
|
||||||
|
<Portal target="body">
|
||||||
|
<ContextMenu {...contextMenuPosition} on:outclick={() => onMenuExit()}>
|
||||||
|
<MenuOption on:click={() => onRenameClicked()} text={`Rename`} />
|
||||||
|
|
||||||
|
{#if selectedLibrary && selectedLibrary.type === LibraryType.External}
|
||||||
|
<MenuOption on:click={() => onEditImportPathClicked()} text="Edit Import Paths" />
|
||||||
|
<MenuOption on:click={() => onScanSettingClicked()} text="Scan Settings" />
|
||||||
|
<hr />
|
||||||
|
<MenuOption on:click={() => onScanNewLibraryClicked()} text="Scan New Library Files" />
|
||||||
|
<MenuOption
|
||||||
|
on:click={() => onScanAllLibraryFilesClicked()}
|
||||||
|
text="Re-scan All Library Files"
|
||||||
|
subtitle={'Only refreshes modified files'}
|
||||||
|
/>
|
||||||
|
<MenuOption
|
||||||
|
on:click={() => onForceScanAllLibraryFilesClicked()}
|
||||||
|
text="Force Re-scan All Library Files"
|
||||||
|
subtitle={'Refreshes every file'}
|
||||||
|
/>
|
||||||
|
<hr />
|
||||||
|
<MenuOption on:click={() => onRemoveOfflineFilesClicked()} text="Remove Offline Files" />
|
||||||
|
<MenuOption on:click={() => onDeleteLibraryClicked()}>
|
||||||
|
<p class="text-red-600">Delete library</p>
|
||||||
|
</MenuOption>
|
||||||
|
{/if}
|
||||||
|
</ContextMenu>
|
||||||
|
</Portal>
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{#if renameLibrary === index}
|
||||||
|
<div transition:slide={{ duration: 250 }}>
|
||||||
|
<LibraryRenameForm
|
||||||
|
{library}
|
||||||
|
on:submit={({ detail }) => handleUpdate(detail)}
|
||||||
|
on:cancel={() => (renameLibrary = null)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if editImportPaths === index}
|
||||||
|
<div transition:slide={{ duration: 250 }}>
|
||||||
|
<LibraryImportPathsForm
|
||||||
|
{library}
|
||||||
|
on:submit={({ detail }) => handleUpdate(detail)}
|
||||||
|
on:cancel={() => (editImportPaths = null)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if editScanSettings === index}
|
||||||
|
<div transition:slide={{ duration: 250 }} class="mb-4 ml-4 mr-4">
|
||||||
|
<LibraryScanSettingsForm
|
||||||
|
{library}
|
||||||
|
on:submit={({ detail }) => handleUpdate(detail.library)}
|
||||||
|
on:cancel={() => (editScanSettings = null)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{/if}
|
||||||
|
<div class="my-2 flex justify-end gap-2">
|
||||||
|
<Button size="sm" on:click={() => handleScanAll()}>Scan All Libraries</Button>
|
||||||
|
<Button size="sm" on:click={() => (toCreateLibrary = true)}>Create Library</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</UserPageLayout>
|
16
web/src/routes/admin/library-management/+page.ts
Normal file
16
web/src/routes/admin/library-management/+page.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { authenticate, requestServerInfo } from '$lib/utils/auth';
|
||||||
|
import { getAllUsers } from '@immich/sdk';
|
||||||
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
|
export const load = (async () => {
|
||||||
|
await authenticate({ admin: true });
|
||||||
|
await requestServerInfo();
|
||||||
|
const allUsers = await getAllUsers({ isAll: false });
|
||||||
|
|
||||||
|
return {
|
||||||
|
allUsers,
|
||||||
|
meta: {
|
||||||
|
title: 'External Library Management',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}) satisfies PageLoad;
|
@ -12,7 +12,7 @@
|
|||||||
import { user } from '$lib/stores/user.store';
|
import { user } from '$lib/stores/user.store';
|
||||||
import { asByteUnitString } from '$lib/utils/byte-units';
|
import { asByteUnitString } from '$lib/utils/byte-units';
|
||||||
import { getAllUsers, type UserResponseDto } from '@immich/sdk';
|
import { getAllUsers, type UserResponseDto } from '@immich/sdk';
|
||||||
import { mdiCheck, mdiClose, mdiDeleteRestore, mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js';
|
import { mdiClose, mdiDeleteRestore, mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
@ -175,7 +175,6 @@
|
|||||||
<th class="w-8/12 sm:w-5/12 lg:w-6/12 xl:w-4/12 2xl:w-5/12 text-center text-sm font-medium">Email</th>
|
<th class="w-8/12 sm:w-5/12 lg:w-6/12 xl:w-4/12 2xl:w-5/12 text-center text-sm font-medium">Email</th>
|
||||||
<th class="hidden sm:block w-3/12 text-center text-sm font-medium">Name</th>
|
<th class="hidden sm:block w-3/12 text-center text-sm font-medium">Name</th>
|
||||||
<th class="hidden xl:block w-3/12 2xl:w-2/12 text-center text-sm font-medium">Has quota</th>
|
<th class="hidden xl:block w-3/12 2xl:w-2/12 text-center text-sm font-medium">Has quota</th>
|
||||||
<th class="hidden xl:block w-3/12 2xl:w-2/12 text-center text-sm font-medium">Can import</th>
|
|
||||||
<th class="w-4/12 lg:w-3/12 xl:w-2/12 text-center text-sm font-medium">Action</th>
|
<th class="w-4/12 lg:w-3/12 xl:w-2/12 text-center text-sm font-medium">Action</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -204,16 +203,6 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="hidden xl:block w-3/12 2xl:w-2/12 text-ellipsis break-all px-2 text-sm">
|
|
||||||
<div class="container mx-auto flex flex-wrap justify-center">
|
|
||||||
{#if immichUser.externalPath}
|
|
||||||
<Icon path={mdiCheck} size="16" />
|
|
||||||
{:else}
|
|
||||||
<Icon path={mdiClose} size="16" />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td class="w-4/12 lg:w-3/12 xl:w-2/12 text-ellipsis break-all text-sm">
|
<td class="w-4/12 lg:w-3/12 xl:w-2/12 text-ellipsis break-all text-sm">
|
||||||
{#if !isDeleted(immichUser)}
|
{#if !isDeleted(immichUser)}
|
||||||
<button
|
<button
|
||||||
|
@ -7,7 +7,6 @@ export const userFactory = Sync.makeFactory<UserResponseDto>({
|
|||||||
email: Sync.each(() => faker.internet.email()),
|
email: Sync.each(() => faker.internet.email()),
|
||||||
name: Sync.each(() => faker.person.fullName()),
|
name: Sync.each(() => faker.person.fullName()),
|
||||||
storageLabel: Sync.each(() => faker.string.alphanumeric()),
|
storageLabel: Sync.each(() => faker.string.alphanumeric()),
|
||||||
externalPath: Sync.each(() => faker.string.alphanumeric()),
|
|
||||||
profileImagePath: '',
|
profileImagePath: '',
|
||||||
shouldChangePassword: Sync.each(() => faker.datatype.boolean()),
|
shouldChangePassword: Sync.each(() => faker.datatype.boolean()),
|
||||||
isAdmin: true,
|
isAdmin: true,
|
||||||
|
Loading…
Reference in New Issue
Block a user