diff --git a/e2e/src/fixtures.ts b/e2e/src/fixtures.ts
index 309ba6b939..6a1a1b3968 100644
--- a/e2e/src/fixtures.ts
+++ b/e2e/src/fixtures.ts
@@ -44,7 +44,6 @@ export const userDto = {
email: signupDto.admin.email,
password: signupDto.admin.password,
storageLabel: 'admin',
- externalPath: null,
oauthId: '',
shouldChangePassword: false,
profileImagePath: '',
@@ -63,7 +62,6 @@ export const userDto = {
email: createUserDto.user1.email,
password: createUserDto.user1.password,
storageLabel: null,
- externalPath: null,
oauthId: '',
shouldChangePassword: false,
profileImagePath: '',
diff --git a/e2e/src/responses.ts b/e2e/src/responses.ts
index 5e6a01eda7..76e289ade2 100644
--- a/e2e/src/responses.ts
+++ b/e2e/src/responses.ts
@@ -65,7 +65,6 @@ export const signupResponseDto = {
name: 'Immich Admin',
email: 'admin@immich.cloud',
storageLabel: 'admin',
- externalPath: null,
profileImagePath: '',
// why? lol
shouldChangePassword: true,
diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md
index 8d8cf234e8..b8548c79e6 100644
Binary files a/mobile/openapi/README.md and b/mobile/openapi/README.md differ
diff --git a/mobile/openapi/doc/CreateLibraryDto.md b/mobile/openapi/doc/CreateLibraryDto.md
index 9e4859cee9..e0caf1c8a5 100644
Binary files a/mobile/openapi/doc/CreateLibraryDto.md and b/mobile/openapi/doc/CreateLibraryDto.md differ
diff --git a/mobile/openapi/doc/CreateUserDto.md b/mobile/openapi/doc/CreateUserDto.md
index 716571752c..0dcc8eca1f 100644
Binary files a/mobile/openapi/doc/CreateUserDto.md and b/mobile/openapi/doc/CreateUserDto.md differ
diff --git a/mobile/openapi/doc/LibraryApi.md b/mobile/openapi/doc/LibraryApi.md
index ecaa73d86b..8a204788bb 100644
Binary files a/mobile/openapi/doc/LibraryApi.md and b/mobile/openapi/doc/LibraryApi.md differ
diff --git a/mobile/openapi/doc/PartnerResponseDto.md b/mobile/openapi/doc/PartnerResponseDto.md
index 46d16e6b28..ce45b32594 100644
Binary files a/mobile/openapi/doc/PartnerResponseDto.md and b/mobile/openapi/doc/PartnerResponseDto.md differ
diff --git a/mobile/openapi/doc/UpdateUserDto.md b/mobile/openapi/doc/UpdateUserDto.md
index 8c0572d1d2..ef4ebf869b 100644
Binary files a/mobile/openapi/doc/UpdateUserDto.md and b/mobile/openapi/doc/UpdateUserDto.md differ
diff --git a/mobile/openapi/doc/UserResponseDto.md b/mobile/openapi/doc/UserResponseDto.md
index 4ea44bb0cb..700a5b849e 100644
Binary files a/mobile/openapi/doc/UserResponseDto.md and b/mobile/openapi/doc/UserResponseDto.md differ
diff --git a/mobile/openapi/lib/api/library_api.dart b/mobile/openapi/lib/api/library_api.dart
index 21cec23eba..befd0aeeff 100644
Binary files a/mobile/openapi/lib/api/library_api.dart and b/mobile/openapi/lib/api/library_api.dart differ
diff --git a/mobile/openapi/lib/model/create_library_dto.dart b/mobile/openapi/lib/model/create_library_dto.dart
index ca4217dcfa..ef656ea2a3 100644
Binary files a/mobile/openapi/lib/model/create_library_dto.dart and b/mobile/openapi/lib/model/create_library_dto.dart differ
diff --git a/mobile/openapi/lib/model/create_user_dto.dart b/mobile/openapi/lib/model/create_user_dto.dart
index bfa26622f7..f272842cbe 100644
Binary files a/mobile/openapi/lib/model/create_user_dto.dart and b/mobile/openapi/lib/model/create_user_dto.dart differ
diff --git a/mobile/openapi/lib/model/partner_response_dto.dart b/mobile/openapi/lib/model/partner_response_dto.dart
index aa96f764bd..008e0c4f26 100644
Binary files a/mobile/openapi/lib/model/partner_response_dto.dart and b/mobile/openapi/lib/model/partner_response_dto.dart differ
diff --git a/mobile/openapi/lib/model/update_user_dto.dart b/mobile/openapi/lib/model/update_user_dto.dart
index 5202401787..8fc85b4868 100644
Binary files a/mobile/openapi/lib/model/update_user_dto.dart and b/mobile/openapi/lib/model/update_user_dto.dart differ
diff --git a/mobile/openapi/lib/model/user_response_dto.dart b/mobile/openapi/lib/model/user_response_dto.dart
index 9da67392bd..d4e0bf07dd 100644
Binary files a/mobile/openapi/lib/model/user_response_dto.dart and b/mobile/openapi/lib/model/user_response_dto.dart differ
diff --git a/mobile/openapi/test/create_library_dto_test.dart b/mobile/openapi/test/create_library_dto_test.dart
index dea1d4e631..88911249e7 100644
Binary files a/mobile/openapi/test/create_library_dto_test.dart and b/mobile/openapi/test/create_library_dto_test.dart differ
diff --git a/mobile/openapi/test/create_user_dto_test.dart b/mobile/openapi/test/create_user_dto_test.dart
index 6614b54a62..9658c02c8a 100644
Binary files a/mobile/openapi/test/create_user_dto_test.dart and b/mobile/openapi/test/create_user_dto_test.dart differ
diff --git a/mobile/openapi/test/library_api_test.dart b/mobile/openapi/test/library_api_test.dart
index 20abcffe70..21afeff544 100644
Binary files a/mobile/openapi/test/library_api_test.dart and b/mobile/openapi/test/library_api_test.dart differ
diff --git a/mobile/openapi/test/partner_response_dto_test.dart b/mobile/openapi/test/partner_response_dto_test.dart
index d6b7769ab4..7fce31d5eb 100644
Binary files a/mobile/openapi/test/partner_response_dto_test.dart and b/mobile/openapi/test/partner_response_dto_test.dart differ
diff --git a/mobile/openapi/test/update_user_dto_test.dart b/mobile/openapi/test/update_user_dto_test.dart
index aaacfa7cbd..10c506666d 100644
Binary files a/mobile/openapi/test/update_user_dto_test.dart and b/mobile/openapi/test/update_user_dto_test.dart differ
diff --git a/mobile/openapi/test/user_response_dto_test.dart b/mobile/openapi/test/user_response_dto_test.dart
index bef6c812e9..d0fdf97e12 100644
Binary files a/mobile/openapi/test/user_response_dto_test.dart and b/mobile/openapi/test/user_response_dto_test.dart differ
diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json
index 5dab83da12..3092c6cc63 100644
--- a/open-api/immich-openapi-specs.json
+++ b/open-api/immich-openapi-specs.json
@@ -3299,8 +3299,17 @@
},
"/library": {
"get": {
- "operationId": "getLibraries",
- "parameters": [],
+ "operationId": "getAllLibraries",
+ "parameters": [
+ {
+ "name": "type",
+ "required": false,
+ "in": "query",
+ "schema": {
+ "$ref": "#/components/schemas/LibraryType"
+ }
+ }
+ ],
"responses": {
"200": {
"content": {
@@ -3407,7 +3416,7 @@
]
},
"get": {
- "operationId": "getLibraryInfo",
+ "operationId": "getLibrary",
"parameters": [
{
"name": "id",
@@ -7592,6 +7601,10 @@
"name": {
"type": "string"
},
+ "ownerId": {
+ "format": "uuid",
+ "type": "string"
+ },
"type": {
"$ref": "#/components/schemas/LibraryType"
}
@@ -7648,10 +7661,6 @@
"email": {
"type": "string"
},
- "externalPath": {
- "nullable": true,
- "type": "string"
- },
"memoriesEnabled": {
"type": "boolean"
},
@@ -8549,10 +8558,6 @@
"email": {
"type": "string"
},
- "externalPath": {
- "nullable": true,
- "type": "string"
- },
"id": {
"type": "string"
},
@@ -8601,7 +8606,6 @@
"createdAt",
"deletedAt",
"email",
- "externalPath",
"id",
"isAdmin",
"name",
@@ -10326,9 +10330,6 @@
"email": {
"type": "string"
},
- "externalPath": {
- "type": "string"
- },
"id": {
"format": "uuid",
"type": "string"
@@ -10455,10 +10456,6 @@
"email": {
"type": "string"
},
- "externalPath": {
- "nullable": true,
- "type": "string"
- },
"id": {
"type": "string"
},
@@ -10504,7 +10501,6 @@
"createdAt",
"deletedAt",
"email",
- "externalPath",
"id",
"isAdmin",
"name",
diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts
index 9b1319f9b5..ce768d518b 100644
--- a/open-api/typescript-sdk/src/fetch-client.ts
+++ b/open-api/typescript-sdk/src/fetch-client.ts
@@ -66,7 +66,6 @@ export type UserResponseDto = {
createdAt: string;
deletedAt: string | null;
email: string;
- externalPath: string | null;
id: string;
isAdmin: boolean;
memoriesEnabled?: boolean;
@@ -462,6 +461,7 @@ export type CreateLibraryDto = {
isVisible?: boolean;
isWatched?: boolean;
name?: string;
+ ownerId?: string;
"type": LibraryType;
};
export type UpdateLibraryDto = {
@@ -506,7 +506,6 @@ export type PartnerResponseDto = {
createdAt: string;
deletedAt: string | null;
email: string;
- externalPath: string | null;
id: string;
inTimeline?: boolean;
isAdmin: boolean;
@@ -950,7 +949,6 @@ export type UpdateTagDto = {
};
export type CreateUserDto = {
email: string;
- externalPath?: string | null;
memoriesEnabled?: boolean;
name: string;
password: string;
@@ -960,7 +958,6 @@ export type CreateUserDto = {
export type UpdateUserDto = {
avatarColor?: UserAvatarColor;
email?: string;
- externalPath?: string;
id: string;
isAdmin?: boolean;
memoriesEnabled?: boolean;
@@ -1841,11 +1838,15 @@ export function sendJobCommand({ id, jobCommandDto }: {
body: jobCommandDto
})));
}
-export function getLibraries(opts?: Oazapfts.RequestOpts) {
+export function getAllLibraries({ $type }: {
+ $type?: LibraryType;
+}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: LibraryResponseDto[];
- }>("/library", {
+ }>(`/library${QS.query(QS.explode({
+ "type": $type
+ }))}`, {
...opts
}));
}
@@ -1869,7 +1870,7 @@ export function deleteLibrary({ id }: {
method: "DELETE"
}));
}
-export function getLibraryInfo({ id }: {
+export function getLibrary({ id }: {
id: string;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
diff --git a/server/e2e/api/specs/asset.e2e-spec.ts b/server/e2e/api/specs/asset.e2e-spec.ts
index d869775c98..7484187182 100644
--- a/server/e2e/api/specs/asset.e2e-spec.ts
+++ b/server/e2e/api/specs/asset.e2e-spec.ts
@@ -41,6 +41,7 @@ describe(`${AssetController.name} (e2e)`, () => {
let app: INestApplication;
let server: any;
let assetRepository: IAssetRepository;
+ let admin: LoginResponseDto;
let user1: LoginResponseDto;
let user2: LoginResponseDto;
let userWithQuota: LoginResponseDto;
@@ -72,7 +73,7 @@ describe(`${AssetController.name} (e2e)`, () => {
await testApp.reset();
await api.authApi.adminSignUp(server);
- const admin = await api.authApi.adminLogin(server);
+ admin = await api.authApi.adminLogin(server);
await Promise.all([
api.userApi.create(server, admin.accessToken, userDto.user1),
@@ -86,12 +87,7 @@ describe(`${AssetController.name} (e2e)`, () => {
api.authApi.login(server, userDto.userWithQuota),
]);
- const [user1Libraries, user2Libraries] = await Promise.all([
- api.libraryApi.getAll(server, user1.accessToken),
- api.libraryApi.getAll(server, user2.accessToken),
- ]);
-
- libraries = [...user1Libraries, ...user2Libraries];
+ libraries = await api.libraryApi.getAll(server, admin.accessToken);
});
beforeEach(async () => {
@@ -615,7 +611,7 @@ describe(`${AssetController.name} (e2e)`, () => {
it("should not upload to another user's library", async () => {
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 });
const { body, status } = await request(server)
diff --git a/server/e2e/api/specs/library.e2e-spec.ts b/server/e2e/api/specs/library.e2e-spec.ts
index 5be9e3035b..edb0a9feb7 100644
--- a/server/e2e/api/specs/library.e2e-spec.ts
+++ b/server/e2e/api/specs/library.e2e-spec.ts
@@ -10,6 +10,7 @@ import { testApp } from '../utils';
describe(`${LibraryController.name} (e2e)`, () => {
let server: any;
let admin: LoginResponseDto;
+ let user: LoginResponseDto;
beforeAll(async () => {
const app = await testApp.create();
@@ -25,6 +26,9 @@ describe(`${LibraryController.name} (e2e)`, () => {
await testApp.reset();
await api.authApi.adminSignUp(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', () => {
@@ -39,18 +43,19 @@ describe(`${LibraryController.name} (e2e)`, () => {
.get('/library')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
- expect(body).toHaveLength(1);
- expect(body).toEqual([
- expect.objectContaining({
- ownerId: admin.userId,
- type: LibraryType.UPLOAD,
- name: 'Default Library',
- refreshedAt: null,
- assetCount: 0,
- importPaths: [],
- exclusionPatterns: [],
- }),
- ]);
+ expect(body).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ ownerId: admin.userId,
+ type: LibraryType.UPLOAD,
+ name: 'Default Library',
+ refreshedAt: null,
+ assetCount: 0,
+ importPaths: [],
+ exclusionPatterns: [],
+ }),
+ ]),
+ );
});
});
@@ -61,6 +66,16 @@ describe(`${LibraryController.name} (e2e)`, () => {
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 () => {
const { status, body } = await request(server)
.post('/library')
@@ -184,29 +199,6 @@ describe(`${LibraryController.name} (e2e)`, () => {
expect(status).toBe(400);
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', () => {
@@ -249,7 +241,6 @@ describe(`${LibraryController.name} (e2e)`, () => {
});
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)
.put(`/library/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
@@ -327,6 +318,14 @@ describe(`${LibraryController.name} (e2e)`, () => {
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 () => {
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', () => {
@@ -390,7 +368,7 @@ describe(`${LibraryController.name} (e2e)`, () => {
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 { status, body } = await request(server)
@@ -401,7 +379,6 @@ describe(`${LibraryController.name} (e2e)`, () => {
expect(body).toEqual({});
const libraries = await api.libraryApi.getAll(server, admin.accessToken);
- expect(libraries).toHaveLength(1);
expect(libraries).not.toEqual(
expect.arrayContaining([
expect.objectContaining({
@@ -455,74 +432,42 @@ describe(`${LibraryController.name} (e2e)`, () => {
library = await api.libraryApi.create(server, admin.accessToken, { type: LibraryType.EXTERNAL });
});
- it('should fail with no external path set', async () => {
- const { status, body } = await request(server)
- .post(`/library/${library.id}/validate`)
- .set('Authorization', `Bearer ${admin.accessToken}`)
-
- .send({ importPaths: [] });
-
- expect(status).toBe(400);
- expect(body).toEqual(errorStub.badRequest('User has no external path set'));
+ it('should pass with no import paths', async () => {
+ const response = await api.libraryApi.validate(server, admin.accessToken, library.id, { importPaths: [] });
+ expect(response.importPaths).toEqual([]);
});
- describe('With external path set', () => {
- beforeEach(async () => {
- await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, IMMICH_TEST_ASSET_TEMP_PATH);
+ 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],
});
- it('should pass with no import paths', async () => {
- const response = await api.libraryApi.validate(server, admin.accessToken, library.id, { importPaths: [] });
- expect(response.importPaths).toEqual([]);
+ 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],
});
- it('should not allow paths outside of the external path', async () => {
- const pathToTest = `${IMMICH_TEST_ASSET_TEMP_PATH}/../`;
- 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(response.importPaths?.length).toEqual(1);
+ const pathResponse = response?.importPaths?.at(0);
- expect(pathResponse).toEqual({
- importPath: pathToTest,
- isValid: false,
- message: `Not contained in user's external path`,
- });
- });
-
- 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)`,
- });
+ expect(pathResponse).toEqual({
+ importPath: pathToTest,
+ isValid: false,
+ message: `Path does not exist (ENOENT)`,
});
});
});
diff --git a/server/e2e/client/user-api.ts b/server/e2e/client/user-api.ts
index 9123b06219..c538db3a8f 100644
--- a/server/e2e/client/user-api.ts
+++ b/server/e2e/client/user-api.ts
@@ -26,7 +26,12 @@ export const userApi = {
return body as UserResponseDto;
},
- setExternalPath: async (server: any, accessToken: string, id: string, externalPath: string) => {
- return await userApi.update(server, accessToken, { id, externalPath });
+ delete: async (server: any, accessToken: string, id: string) => {
+ 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;
},
};
diff --git a/server/e2e/jobs/specs/library-watcher.e2e-spec.ts b/server/e2e/jobs/specs/library-watcher.e2e-spec.ts
index 0215a4976e..93f7163531 100644
--- a/server/e2e/jobs/specs/library-watcher.e2e-spec.ts
+++ b/server/e2e/jobs/specs/library-watcher.e2e-spec.ts
@@ -30,8 +30,6 @@ describe(`Library watcher (e2e)`, () => {
await restoreTempFolder();
await api.authApi.adminSignUp(server);
admin = await api.authApi.adminLogin(server);
-
- await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/');
});
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}/dir2`, { recursive: true });
await fs.mkdir(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir3`, { recursive: true });
diff --git a/server/e2e/jobs/specs/library.e2e-spec.ts b/server/e2e/jobs/specs/library.e2e-spec.ts
index cb19117668..33208fde29 100644
--- a/server/e2e/jobs/specs/library.e2e-spec.ts
+++ b/server/e2e/jobs/specs/library.e2e-spec.ts
@@ -40,8 +40,6 @@ describe(`${LibraryController.name} (e2e)`, () => {
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);
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
@@ -79,8 +77,6 @@ describe(`${LibraryController.name} (e2e)`, () => {
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);
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 () => {
- await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/not/a/real/path');
-
const library = await api.libraryApi.create(server, admin.accessToken, {
type: LibraryType.EXTERNAL,
importPaths: [`${IMMICH_TEST_ASSET_PATH}/albums/nature`],
exclusionPatterns: ['**/el_corcal*'],
});
- await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/');
-
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
@@ -163,7 +155,6 @@ describe(`${LibraryController.name} (e2e)`, () => {
type: LibraryType.EXTERNAL,
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
});
- await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/');
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 () => {
const library = await api.libraryApi.create(server, admin.accessToken, {
type: LibraryType.EXTERNAL,
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
});
- await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/');
await fs.promises.cp(
`${IMMICH_TEST_ASSET_PATH}/albums/nature/silver_fir.jpg`,
@@ -258,7 +221,6 @@ describe(`${LibraryController.name} (e2e)`, () => {
type: LibraryType.EXTERNAL,
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
});
- await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/');
await fs.promises.cp(
`${IMMICH_TEST_ASSET_PATH}/albums/nature/el_torcal_rocks.jpg`,
@@ -305,7 +267,6 @@ describe(`${LibraryController.name} (e2e)`, () => {
type: LibraryType.EXTERNAL,
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
});
- await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/');
await fs.promises.cp(
`${IMMICH_TEST_ASSET_PATH}/albums/nature/el_torcal_rocks.jpg`,
@@ -345,7 +306,6 @@ describe(`${LibraryController.name} (e2e)`, () => {
type: LibraryType.EXTERNAL,
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
});
- await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/');
await fs.promises.cp(
`${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 () => {
const library = await api.libraryApi.create(server, admin.accessToken, {
type: LibraryType.UPLOAD,
@@ -484,7 +378,6 @@ describe(`${LibraryController.name} (e2e)`, () => {
type: LibraryType.EXTERNAL,
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
});
- await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/');
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
@@ -506,12 +399,11 @@ describe(`${LibraryController.name} (e2e)`, () => {
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, {
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);
diff --git a/server/src/domain/library/library.dto.ts b/server/src/domain/library/library.dto.ts
index db6119d4de..b57d56e7b2 100644
--- a/server/src/domain/library/library.dto.ts
+++ b/server/src/domain/library/library.dto.ts
@@ -1,59 +1,62 @@
import { LibraryEntity, LibraryType } from '@app/infra/entities';
import { ApiProperty } from '@nestjs/swagger';
-import { ArrayMaxSize, ArrayUnique, IsBoolean, IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator';
-import { ValidateUUID } from '../domain.util';
+import { ArrayMaxSize, ArrayUnique, IsBoolean, IsEnum, IsNotEmpty, IsString } from 'class-validator';
+import { Optional, ValidateUUID } from '../domain.util';
export class CreateLibraryDto {
@IsEnum(LibraryType)
@ApiProperty({ enumName: 'LibraryType', enum: LibraryType })
type!: LibraryType;
+ @ValidateUUID({ optional: true })
+ ownerId?: string;
+
@IsString()
- @IsOptional()
+ @Optional()
@IsNotEmpty()
name?: string;
- @IsOptional()
+ @Optional()
@IsBoolean()
isVisible?: boolean;
- @IsOptional()
+ @Optional()
@IsString({ each: true })
@IsNotEmpty({ each: true })
@ArrayUnique()
@ArrayMaxSize(128)
importPaths?: string[];
- @IsOptional()
+ @Optional()
@IsString({ each: true })
@IsNotEmpty({ each: true })
@ArrayUnique()
@ArrayMaxSize(128)
exclusionPatterns?: string[];
- @IsOptional()
+ @Optional()
@IsBoolean()
isWatched?: boolean;
}
export class UpdateLibraryDto {
- @IsOptional()
+ @Optional()
@IsString()
@IsNotEmpty()
name?: string;
- @IsOptional()
+ @Optional()
@IsBoolean()
isVisible?: boolean;
- @IsOptional()
+ @Optional()
@IsString({ each: true })
@IsNotEmpty({ each: true })
@ArrayUnique()
@ArrayMaxSize(128)
importPaths?: string[];
- @IsOptional()
+ @Optional()
@IsNotEmpty({ each: true })
@IsString({ each: true })
@ArrayUnique()
@@ -68,14 +71,14 @@ export class CrawlOptionsDto {
}
export class ValidateLibraryDto {
- @IsOptional()
+ @Optional()
@IsString({ each: true })
@IsNotEmpty({ each: true })
@ArrayUnique()
@ArrayMaxSize(128)
importPaths?: string[];
- @IsOptional()
+ @Optional()
@IsNotEmpty({ each: true })
@IsString({ each: true })
@ArrayUnique()
@@ -100,14 +103,21 @@ export class LibrarySearchDto {
export class ScanLibraryDto {
@IsBoolean()
- @IsOptional()
+ @Optional()
refreshModifiedFiles?: boolean;
@IsBoolean()
- @IsOptional()
+ @Optional()
refreshAllFiles?: boolean = false;
}
+export class SearchLibraryDto {
+ @IsEnum(LibraryType)
+ @ApiProperty({ enumName: 'LibraryType', enum: LibraryType })
+ @Optional()
+ type?: LibraryType;
+}
+
export class LibraryResponseDto {
id!: string;
ownerId!: string;
diff --git a/server/src/domain/library/library.service.spec.ts b/server/src/domain/library/library.service.spec.ts
index cafe70b4d7..ba1dd8374b 100644
--- a/server/src/domain/library/library.service.spec.ts
+++ b/server/src/domain/library/library.service.spec.ts
@@ -140,24 +140,6 @@ describe(LibraryService.name, () => {
});
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 () => {
const mockLibraryJob: ILibraryRefreshJob = {
id: libraryStub.externalLibrary1.id,
@@ -168,8 +150,7 @@ describe(LibraryService.name, () => {
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
storageMock.crawl.mockResolvedValue(['/data/user1/photo.jpg']);
assetMock.getByLibraryId.mockResolvedValue([]);
- libraryMock.getOnlineAssetPaths.mockResolvedValue([]);
- userMock.get.mockResolvedValue(userStub.externalPath1);
+ userMock.get.mockResolvedValue(userStub.admin);
await sut.handleQueueAssetRefresh(mockLibraryJob);
@@ -196,8 +177,7 @@ describe(LibraryService.name, () => {
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
storageMock.crawl.mockResolvedValue(['/data/user1/photo.jpg']);
assetMock.getByLibraryId.mockResolvedValue([]);
- libraryMock.getOnlineAssetPaths.mockResolvedValue([]);
- userMock.get.mockResolvedValue(userStub.externalPath1);
+ userMock.get.mockResolvedValue(userStub.admin);
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 () => {
const mockLibraryJob: ILibraryRefreshJob = {
id: libraryStub.externalLibrary1.id,
@@ -287,7 +228,6 @@ describe(LibraryService.name, () => {
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
storageMock.crawl.mockResolvedValue([]);
assetMock.getByLibraryId.mockResolvedValue([]);
- libraryMock.getOnlineAssetPaths.mockResolvedValue([]);
userMock.get.mockResolvedValue(userStub.externalPathRoot);
await sut.handleQueueAssetRefresh(mockLibraryJob);
@@ -303,7 +243,7 @@ describe(LibraryService.name, () => {
let mockUser: UserEntity;
beforeEach(() => {
- mockUser = userStub.externalPath1;
+ mockUser = userStub.admin;
userMock.get.mockResolvedValue(mockUser);
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', () => {
it('should return library statistics', async () => {
libraryMock.getStatistics.mockResolvedValue({ photos: 10, videos: 0, total: 10, usage: 1337 });
@@ -1144,12 +1064,12 @@ describe(LibraryService.name, () => {
storageMock.checkFileExists.mockResolvedValue(true);
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));
expect(libraryMock.update).toHaveBeenCalledWith(
expect.objectContaining({
- id: authStub.external1.user.id,
+ id: authStub.admin.user.id,
}),
);
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 () => {
storageMock.stat.mockImplementation(() => {
const error = { code: 'ENOENT' } as any;
diff --git a/server/src/domain/library/library.service.ts b/server/src/domain/library/library.service.ts
index 6a5982abef..4d89126859 100644
--- a/server/src/domain/library/library.service.ts
+++ b/server/src/domain/library/library.service.ts
@@ -29,6 +29,7 @@ import {
LibraryResponseDto,
LibraryStatsResponseDto,
ScanLibraryDto,
+ SearchLibraryDto,
UpdateLibraryDto,
ValidateLibraryDto,
ValidateLibraryImportPathResponseDto,
@@ -182,6 +183,7 @@ export class LibraryService extends EventEmitter {
async getStatistics(auth: AuthDto, id: string): Promise
- Note: Absolute path of parent import directory. A user can only import files if they exist at or under this - path. -
-{error}
{/if} diff --git a/web/src/lib/components/forms/library-import-paths-form.svelte b/web/src/lib/components/forms/library-import-paths-form.svelte index 039ac8ea04..845cd9e2c4 100644 --- a/web/src/lib/components/forms/library-import-paths-form.svelte +++ b/web/src/lib/components/forms/library-import-paths-form.svelte @@ -243,8 +243,8 @@ addImportPath = true; }}>Add path + > +Type | -Name | -Assets | -Size | -- | ||
---|---|---|---|---|---|---|
- {#if library.type === LibraryType.External}
- |
-
- {library.name} | - {#if totalCount[index] == undefined} -
- |
- {:else}
- - {totalCount[index].toLocaleString($locale)} - | -{diskUsage[index]} {diskUsageUnit[index]} | - {/if} - -
-
-
- {#if showContextMenu}
- - - Delete library - |
-
Type | +Name | +Owner | +Assets | +Size | ++ | ||
---|---|---|---|---|---|---|---|
+ {#if library.type === LibraryType.External}
+ |
+
+ {library.name} | +
+ {#if owner[index] == undefined}
+ |
+
+ {#if totalCount[index] == undefined}
+
+ |
+ {:else}
+ + {totalCount[index]} + | +{diskUsage[index]} {diskUsageUnit[index]} | + {/if} + +
+
+
+ {#if showContextMenu}
+ + + Delete library + |
+