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