diff --git a/e2e/src/fixtures.ts b/e2e/src/fixtures.ts
index 309ba6b93..6a1a1b396 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 5e6a01eda..76e289ade 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 8d8cf234e..b8548c79e 100644
--- a/mobile/openapi/README.md
+++ b/mobile/openapi/README.md
@@ -135,8 +135,8 @@ Class | Method | HTTP request | Description
*JobApi* | [**sendJobCommand**](doc//JobApi.md#sendjobcommand) | **PUT** /jobs/{id} |
*LibraryApi* | [**createLibrary**](doc//LibraryApi.md#createlibrary) | **POST** /library |
*LibraryApi* | [**deleteLibrary**](doc//LibraryApi.md#deletelibrary) | **DELETE** /library/{id} |
-*LibraryApi* | [**getLibraries**](doc//LibraryApi.md#getlibraries) | **GET** /library |
-*LibraryApi* | [**getLibraryInfo**](doc//LibraryApi.md#getlibraryinfo) | **GET** /library/{id} |
+*LibraryApi* | [**getAllLibraries**](doc//LibraryApi.md#getalllibraries) | **GET** /library |
+*LibraryApi* | [**getLibrary**](doc//LibraryApi.md#getlibrary) | **GET** /library/{id} |
*LibraryApi* | [**getLibraryStatistics**](doc//LibraryApi.md#getlibrarystatistics) | **GET** /library/{id}/statistics |
*LibraryApi* | [**removeOfflineFiles**](doc//LibraryApi.md#removeofflinefiles) | **POST** /library/{id}/removeOffline |
*LibraryApi* | [**scanLibrary**](doc//LibraryApi.md#scanlibrary) | **POST** /library/{id}/scan |
diff --git a/mobile/openapi/doc/CreateLibraryDto.md b/mobile/openapi/doc/CreateLibraryDto.md
index 9e4859cee..e0caf1c8a 100644
--- a/mobile/openapi/doc/CreateLibraryDto.md
+++ b/mobile/openapi/doc/CreateLibraryDto.md
@@ -13,6 +13,7 @@ Name | Type | Description | Notes
**isVisible** | **bool** | | [optional]
**isWatched** | **bool** | | [optional]
**name** | **String** | | [optional]
+**ownerId** | **String** | | [optional]
**type** | [**LibraryType**](LibraryType.md) | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
diff --git a/mobile/openapi/doc/CreateUserDto.md b/mobile/openapi/doc/CreateUserDto.md
index 716571752..0dcc8eca1 100644
--- a/mobile/openapi/doc/CreateUserDto.md
+++ b/mobile/openapi/doc/CreateUserDto.md
@@ -9,7 +9,6 @@ import 'package:openapi/api.dart';
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**email** | **String** | |
-**externalPath** | **String** | | [optional]
**memoriesEnabled** | **bool** | | [optional]
**name** | **String** | |
**password** | **String** | |
diff --git a/mobile/openapi/doc/LibraryApi.md b/mobile/openapi/doc/LibraryApi.md
index ecaa73d86..8a204788b 100644
--- a/mobile/openapi/doc/LibraryApi.md
+++ b/mobile/openapi/doc/LibraryApi.md
@@ -11,8 +11,8 @@ Method | HTTP request | Description
------------- | ------------- | -------------
[**createLibrary**](LibraryApi.md#createlibrary) | **POST** /library |
[**deleteLibrary**](LibraryApi.md#deletelibrary) | **DELETE** /library/{id} |
-[**getLibraries**](LibraryApi.md#getlibraries) | **GET** /library |
-[**getLibraryInfo**](LibraryApi.md#getlibraryinfo) | **GET** /library/{id} |
+[**getAllLibraries**](LibraryApi.md#getalllibraries) | **GET** /library |
+[**getLibrary**](LibraryApi.md#getlibrary) | **GET** /library/{id} |
[**getLibraryStatistics**](LibraryApi.md#getlibrarystatistics) | **GET** /library/{id}/statistics |
[**removeOfflineFiles**](LibraryApi.md#removeofflinefiles) | **POST** /library/{id}/removeOffline |
[**scanLibrary**](LibraryApi.md#scanlibrary) | **POST** /library/{id}/scan |
@@ -129,8 +129,8 @@ void (empty response body)
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
-# **getLibraries**
-> List getLibraries()
+# **getAllLibraries**
+> List getAllLibraries(type)
@@ -153,17 +153,21 @@ import 'package:openapi/api.dart';
//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = LibraryApi();
+final type = ; // LibraryType |
try {
- final result = api_instance.getLibraries();
+ final result = api_instance.getAllLibraries(type);
print(result);
} catch (e) {
- print('Exception when calling LibraryApi->getLibraries: $e\n');
+ print('Exception when calling LibraryApi->getAllLibraries: $e\n');
}
```
### Parameters
-This endpoint does not need any parameter.
+
+Name | Type | Description | Notes
+------------- | ------------- | ------------- | -------------
+ **type** | [**LibraryType**](.md)| | [optional]
### Return type
@@ -180,8 +184,8 @@ This endpoint does not need any parameter.
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
-# **getLibraryInfo**
-> LibraryResponseDto getLibraryInfo(id)
+# **getLibrary**
+> LibraryResponseDto getLibrary(id)
@@ -207,10 +211,10 @@ final api_instance = LibraryApi();
final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
try {
- final result = api_instance.getLibraryInfo(id);
+ final result = api_instance.getLibrary(id);
print(result);
} catch (e) {
- print('Exception when calling LibraryApi->getLibraryInfo: $e\n');
+ print('Exception when calling LibraryApi->getLibrary: $e\n');
}
```
diff --git a/mobile/openapi/doc/PartnerResponseDto.md b/mobile/openapi/doc/PartnerResponseDto.md
index 46d16e6b2..ce45b3259 100644
--- a/mobile/openapi/doc/PartnerResponseDto.md
+++ b/mobile/openapi/doc/PartnerResponseDto.md
@@ -12,7 +12,6 @@ Name | Type | Description | Notes
**createdAt** | [**DateTime**](DateTime.md) | |
**deletedAt** | [**DateTime**](DateTime.md) | |
**email** | **String** | |
-**externalPath** | **String** | |
**id** | **String** | |
**inTimeline** | **bool** | | [optional]
**isAdmin** | **bool** | |
diff --git a/mobile/openapi/doc/UpdateUserDto.md b/mobile/openapi/doc/UpdateUserDto.md
index 8c0572d1d..ef4ebf869 100644
--- a/mobile/openapi/doc/UpdateUserDto.md
+++ b/mobile/openapi/doc/UpdateUserDto.md
@@ -10,7 +10,6 @@ Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**avatarColor** | [**UserAvatarColor**](UserAvatarColor.md) | | [optional]
**email** | **String** | | [optional]
-**externalPath** | **String** | | [optional]
**id** | **String** | |
**isAdmin** | **bool** | | [optional]
**memoriesEnabled** | **bool** | | [optional]
diff --git a/mobile/openapi/doc/UserResponseDto.md b/mobile/openapi/doc/UserResponseDto.md
index 4ea44bb0c..700a5b849 100644
--- a/mobile/openapi/doc/UserResponseDto.md
+++ b/mobile/openapi/doc/UserResponseDto.md
@@ -12,7 +12,6 @@ Name | Type | Description | Notes
**createdAt** | [**DateTime**](DateTime.md) | |
**deletedAt** | [**DateTime**](DateTime.md) | |
**email** | **String** | |
-**externalPath** | **String** | |
**id** | **String** | |
**isAdmin** | **bool** | |
**memoriesEnabled** | **bool** | | [optional]
diff --git a/mobile/openapi/lib/api/library_api.dart b/mobile/openapi/lib/api/library_api.dart
index 21cec23eb..befd0aeef 100644
--- a/mobile/openapi/lib/api/library_api.dart
+++ b/mobile/openapi/lib/api/library_api.dart
@@ -104,7 +104,10 @@ class LibraryApi {
}
/// Performs an HTTP 'GET /library' operation and returns the [Response].
- Future getLibrariesWithHttpInfo() async {
+ /// Parameters:
+ ///
+ /// * [LibraryType] type:
+ Future getAllLibrariesWithHttpInfo({ LibraryType? type, }) async {
// ignore: prefer_const_declarations
final path = r'/library';
@@ -115,6 +118,10 @@ class LibraryApi {
final headerParams = {};
final formParams = {};
+ if (type != null) {
+ queryParams.addAll(_queryParams('', 'type', type));
+ }
+
const contentTypes = [];
@@ -129,8 +136,11 @@ class LibraryApi {
);
}
- Future?> getLibraries() async {
- final response = await getLibrariesWithHttpInfo();
+ /// Parameters:
+ ///
+ /// * [LibraryType] type:
+ Future?> getAllLibraries({ LibraryType? type, }) async {
+ final response = await getAllLibrariesWithHttpInfo( type: type, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
@@ -151,7 +161,7 @@ class LibraryApi {
/// Parameters:
///
/// * [String] id (required):
- Future getLibraryInfoWithHttpInfo(String id,) async {
+ Future getLibraryWithHttpInfo(String id,) async {
// ignore: prefer_const_declarations
final path = r'/library/{id}'
.replaceAll('{id}', id);
@@ -180,8 +190,8 @@ class LibraryApi {
/// Parameters:
///
/// * [String] id (required):
- Future getLibraryInfo(String id,) async {
- final response = await getLibraryInfoWithHttpInfo(id,);
+ Future getLibrary(String id,) async {
+ final response = await getLibraryWithHttpInfo(id,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
diff --git a/mobile/openapi/lib/model/create_library_dto.dart b/mobile/openapi/lib/model/create_library_dto.dart
index ca4217dcf..ef656ea2a 100644
--- a/mobile/openapi/lib/model/create_library_dto.dart
+++ b/mobile/openapi/lib/model/create_library_dto.dart
@@ -18,6 +18,7 @@ class CreateLibraryDto {
this.isVisible,
this.isWatched,
this.name,
+ this.ownerId,
required this.type,
});
@@ -49,6 +50,14 @@ class CreateLibraryDto {
///
String? name;
+ ///
+ /// Please note: This property should have been non-nullable! Since the specification file
+ /// does not include a default value (using the "default:" property), however, the generated
+ /// source code must fall back to having a nullable type.
+ /// Consider adding a "default:" property in the specification file to hide this note.
+ ///
+ String? ownerId;
+
LibraryType type;
@override
@@ -58,6 +67,7 @@ class CreateLibraryDto {
other.isVisible == isVisible &&
other.isWatched == isWatched &&
other.name == name &&
+ other.ownerId == ownerId &&
other.type == type;
@override
@@ -68,10 +78,11 @@ class CreateLibraryDto {
(isVisible == null ? 0 : isVisible!.hashCode) +
(isWatched == null ? 0 : isWatched!.hashCode) +
(name == null ? 0 : name!.hashCode) +
+ (ownerId == null ? 0 : ownerId!.hashCode) +
(type.hashCode);
@override
- String toString() => 'CreateLibraryDto[exclusionPatterns=$exclusionPatterns, importPaths=$importPaths, isVisible=$isVisible, isWatched=$isWatched, name=$name, type=$type]';
+ String toString() => 'CreateLibraryDto[exclusionPatterns=$exclusionPatterns, importPaths=$importPaths, isVisible=$isVisible, isWatched=$isWatched, name=$name, ownerId=$ownerId, type=$type]';
Map toJson() {
final json = {};
@@ -91,6 +102,11 @@ class CreateLibraryDto {
json[r'name'] = this.name;
} else {
// json[r'name'] = null;
+ }
+ if (this.ownerId != null) {
+ json[r'ownerId'] = this.ownerId;
+ } else {
+ // json[r'ownerId'] = null;
}
json[r'type'] = this.type;
return json;
@@ -113,6 +129,7 @@ class CreateLibraryDto {
isVisible: mapValueOfType(json, r'isVisible'),
isWatched: mapValueOfType(json, r'isWatched'),
name: mapValueOfType(json, r'name'),
+ ownerId: mapValueOfType(json, r'ownerId'),
type: LibraryType.fromJson(json[r'type'])!,
);
}
diff --git a/mobile/openapi/lib/model/create_user_dto.dart b/mobile/openapi/lib/model/create_user_dto.dart
index bfa26622f..f272842cb 100644
--- a/mobile/openapi/lib/model/create_user_dto.dart
+++ b/mobile/openapi/lib/model/create_user_dto.dart
@@ -14,7 +14,6 @@ class CreateUserDto {
/// Returns a new [CreateUserDto] instance.
CreateUserDto({
required this.email,
- this.externalPath,
this.memoriesEnabled,
required this.name,
required this.password,
@@ -24,8 +23,6 @@ class CreateUserDto {
String email;
- String? externalPath;
-
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
@@ -45,7 +42,6 @@ class CreateUserDto {
@override
bool operator ==(Object other) => identical(this, other) || other is CreateUserDto &&
other.email == email &&
- other.externalPath == externalPath &&
other.memoriesEnabled == memoriesEnabled &&
other.name == name &&
other.password == password &&
@@ -56,7 +52,6 @@ class CreateUserDto {
int get hashCode =>
// ignore: unnecessary_parenthesis
(email.hashCode) +
- (externalPath == null ? 0 : externalPath!.hashCode) +
(memoriesEnabled == null ? 0 : memoriesEnabled!.hashCode) +
(name.hashCode) +
(password.hashCode) +
@@ -64,16 +59,11 @@ class CreateUserDto {
(storageLabel == null ? 0 : storageLabel!.hashCode);
@override
- String toString() => 'CreateUserDto[email=$email, externalPath=$externalPath, memoriesEnabled=$memoriesEnabled, name=$name, password=$password, quotaSizeInBytes=$quotaSizeInBytes, storageLabel=$storageLabel]';
+ String toString() => 'CreateUserDto[email=$email, memoriesEnabled=$memoriesEnabled, name=$name, password=$password, quotaSizeInBytes=$quotaSizeInBytes, storageLabel=$storageLabel]';
Map toJson() {
final json = {};
json[r'email'] = this.email;
- if (this.externalPath != null) {
- json[r'externalPath'] = this.externalPath;
- } else {
- // json[r'externalPath'] = null;
- }
if (this.memoriesEnabled != null) {
json[r'memoriesEnabled'] = this.memoriesEnabled;
} else {
@@ -103,7 +93,6 @@ class CreateUserDto {
return CreateUserDto(
email: mapValueOfType(json, r'email')!,
- externalPath: mapValueOfType(json, r'externalPath'),
memoriesEnabled: mapValueOfType(json, r'memoriesEnabled'),
name: mapValueOfType(json, r'name')!,
password: mapValueOfType(json, r'password')!,
diff --git a/mobile/openapi/lib/model/partner_response_dto.dart b/mobile/openapi/lib/model/partner_response_dto.dart
index aa96f764b..008e0c4f2 100644
--- a/mobile/openapi/lib/model/partner_response_dto.dart
+++ b/mobile/openapi/lib/model/partner_response_dto.dart
@@ -17,7 +17,6 @@ class PartnerResponseDto {
required this.createdAt,
required this.deletedAt,
required this.email,
- required this.externalPath,
required this.id,
this.inTimeline,
required this.isAdmin,
@@ -40,8 +39,6 @@ class PartnerResponseDto {
String email;
- String? externalPath;
-
String id;
///
@@ -84,7 +81,6 @@ class PartnerResponseDto {
other.createdAt == createdAt &&
other.deletedAt == deletedAt &&
other.email == email &&
- other.externalPath == externalPath &&
other.id == id &&
other.inTimeline == inTimeline &&
other.isAdmin == isAdmin &&
@@ -105,7 +101,6 @@ class PartnerResponseDto {
(createdAt.hashCode) +
(deletedAt == null ? 0 : deletedAt!.hashCode) +
(email.hashCode) +
- (externalPath == null ? 0 : externalPath!.hashCode) +
(id.hashCode) +
(inTimeline == null ? 0 : inTimeline!.hashCode) +
(isAdmin.hashCode) +
@@ -120,7 +115,7 @@ class PartnerResponseDto {
(updatedAt.hashCode);
@override
- String toString() => 'PartnerResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, externalPath=$externalPath, id=$id, inTimeline=$inTimeline, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, quotaSizeInBytes=$quotaSizeInBytes, quotaUsageInBytes=$quotaUsageInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel, updatedAt=$updatedAt]';
+ String toString() => 'PartnerResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, id=$id, inTimeline=$inTimeline, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, quotaSizeInBytes=$quotaSizeInBytes, quotaUsageInBytes=$quotaUsageInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel, updatedAt=$updatedAt]';
Map toJson() {
final json = {};
@@ -132,11 +127,6 @@ class PartnerResponseDto {
// json[r'deletedAt'] = null;
}
json[r'email'] = this.email;
- if (this.externalPath != null) {
- json[r'externalPath'] = this.externalPath;
- } else {
- // json[r'externalPath'] = null;
- }
json[r'id'] = this.id;
if (this.inTimeline != null) {
json[r'inTimeline'] = this.inTimeline;
@@ -184,7 +174,6 @@ class PartnerResponseDto {
createdAt: mapDateTime(json, r'createdAt', r'')!,
deletedAt: mapDateTime(json, r'deletedAt', r''),
email: mapValueOfType(json, r'email')!,
- externalPath: mapValueOfType(json, r'externalPath'),
id: mapValueOfType(json, r'id')!,
inTimeline: mapValueOfType(json, r'inTimeline'),
isAdmin: mapValueOfType(json, r'isAdmin')!,
@@ -248,7 +237,6 @@ class PartnerResponseDto {
'createdAt',
'deletedAt',
'email',
- 'externalPath',
'id',
'isAdmin',
'name',
diff --git a/mobile/openapi/lib/model/update_user_dto.dart b/mobile/openapi/lib/model/update_user_dto.dart
index 520240178..8fc85b486 100644
--- a/mobile/openapi/lib/model/update_user_dto.dart
+++ b/mobile/openapi/lib/model/update_user_dto.dart
@@ -15,7 +15,6 @@ class UpdateUserDto {
UpdateUserDto({
this.avatarColor,
this.email,
- this.externalPath,
required this.id,
this.isAdmin,
this.memoriesEnabled,
@@ -42,14 +41,6 @@ class UpdateUserDto {
///
String? email;
- ///
- /// Please note: This property should have been non-nullable! Since the specification file
- /// does not include a default value (using the "default:" property), however, the generated
- /// source code must fall back to having a nullable type.
- /// Consider adding a "default:" property in the specification file to hide this note.
- ///
- String? externalPath;
-
String id;
///
@@ -106,7 +97,6 @@ class UpdateUserDto {
bool operator ==(Object other) => identical(this, other) || other is UpdateUserDto &&
other.avatarColor == avatarColor &&
other.email == email &&
- other.externalPath == externalPath &&
other.id == id &&
other.isAdmin == isAdmin &&
other.memoriesEnabled == memoriesEnabled &&
@@ -121,7 +111,6 @@ class UpdateUserDto {
// ignore: unnecessary_parenthesis
(avatarColor == null ? 0 : avatarColor!.hashCode) +
(email == null ? 0 : email!.hashCode) +
- (externalPath == null ? 0 : externalPath!.hashCode) +
(id.hashCode) +
(isAdmin == null ? 0 : isAdmin!.hashCode) +
(memoriesEnabled == null ? 0 : memoriesEnabled!.hashCode) +
@@ -132,7 +121,7 @@ class UpdateUserDto {
(storageLabel == null ? 0 : storageLabel!.hashCode);
@override
- String toString() => 'UpdateUserDto[avatarColor=$avatarColor, email=$email, externalPath=$externalPath, id=$id, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, password=$password, quotaSizeInBytes=$quotaSizeInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]';
+ String toString() => 'UpdateUserDto[avatarColor=$avatarColor, email=$email, id=$id, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, password=$password, quotaSizeInBytes=$quotaSizeInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]';
Map toJson() {
final json = {};
@@ -145,11 +134,6 @@ class UpdateUserDto {
json[r'email'] = this.email;
} else {
// json[r'email'] = null;
- }
- if (this.externalPath != null) {
- json[r'externalPath'] = this.externalPath;
- } else {
- // json[r'externalPath'] = null;
}
json[r'id'] = this.id;
if (this.isAdmin != null) {
@@ -200,7 +184,6 @@ class UpdateUserDto {
return UpdateUserDto(
avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']),
email: mapValueOfType(json, r'email'),
- externalPath: mapValueOfType(json, r'externalPath'),
id: mapValueOfType(json, r'id')!,
isAdmin: mapValueOfType(json, r'isAdmin'),
memoriesEnabled: mapValueOfType(json, r'memoriesEnabled'),
diff --git a/mobile/openapi/lib/model/user_response_dto.dart b/mobile/openapi/lib/model/user_response_dto.dart
index 9da67392b..d4e0bf07d 100644
--- a/mobile/openapi/lib/model/user_response_dto.dart
+++ b/mobile/openapi/lib/model/user_response_dto.dart
@@ -17,7 +17,6 @@ class UserResponseDto {
required this.createdAt,
required this.deletedAt,
required this.email,
- required this.externalPath,
required this.id,
required this.isAdmin,
this.memoriesEnabled,
@@ -39,8 +38,6 @@ class UserResponseDto {
String email;
- String? externalPath;
-
String id;
bool isAdmin;
@@ -75,7 +72,6 @@ class UserResponseDto {
other.createdAt == createdAt &&
other.deletedAt == deletedAt &&
other.email == email &&
- other.externalPath == externalPath &&
other.id == id &&
other.isAdmin == isAdmin &&
other.memoriesEnabled == memoriesEnabled &&
@@ -95,7 +91,6 @@ class UserResponseDto {
(createdAt.hashCode) +
(deletedAt == null ? 0 : deletedAt!.hashCode) +
(email.hashCode) +
- (externalPath == null ? 0 : externalPath!.hashCode) +
(id.hashCode) +
(isAdmin.hashCode) +
(memoriesEnabled == null ? 0 : memoriesEnabled!.hashCode) +
@@ -109,7 +104,7 @@ class UserResponseDto {
(updatedAt.hashCode);
@override
- String toString() => 'UserResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, externalPath=$externalPath, id=$id, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, quotaSizeInBytes=$quotaSizeInBytes, quotaUsageInBytes=$quotaUsageInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel, updatedAt=$updatedAt]';
+ String toString() => 'UserResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, id=$id, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, quotaSizeInBytes=$quotaSizeInBytes, quotaUsageInBytes=$quotaUsageInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel, updatedAt=$updatedAt]';
Map toJson() {
final json = {};
@@ -121,11 +116,6 @@ class UserResponseDto {
// json[r'deletedAt'] = null;
}
json[r'email'] = this.email;
- if (this.externalPath != null) {
- json[r'externalPath'] = this.externalPath;
- } else {
- // json[r'externalPath'] = null;
- }
json[r'id'] = this.id;
json[r'isAdmin'] = this.isAdmin;
if (this.memoriesEnabled != null) {
@@ -168,7 +158,6 @@ class UserResponseDto {
createdAt: mapDateTime(json, r'createdAt', r'')!,
deletedAt: mapDateTime(json, r'deletedAt', r''),
email: mapValueOfType(json, r'email')!,
- externalPath: mapValueOfType(json, r'externalPath'),
id: mapValueOfType(json, r'id')!,
isAdmin: mapValueOfType(json, r'isAdmin')!,
memoriesEnabled: mapValueOfType(json, r'memoriesEnabled'),
@@ -231,7 +220,6 @@ class UserResponseDto {
'createdAt',
'deletedAt',
'email',
- 'externalPath',
'id',
'isAdmin',
'name',
diff --git a/mobile/openapi/test/create_library_dto_test.dart b/mobile/openapi/test/create_library_dto_test.dart
index dea1d4e63..88911249e 100644
--- a/mobile/openapi/test/create_library_dto_test.dart
+++ b/mobile/openapi/test/create_library_dto_test.dart
@@ -41,6 +41,11 @@ void main() {
// TODO
});
+ // String ownerId
+ test('to test the property `ownerId`', () async {
+ // TODO
+ });
+
// LibraryType type
test('to test the property `type`', () async {
// TODO
diff --git a/mobile/openapi/test/create_user_dto_test.dart b/mobile/openapi/test/create_user_dto_test.dart
index 6614b54a6..9658c02c8 100644
--- a/mobile/openapi/test/create_user_dto_test.dart
+++ b/mobile/openapi/test/create_user_dto_test.dart
@@ -21,11 +21,6 @@ void main() {
// TODO
});
- // String externalPath
- test('to test the property `externalPath`', () async {
- // TODO
- });
-
// bool memoriesEnabled
test('to test the property `memoriesEnabled`', () async {
// TODO
diff --git a/mobile/openapi/test/library_api_test.dart b/mobile/openapi/test/library_api_test.dart
index 20abcffe7..21afeff54 100644
--- a/mobile/openapi/test/library_api_test.dart
+++ b/mobile/openapi/test/library_api_test.dart
@@ -27,13 +27,13 @@ void main() {
// TODO
});
- //Future> getLibraries() async
- test('test getLibraries', () async {
+ //Future> getAllLibraries({ LibraryType type }) async
+ test('test getAllLibraries', () async {
// TODO
});
- //Future getLibraryInfo(String id) async
- test('test getLibraryInfo', () async {
+ //Future getLibrary(String id) async
+ test('test getLibrary', () async {
// TODO
});
diff --git a/mobile/openapi/test/partner_response_dto_test.dart b/mobile/openapi/test/partner_response_dto_test.dart
index d6b7769ab..7fce31d5e 100644
--- a/mobile/openapi/test/partner_response_dto_test.dart
+++ b/mobile/openapi/test/partner_response_dto_test.dart
@@ -36,11 +36,6 @@ void main() {
// TODO
});
- // String externalPath
- test('to test the property `externalPath`', () async {
- // TODO
- });
-
// String id
test('to test the property `id`', () async {
// TODO
diff --git a/mobile/openapi/test/update_user_dto_test.dart b/mobile/openapi/test/update_user_dto_test.dart
index aaacfa7cb..10c506666 100644
--- a/mobile/openapi/test/update_user_dto_test.dart
+++ b/mobile/openapi/test/update_user_dto_test.dart
@@ -26,11 +26,6 @@ void main() {
// TODO
});
- // String externalPath
- test('to test the property `externalPath`', () async {
- // TODO
- });
-
// String id
test('to test the property `id`', () async {
// TODO
diff --git a/mobile/openapi/test/user_response_dto_test.dart b/mobile/openapi/test/user_response_dto_test.dart
index bef6c812e..d0fdf97e1 100644
--- a/mobile/openapi/test/user_response_dto_test.dart
+++ b/mobile/openapi/test/user_response_dto_test.dart
@@ -36,11 +36,6 @@ void main() {
// TODO
});
- // String externalPath
- test('to test the property `externalPath`', () async {
- // TODO
- });
-
// String id
test('to test the property `id`', () async {
// TODO
diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json
index 5dab83da1..3092c6cc6 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 9b1319f9b..ce768d518 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 d869775c9..748418718 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 5be9e3035..edb0a9feb 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 9123b0621..c538db3a8 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 0215a4976..93f716353 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 cb1911766..33208fde2 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 db6119d4d..b57d56e7b 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 cafe70b4d..ba1dd8374 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 6a5982abe..4d8912685 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 0f9e173b7..2bc5f3ca9 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 7ba6cd409..395373bcc 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 36a752458..824a1066c 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 179974eae..e6dbb8167 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 3c6977ff0..1cab11627 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 15800b993..a82337945 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 691bc7de4..6134e97ce 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 173e63211..fb68b2626 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 dbc563ee0..c574595ea 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 000000000..6c43e351f
--- /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 5e104bc1a..3997dd1a2 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 78eab13b3..3f6b207ce 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 c791b2c8a..433ab6fba 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 5f4321068..6cac1c44f 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 29a2ee530..e4c7d3a31 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 0f496504f..b51e53106 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 232c1477f..79993a4da 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 3a203ce87..ec8e19683 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 1b8a3b3fe..e0d9113c6 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 264acc08e..740f4c483 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 4caa5d6ab..f55d341a7 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 039ac8ea0..845cd9e2c 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 000000000..b3c70adb6
--- /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 019063ef8..0d79ae961 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 440becde0..000000000
--- 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 f24f0ac7f..053d852dd 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 a14c2a8c8..b46768c3e 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 dbb60df3e..af5558c26 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 000000000..e9ac41b83
--- /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 000000000..78831d38c
--- /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 a3a93e0f3..84917d591 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)}
|