From efa6efd200439d052cd17f0658e2d37c8e9d6e07 Mon Sep 17 00:00:00 2001
From: Jonathan Jogenfors
Date: Thu, 29 Feb 2024 19:35:37 +0100
Subject: [PATCH] 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
Co-authored-by: Alex Tran
---
e2e/src/fixtures.ts | 2 -
e2e/src/responses.ts | 1 -
mobile/openapi/README.md | Bin 24959 -> 24957 bytes
mobile/openapi/doc/CreateLibraryDto.md | Bin 708 -> 750 bytes
mobile/openapi/doc/CreateUserDto.md | Bin 658 -> 611 bytes
mobile/openapi/doc/LibraryApi.md | Bin 18679 -> 18820 bytes
mobile/openapi/doc/PartnerResponseDto.md | Bin 1063 -> 1027 bytes
mobile/openapi/doc/UpdateUserDto.md | Bin 887 -> 840 bytes
mobile/openapi/doc/UserResponseDto.md | Bin 1017 -> 981 bytes
mobile/openapi/lib/api/library_api.dart | Bin 13728 -> 13975 bytes
.../openapi/lib/model/create_library_dto.dart | Bin 5627 -> 6294 bytes
mobile/openapi/lib/model/create_user_dto.dart | Bin 5158 -> 4770 bytes
.../lib/model/partner_response_dto.dart | Bin 8905 -> 8488 bytes
mobile/openapi/lib/model/update_user_dto.dart | Bin 9561 -> 8824 bytes
.../openapi/lib/model/user_response_dto.dart | Bin 8146 -> 7729 bytes
.../openapi/test/create_library_dto_test.dart | Bin 1154 -> 1253 bytes
mobile/openapi/test/create_user_dto_test.dart | Bin 1196 -> 1087 bytes
mobile/openapi/test/library_api_test.dart | Bin 1708 -> 1726 bytes
.../test/partner_response_dto_test.dart | Bin 2285 -> 2176 bytes
mobile/openapi/test/update_user_dto_test.dart | Bin 1621 -> 1512 bytes
.../openapi/test/user_response_dto_test.dart | Bin 2173 -> 2064 bytes
open-api/immich-openapi-specs.json | 36 +-
open-api/typescript-sdk/src/fetch-client.ts | 15 +-
server/e2e/api/specs/asset.e2e-spec.ts | 12 +-
server/e2e/api/specs/library.e2e-spec.ts | 185 +++----
server/e2e/client/user-api.ts | 9 +-
.../jobs/specs/library-watcher.e2e-spec.ts | 4 -
server/e2e/jobs/specs/library.e2e-spec.ts | 110 +----
server/src/domain/library/library.dto.ts | 40 +-
.../domain/library/library.service.spec.ts | 110 +----
server/src/domain/library/library.service.ts | 49 +-
.../domain/partner/partner.service.spec.ts | 2 -
.../domain/repositories/library.repository.ts | 3 -
.../search/dto/search-suggestion.dto.ts | 11 +-
server/src/domain/user/dto/create-user.dto.ts | 4 -
server/src/domain/user/dto/update-user.dto.ts | 4 -
.../user/response-dto/user-response.dto.ts | 2 -
server/src/domain/user/user.core.ts | 8 -
.../immich/controllers/library.controller.ts | 12 +-
server/src/infra/entities/user.entity.ts | 3 -
.../1708425975121-RemoveExternalPath.ts | 13 +
server/src/infra/sql/album.repository.sql | 13 -
server/src/infra/sql/api.key.repository.sql | 1 -
server/src/infra/sql/library.repository.sql | 4 -
.../src/infra/sql/shared.link.repository.sql | 3 -
server/src/infra/sql/user.repository.sql | 4 -
.../src/infra/sql/user.token.repository.sql | 1 -
server/test/fixtures/auth.stub.ts | 1 -
server/test/fixtures/library.stub.ts | 20 +-
server/test/fixtures/user.stub.ts | 44 --
.../repositories/library.repository.mock.ts | 3 -
.../components/forms/edit-user-form.svelte | 19 +-
.../forms/library-import-paths-form.svelte | 4 +-
.../forms/library-user-picker-form.svelte | 54 +++
.../side-bar/admin-side-bar.svelte | 9 +-
.../user-settings-page/library-list.svelte | 416 ----------------
.../user-profile-settings.svelte | 8 -
.../user-settings-list.svelte | 5 -
web/src/lib/constants.ts | 1 +
.../admin/library-management/+page.svelte | 450 ++++++++++++++++++
.../routes/admin/library-management/+page.ts | 16 +
.../routes/admin/user-management/+page.svelte | 13 +-
web/src/test-data/factories/user-factory.ts | 1 -
63 files changed, 718 insertions(+), 1007 deletions(-)
create mode 100644 server/src/infra/migrations/1708425975121-RemoveExternalPath.ts
create mode 100644 web/src/lib/components/forms/library-user-picker-form.svelte
delete mode 100644 web/src/lib/components/user-settings-page/library-list.svelte
create mode 100644 web/src/routes/admin/library-management/+page.svelte
create mode 100644 web/src/routes/admin/library-management/+page.ts
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 8d8cf234e8e03fef28bd3fb66abadf875e679231..b8548c79e60aba7c0bc38b2e5da2af7401dd1b58 100644
GIT binary patch
delta 54
zcmV-60LlOV!U6rl0k9<@3qfpbOle|rVRC6@lkg!M17U1zlSm;wlkp)TlTjfQlQ1F$
MlMEprv*sb0KU13&H2?qr
delta 47
zcmex+i1Gg+#to{9lkX|AP1aMCo$RM*%i@`rmOt50QHnb!GpQ)Cs4^4C-@HcgqCEh*
CfD#%2
diff --git a/mobile/openapi/doc/CreateLibraryDto.md b/mobile/openapi/doc/CreateLibraryDto.md
index 9e4859cee9f0ef884b512e3e701343740c808012..e0caf1c8a5b33023fc49601759777726e1172b3b 100644
GIT binary patch
delta 19
bcmX@Y`i^zOJ4W{W^1Rd{&y>lh8Gix*Q>_S-
delta 11
ScmaFIdW3buJI2WpOxFM(S_Iz!
diff --git a/mobile/openapi/doc/CreateUserDto.md b/mobile/openapi/doc/CreateUserDto.md
index 716571752c3d44cc178df22a6f29c6ef213333e5..0dcc8eca1f678a7c8d0a1a995b6e36abdf314bff 100644
GIT binary patch
delta 11
ScmbQl`j}wu^>|~H$^!;wIneoXYvF_y~)>^
zgeLbgaxzv-}abk0<;E0@8ks>LA)pulMVFR^+3jGX(`kwL~Cgw83S@U
z(7~ED3LvTcf|AU9prx^sFW51jsa~7LCjlu-hh|F|nI0DZo8Bz*c6m
dts2+lK(+YECrsx|KBp%%Szdy5^Imx?O#s%gX9@rS
delta 262
zcmZpf%=mpGRwJ+&kUCOi27qsZjVjDjkj
zd1?80lvZW}b^HK`(GxJImQu0%a74q^+6!KD2Qxp>ODisP6ixP8FOHzyUCdb)JY(6M)
zgpmd0l*tE}B__|6;sSFvKh(@-nY>O-4I*_v!GiMhpki76>SOElt<#5B~^brjS=T7fi(($usD>#Uu;fmvme$QvOfyCLqi
zn!K1#LmAn%xLg>(s;Y_4V_*xdpdQ-1QTiU^=8po47$@J*=G}aOV+!NsXsO$iW3~CX
VfQCSwFK3MenIxmHwJ0RRtgdbR)n
delta 156
zcmbQ9yC8c*1JC3wJR+0(h1mo$Q;WkhOENr4N(wyl((*MXuNC&2{D8@NaszM2WP8Q1
z$=ms+PHx~=lo>~GIK`~(SN1c0&o4*JyVq^gsxOtlJ6sF1Z
ogk>kkv5HTQ65|4kPVVMjve`y<4&&qt?D3Pmcz8DROLoWt05D!TvH$=8
diff --git a/mobile/openapi/lib/model/create_library_dto.dart b/mobile/openapi/lib/model/create_library_dto.dart
index ca4217dcfa03970eb470b3a6963c0d0830616e81..ef656ea2a3f2b326b14e52587fb47df9b11dd967 100644
GIT binary patch
delta 205
zcmeyZJgo;A!c&gA9%iZJ%(GyM8&02ebv`Tzg`
delta 41
zcmV+^0M`GOG5ag90|K)Z0<8qIGYDz{v%Lu~0h1gHzO(ELIs&ty4wnP7a}py4E-DS%
diff --git a/mobile/openapi/lib/model/create_user_dto.dart b/mobile/openapi/lib/model/create_user_dto.dart
index bfa26622f75a7cfc085998b17b53b891247426c2..f272842cbe45ea0cd74ab7f068326142e700bf8b 100644
GIT binary patch
delta 48
zcmZ3cu}F2pOvcG>ocxpbGd69GVhUs0e2YbiakCklI^*O@_P-#h%_5wWnK!@Tv0?`R
DpUMy`
delta 279
zcmZ3ax=drkOh%s6ijvf#yu_S<#FC83eT;KNf=h}r^V01VkR>*&GDS1VAd4v2+M=o`
zVliM;M^}+onvahy8
q3MCnt#d^r1lP7Y@q3YA&l-qoaeGxON=zDHOK4g9Bnw#UeUDyGUvSp3{
diff --git a/mobile/openapi/lib/model/partner_response_dto.dart b/mobile/openapi/lib/model/partner_response_dto.dart
index aa96f764bd8be231f713072444355e2eec72c054..008e0c4f2673a6e20190ef23e857e4cf0bf8ca2f 100644
GIT binary patch
delta 46
zcmV+}0MY-+MW{ls3Iek#0>S~a1q8tav+xM40<+c&djhj<4#EPnz7d`Vv!NJM36lsS
E!K73Xpa1{>
delta 324
zcmZ4CbkcPL7ZXovMM-K=USdu_VoAniMJCbB+)M`<`Ro;t#Wx>hKEon|ETUj*i>jiY
zV;7S;x{AEgoE!yv1p@^u6vdMZxJ5QwaaS=(A#1QzLFR4V#CwD(ATv!tqa-7waP#4DTx01usWBLDyZ
diff --git a/mobile/openapi/lib/model/update_user_dto.dart b/mobile/openapi/lib/model/update_user_dto.dart
index 52024017872f23409063befd6d1b4b2e857596e3..8fc85b48680ea76ef84a6629062c5a1a357962f0 100644
GIT binary patch
delta 42
zcmV+_0M-B5O87*up#ihF0jC4A)dr9WvyTwp0kfzQ`vJ4R6cqxqV;9;5vw|Jh2c}pO
AV*mgE
delta 284
zcmez2a?@+WLPo)ojLc%a)QXbSqP)bMfW(rF&9fQTGH(uJjb)t7&&R=oBIV3Fl~V>;
zM8Vcp0hzz~6yIA$bz~8cioDXC90hv?0|hHo#Y+VEn52*u+o~Y*Hg6CTXPO)@%8BBL
w$&BKXO32F8V-;)_FuXCjUqqf8$y<{ZB}6wL7rDiXYFL4cB8tG~sZx*G0sReU`v3p{
diff --git a/mobile/openapi/lib/model/user_response_dto.dart b/mobile/openapi/lib/model/user_response_dto.dart
index 9da67392bd788efb8cd54a93abebd706ab672851..d4e0bf07dd3b2b5a76188875a5f5daceed9e3154 100644
GIT binary patch
delta 46
zcmV+}0MY-_Ke0Tp0Rpoj0<8hF=L1Xwvt0*<0<#?oLISgU45|XNv=0sjvj-OT2$K;X
E!F-bsp#T5?
delta 300
zcmdmJbIE=KBNI<*MM-K=USdu_VoAp2049;mj7)18`Ro;t#Wycz4ri7@7E!RZMOE>J
zZ3>e*x{AEgoE!yv1p@^u6vdM_atUv4;B;b=Le^lbg3Q}|fO`#-Xl9y%MoC6yu^zJ2
zr~YsB11R5aC4Cl_7f)
E0EBIA5&!@I
diff --git a/mobile/openapi/test/create_library_dto_test.dart b/mobile/openapi/test/create_library_dto_test.dart
index dea1d4e631c05f6a9fc0186d8856ff7d19cd7680..88911249e7b9938292d49d62d565eba276450c79 100644
GIT binary patch
delta 35
jcmZqTe9E~&mSyq*W_I@c^1Rd{&y>k?nH1sd56qJQ+o=rE
delta 11
ScmaFL*~Ga)mSu7SODX^u;RDA2
diff --git a/mobile/openapi/test/create_user_dto_test.dart b/mobile/openapi/test/create_user_dto_test.dart
index 6614b54a625faa0db286e56eb2e1e987252fdb4b..9658c02c8a4b626bfd081e132ca8ee31114274cc 100644
GIT binary patch
delta 12
TcmZ3(xu0W$7SrbGOrlHx9Q*^k
delta 50
ucmdnbv4(Sl7SrVO%Px#
delta 61
zcmdnTyM}keCnheR%%q~kqRiA{jmh;);>_u(C6f)9BUwE2(()(wuqaMm#moiaOk`7^
NoWLx!`7QHBCIFbx6#@VN
diff --git a/mobile/openapi/test/partner_response_dto_test.dart b/mobile/openapi/test/partner_response_dto_test.dart
index d6b7769ab401b7632a53f9509172ba1efc036ef2..7fce31d5ebd4679146f4532520d6455ed004ba6d 100644
GIT binary patch
delta 12
TcmaDW*dVx}jd}BD=3guTBm@O;
delta 46
pcmZn=d@H!2jhQF4q9nB_FEJ+|u_R-10+S-DfFrBP<~HVEEC7`z5gq^l
diff --git a/mobile/openapi/test/update_user_dto_test.dart b/mobile/openapi/test/update_user_dto_test.dart
index aaacfa7cbd66ecac628899f40451dc01ef8188b4..10c506666dd48dd933d69e5fdf43af4795f6a954 100644
GIT binary patch
delta 12
Tcmcc0^MZTBQl`y9%+AaJB4-3T
delta 42
ncmaFCeU)d!QYN0%ijvf#yu_S<#FC83^H~&81U45kl`{hXX&(?9
diff --git a/mobile/openapi/test/user_response_dto_test.dart b/mobile/openapi/test/user_response_dto_test.dart
index bef6c812e9c39679a863651bf7f94c82136b9a0b..d0fdf97e1295478c37c7d1e62a8620ea5232cf9e 100644
GIT binary patch
delta 12
Tcmew>FhO8LE%WAA%#&FFBp(G8
delta 46
qcmbOr@K<0%Ei+GQMM-K=USdu_VoAp215Apj0vB0KHrFywW&r@D("/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 {
await this.access.requirePermission(auth, Permission.LIBRARY_READ, id);
+
return this.repository.getStatistics(id);
}
@@ -189,17 +191,18 @@ export class LibraryService extends EventEmitter {
return this.repository.getCountForUser(auth.user.id);
}
- async getAllForUser(auth: AuthDto): Promise {
- const libraries = await this.repository.getAllByUserId(auth.user.id);
- return libraries.map((library) => mapLibrary(library));
- }
-
async get(auth: AuthDto, id: string): Promise {
await this.access.requirePermission(auth, Permission.LIBRARY_READ, id);
+
const library = await this.findOrFail(id);
return mapLibrary(library);
}
+ async getAll(auth: AuthDto, dto: SearchLibraryDto): Promise {
+ const libraries = await this.repository.getAll(false, dto.type);
+ return libraries.map((library) => mapLibrary(library));
+ }
+
async handleQueueCleanup(): Promise {
this.logger.debug('Cleaning up any pending library deletions');
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({
- ownerId: auth.user.id,
+ ownerId,
name: dto.name,
type: dto.type,
importPaths: dto.importPaths ?? [],
@@ -300,24 +309,11 @@ export class LibraryService extends EventEmitter {
public async validate(auth: AuthDto, id: string, dto: ValidateLibraryDto): Promise {
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();
if (dto.importPaths) {
response.importPaths = await Promise.all(
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);
}),
);
@@ -328,6 +324,7 @@ export class LibraryService extends EventEmitter {
async update(auth: AuthDto, id: string, dto: UpdateLibraryDto): Promise {
await this.access.requirePermission(auth, Permission.LIBRARY_UPDATE, id);
+
const library = await this.repository.update({ id, ...dto });
if (dto.importPaths) {
@@ -404,7 +401,7 @@ export class LibraryService extends EventEmitter {
return true;
} else {
// 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;
}
- 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}`);
const pathValidation = await Promise.all(
@@ -618,11 +609,7 @@ export class LibraryService extends EventEmitter {
exclusionPatterns: library.exclusionPatterns,
});
- const crawledAssetPaths = rawPaths
- // 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[];
+ const crawledAssetPaths = rawPaths.map((filePath) => path.normalize(filePath));
this.logger.debug(`Found ${crawledAssetPaths.length} asset(s) when crawling import paths ${library.importPaths}`);
const assetsInLibrary = await this.assetRepository.getByLibraryId([job.id]);
diff --git a/server/src/domain/partner/partner.service.spec.ts b/server/src/domain/partner/partner.service.spec.ts
index 0f9e173b70..2bc5f3ca90 100644
--- a/server/src/domain/partner/partner.service.spec.ts
+++ b/server/src/domain/partner/partner.service.spec.ts
@@ -18,7 +18,6 @@ const responseDto = {
createdAt: new Date('2021-01-01'),
deletedAt: null,
updatedAt: new Date('2021-01-01'),
- externalPath: null,
memoriesEnabled: true,
avatarColor: UserAvatarColor.PRIMARY,
quotaSizeInBytes: null,
@@ -37,7 +36,6 @@ const responseDto = {
createdAt: new Date('2021-01-01'),
deletedAt: null,
updatedAt: new Date('2021-01-01'),
- externalPath: null,
memoriesEnabled: true,
avatarColor: UserAvatarColor.PRIMARY,
inTimeline: true,
diff --git a/server/src/domain/repositories/library.repository.ts b/server/src/domain/repositories/library.repository.ts
index 7ba6cd409f..395373bcc9 100644
--- a/server/src/domain/repositories/library.repository.ts
+++ b/server/src/domain/repositories/library.repository.ts
@@ -5,7 +5,6 @@ export const ILibraryRepository = 'ILibraryRepository';
export interface ILibraryRepository {
getCountForUser(ownerId: string): Promise;
- getAllByUserId(userId: string, type?: LibraryType): Promise;
getAll(withDeleted?: boolean, type?: LibraryType): Promise;
getAllDeleted(): Promise;
get(id: string, withDeleted?: boolean): Promise;
@@ -16,7 +15,5 @@ export interface ILibraryRepository {
getUploadLibraryCount(ownerId: string): Promise;
update(library: Partial): Promise;
getStatistics(id: string): Promise;
- getOnlineAssetPaths(id: string): Promise;
getAssetIds(id: string, withDeleted?: boolean): Promise;
- existsByName(name: string, withDeleted?: boolean): Promise;
}
diff --git a/server/src/domain/search/dto/search-suggestion.dto.ts b/server/src/domain/search/dto/search-suggestion.dto.ts
index 36a7524587..824a1066c4 100644
--- a/server/src/domain/search/dto/search-suggestion.dto.ts
+++ b/server/src/domain/search/dto/search-suggestion.dto.ts
@@ -1,5 +1,6 @@
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 {
COUNTRY = 'country',
@@ -16,18 +17,18 @@ export class SearchSuggestionRequestDto {
type!: SearchSuggestionType;
@IsString()
- @IsOptional()
+ @Optional()
country?: string;
@IsString()
- @IsOptional()
+ @Optional()
state?: string;
@IsString()
- @IsOptional()
+ @Optional()
make?: string;
@IsString()
- @IsOptional()
+ @Optional()
model?: string;
}
diff --git a/server/src/domain/user/dto/create-user.dto.ts b/server/src/domain/user/dto/create-user.dto.ts
index 179974eae8..e6dbb81675 100644
--- a/server/src/domain/user/dto/create-user.dto.ts
+++ b/server/src/domain/user/dto/create-user.dto.ts
@@ -21,10 +21,6 @@ export class CreateUserDto {
@Transform(toSanitized)
storageLabel?: string | null;
- @Optional({ nullable: true })
- @IsString()
- externalPath?: string | null;
-
@Optional()
@IsBoolean()
memoriesEnabled?: boolean;
diff --git a/server/src/domain/user/dto/update-user.dto.ts b/server/src/domain/user/dto/update-user.dto.ts
index 3c6977ff05..1cab11627b 100644
--- a/server/src/domain/user/dto/update-user.dto.ts
+++ b/server/src/domain/user/dto/update-user.dto.ts
@@ -25,10 +25,6 @@ export class UpdateUserDto {
@Transform(toSanitized)
storageLabel?: string;
- @Optional()
- @IsString()
- externalPath?: string;
-
@IsNotEmpty()
@IsUUID('4')
@ApiProperty({ format: 'uuid' })
diff --git a/server/src/domain/user/response-dto/user-response.dto.ts b/server/src/domain/user/response-dto/user-response.dto.ts
index 15800b9933..a82337945e 100644
--- a/server/src/domain/user/response-dto/user-response.dto.ts
+++ b/server/src/domain/user/response-dto/user-response.dto.ts
@@ -22,7 +22,6 @@ export class UserDto {
export class UserResponseDto extends UserDto {
storageLabel!: string | null;
- externalPath!: string | null;
shouldChangePassword!: boolean;
isAdmin!: boolean;
createdAt!: Date;
@@ -50,7 +49,6 @@ export function mapUser(entity: UserEntity): UserResponseDto {
return {
...mapSimpleUser(entity),
storageLabel: entity.storageLabel,
- externalPath: entity.externalPath,
shouldChangePassword: entity.shouldChangePassword,
isAdmin: entity.isAdmin,
createdAt: entity.createdAt,
diff --git a/server/src/domain/user/user.core.ts b/server/src/domain/user/user.core.ts
index 691bc7de49..6134e97cea 100644
--- a/server/src/domain/user/user.core.ts
+++ b/server/src/domain/user/user.core.ts
@@ -1,6 +1,5 @@
import { LibraryType, UserEntity } from '@app/infra/entities';
import { BadRequestException, ForbiddenException } from '@nestjs/common';
-import path from 'node:path';
import sanitize from 'sanitize-filename';
import { ICryptoRepository, ILibraryRepository, IUserRepository } from '../repositories';
import { UserResponseDto } from './response-dto';
@@ -42,7 +41,6 @@ export class UserCore {
// Users can never update the isAdmin property.
delete dto.isAdmin;
delete dto.storageLabel;
- delete dto.externalPath;
} else if (dto.isAdmin && user.id !== id) {
// Admin cannot create another admin.
throw new BadRequestException('The server already has an admin');
@@ -70,12 +68,6 @@ export class UserCore {
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);
}
diff --git a/server/src/immich/controllers/library.controller.ts b/server/src/immich/controllers/library.controller.ts
index 173e632110..fb68b2626f 100644
--- a/server/src/immich/controllers/library.controller.ts
+++ b/server/src/immich/controllers/library.controller.ts
@@ -5,13 +5,14 @@ import {
LibraryStatsResponseDto,
LibraryResponseDto as ResponseDto,
ScanLibraryDto,
+ SearchLibraryDto,
UpdateLibraryDto as UpdateDto,
ValidateLibraryDto,
ValidateLibraryResponseDto,
} 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 { Auth, Authenticated } from '../app.guard';
+import { AdminRoute, Auth, Authenticated } from '../app.guard';
import { UseValidation } from '../app.utils';
import { UUIDParamDto } from './dto/uuid-param.dto';
@@ -19,12 +20,13 @@ import { UUIDParamDto } from './dto/uuid-param.dto';
@Controller('library')
@Authenticated()
@UseValidation()
+@AdminRoute()
export class LibraryController {
constructor(private service: LibraryService) {}
@Get()
- getLibraries(@Auth() auth: AuthDto): Promise {
- return this.service.getAllForUser(auth);
+ getAllLibraries(@Auth() auth: AuthDto, @Query() dto: SearchLibraryDto): Promise {
+ return this.service.getAll(auth, dto);
}
@Post()
@@ -38,7 +40,7 @@ export class LibraryController {
}
@Get(':id')
- getLibraryInfo(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise {
+ getLibrary(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise {
return this.service.get(auth, id);
}
diff --git a/server/src/infra/entities/user.entity.ts b/server/src/infra/entities/user.entity.ts
index dbc563ee0a..c574595ea8 100644
--- a/server/src/infra/entities/user.entity.ts
+++ b/server/src/infra/entities/user.entity.ts
@@ -43,9 +43,6 @@ export class UserEntity {
@Column({ type: 'varchar', unique: true, default: null })
storageLabel!: string | null;
- @Column({ type: 'varchar', default: null })
- externalPath!: string | null;
-
@Column({ default: '', select: false })
password?: string;
diff --git a/server/src/infra/migrations/1708425975121-RemoveExternalPath.ts b/server/src/infra/migrations/1708425975121-RemoveExternalPath.ts
new file mode 100644
index 0000000000..6c43e351f9
--- /dev/null
+++ b/server/src/infra/migrations/1708425975121-RemoveExternalPath.ts
@@ -0,0 +1,13 @@
+import { MigrationInterface, QueryRunner } from 'typeorm';
+
+export class RemoveExternalPath1708425975121 implements MigrationInterface {
+ name = 'RemoveExternalPath1708425975121';
+
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "externalPath"`);
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`ALTER TABLE "users" ADD "externalPath" character varying`);
+ }
+}
diff --git a/server/src/infra/sql/album.repository.sql b/server/src/infra/sql/album.repository.sql
index 5e104bc1a2..3997dd1a22 100644
--- a/server/src/infra/sql/album.repository.sql
+++ b/server/src/infra/sql/album.repository.sql
@@ -21,7 +21,6 @@ FROM
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
"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"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath",
"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"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email",
"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"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath",
"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"."email" AS "AlbumEntity__AlbumEntity_owner_email",
"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"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath",
"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"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email",
"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"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath",
"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"."email" AS "AlbumEntity__AlbumEntity_owner_email",
"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"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath",
"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"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email",
"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"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath",
"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"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email",
"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"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath",
"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"."email" AS "AlbumEntity__AlbumEntity_owner_email",
"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"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath",
"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"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email",
"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"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath",
"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"."email" AS "AlbumEntity__AlbumEntity_owner_email",
"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"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath",
"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"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email",
"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"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath",
"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"."email" AS "AlbumEntity__AlbumEntity_owner_email",
"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"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath",
"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"."email" AS "AlbumEntity__AlbumEntity_owner_email",
"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"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath",
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
diff --git a/server/src/infra/sql/api.key.repository.sql b/server/src/infra/sql/api.key.repository.sql
index 78eab13b30..3f6b207ce1 100644
--- a/server/src/infra/sql/api.key.repository.sql
+++ b/server/src/infra/sql/api.key.repository.sql
@@ -15,7 +15,6 @@ FROM
"APIKeyEntity__APIKeyEntity_user"."isAdmin" AS "APIKeyEntity__APIKeyEntity_user_isAdmin",
"APIKeyEntity__APIKeyEntity_user"."email" AS "APIKeyEntity__APIKeyEntity_user_email",
"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"."profileImagePath" AS "APIKeyEntity__APIKeyEntity_user_profileImagePath",
"APIKeyEntity__APIKeyEntity_user"."shouldChangePassword" AS "APIKeyEntity__APIKeyEntity_user_shouldChangePassword",
diff --git a/server/src/infra/sql/library.repository.sql b/server/src/infra/sql/library.repository.sql
index c791b2c8a5..433ab6fbac 100644
--- a/server/src/infra/sql/library.repository.sql
+++ b/server/src/infra/sql/library.repository.sql
@@ -23,7 +23,6 @@ FROM
"LibraryEntity__LibraryEntity_owner"."isAdmin" AS "LibraryEntity__LibraryEntity_owner_isAdmin",
"LibraryEntity__LibraryEntity_owner"."email" AS "LibraryEntity__LibraryEntity_owner_email",
"LibraryEntity__LibraryEntity_owner"."storageLabel" AS "LibraryEntity__LibraryEntity_owner_storageLabel",
- "LibraryEntity__LibraryEntity_owner"."externalPath" AS "LibraryEntity__LibraryEntity_owner_externalPath",
"LibraryEntity__LibraryEntity_owner"."oauthId" AS "LibraryEntity__LibraryEntity_owner_oauthId",
"LibraryEntity__LibraryEntity_owner"."profileImagePath" AS "LibraryEntity__LibraryEntity_owner_profileImagePath",
"LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword",
@@ -139,7 +138,6 @@ SELECT
"LibraryEntity__LibraryEntity_owner"."isAdmin" AS "LibraryEntity__LibraryEntity_owner_isAdmin",
"LibraryEntity__LibraryEntity_owner"."email" AS "LibraryEntity__LibraryEntity_owner_email",
"LibraryEntity__LibraryEntity_owner"."storageLabel" AS "LibraryEntity__LibraryEntity_owner_storageLabel",
- "LibraryEntity__LibraryEntity_owner"."externalPath" AS "LibraryEntity__LibraryEntity_owner_externalPath",
"LibraryEntity__LibraryEntity_owner"."oauthId" AS "LibraryEntity__LibraryEntity_owner_oauthId",
"LibraryEntity__LibraryEntity_owner"."profileImagePath" AS "LibraryEntity__LibraryEntity_owner_profileImagePath",
"LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword",
@@ -185,7 +183,6 @@ SELECT
"LibraryEntity__LibraryEntity_owner"."isAdmin" AS "LibraryEntity__LibraryEntity_owner_isAdmin",
"LibraryEntity__LibraryEntity_owner"."email" AS "LibraryEntity__LibraryEntity_owner_email",
"LibraryEntity__LibraryEntity_owner"."storageLabel" AS "LibraryEntity__LibraryEntity_owner_storageLabel",
- "LibraryEntity__LibraryEntity_owner"."externalPath" AS "LibraryEntity__LibraryEntity_owner_externalPath",
"LibraryEntity__LibraryEntity_owner"."oauthId" AS "LibraryEntity__LibraryEntity_owner_oauthId",
"LibraryEntity__LibraryEntity_owner"."profileImagePath" AS "LibraryEntity__LibraryEntity_owner_profileImagePath",
"LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword",
@@ -225,7 +222,6 @@ SELECT
"LibraryEntity__LibraryEntity_owner"."isAdmin" AS "LibraryEntity__LibraryEntity_owner_isAdmin",
"LibraryEntity__LibraryEntity_owner"."email" AS "LibraryEntity__LibraryEntity_owner_email",
"LibraryEntity__LibraryEntity_owner"."storageLabel" AS "LibraryEntity__LibraryEntity_owner_storageLabel",
- "LibraryEntity__LibraryEntity_owner"."externalPath" AS "LibraryEntity__LibraryEntity_owner_externalPath",
"LibraryEntity__LibraryEntity_owner"."oauthId" AS "LibraryEntity__LibraryEntity_owner_oauthId",
"LibraryEntity__LibraryEntity_owner"."profileImagePath" AS "LibraryEntity__LibraryEntity_owner_profileImagePath",
"LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword",
diff --git a/server/src/infra/sql/shared.link.repository.sql b/server/src/infra/sql/shared.link.repository.sql
index 5f43210685..6cac1c44fc 100644
--- a/server/src/infra/sql/shared.link.repository.sql
+++ b/server/src/infra/sql/shared.link.repository.sql
@@ -150,7 +150,6 @@ FROM
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."isAdmin" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_isAdmin",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."email" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_email",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."storageLabel" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_storageLabel",
- "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."externalPath" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_externalPath",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."oauthId" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_oauthId",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."profileImagePath" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_profileImagePath",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."shouldChangePassword" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_shouldChangePassword",
@@ -254,7 +253,6 @@ SELECT
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."isAdmin" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_isAdmin",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."email" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_email",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."storageLabel" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_storageLabel",
- "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."externalPath" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_externalPath",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."oauthId" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_oauthId",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."profileImagePath" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_profileImagePath",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."shouldChangePassword" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_shouldChangePassword",
@@ -308,7 +306,6 @@ FROM
"SharedLinkEntity__SharedLinkEntity_user"."isAdmin" AS "SharedLinkEntity__SharedLinkEntity_user_isAdmin",
"SharedLinkEntity__SharedLinkEntity_user"."email" AS "SharedLinkEntity__SharedLinkEntity_user_email",
"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"."profileImagePath" AS "SharedLinkEntity__SharedLinkEntity_user_profileImagePath",
"SharedLinkEntity__SharedLinkEntity_user"."shouldChangePassword" AS "SharedLinkEntity__SharedLinkEntity_user_shouldChangePassword",
diff --git a/server/src/infra/sql/user.repository.sql b/server/src/infra/sql/user.repository.sql
index 29a2ee5301..e4c7d3a314 100644
--- a/server/src/infra/sql/user.repository.sql
+++ b/server/src/infra/sql/user.repository.sql
@@ -8,7 +8,6 @@ SELECT
"UserEntity"."isAdmin" AS "UserEntity_isAdmin",
"UserEntity"."email" AS "UserEntity_email",
"UserEntity"."storageLabel" AS "UserEntity_storageLabel",
- "UserEntity"."externalPath" AS "UserEntity_externalPath",
"UserEntity"."oauthId" AS "UserEntity_oauthId",
"UserEntity"."profileImagePath" AS "UserEntity_profileImagePath",
"UserEntity"."shouldChangePassword" AS "UserEntity_shouldChangePassword",
@@ -55,7 +54,6 @@ SELECT
"user"."isAdmin" AS "user_isAdmin",
"user"."email" AS "user_email",
"user"."storageLabel" AS "user_storageLabel",
- "user"."externalPath" AS "user_externalPath",
"user"."oauthId" AS "user_oauthId",
"user"."profileImagePath" AS "user_profileImagePath",
"user"."shouldChangePassword" AS "user_shouldChangePassword",
@@ -79,7 +77,6 @@ SELECT
"UserEntity"."isAdmin" AS "UserEntity_isAdmin",
"UserEntity"."email" AS "UserEntity_email",
"UserEntity"."storageLabel" AS "UserEntity_storageLabel",
- "UserEntity"."externalPath" AS "UserEntity_externalPath",
"UserEntity"."oauthId" AS "UserEntity_oauthId",
"UserEntity"."profileImagePath" AS "UserEntity_profileImagePath",
"UserEntity"."shouldChangePassword" AS "UserEntity_shouldChangePassword",
@@ -105,7 +102,6 @@ SELECT
"UserEntity"."isAdmin" AS "UserEntity_isAdmin",
"UserEntity"."email" AS "UserEntity_email",
"UserEntity"."storageLabel" AS "UserEntity_storageLabel",
- "UserEntity"."externalPath" AS "UserEntity_externalPath",
"UserEntity"."oauthId" AS "UserEntity_oauthId",
"UserEntity"."profileImagePath" AS "UserEntity_profileImagePath",
"UserEntity"."shouldChangePassword" AS "UserEntity_shouldChangePassword",
diff --git a/server/src/infra/sql/user.token.repository.sql b/server/src/infra/sql/user.token.repository.sql
index 0f496504f5..b51e53106e 100644
--- a/server/src/infra/sql/user.token.repository.sql
+++ b/server/src/infra/sql/user.token.repository.sql
@@ -18,7 +18,6 @@ FROM
"UserTokenEntity__UserTokenEntity_user"."isAdmin" AS "UserTokenEntity__UserTokenEntity_user_isAdmin",
"UserTokenEntity__UserTokenEntity_user"."email" AS "UserTokenEntity__UserTokenEntity_user_email",
"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"."profileImagePath" AS "UserTokenEntity__UserTokenEntity_user_profileImagePath",
"UserTokenEntity__UserTokenEntity_user"."shouldChangePassword" AS "UserTokenEntity__UserTokenEntity_user_shouldChangePassword",
diff --git a/server/test/fixtures/auth.stub.ts b/server/test/fixtures/auth.stub.ts
index 232c1477f5..79993a4da2 100644
--- a/server/test/fixtures/auth.stub.ts
+++ b/server/test/fixtures/auth.stub.ts
@@ -52,7 +52,6 @@ export const authStub = {
id: 'user-id',
email: 'immich@test.com',
isAdmin: false,
- externalPath: '/data/user1',
} as UserEntity,
userToken: {
id: 'token-id',
diff --git a/server/test/fixtures/library.stub.ts b/server/test/fixtures/library.stub.ts
index 3a203ce87c..ec8e196835 100644
--- a/server/test/fixtures/library.stub.ts
+++ b/server/test/fixtures/library.stub.ts
@@ -20,8 +20,8 @@ export const libraryStub = {
id: 'library-id',
name: 'test_library',
assets: [],
- owner: userStub.externalPath1,
- ownerId: 'user-id',
+ owner: userStub.admin,
+ ownerId: 'admin_id',
type: LibraryType.EXTERNAL,
importPaths: [],
createdAt: new Date('2023-01-01'),
@@ -34,8 +34,8 @@ export const libraryStub = {
id: 'library-id2',
name: 'test_library2',
assets: [],
- owner: userStub.externalPath1,
- ownerId: 'user-id',
+ owner: userStub.admin,
+ ownerId: 'admin_id',
type: LibraryType.EXTERNAL,
importPaths: [],
createdAt: new Date('2021-01-01'),
@@ -48,8 +48,8 @@ export const libraryStub = {
id: 'library-id-with-paths1',
name: 'library-with-import-paths1',
assets: [],
- owner: userStub.externalPath1,
- ownerId: 'user-id',
+ owner: userStub.admin,
+ ownerId: 'admin_id',
type: LibraryType.EXTERNAL,
importPaths: ['/foo', '/bar'],
createdAt: new Date('2023-01-01'),
@@ -62,8 +62,8 @@ export const libraryStub = {
id: 'library-id-with-paths2',
name: 'library-with-import-paths2',
assets: [],
- owner: userStub.externalPath1,
- ownerId: 'user-id',
+ owner: userStub.admin,
+ ownerId: 'admin_id',
type: LibraryType.EXTERNAL,
importPaths: ['/xyz', '/asdf'],
createdAt: new Date('2023-01-01'),
@@ -76,7 +76,7 @@ export const libraryStub = {
id: 'library-id',
name: 'test_library',
assets: [],
- owner: userStub.externalPath1,
+ owner: userStub.admin,
ownerId: 'user-id',
type: LibraryType.EXTERNAL,
importPaths: [],
@@ -90,7 +90,7 @@ export const libraryStub = {
id: 'library-id1337',
name: 'importpath-exclusion-library1',
assets: [],
- owner: userStub.externalPath1,
+ owner: userStub.admin,
ownerId: 'user-id',
type: LibraryType.EXTERNAL,
importPaths: ['/xyz', '/asdf'],
diff --git a/server/test/fixtures/user.stub.ts b/server/test/fixtures/user.stub.ts
index 1b8a3b3fed..e0d9113c65 100644
--- a/server/test/fixtures/user.stub.ts
+++ b/server/test/fixtures/user.stub.ts
@@ -31,7 +31,6 @@ export const userStub = {
password: 'admin_password',
name: 'admin_name',
storageLabel: 'admin',
- externalPath: null,
oauthId: '',
shouldChangePassword: false,
profileImagePath: '',
@@ -50,7 +49,6 @@ export const userStub = {
password: 'immich_password',
name: 'immich_name',
storageLabel: null,
- externalPath: null,
oauthId: '',
shouldChangePassword: false,
profileImagePath: '',
@@ -69,7 +67,6 @@ export const userStub = {
password: 'immich_password',
name: 'immich_name',
storageLabel: null,
- externalPath: null,
oauthId: '',
shouldChangePassword: false,
profileImagePath: '',
@@ -88,45 +85,6 @@ export const userStub = {
password: 'immich_password',
name: 'immich_name',
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({
- ...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({
- ...authStub.user1.user,
- password: 'immich_password',
- name: 'immich_name',
- storageLabel: 'label-1',
- externalPath: '/data/user2',
oauthId: '',
shouldChangePassword: false,
profileImagePath: '',
@@ -145,7 +103,6 @@ export const userStub = {
password: 'immich_password',
name: 'immich_name',
storageLabel: 'label-1',
- externalPath: '/',
oauthId: '',
shouldChangePassword: false,
profileImagePath: '',
@@ -164,7 +121,6 @@ export const userStub = {
password: 'immich_password',
name: 'immich_name',
storageLabel: 'label-1',
- externalPath: null,
oauthId: '',
shouldChangePassword: false,
profileImagePath: '/path/to/profile.jpg',
diff --git a/server/test/repositories/library.repository.mock.ts b/server/test/repositories/library.repository.mock.ts
index 264acc08ef..740f4c4837 100644
--- a/server/test/repositories/library.repository.mock.ts
+++ b/server/test/repositories/library.repository.mock.ts
@@ -4,7 +4,6 @@ export const newLibraryRepositoryMock = (): jest.Mocked => {
return {
get: jest.fn(),
getCountForUser: jest.fn(),
- getAllByUserId: jest.fn(),
create: jest.fn(),
delete: jest.fn(),
softDelete: jest.fn(),
@@ -12,9 +11,7 @@ export const newLibraryRepositoryMock = (): jest.Mocked => {
getStatistics: jest.fn(),
getDefaultUploadLibrary: jest.fn(),
getUploadLibraryCount: jest.fn(),
- getOnlineAssetPaths: jest.fn(),
getAssetIds: jest.fn(),
- existsByName: jest.fn(),
getAllDeleted: jest.fn(),
getAll: jest.fn(),
};
diff --git a/web/src/lib/components/forms/edit-user-form.svelte b/web/src/lib/components/forms/edit-user-form.svelte
index 4caa5d6ab1..f55d341a71 100644
--- a/web/src/lib/components/forms/edit-user-form.svelte
+++ b/web/src/lib/components/forms/edit-user-form.svelte
@@ -34,14 +34,13 @@
const editUser = async () => {
try {
- const { id, email, name, storageLabel, externalPath } = user;
+ const { id, email, name, storageLabel } = user;
await updateUser({
updateUserDto: {
id,
email,
name,
storageLabel: storageLabel || '',
- externalPath: externalPath || '',
quotaSizeInBytes: quotaSize ? convertToBytes(Number(quotaSize), 'GiB') : null,
},
});
@@ -126,22 +125,6 @@
-
-
-
-
-
- Note: Absolute path of parent import directory. A user can only import files if they exist at or under this
- path.
-
-
-
{#if error}
{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
+ >
+
diff --git a/web/src/lib/components/forms/library-user-picker-form.svelte b/web/src/lib/components/forms/library-user-picker-form.svelte
new file mode 100644
index 0000000000..b3c70adb6d
--- /dev/null
+++ b/web/src/lib/components/forms/library-user-picker-form.svelte
@@ -0,0 +1,54 @@
+
+
+
handleCancel()}>
+
+
+
+
Select library owner
+
+
+
+
+
diff --git a/web/src/lib/components/shared-components/side-bar/admin-side-bar.svelte b/web/src/lib/components/shared-components/side-bar/admin-side-bar.svelte
index 019063ef87..0d79ae9611 100644
--- a/web/src/lib/components/shared-components/side-bar/admin-side-bar.svelte
+++ b/web/src/lib/components/shared-components/side-bar/admin-side-bar.svelte
@@ -4,7 +4,7 @@
import SideBarSection from '$lib/components/shared-components/side-bar/side-bar-section.svelte';
import StatusBox from '$lib/components/shared-components/status-box.svelte';
import { AppRoute } from '$lib/constants';
- import { mdiAccountMultipleOutline, mdiCog, mdiServer, mdiSync, mdiTools } from '@mdi/js';
+ import { mdiAccountMultipleOutline, mdiBookshelf, mdiCog, mdiServer, mdiSync, mdiTools } from '@mdi/js';
@@ -21,6 +21,13 @@
+
+
+
diff --git a/web/src/lib/components/user-settings-page/library-list.svelte b/web/src/lib/components/user-settings-page/library-list.svelte
deleted file mode 100644
index 440becde0b..0000000000
--- a/web/src/lib/components/user-settings-page/library-list.svelte
+++ /dev/null
@@ -1,416 +0,0 @@
-
-
-{#if confirmDeleteLibrary}
- (confirmDeleteLibrary = null)}
- />
-{/if}
-
-
-
- {#if libraries.length > 0}
-
-
-
- Type |
- Name |
- Assets |
- Size |
- |
-
-
-
- {#each libraries as library, index (library.id)}
-
-
- {#if library.type === LibraryType.External}
-
- {:else if library.type === LibraryType.Upload}
-
- {/if} |
-
- {library.name} |
- {#if totalCount[index] == undefined}
-
-
- |
- {:else}
-
- {totalCount[index].toLocaleString($locale)}
- |
- {diskUsage[index]} {diskUsageUnit[index]} |
- {/if}
-
-
-
-
- {#if showContextMenu}
-
-
-
-
- {#if selectedLibrary && selectedLibrary.type === LibraryType.External}
-
-
-
-
-
-
-
-
-
- Delete library
-
- {/if}
-
-
- {/if}
- |
-
- {#if renameLibrary === index}
-
- handleUpdate(detail)}
- on:cancel={() => (renameLibrary = null)}
- />
-
- {/if}
- {#if editImportPaths === index}
-
- handleUpdate(detail)}
- on:cancel={() => (editImportPaths = null)}
- />
-
- {/if}
- {#if editScanSettings === index}
-
- handleUpdate(detail.library)}
- on:cancel={() => (editScanSettings = null)}
- />
-
- {/if}
- {/each}
-
-
- {/if}
-
-
-
-
-
-
diff --git a/web/src/lib/components/user-settings-page/user-profile-settings.svelte b/web/src/lib/components/user-settings-page/user-profile-settings.svelte
index f24f0ac7f6..053d852dda 100644
--- a/web/src/lib/components/user-settings-page/user-profile-settings.svelte
+++ b/web/src/lib/components/user-settings-page/user-profile-settings.svelte
@@ -66,14 +66,6 @@
required={false}
/>
-
-
diff --git a/web/src/lib/components/user-settings-page/user-settings-list.svelte b/web/src/lib/components/user-settings-page/user-settings-list.svelte
index a14c2a8c85..b46768c3e7 100644
--- a/web/src/lib/components/user-settings-page/user-settings-list.svelte
+++ b/web/src/lib/components/user-settings-page/user-settings-list.svelte
@@ -9,7 +9,6 @@
import AppearanceSettings from './appearance-settings.svelte';
import ChangePasswordSettings from './change-password-settings.svelte';
import DeviceList from './device-list.svelte';
- import LibraryList from './library-list.svelte';
import MemoriesSettings from './memories-settings.svelte';
import OAuthSettings from './oauth-settings.svelte';
import PartnerSettings from './partner-settings.svelte';
@@ -43,10 +42,6 @@
-
-
-
-
diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts
index dbb60df3e9..af5558c261 100644
--- a/web/src/lib/constants.ts
+++ b/web/src/lib/constants.ts
@@ -11,6 +11,7 @@ export enum AssetAction {
export enum AppRoute {
ADMIN_USER_MANAGEMENT = '/admin/user-management',
+ ADMIN_LIBRARY_MANAGEMENT = '/admin/library-management',
ADMIN_SETTINGS = '/admin/system-settings',
ADMIN_STATS = '/admin/server-status',
ADMIN_JOBS = '/admin/jobs-status',
diff --git a/web/src/routes/admin/library-management/+page.svelte b/web/src/routes/admin/library-management/+page.svelte
new file mode 100644
index 0000000000..e9ac41b83e
--- /dev/null
+++ b/web/src/routes/admin/library-management/+page.svelte
@@ -0,0 +1,450 @@
+
+
+{#if confirmDeleteLibrary}
+ (confirmDeleteLibrary = null)}
+ />
+{/if}
+
+{#if toCreateLibrary}
+ handleCreate(detail.ownerId)}
+ on:cancel={() => (toCreateLibrary = false)}
+ />
+{/if}
+
+
+
+
+ {#if libraries.length > 0}
+
+
+
+ Type |
+ Name |
+ Owner |
+ Assets |
+ Size |
+ |
+
+
+
+ {#each libraries as library, index (library.id)}
+
+
+ {#if library.type === LibraryType.External}
+
+ {:else if library.type === LibraryType.Upload}
+
+ {/if} |
+
+ {library.name} |
+
+ {#if owner[index] == undefined}
+
+ {:else}{owner[index].name}{/if}
+ |
+
+ {#if totalCount[index] == undefined}
+
+
+ |
+ {:else}
+
+ {totalCount[index]}
+ |
+ {diskUsage[index]} {diskUsageUnit[index]} |
+ {/if}
+
+
+
+
+ {#if showContextMenu}
+
+ onMenuExit()}>
+ onRenameClicked()} text={`Rename`} />
+
+ {#if selectedLibrary && selectedLibrary.type === LibraryType.External}
+ onEditImportPathClicked()} text="Edit Import Paths" />
+ onScanSettingClicked()} text="Scan Settings" />
+
+ onScanNewLibraryClicked()} text="Scan New Library Files" />
+ onScanAllLibraryFilesClicked()}
+ text="Re-scan All Library Files"
+ subtitle={'Only refreshes modified files'}
+ />
+ onForceScanAllLibraryFilesClicked()}
+ text="Force Re-scan All Library Files"
+ subtitle={'Refreshes every file'}
+ />
+
+ onRemoveOfflineFilesClicked()} text="Remove Offline Files" />
+ onDeleteLibraryClicked()}>
+ Delete library
+
+ {/if}
+
+
+ {/if}
+ |
+
+ {#if renameLibrary === index}
+
+ handleUpdate(detail)}
+ on:cancel={() => (renameLibrary = null)}
+ />
+
+ {/if}
+ {#if editImportPaths === index}
+
+ handleUpdate(detail)}
+ on:cancel={() => (editImportPaths = null)}
+ />
+
+ {/if}
+ {#if editScanSettings === index}
+
+ handleUpdate(detail.library)}
+ on:cancel={() => (editScanSettings = null)}
+ />
+
+ {/if}
+ {/each}
+
+
+ {/if}
+
+
+
+
+
+
+
diff --git a/web/src/routes/admin/library-management/+page.ts b/web/src/routes/admin/library-management/+page.ts
new file mode 100644
index 0000000000..78831d38c7
--- /dev/null
+++ b/web/src/routes/admin/library-management/+page.ts
@@ -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;
diff --git a/web/src/routes/admin/user-management/+page.svelte b/web/src/routes/admin/user-management/+page.svelte
index a3a93e0f35..84917d591e 100644
--- a/web/src/routes/admin/user-management/+page.svelte
+++ b/web/src/routes/admin/user-management/+page.svelte
@@ -12,7 +12,7 @@
import { user } from '$lib/stores/user.store';
import { asByteUnitString } from '$lib/utils/byte-units';
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 type { PageData } from './$types';
@@ -175,7 +175,6 @@
Email |
Name |
Has quota |
- Can import |
Action |
@@ -204,16 +203,6 @@
{/if}
-
-
- {#if immichUser.externalPath}
-
- {:else}
-
- {/if}
-
- |
-
{#if !isDeleted(immichUser)}
|