mirror of
https://github.com/immich-app/immich.git
synced 2025-03-01 15:11:21 +01:00
feat(server): support for read-only assets and importing existing items in the filesystem (#2715)
* Added read-only flag for assets, endpoint to trigger file import vs upload * updated fixtures with new property * if upload is 'read-only', ensure there is no existing asset at the designated originalPath * added test for file import as well as detecting existing image at read-only destination location * Added storage service test for a case where it should not move read-only assets * upload doesn't need the read-only flag available, just importing * default isReadOnly on import endpoint to true * formatting fixes * create-asset dto needs isReadOnly, so set it to false by default on create, updated api generation * updated code to reflect changes in MR * fixed read stream promise return type * new index for originalPath, check for existing path on import, reglardless of user, to prevent duplicates * refactor: import asset * chore: open api * chore: tests * Added externalPath support for individual users, updated UI to allow this to be set by admin * added missing var for externalPath in ui * chore: open api * fix: compilation issues * fix: server test * built api, fixed user-response dto to include externalPath * reverted accidental commit * bad commit of duplicate externalPath in user response dto * fixed tests to include externalPath on expected result * fix: unit tests * centralized supported filetypes, perform file type checking of asset and sidecar during file import process * centralized supported filetype check method to keep regex DRY * fixed typo * combined migrations into one * update api * Removed externalPath from shared-link code, added column to admin user page whether external paths / import is enabled or not * update mimetype * Fixed detect correct mimetype * revert asset-upload config * reverted domain.constant * refactor * fix mime-type issue * fix format --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
7f44d508dc
commit
e171fec5aa
55 changed files with 1321 additions and 128 deletions
3
mobile/openapi/.openapi-generator/FILES
generated
3
mobile/openapi/.openapi-generator/FILES
generated
|
@ -49,6 +49,7 @@ doc/DownloadFilesDto.md
|
||||||
doc/ExifResponseDto.md
|
doc/ExifResponseDto.md
|
||||||
doc/GetAssetByTimeBucketDto.md
|
doc/GetAssetByTimeBucketDto.md
|
||||||
doc/GetAssetCountByTimeBucketDto.md
|
doc/GetAssetCountByTimeBucketDto.md
|
||||||
|
doc/ImportAssetDto.md
|
||||||
doc/JobApi.md
|
doc/JobApi.md
|
||||||
doc/JobCommand.md
|
doc/JobCommand.md
|
||||||
doc/JobCommandDto.md
|
doc/JobCommandDto.md
|
||||||
|
@ -181,6 +182,7 @@ lib/model/download_files_dto.dart
|
||||||
lib/model/exif_response_dto.dart
|
lib/model/exif_response_dto.dart
|
||||||
lib/model/get_asset_by_time_bucket_dto.dart
|
lib/model/get_asset_by_time_bucket_dto.dart
|
||||||
lib/model/get_asset_count_by_time_bucket_dto.dart
|
lib/model/get_asset_count_by_time_bucket_dto.dart
|
||||||
|
lib/model/import_asset_dto.dart
|
||||||
lib/model/job_command.dart
|
lib/model/job_command.dart
|
||||||
lib/model/job_command_dto.dart
|
lib/model/job_command_dto.dart
|
||||||
lib/model/job_counts_dto.dart
|
lib/model/job_counts_dto.dart
|
||||||
|
@ -284,6 +286,7 @@ test/download_files_dto_test.dart
|
||||||
test/exif_response_dto_test.dart
|
test/exif_response_dto_test.dart
|
||||||
test/get_asset_by_time_bucket_dto_test.dart
|
test/get_asset_by_time_bucket_dto_test.dart
|
||||||
test/get_asset_count_by_time_bucket_dto_test.dart
|
test/get_asset_count_by_time_bucket_dto_test.dart
|
||||||
|
test/import_asset_dto_test.dart
|
||||||
test/job_api_test.dart
|
test/job_api_test.dart
|
||||||
test/job_command_dto_test.dart
|
test/job_command_dto_test.dart
|
||||||
test/job_command_test.dart
|
test/job_command_test.dart
|
||||||
|
|
2
mobile/openapi/README.md
generated
2
mobile/openapi/README.md
generated
|
@ -108,6 +108,7 @@ Class | Method | HTTP request | Description
|
||||||
*AssetApi* | [**getMapMarkers**](doc//AssetApi.md#getmapmarkers) | **GET** /asset/map-marker |
|
*AssetApi* | [**getMapMarkers**](doc//AssetApi.md#getmapmarkers) | **GET** /asset/map-marker |
|
||||||
*AssetApi* | [**getMemoryLane**](doc//AssetApi.md#getmemorylane) | **GET** /asset/memory-lane |
|
*AssetApi* | [**getMemoryLane**](doc//AssetApi.md#getmemorylane) | **GET** /asset/memory-lane |
|
||||||
*AssetApi* | [**getUserAssetsByDeviceId**](doc//AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} |
|
*AssetApi* | [**getUserAssetsByDeviceId**](doc//AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} |
|
||||||
|
*AssetApi* | [**importFile**](doc//AssetApi.md#importfile) | **POST** /asset/import |
|
||||||
*AssetApi* | [**searchAsset**](doc//AssetApi.md#searchasset) | **POST** /asset/search |
|
*AssetApi* | [**searchAsset**](doc//AssetApi.md#searchasset) | **POST** /asset/search |
|
||||||
*AssetApi* | [**serveFile**](doc//AssetApi.md#servefile) | **GET** /asset/file/{id} |
|
*AssetApi* | [**serveFile**](doc//AssetApi.md#servefile) | **GET** /asset/file/{id} |
|
||||||
*AssetApi* | [**updateAsset**](doc//AssetApi.md#updateasset) | **PUT** /asset/{id} |
|
*AssetApi* | [**updateAsset**](doc//AssetApi.md#updateasset) | **PUT** /asset/{id} |
|
||||||
|
@ -218,6 +219,7 @@ Class | Method | HTTP request | Description
|
||||||
- [ExifResponseDto](doc//ExifResponseDto.md)
|
- [ExifResponseDto](doc//ExifResponseDto.md)
|
||||||
- [GetAssetByTimeBucketDto](doc//GetAssetByTimeBucketDto.md)
|
- [GetAssetByTimeBucketDto](doc//GetAssetByTimeBucketDto.md)
|
||||||
- [GetAssetCountByTimeBucketDto](doc//GetAssetCountByTimeBucketDto.md)
|
- [GetAssetCountByTimeBucketDto](doc//GetAssetCountByTimeBucketDto.md)
|
||||||
|
- [ImportAssetDto](doc//ImportAssetDto.md)
|
||||||
- [JobCommand](doc//JobCommand.md)
|
- [JobCommand](doc//JobCommand.md)
|
||||||
- [JobCommandDto](doc//JobCommandDto.md)
|
- [JobCommandDto](doc//JobCommandDto.md)
|
||||||
- [JobCountsDto](doc//JobCountsDto.md)
|
- [JobCountsDto](doc//JobCountsDto.md)
|
||||||
|
|
66
mobile/openapi/doc/AssetApi.md
generated
66
mobile/openapi/doc/AssetApi.md
generated
|
@ -29,6 +29,7 @@ Method | HTTP request | Description
|
||||||
[**getMapMarkers**](AssetApi.md#getmapmarkers) | **GET** /asset/map-marker |
|
[**getMapMarkers**](AssetApi.md#getmapmarkers) | **GET** /asset/map-marker |
|
||||||
[**getMemoryLane**](AssetApi.md#getmemorylane) | **GET** /asset/memory-lane |
|
[**getMemoryLane**](AssetApi.md#getmemorylane) | **GET** /asset/memory-lane |
|
||||||
[**getUserAssetsByDeviceId**](AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} |
|
[**getUserAssetsByDeviceId**](AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} |
|
||||||
|
[**importFile**](AssetApi.md#importfile) | **POST** /asset/import |
|
||||||
[**searchAsset**](AssetApi.md#searchasset) | **POST** /asset/search |
|
[**searchAsset**](AssetApi.md#searchasset) | **POST** /asset/search |
|
||||||
[**serveFile**](AssetApi.md#servefile) | **GET** /asset/file/{id} |
|
[**serveFile**](AssetApi.md#servefile) | **GET** /asset/file/{id} |
|
||||||
[**updateAsset**](AssetApi.md#updateasset) | **PUT** /asset/{id} |
|
[**updateAsset**](AssetApi.md#updateasset) | **PUT** /asset/{id} |
|
||||||
|
@ -1159,6 +1160,61 @@ Name | Type | Description | Notes
|
||||||
|
|
||||||
[[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)
|
||||||
|
|
||||||
|
# **importFile**
|
||||||
|
> AssetFileUploadResponseDto importFile(importAssetDto)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Example
|
||||||
|
```dart
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
// TODO Configure API key authorization: cookie
|
||||||
|
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
|
||||||
|
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||||
|
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
|
||||||
|
// TODO Configure API key authorization: api_key
|
||||||
|
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
|
||||||
|
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||||
|
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
|
||||||
|
// TODO Configure HTTP Bearer authorization: bearer
|
||||||
|
// Case 1. Use String Token
|
||||||
|
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
|
||||||
|
// Case 2. Use Function which generate token.
|
||||||
|
// String yourTokenGeneratorFunction() { ... }
|
||||||
|
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
||||||
|
|
||||||
|
final api_instance = AssetApi();
|
||||||
|
final importAssetDto = ImportAssetDto(); // ImportAssetDto |
|
||||||
|
|
||||||
|
try {
|
||||||
|
final result = api_instance.importFile(importAssetDto);
|
||||||
|
print(result);
|
||||||
|
} catch (e) {
|
||||||
|
print('Exception when calling AssetApi->importFile: $e\n');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
Name | Type | Description | Notes
|
||||||
|
------------- | ------------- | ------------- | -------------
|
||||||
|
**importAssetDto** | [**ImportAssetDto**](ImportAssetDto.md)| |
|
||||||
|
|
||||||
|
### Return type
|
||||||
|
|
||||||
|
[**AssetFileUploadResponseDto**](AssetFileUploadResponseDto.md)
|
||||||
|
|
||||||
|
### Authorization
|
||||||
|
|
||||||
|
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
|
||||||
|
|
||||||
|
### HTTP request headers
|
||||||
|
|
||||||
|
- **Content-Type**: application/json
|
||||||
|
- **Accept**: application/json
|
||||||
|
|
||||||
|
[[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)
|
||||||
|
|
||||||
# **searchAsset**
|
# **searchAsset**
|
||||||
> List<AssetResponseDto> searchAsset(searchAssetDto)
|
> List<AssetResponseDto> searchAsset(searchAssetDto)
|
||||||
|
|
||||||
|
@ -1335,7 +1391,7 @@ Name | Type | Description | Notes
|
||||||
[[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)
|
||||||
|
|
||||||
# **uploadFile**
|
# **uploadFile**
|
||||||
> AssetFileUploadResponseDto uploadFile(assetType, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, fileExtension, key, livePhotoData, sidecarData, isArchived, isVisible, duration)
|
> AssetFileUploadResponseDto uploadFile(assetType, assetData, fileExtension, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, key, livePhotoData, sidecarData, isReadOnly, isArchived, isVisible, duration)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -1360,21 +1416,22 @@ import 'package:openapi/api.dart';
|
||||||
final api_instance = AssetApi();
|
final api_instance = AssetApi();
|
||||||
final assetType = ; // AssetTypeEnum |
|
final assetType = ; // AssetTypeEnum |
|
||||||
final assetData = BINARY_DATA_HERE; // MultipartFile |
|
final assetData = BINARY_DATA_HERE; // MultipartFile |
|
||||||
|
final fileExtension = fileExtension_example; // String |
|
||||||
final deviceAssetId = deviceAssetId_example; // String |
|
final deviceAssetId = deviceAssetId_example; // String |
|
||||||
final deviceId = deviceId_example; // String |
|
final deviceId = deviceId_example; // String |
|
||||||
final fileCreatedAt = 2013-10-20T19:20:30+01:00; // DateTime |
|
final fileCreatedAt = 2013-10-20T19:20:30+01:00; // DateTime |
|
||||||
final fileModifiedAt = 2013-10-20T19:20:30+01:00; // DateTime |
|
final fileModifiedAt = 2013-10-20T19:20:30+01:00; // DateTime |
|
||||||
final isFavorite = true; // bool |
|
final isFavorite = true; // bool |
|
||||||
final fileExtension = fileExtension_example; // String |
|
|
||||||
final key = key_example; // String |
|
final key = key_example; // String |
|
||||||
final livePhotoData = BINARY_DATA_HERE; // MultipartFile |
|
final livePhotoData = BINARY_DATA_HERE; // MultipartFile |
|
||||||
final sidecarData = BINARY_DATA_HERE; // MultipartFile |
|
final sidecarData = BINARY_DATA_HERE; // MultipartFile |
|
||||||
|
final isReadOnly = true; // bool |
|
||||||
final isArchived = true; // bool |
|
final isArchived = true; // bool |
|
||||||
final isVisible = true; // bool |
|
final isVisible = true; // bool |
|
||||||
final duration = duration_example; // String |
|
final duration = duration_example; // String |
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final result = api_instance.uploadFile(assetType, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, fileExtension, key, livePhotoData, sidecarData, isArchived, isVisible, duration);
|
final result = api_instance.uploadFile(assetType, assetData, fileExtension, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, key, livePhotoData, sidecarData, isReadOnly, isArchived, isVisible, duration);
|
||||||
print(result);
|
print(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Exception when calling AssetApi->uploadFile: $e\n');
|
print('Exception when calling AssetApi->uploadFile: $e\n');
|
||||||
|
@ -1387,15 +1444,16 @@ Name | Type | Description | Notes
|
||||||
------------- | ------------- | ------------- | -------------
|
------------- | ------------- | ------------- | -------------
|
||||||
**assetType** | [**AssetTypeEnum**](AssetTypeEnum.md)| |
|
**assetType** | [**AssetTypeEnum**](AssetTypeEnum.md)| |
|
||||||
**assetData** | **MultipartFile**| |
|
**assetData** | **MultipartFile**| |
|
||||||
|
**fileExtension** | **String**| |
|
||||||
**deviceAssetId** | **String**| |
|
**deviceAssetId** | **String**| |
|
||||||
**deviceId** | **String**| |
|
**deviceId** | **String**| |
|
||||||
**fileCreatedAt** | **DateTime**| |
|
**fileCreatedAt** | **DateTime**| |
|
||||||
**fileModifiedAt** | **DateTime**| |
|
**fileModifiedAt** | **DateTime**| |
|
||||||
**isFavorite** | **bool**| |
|
**isFavorite** | **bool**| |
|
||||||
**fileExtension** | **String**| |
|
|
||||||
**key** | **String**| | [optional]
|
**key** | **String**| | [optional]
|
||||||
**livePhotoData** | **MultipartFile**| | [optional]
|
**livePhotoData** | **MultipartFile**| | [optional]
|
||||||
**sidecarData** | **MultipartFile**| | [optional]
|
**sidecarData** | **MultipartFile**| | [optional]
|
||||||
|
**isReadOnly** | **bool**| | [optional] [default to false]
|
||||||
**isArchived** | **bool**| | [optional]
|
**isArchived** | **bool**| | [optional]
|
||||||
**isVisible** | **bool**| | [optional]
|
**isVisible** | **bool**| | [optional]
|
||||||
**duration** | **String**| | [optional]
|
**duration** | **String**| | [optional]
|
||||||
|
|
1
mobile/openapi/doc/CreateUserDto.md
generated
1
mobile/openapi/doc/CreateUserDto.md
generated
|
@ -13,6 +13,7 @@ Name | Type | Description | Notes
|
||||||
**firstName** | **String** | |
|
**firstName** | **String** | |
|
||||||
**lastName** | **String** | |
|
**lastName** | **String** | |
|
||||||
**storageLabel** | **String** | | [optional]
|
**storageLabel** | **String** | | [optional]
|
||||||
|
**externalPath** | **String** | | [optional]
|
||||||
|
|
||||||
[[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)
|
||||||
|
|
||||||
|
|
26
mobile/openapi/doc/ImportAssetDto.md
generated
Normal file
26
mobile/openapi/doc/ImportAssetDto.md
generated
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
# openapi.model.ImportAssetDto
|
||||||
|
|
||||||
|
## Load the model package
|
||||||
|
```dart
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
Name | Type | Description | Notes
|
||||||
|
------------ | ------------- | ------------- | -------------
|
||||||
|
**assetType** | [**AssetTypeEnum**](AssetTypeEnum.md) | |
|
||||||
|
**isReadOnly** | **bool** | | [optional] [default to true]
|
||||||
|
**assetPath** | **String** | |
|
||||||
|
**sidecarPath** | **String** | | [optional]
|
||||||
|
**deviceAssetId** | **String** | |
|
||||||
|
**deviceId** | **String** | |
|
||||||
|
**fileCreatedAt** | [**DateTime**](DateTime.md) | |
|
||||||
|
**fileModifiedAt** | [**DateTime**](DateTime.md) | |
|
||||||
|
**isFavorite** | **bool** | |
|
||||||
|
**isArchived** | **bool** | | [optional]
|
||||||
|
**isVisible** | **bool** | | [optional]
|
||||||
|
**duration** | **String** | | [optional]
|
||||||
|
|
||||||
|
[[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/UpdateUserDto.md
generated
1
mobile/openapi/doc/UpdateUserDto.md
generated
|
@ -14,6 +14,7 @@ Name | Type | Description | Notes
|
||||||
**firstName** | **String** | | [optional]
|
**firstName** | **String** | | [optional]
|
||||||
**lastName** | **String** | | [optional]
|
**lastName** | **String** | | [optional]
|
||||||
**storageLabel** | **String** | | [optional]
|
**storageLabel** | **String** | | [optional]
|
||||||
|
**externalPath** | **String** | | [optional]
|
||||||
**isAdmin** | **bool** | | [optional]
|
**isAdmin** | **bool** | | [optional]
|
||||||
**shouldChangePassword** | **bool** | | [optional]
|
**shouldChangePassword** | **bool** | | [optional]
|
||||||
|
|
||||||
|
|
1
mobile/openapi/doc/UserResponseDto.md
generated
1
mobile/openapi/doc/UserResponseDto.md
generated
|
@ -13,6 +13,7 @@ Name | Type | Description | Notes
|
||||||
**firstName** | **String** | |
|
**firstName** | **String** | |
|
||||||
**lastName** | **String** | |
|
**lastName** | **String** | |
|
||||||
**storageLabel** | **String** | |
|
**storageLabel** | **String** | |
|
||||||
|
**externalPath** | **String** | |
|
||||||
**profileImagePath** | **String** | |
|
**profileImagePath** | **String** | |
|
||||||
**shouldChangePassword** | **bool** | |
|
**shouldChangePassword** | **bool** | |
|
||||||
**isAdmin** | **bool** | |
|
**isAdmin** | **bool** | |
|
||||||
|
|
1
mobile/openapi/lib/api.dart
generated
1
mobile/openapi/lib/api.dart
generated
|
@ -85,6 +85,7 @@ part 'model/download_files_dto.dart';
|
||||||
part 'model/exif_response_dto.dart';
|
part 'model/exif_response_dto.dart';
|
||||||
part 'model/get_asset_by_time_bucket_dto.dart';
|
part 'model/get_asset_by_time_bucket_dto.dart';
|
||||||
part 'model/get_asset_count_by_time_bucket_dto.dart';
|
part 'model/get_asset_count_by_time_bucket_dto.dart';
|
||||||
|
part 'model/import_asset_dto.dart';
|
||||||
part 'model/job_command.dart';
|
part 'model/job_command.dart';
|
||||||
part 'model/job_command_dto.dart';
|
part 'model/job_command_dto.dart';
|
||||||
part 'model/job_counts_dto.dart';
|
part 'model/job_counts_dto.dart';
|
||||||
|
|
77
mobile/openapi/lib/api/asset_api.dart
generated
77
mobile/openapi/lib/api/asset_api.dart
generated
|
@ -1123,6 +1123,53 @@ class AssetApi {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Performs an HTTP 'POST /asset/import' operation and returns the [Response].
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [ImportAssetDto] importAssetDto (required):
|
||||||
|
Future<Response> importFileWithHttpInfo(ImportAssetDto importAssetDto,) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final path = r'/asset/import';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody = importAssetDto;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>['application/json'];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
path,
|
||||||
|
'POST',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [ImportAssetDto] importAssetDto (required):
|
||||||
|
Future<AssetFileUploadResponseDto?> importFile(ImportAssetDto importAssetDto,) async {
|
||||||
|
final response = await importFileWithHttpInfo(importAssetDto,);
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||||
|
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||||
|
// FormatException when trying to decode an empty string.
|
||||||
|
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||||
|
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AssetFileUploadResponseDto',) as AssetFileUploadResponseDto;
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// Performs an HTTP 'POST /asset/search' operation and returns the [Response].
|
/// Performs an HTTP 'POST /asset/search' operation and returns the [Response].
|
||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
|
@ -1307,6 +1354,8 @@ class AssetApi {
|
||||||
///
|
///
|
||||||
/// * [MultipartFile] assetData (required):
|
/// * [MultipartFile] assetData (required):
|
||||||
///
|
///
|
||||||
|
/// * [String] fileExtension (required):
|
||||||
|
///
|
||||||
/// * [String] deviceAssetId (required):
|
/// * [String] deviceAssetId (required):
|
||||||
///
|
///
|
||||||
/// * [String] deviceId (required):
|
/// * [String] deviceId (required):
|
||||||
|
@ -1317,20 +1366,20 @@ class AssetApi {
|
||||||
///
|
///
|
||||||
/// * [bool] isFavorite (required):
|
/// * [bool] isFavorite (required):
|
||||||
///
|
///
|
||||||
/// * [String] fileExtension (required):
|
|
||||||
///
|
|
||||||
/// * [String] key:
|
/// * [String] key:
|
||||||
///
|
///
|
||||||
/// * [MultipartFile] livePhotoData:
|
/// * [MultipartFile] livePhotoData:
|
||||||
///
|
///
|
||||||
/// * [MultipartFile] sidecarData:
|
/// * [MultipartFile] sidecarData:
|
||||||
///
|
///
|
||||||
|
/// * [bool] isReadOnly:
|
||||||
|
///
|
||||||
/// * [bool] isArchived:
|
/// * [bool] isArchived:
|
||||||
///
|
///
|
||||||
/// * [bool] isVisible:
|
/// * [bool] isVisible:
|
||||||
///
|
///
|
||||||
/// * [String] duration:
|
/// * [String] duration:
|
||||||
Future<Response> uploadFileWithHttpInfo(AssetTypeEnum assetType, MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, bool isFavorite, String fileExtension, { String? key, MultipartFile? livePhotoData, MultipartFile? sidecarData, bool? isArchived, bool? isVisible, String? duration, }) async {
|
Future<Response> uploadFileWithHttpInfo(AssetTypeEnum assetType, MultipartFile assetData, String fileExtension, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, bool isFavorite, { String? key, MultipartFile? livePhotoData, MultipartFile? sidecarData, bool? isReadOnly, bool? isArchived, bool? isVisible, String? duration, }) async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
final path = r'/asset/upload';
|
final path = r'/asset/upload';
|
||||||
|
|
||||||
|
@ -1368,6 +1417,14 @@ class AssetApi {
|
||||||
mp.fields[r'sidecarData'] = sidecarData.field;
|
mp.fields[r'sidecarData'] = sidecarData.field;
|
||||||
mp.files.add(sidecarData);
|
mp.files.add(sidecarData);
|
||||||
}
|
}
|
||||||
|
if (isReadOnly != null) {
|
||||||
|
hasFields = true;
|
||||||
|
mp.fields[r'isReadOnly'] = parameterToString(isReadOnly);
|
||||||
|
}
|
||||||
|
if (fileExtension != null) {
|
||||||
|
hasFields = true;
|
||||||
|
mp.fields[r'fileExtension'] = parameterToString(fileExtension);
|
||||||
|
}
|
||||||
if (deviceAssetId != null) {
|
if (deviceAssetId != null) {
|
||||||
hasFields = true;
|
hasFields = true;
|
||||||
mp.fields[r'deviceAssetId'] = parameterToString(deviceAssetId);
|
mp.fields[r'deviceAssetId'] = parameterToString(deviceAssetId);
|
||||||
|
@ -1396,10 +1453,6 @@ class AssetApi {
|
||||||
hasFields = true;
|
hasFields = true;
|
||||||
mp.fields[r'isVisible'] = parameterToString(isVisible);
|
mp.fields[r'isVisible'] = parameterToString(isVisible);
|
||||||
}
|
}
|
||||||
if (fileExtension != null) {
|
|
||||||
hasFields = true;
|
|
||||||
mp.fields[r'fileExtension'] = parameterToString(fileExtension);
|
|
||||||
}
|
|
||||||
if (duration != null) {
|
if (duration != null) {
|
||||||
hasFields = true;
|
hasFields = true;
|
||||||
mp.fields[r'duration'] = parameterToString(duration);
|
mp.fields[r'duration'] = parameterToString(duration);
|
||||||
|
@ -1425,6 +1478,8 @@ class AssetApi {
|
||||||
///
|
///
|
||||||
/// * [MultipartFile] assetData (required):
|
/// * [MultipartFile] assetData (required):
|
||||||
///
|
///
|
||||||
|
/// * [String] fileExtension (required):
|
||||||
|
///
|
||||||
/// * [String] deviceAssetId (required):
|
/// * [String] deviceAssetId (required):
|
||||||
///
|
///
|
||||||
/// * [String] deviceId (required):
|
/// * [String] deviceId (required):
|
||||||
|
@ -1435,21 +1490,21 @@ class AssetApi {
|
||||||
///
|
///
|
||||||
/// * [bool] isFavorite (required):
|
/// * [bool] isFavorite (required):
|
||||||
///
|
///
|
||||||
/// * [String] fileExtension (required):
|
|
||||||
///
|
|
||||||
/// * [String] key:
|
/// * [String] key:
|
||||||
///
|
///
|
||||||
/// * [MultipartFile] livePhotoData:
|
/// * [MultipartFile] livePhotoData:
|
||||||
///
|
///
|
||||||
/// * [MultipartFile] sidecarData:
|
/// * [MultipartFile] sidecarData:
|
||||||
///
|
///
|
||||||
|
/// * [bool] isReadOnly:
|
||||||
|
///
|
||||||
/// * [bool] isArchived:
|
/// * [bool] isArchived:
|
||||||
///
|
///
|
||||||
/// * [bool] isVisible:
|
/// * [bool] isVisible:
|
||||||
///
|
///
|
||||||
/// * [String] duration:
|
/// * [String] duration:
|
||||||
Future<AssetFileUploadResponseDto?> uploadFile(AssetTypeEnum assetType, MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, bool isFavorite, String fileExtension, { String? key, MultipartFile? livePhotoData, MultipartFile? sidecarData, bool? isArchived, bool? isVisible, String? duration, }) async {
|
Future<AssetFileUploadResponseDto?> uploadFile(AssetTypeEnum assetType, MultipartFile assetData, String fileExtension, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, bool isFavorite, { String? key, MultipartFile? livePhotoData, MultipartFile? sidecarData, bool? isReadOnly, bool? isArchived, bool? isVisible, String? duration, }) async {
|
||||||
final response = await uploadFileWithHttpInfo(assetType, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, fileExtension, key: key, livePhotoData: livePhotoData, sidecarData: sidecarData, isArchived: isArchived, isVisible: isVisible, duration: duration, );
|
final response = await uploadFileWithHttpInfo(assetType, assetData, fileExtension, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, key: key, livePhotoData: livePhotoData, sidecarData: sidecarData, isReadOnly: isReadOnly, isArchived: isArchived, isVisible: isVisible, duration: duration, );
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
}
|
}
|
||||||
|
|
2
mobile/openapi/lib/api_client.dart
generated
2
mobile/openapi/lib/api_client.dart
generated
|
@ -265,6 +265,8 @@ class ApiClient {
|
||||||
return GetAssetByTimeBucketDto.fromJson(value);
|
return GetAssetByTimeBucketDto.fromJson(value);
|
||||||
case 'GetAssetCountByTimeBucketDto':
|
case 'GetAssetCountByTimeBucketDto':
|
||||||
return GetAssetCountByTimeBucketDto.fromJson(value);
|
return GetAssetCountByTimeBucketDto.fromJson(value);
|
||||||
|
case 'ImportAssetDto':
|
||||||
|
return ImportAssetDto.fromJson(value);
|
||||||
case 'JobCommand':
|
case 'JobCommand':
|
||||||
return JobCommandTypeTransformer().decode(value);
|
return JobCommandTypeTransformer().decode(value);
|
||||||
case 'JobCommandDto':
|
case 'JobCommandDto':
|
||||||
|
|
17
mobile/openapi/lib/model/create_user_dto.dart
generated
17
mobile/openapi/lib/model/create_user_dto.dart
generated
|
@ -18,6 +18,7 @@ class CreateUserDto {
|
||||||
required this.firstName,
|
required this.firstName,
|
||||||
required this.lastName,
|
required this.lastName,
|
||||||
this.storageLabel,
|
this.storageLabel,
|
||||||
|
this.externalPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
String email;
|
String email;
|
||||||
|
@ -30,13 +31,16 @@ class CreateUserDto {
|
||||||
|
|
||||||
String? storageLabel;
|
String? storageLabel;
|
||||||
|
|
||||||
|
String? externalPath;
|
||||||
|
|
||||||
@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.password == password &&
|
other.password == password &&
|
||||||
other.firstName == firstName &&
|
other.firstName == firstName &&
|
||||||
other.lastName == lastName &&
|
other.lastName == lastName &&
|
||||||
other.storageLabel == storageLabel;
|
other.storageLabel == storageLabel &&
|
||||||
|
other.externalPath == externalPath;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
|
@ -45,10 +49,11 @@ class CreateUserDto {
|
||||||
(password.hashCode) +
|
(password.hashCode) +
|
||||||
(firstName.hashCode) +
|
(firstName.hashCode) +
|
||||||
(lastName.hashCode) +
|
(lastName.hashCode) +
|
||||||
(storageLabel == null ? 0 : storageLabel!.hashCode);
|
(storageLabel == null ? 0 : storageLabel!.hashCode) +
|
||||||
|
(externalPath == null ? 0 : externalPath!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'CreateUserDto[email=$email, password=$password, firstName=$firstName, lastName=$lastName, storageLabel=$storageLabel]';
|
String toString() => 'CreateUserDto[email=$email, password=$password, firstName=$firstName, lastName=$lastName, storageLabel=$storageLabel, externalPath=$externalPath]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
|
@ -61,6 +66,11 @@ class CreateUserDto {
|
||||||
} else {
|
} else {
|
||||||
// json[r'storageLabel'] = null;
|
// json[r'storageLabel'] = null;
|
||||||
}
|
}
|
||||||
|
if (this.externalPath != null) {
|
||||||
|
json[r'externalPath'] = this.externalPath;
|
||||||
|
} else {
|
||||||
|
// json[r'externalPath'] = null;
|
||||||
|
}
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,6 +98,7 @@ class CreateUserDto {
|
||||||
firstName: mapValueOfType<String>(json, r'firstName')!,
|
firstName: mapValueOfType<String>(json, r'firstName')!,
|
||||||
lastName: mapValueOfType<String>(json, r'lastName')!,
|
lastName: mapValueOfType<String>(json, r'lastName')!,
|
||||||
storageLabel: mapValueOfType<String>(json, r'storageLabel'),
|
storageLabel: mapValueOfType<String>(json, r'storageLabel'),
|
||||||
|
externalPath: mapValueOfType<String>(json, r'externalPath'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
232
mobile/openapi/lib/model/import_asset_dto.dart
generated
Normal file
232
mobile/openapi/lib/model/import_asset_dto.dart
generated
Normal file
|
@ -0,0 +1,232 @@
|
||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.12
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
class ImportAssetDto {
|
||||||
|
/// Returns a new [ImportAssetDto] instance.
|
||||||
|
ImportAssetDto({
|
||||||
|
required this.assetType,
|
||||||
|
this.isReadOnly = true,
|
||||||
|
required this.assetPath,
|
||||||
|
this.sidecarPath,
|
||||||
|
required this.deviceAssetId,
|
||||||
|
required this.deviceId,
|
||||||
|
required this.fileCreatedAt,
|
||||||
|
required this.fileModifiedAt,
|
||||||
|
required this.isFavorite,
|
||||||
|
this.isArchived,
|
||||||
|
this.isVisible,
|
||||||
|
this.duration,
|
||||||
|
});
|
||||||
|
|
||||||
|
AssetTypeEnum assetType;
|
||||||
|
|
||||||
|
bool isReadOnly;
|
||||||
|
|
||||||
|
String assetPath;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// 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? sidecarPath;
|
||||||
|
|
||||||
|
String deviceAssetId;
|
||||||
|
|
||||||
|
String deviceId;
|
||||||
|
|
||||||
|
DateTime fileCreatedAt;
|
||||||
|
|
||||||
|
DateTime fileModifiedAt;
|
||||||
|
|
||||||
|
bool isFavorite;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// 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.
|
||||||
|
///
|
||||||
|
bool? isArchived;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// 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.
|
||||||
|
///
|
||||||
|
bool? isVisible;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// 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? duration;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is ImportAssetDto &&
|
||||||
|
other.assetType == assetType &&
|
||||||
|
other.isReadOnly == isReadOnly &&
|
||||||
|
other.assetPath == assetPath &&
|
||||||
|
other.sidecarPath == sidecarPath &&
|
||||||
|
other.deviceAssetId == deviceAssetId &&
|
||||||
|
other.deviceId == deviceId &&
|
||||||
|
other.fileCreatedAt == fileCreatedAt &&
|
||||||
|
other.fileModifiedAt == fileModifiedAt &&
|
||||||
|
other.isFavorite == isFavorite &&
|
||||||
|
other.isArchived == isArchived &&
|
||||||
|
other.isVisible == isVisible &&
|
||||||
|
other.duration == duration;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(assetType.hashCode) +
|
||||||
|
(isReadOnly.hashCode) +
|
||||||
|
(assetPath.hashCode) +
|
||||||
|
(sidecarPath == null ? 0 : sidecarPath!.hashCode) +
|
||||||
|
(deviceAssetId.hashCode) +
|
||||||
|
(deviceId.hashCode) +
|
||||||
|
(fileCreatedAt.hashCode) +
|
||||||
|
(fileModifiedAt.hashCode) +
|
||||||
|
(isFavorite.hashCode) +
|
||||||
|
(isArchived == null ? 0 : isArchived!.hashCode) +
|
||||||
|
(isVisible == null ? 0 : isVisible!.hashCode) +
|
||||||
|
(duration == null ? 0 : duration!.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'ImportAssetDto[assetType=$assetType, isReadOnly=$isReadOnly, assetPath=$assetPath, sidecarPath=$sidecarPath, deviceAssetId=$deviceAssetId, deviceId=$deviceId, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, isFavorite=$isFavorite, isArchived=$isArchived, isVisible=$isVisible, duration=$duration]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'assetType'] = this.assetType;
|
||||||
|
json[r'isReadOnly'] = this.isReadOnly;
|
||||||
|
json[r'assetPath'] = this.assetPath;
|
||||||
|
if (this.sidecarPath != null) {
|
||||||
|
json[r'sidecarPath'] = this.sidecarPath;
|
||||||
|
} else {
|
||||||
|
// json[r'sidecarPath'] = null;
|
||||||
|
}
|
||||||
|
json[r'deviceAssetId'] = this.deviceAssetId;
|
||||||
|
json[r'deviceId'] = this.deviceId;
|
||||||
|
json[r'fileCreatedAt'] = this.fileCreatedAt.toUtc().toIso8601String();
|
||||||
|
json[r'fileModifiedAt'] = this.fileModifiedAt.toUtc().toIso8601String();
|
||||||
|
json[r'isFavorite'] = this.isFavorite;
|
||||||
|
if (this.isArchived != null) {
|
||||||
|
json[r'isArchived'] = this.isArchived;
|
||||||
|
} else {
|
||||||
|
// json[r'isArchived'] = null;
|
||||||
|
}
|
||||||
|
if (this.isVisible != null) {
|
||||||
|
json[r'isVisible'] = this.isVisible;
|
||||||
|
} else {
|
||||||
|
// json[r'isVisible'] = null;
|
||||||
|
}
|
||||||
|
if (this.duration != null) {
|
||||||
|
json[r'duration'] = this.duration;
|
||||||
|
} else {
|
||||||
|
// json[r'duration'] = null;
|
||||||
|
}
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [ImportAssetDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static ImportAssetDto? fromJson(dynamic value) {
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
// Ensure that the map contains the required keys.
|
||||||
|
// Note 1: the values aren't checked for validity beyond being non-null.
|
||||||
|
// Note 2: this code is stripped in release mode!
|
||||||
|
assert(() {
|
||||||
|
requiredKeys.forEach((key) {
|
||||||
|
assert(json.containsKey(key), 'Required key "ImportAssetDto[$key]" is missing from JSON.');
|
||||||
|
assert(json[key] != null, 'Required key "ImportAssetDto[$key]" has a null value in JSON.');
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}());
|
||||||
|
|
||||||
|
return ImportAssetDto(
|
||||||
|
assetType: AssetTypeEnum.fromJson(json[r'assetType'])!,
|
||||||
|
isReadOnly: mapValueOfType<bool>(json, r'isReadOnly') ?? true,
|
||||||
|
assetPath: mapValueOfType<String>(json, r'assetPath')!,
|
||||||
|
sidecarPath: mapValueOfType<String>(json, r'sidecarPath'),
|
||||||
|
deviceAssetId: mapValueOfType<String>(json, r'deviceAssetId')!,
|
||||||
|
deviceId: mapValueOfType<String>(json, r'deviceId')!,
|
||||||
|
fileCreatedAt: mapDateTime(json, r'fileCreatedAt', '')!,
|
||||||
|
fileModifiedAt: mapDateTime(json, r'fileModifiedAt', '')!,
|
||||||
|
isFavorite: mapValueOfType<bool>(json, r'isFavorite')!,
|
||||||
|
isArchived: mapValueOfType<bool>(json, r'isArchived'),
|
||||||
|
isVisible: mapValueOfType<bool>(json, r'isVisible'),
|
||||||
|
duration: mapValueOfType<String>(json, r'duration'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<ImportAssetDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <ImportAssetDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = ImportAssetDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, ImportAssetDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, ImportAssetDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = ImportAssetDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of ImportAssetDto-objects as value to a dart map
|
||||||
|
static Map<String, List<ImportAssetDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<ImportAssetDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = ImportAssetDto.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'assetType',
|
||||||
|
'assetPath',
|
||||||
|
'deviceAssetId',
|
||||||
|
'deviceId',
|
||||||
|
'fileCreatedAt',
|
||||||
|
'fileModifiedAt',
|
||||||
|
'isFavorite',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
19
mobile/openapi/lib/model/update_user_dto.dart
generated
19
mobile/openapi/lib/model/update_user_dto.dart
generated
|
@ -19,6 +19,7 @@ class UpdateUserDto {
|
||||||
this.firstName,
|
this.firstName,
|
||||||
this.lastName,
|
this.lastName,
|
||||||
this.storageLabel,
|
this.storageLabel,
|
||||||
|
this.externalPath,
|
||||||
this.isAdmin,
|
this.isAdmin,
|
||||||
this.shouldChangePassword,
|
this.shouldChangePassword,
|
||||||
});
|
});
|
||||||
|
@ -65,6 +66,14 @@ class UpdateUserDto {
|
||||||
///
|
///
|
||||||
String? storageLabel;
|
String? storageLabel;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// 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;
|
||||||
|
|
||||||
///
|
///
|
||||||
/// 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
|
||||||
|
@ -89,6 +98,7 @@ class UpdateUserDto {
|
||||||
other.firstName == firstName &&
|
other.firstName == firstName &&
|
||||||
other.lastName == lastName &&
|
other.lastName == lastName &&
|
||||||
other.storageLabel == storageLabel &&
|
other.storageLabel == storageLabel &&
|
||||||
|
other.externalPath == externalPath &&
|
||||||
other.isAdmin == isAdmin &&
|
other.isAdmin == isAdmin &&
|
||||||
other.shouldChangePassword == shouldChangePassword;
|
other.shouldChangePassword == shouldChangePassword;
|
||||||
|
|
||||||
|
@ -101,11 +111,12 @@ class UpdateUserDto {
|
||||||
(firstName == null ? 0 : firstName!.hashCode) +
|
(firstName == null ? 0 : firstName!.hashCode) +
|
||||||
(lastName == null ? 0 : lastName!.hashCode) +
|
(lastName == null ? 0 : lastName!.hashCode) +
|
||||||
(storageLabel == null ? 0 : storageLabel!.hashCode) +
|
(storageLabel == null ? 0 : storageLabel!.hashCode) +
|
||||||
|
(externalPath == null ? 0 : externalPath!.hashCode) +
|
||||||
(isAdmin == null ? 0 : isAdmin!.hashCode) +
|
(isAdmin == null ? 0 : isAdmin!.hashCode) +
|
||||||
(shouldChangePassword == null ? 0 : shouldChangePassword!.hashCode);
|
(shouldChangePassword == null ? 0 : shouldChangePassword!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'UpdateUserDto[id=$id, email=$email, password=$password, firstName=$firstName, lastName=$lastName, storageLabel=$storageLabel, isAdmin=$isAdmin, shouldChangePassword=$shouldChangePassword]';
|
String toString() => 'UpdateUserDto[id=$id, email=$email, password=$password, firstName=$firstName, lastName=$lastName, storageLabel=$storageLabel, externalPath=$externalPath, isAdmin=$isAdmin, shouldChangePassword=$shouldChangePassword]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
|
@ -135,6 +146,11 @@ class UpdateUserDto {
|
||||||
} else {
|
} else {
|
||||||
// json[r'storageLabel'] = null;
|
// json[r'storageLabel'] = null;
|
||||||
}
|
}
|
||||||
|
if (this.externalPath != null) {
|
||||||
|
json[r'externalPath'] = this.externalPath;
|
||||||
|
} else {
|
||||||
|
// json[r'externalPath'] = null;
|
||||||
|
}
|
||||||
if (this.isAdmin != null) {
|
if (this.isAdmin != null) {
|
||||||
json[r'isAdmin'] = this.isAdmin;
|
json[r'isAdmin'] = this.isAdmin;
|
||||||
} else {
|
} else {
|
||||||
|
@ -173,6 +189,7 @@ class UpdateUserDto {
|
||||||
firstName: mapValueOfType<String>(json, r'firstName'),
|
firstName: mapValueOfType<String>(json, r'firstName'),
|
||||||
lastName: mapValueOfType<String>(json, r'lastName'),
|
lastName: mapValueOfType<String>(json, r'lastName'),
|
||||||
storageLabel: mapValueOfType<String>(json, r'storageLabel'),
|
storageLabel: mapValueOfType<String>(json, r'storageLabel'),
|
||||||
|
externalPath: mapValueOfType<String>(json, r'externalPath'),
|
||||||
isAdmin: mapValueOfType<bool>(json, r'isAdmin'),
|
isAdmin: mapValueOfType<bool>(json, r'isAdmin'),
|
||||||
shouldChangePassword: mapValueOfType<bool>(json, r'shouldChangePassword'),
|
shouldChangePassword: mapValueOfType<bool>(json, r'shouldChangePassword'),
|
||||||
);
|
);
|
||||||
|
|
14
mobile/openapi/lib/model/user_response_dto.dart
generated
14
mobile/openapi/lib/model/user_response_dto.dart
generated
|
@ -18,6 +18,7 @@ class UserResponseDto {
|
||||||
required this.firstName,
|
required this.firstName,
|
||||||
required this.lastName,
|
required this.lastName,
|
||||||
required this.storageLabel,
|
required this.storageLabel,
|
||||||
|
required this.externalPath,
|
||||||
required this.profileImagePath,
|
required this.profileImagePath,
|
||||||
required this.shouldChangePassword,
|
required this.shouldChangePassword,
|
||||||
required this.isAdmin,
|
required this.isAdmin,
|
||||||
|
@ -37,6 +38,8 @@ class UserResponseDto {
|
||||||
|
|
||||||
String? storageLabel;
|
String? storageLabel;
|
||||||
|
|
||||||
|
String? externalPath;
|
||||||
|
|
||||||
String profileImagePath;
|
String profileImagePath;
|
||||||
|
|
||||||
bool shouldChangePassword;
|
bool shouldChangePassword;
|
||||||
|
@ -58,6 +61,7 @@ class UserResponseDto {
|
||||||
other.firstName == firstName &&
|
other.firstName == firstName &&
|
||||||
other.lastName == lastName &&
|
other.lastName == lastName &&
|
||||||
other.storageLabel == storageLabel &&
|
other.storageLabel == storageLabel &&
|
||||||
|
other.externalPath == externalPath &&
|
||||||
other.profileImagePath == profileImagePath &&
|
other.profileImagePath == profileImagePath &&
|
||||||
other.shouldChangePassword == shouldChangePassword &&
|
other.shouldChangePassword == shouldChangePassword &&
|
||||||
other.isAdmin == isAdmin &&
|
other.isAdmin == isAdmin &&
|
||||||
|
@ -74,6 +78,7 @@ class UserResponseDto {
|
||||||
(firstName.hashCode) +
|
(firstName.hashCode) +
|
||||||
(lastName.hashCode) +
|
(lastName.hashCode) +
|
||||||
(storageLabel == null ? 0 : storageLabel!.hashCode) +
|
(storageLabel == null ? 0 : storageLabel!.hashCode) +
|
||||||
|
(externalPath == null ? 0 : externalPath!.hashCode) +
|
||||||
(profileImagePath.hashCode) +
|
(profileImagePath.hashCode) +
|
||||||
(shouldChangePassword.hashCode) +
|
(shouldChangePassword.hashCode) +
|
||||||
(isAdmin.hashCode) +
|
(isAdmin.hashCode) +
|
||||||
|
@ -83,7 +88,7 @@ class UserResponseDto {
|
||||||
(oauthId.hashCode);
|
(oauthId.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'UserResponseDto[id=$id, email=$email, firstName=$firstName, lastName=$lastName, storageLabel=$storageLabel, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, isAdmin=$isAdmin, createdAt=$createdAt, deletedAt=$deletedAt, updatedAt=$updatedAt, oauthId=$oauthId]';
|
String toString() => 'UserResponseDto[id=$id, email=$email, firstName=$firstName, lastName=$lastName, storageLabel=$storageLabel, externalPath=$externalPath, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, isAdmin=$isAdmin, createdAt=$createdAt, deletedAt=$deletedAt, updatedAt=$updatedAt, oauthId=$oauthId]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
|
@ -95,6 +100,11 @@ class UserResponseDto {
|
||||||
json[r'storageLabel'] = this.storageLabel;
|
json[r'storageLabel'] = this.storageLabel;
|
||||||
} else {
|
} else {
|
||||||
// json[r'storageLabel'] = null;
|
// json[r'storageLabel'] = null;
|
||||||
|
}
|
||||||
|
if (this.externalPath != null) {
|
||||||
|
json[r'externalPath'] = this.externalPath;
|
||||||
|
} else {
|
||||||
|
// json[r'externalPath'] = null;
|
||||||
}
|
}
|
||||||
json[r'profileImagePath'] = this.profileImagePath;
|
json[r'profileImagePath'] = this.profileImagePath;
|
||||||
json[r'shouldChangePassword'] = this.shouldChangePassword;
|
json[r'shouldChangePassword'] = this.shouldChangePassword;
|
||||||
|
@ -134,6 +144,7 @@ class UserResponseDto {
|
||||||
firstName: mapValueOfType<String>(json, r'firstName')!,
|
firstName: mapValueOfType<String>(json, r'firstName')!,
|
||||||
lastName: mapValueOfType<String>(json, r'lastName')!,
|
lastName: mapValueOfType<String>(json, r'lastName')!,
|
||||||
storageLabel: mapValueOfType<String>(json, r'storageLabel'),
|
storageLabel: mapValueOfType<String>(json, r'storageLabel'),
|
||||||
|
externalPath: mapValueOfType<String>(json, r'externalPath'),
|
||||||
profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!,
|
profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!,
|
||||||
shouldChangePassword: mapValueOfType<bool>(json, r'shouldChangePassword')!,
|
shouldChangePassword: mapValueOfType<bool>(json, r'shouldChangePassword')!,
|
||||||
isAdmin: mapValueOfType<bool>(json, r'isAdmin')!,
|
isAdmin: mapValueOfType<bool>(json, r'isAdmin')!,
|
||||||
|
@ -193,6 +204,7 @@ class UserResponseDto {
|
||||||
'firstName',
|
'firstName',
|
||||||
'lastName',
|
'lastName',
|
||||||
'storageLabel',
|
'storageLabel',
|
||||||
|
'externalPath',
|
||||||
'profileImagePath',
|
'profileImagePath',
|
||||||
'shouldChangePassword',
|
'shouldChangePassword',
|
||||||
'isAdmin',
|
'isAdmin',
|
||||||
|
|
7
mobile/openapi/test/asset_api_test.dart
generated
7
mobile/openapi/test/asset_api_test.dart
generated
|
@ -131,6 +131,11 @@ void main() {
|
||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//Future<AssetFileUploadResponseDto> importFile(ImportAssetDto importAssetDto) async
|
||||||
|
test('test importFile', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
//Future<List<AssetResponseDto>> searchAsset(SearchAssetDto searchAssetDto) async
|
//Future<List<AssetResponseDto>> searchAsset(SearchAssetDto searchAssetDto) async
|
||||||
test('test searchAsset', () async {
|
test('test searchAsset', () async {
|
||||||
// TODO
|
// TODO
|
||||||
|
@ -148,7 +153,7 @@ void main() {
|
||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
//Future<AssetFileUploadResponseDto> uploadFile(AssetTypeEnum assetType, MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, bool isFavorite, String fileExtension, { String key, MultipartFile livePhotoData, MultipartFile sidecarData, bool isArchived, bool isVisible, String duration }) async
|
//Future<AssetFileUploadResponseDto> uploadFile(AssetTypeEnum assetType, MultipartFile assetData, String fileExtension, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, bool isFavorite, { String key, MultipartFile livePhotoData, MultipartFile sidecarData, bool isReadOnly, bool isArchived, bool isVisible, String duration }) async
|
||||||
test('test uploadFile', () async {
|
test('test uploadFile', () async {
|
||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
5
mobile/openapi/test/create_user_dto_test.dart
generated
5
mobile/openapi/test/create_user_dto_test.dart
generated
|
@ -41,6 +41,11 @@ void main() {
|
||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// String externalPath
|
||||||
|
test('to test the property `externalPath`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
82
mobile/openapi/test/import_asset_dto_test.dart
generated
Normal file
82
mobile/openapi/test/import_asset_dto_test.dart
generated
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.12
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
// tests for ImportAssetDto
|
||||||
|
void main() {
|
||||||
|
// final instance = ImportAssetDto();
|
||||||
|
|
||||||
|
group('test ImportAssetDto', () {
|
||||||
|
// AssetTypeEnum assetType
|
||||||
|
test('to test the property `assetType`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// bool isReadOnly (default value: true)
|
||||||
|
test('to test the property `isReadOnly`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// String assetPath
|
||||||
|
test('to test the property `assetPath`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// String sidecarPath
|
||||||
|
test('to test the property `sidecarPath`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// String deviceAssetId
|
||||||
|
test('to test the property `deviceAssetId`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// String deviceId
|
||||||
|
test('to test the property `deviceId`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// DateTime fileCreatedAt
|
||||||
|
test('to test the property `fileCreatedAt`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// DateTime fileModifiedAt
|
||||||
|
test('to test the property `fileModifiedAt`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// bool isFavorite
|
||||||
|
test('to test the property `isFavorite`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// bool isArchived
|
||||||
|
test('to test the property `isArchived`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// bool isVisible
|
||||||
|
test('to test the property `isVisible`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// String duration
|
||||||
|
test('to test the property `duration`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
5
mobile/openapi/test/update_user_dto_test.dart
generated
5
mobile/openapi/test/update_user_dto_test.dart
generated
|
@ -46,6 +46,11 @@ void main() {
|
||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// String externalPath
|
||||||
|
test('to test the property `externalPath`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
// bool isAdmin
|
// bool isAdmin
|
||||||
test('to test the property `isAdmin`', () async {
|
test('to test the property `isAdmin`', () async {
|
||||||
// TODO
|
// TODO
|
||||||
|
|
5
mobile/openapi/test/user_response_dto_test.dart
generated
5
mobile/openapi/test/user_response_dto_test.dart
generated
|
@ -41,6 +41,11 @@ void main() {
|
||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// String externalPath
|
||||||
|
test('to test the property `externalPath`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
// String profileImagePath
|
// String profileImagePath
|
||||||
test('to test the property `profileImagePath`', () async {
|
test('to test the property `profileImagePath`', () async {
|
||||||
// TODO
|
// TODO
|
||||||
|
|
|
@ -105,6 +105,7 @@ describe('User', () => {
|
||||||
updatedAt: expect.anything(),
|
updatedAt: expect.anything(),
|
||||||
oauthId: '',
|
oauthId: '',
|
||||||
storageLabel: null,
|
storageLabel: null,
|
||||||
|
externalPath: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
email: userTwoEmail,
|
email: userTwoEmail,
|
||||||
|
@ -119,6 +120,7 @@ describe('User', () => {
|
||||||
updatedAt: expect.anything(),
|
updatedAt: expect.anything(),
|
||||||
oauthId: '',
|
oauthId: '',
|
||||||
storageLabel: null,
|
storageLabel: null,
|
||||||
|
externalPath: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
email: authUserEmail,
|
email: authUserEmail,
|
||||||
|
@ -133,6 +135,7 @@ describe('User', () => {
|
||||||
updatedAt: expect.anything(),
|
updatedAt: expect.anything(),
|
||||||
oauthId: '',
|
oauthId: '',
|
||||||
storageLabel: 'admin',
|
storageLabel: 'admin',
|
||||||
|
externalPath: null,
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
|
|
@ -1430,6 +1430,48 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/asset/import": {
|
||||||
|
"post": {
|
||||||
|
"operationId": "importFile",
|
||||||
|
"parameters": [],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ImportAssetDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/AssetFileUploadResponseDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Asset"
|
||||||
|
],
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/asset/map-marker": {
|
"/asset/map-marker": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "getMapMarkers",
|
"operationId": "getMapMarkers",
|
||||||
|
@ -5085,6 +5127,13 @@
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "binary"
|
"format": "binary"
|
||||||
},
|
},
|
||||||
|
"isReadOnly": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"fileExtension": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"deviceAssetId": {
|
"deviceAssetId": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
@ -5108,9 +5157,6 @@
|
||||||
"isVisible": {
|
"isVisible": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"fileExtension": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"duration": {
|
"duration": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
@ -5118,12 +5164,12 @@
|
||||||
"required": [
|
"required": [
|
||||||
"assetType",
|
"assetType",
|
||||||
"assetData",
|
"assetData",
|
||||||
|
"fileExtension",
|
||||||
"deviceAssetId",
|
"deviceAssetId",
|
||||||
"deviceId",
|
"deviceId",
|
||||||
"fileCreatedAt",
|
"fileCreatedAt",
|
||||||
"fileModifiedAt",
|
"fileModifiedAt",
|
||||||
"isFavorite",
|
"isFavorite"
|
||||||
"fileExtension"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"CreateProfileImageDto": {
|
"CreateProfileImageDto": {
|
||||||
|
@ -5186,6 +5232,10 @@
|
||||||
"storageLabel": {
|
"storageLabel": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"nullable": true
|
"nullable": true
|
||||||
|
},
|
||||||
|
"externalPath": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
@ -5461,6 +5511,59 @@
|
||||||
"timeGroup"
|
"timeGroup"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"ImportAssetDto": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"assetType": {
|
||||||
|
"$ref": "#/components/schemas/AssetTypeEnum"
|
||||||
|
},
|
||||||
|
"isReadOnly": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"assetPath": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"sidecarPath": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"deviceAssetId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"deviceId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"fileCreatedAt": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"fileModifiedAt": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"isFavorite": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"isArchived": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"isVisible": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"duration": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"assetType",
|
||||||
|
"assetPath",
|
||||||
|
"deviceAssetId",
|
||||||
|
"deviceId",
|
||||||
|
"fileCreatedAt",
|
||||||
|
"fileModifiedAt",
|
||||||
|
"isFavorite"
|
||||||
|
]
|
||||||
|
},
|
||||||
"JobCommand": {
|
"JobCommand": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
|
@ -6592,6 +6695,9 @@
|
||||||
"storageLabel": {
|
"storageLabel": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"externalPath": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"isAdmin": {
|
"isAdmin": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
@ -6665,6 +6771,10 @@
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"nullable": true
|
"nullable": true
|
||||||
},
|
},
|
||||||
|
"externalPath": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
"profileImagePath": {
|
"profileImagePath": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
@ -6697,6 +6807,7 @@
|
||||||
"firstName",
|
"firstName",
|
||||||
"lastName",
|
"lastName",
|
||||||
"storageLabel",
|
"storageLabel",
|
||||||
|
"externalPath",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"shouldChangePassword",
|
"shouldChangePassword",
|
||||||
"isAdmin",
|
"isAdmin",
|
||||||
|
|
12
server/package-lock.json
generated
12
server/package-lock.json
generated
|
@ -21,6 +21,7 @@
|
||||||
"@nestjs/typeorm": "^9.0.1",
|
"@nestjs/typeorm": "^9.0.1",
|
||||||
"@nestjs/websockets": "^9.2.1",
|
"@nestjs/websockets": "^9.2.1",
|
||||||
"@socket.io/redis-adapter": "^8.0.1",
|
"@socket.io/redis-adapter": "^8.0.1",
|
||||||
|
"@types/mime-types": "^2.1.1",
|
||||||
"archiver": "^5.3.1",
|
"archiver": "^5.3.1",
|
||||||
"axios": "^0.26.0",
|
"axios": "^0.26.0",
|
||||||
"bcrypt": "^5.0.1",
|
"bcrypt": "^5.0.1",
|
||||||
|
@ -38,6 +39,7 @@
|
||||||
"local-reverse-geocoder": "0.12.5",
|
"local-reverse-geocoder": "0.12.5",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"luxon": "^3.0.3",
|
"luxon": "^3.0.3",
|
||||||
|
"mime-types": "^2.1.35",
|
||||||
"mv": "^2.1.1",
|
"mv": "^2.1.1",
|
||||||
"nest-commander": "^3.3.0",
|
"nest-commander": "^3.3.0",
|
||||||
"openid-client": "^5.2.1",
|
"openid-client": "^5.2.1",
|
||||||
|
@ -3018,6 +3020,11 @@
|
||||||
"integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==",
|
"integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/mime-types": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-vXOTGVSLR2jMw440moWTC7H19iUyLtP3Z1YTj7cSsubOICinjMxFeb/V57v9QdyyPGbbWolUFSSmSiRSn94tFw=="
|
||||||
|
},
|
||||||
"node_modules/@types/multer": {
|
"node_modules/@types/multer": {
|
||||||
"version": "1.4.7",
|
"version": "1.4.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.7.tgz",
|
||||||
|
@ -14296,6 +14303,11 @@
|
||||||
"integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==",
|
"integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@types/mime-types": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-vXOTGVSLR2jMw440moWTC7H19iUyLtP3Z1YTj7cSsubOICinjMxFeb/V57v9QdyyPGbbWolUFSSmSiRSn94tFw=="
|
||||||
|
},
|
||||||
"@types/multer": {
|
"@types/multer": {
|
||||||
"version": "1.4.7",
|
"version": "1.4.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.7.tgz",
|
||||||
|
|
|
@ -50,6 +50,7 @@
|
||||||
"@nestjs/typeorm": "^9.0.1",
|
"@nestjs/typeorm": "^9.0.1",
|
||||||
"@nestjs/websockets": "^9.2.1",
|
"@nestjs/websockets": "^9.2.1",
|
||||||
"@socket.io/redis-adapter": "^8.0.1",
|
"@socket.io/redis-adapter": "^8.0.1",
|
||||||
|
"@types/mime-types": "^2.1.1",
|
||||||
"archiver": "^5.3.1",
|
"archiver": "^5.3.1",
|
||||||
"axios": "^0.26.0",
|
"axios": "^0.26.0",
|
||||||
"bcrypt": "^5.0.1",
|
"bcrypt": "^5.0.1",
|
||||||
|
@ -67,6 +68,7 @@
|
||||||
"local-reverse-geocoder": "0.12.5",
|
"local-reverse-geocoder": "0.12.5",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"luxon": "^3.0.3",
|
"luxon": "^3.0.3",
|
||||||
|
"mime-types": "^2.1.35",
|
||||||
"mv": "^2.1.1",
|
"mv": "^2.1.1",
|
||||||
"nest-commander": "^3.3.0",
|
"nest-commander": "^3.3.0",
|
||||||
"openid-client": "^5.2.1",
|
"openid-client": "^5.2.1",
|
||||||
|
|
|
@ -169,6 +169,7 @@ describe(AlbumService.name, () => {
|
||||||
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,
|
||||||
},
|
},
|
||||||
ownerId: 'admin_id',
|
ownerId: 'admin_id',
|
||||||
shared: false,
|
shared: false,
|
||||||
|
|
|
@ -19,6 +19,7 @@ export class APIKeyCore {
|
||||||
isAdmin: user.isAdmin,
|
isAdmin: user.isAdmin,
|
||||||
isPublicUser: false,
|
isPublicUser: false,
|
||||||
isAllowUpload: true,
|
isAllowUpload: true,
|
||||||
|
externalPath: user.externalPath,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,4 +8,5 @@ export class AuthUserDto {
|
||||||
isAllowDownload?: boolean;
|
isAllowDownload?: boolean;
|
||||||
isShowExif?: boolean;
|
isShowExif?: boolean;
|
||||||
accessTokenId?: string;
|
accessTokenId?: string;
|
||||||
|
externalPath?: string | null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ export const ICryptoRepository = 'ICryptoRepository';
|
||||||
|
|
||||||
export interface ICryptoRepository {
|
export interface ICryptoRepository {
|
||||||
randomBytes(size: number): Buffer;
|
randomBytes(size: number): Buffer;
|
||||||
|
hashFile(filePath: string): Promise<Buffer>;
|
||||||
hashSha256(data: string): string;
|
hashSha256(data: string): string;
|
||||||
hashBcrypt(data: string | Buffer, saltOrRounds: string | number): Promise<string>;
|
hashBcrypt(data: string | Buffer, saltOrRounds: string | number): Promise<string>;
|
||||||
compareBcrypt(data: string | Buffer, encrypted: string): boolean;
|
compareBcrypt(data: string | Buffer, encrypted: string): boolean;
|
||||||
|
|
|
@ -27,3 +27,60 @@ export function assertMachineLearningEnabled() {
|
||||||
throw new BadRequestException('Machine learning is not enabled.');
|
throw new BadRequestException('Machine learning is not enabled.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const validMimeTypes = [
|
||||||
|
'image/avif',
|
||||||
|
'image/gif',
|
||||||
|
'image/heic',
|
||||||
|
'image/heif',
|
||||||
|
'image/jpeg',
|
||||||
|
'image/jxl',
|
||||||
|
'image/png',
|
||||||
|
'image/tiff',
|
||||||
|
'image/webp',
|
||||||
|
'image/x-adobe-dng',
|
||||||
|
'image/x-arriflex-ari',
|
||||||
|
'image/x-canon-cr2',
|
||||||
|
'image/x-canon-cr3',
|
||||||
|
'image/x-canon-crw',
|
||||||
|
'image/x-epson-erf',
|
||||||
|
'image/x-fuji-raf',
|
||||||
|
'image/x-hasselblad-3fr',
|
||||||
|
'image/x-hasselblad-fff',
|
||||||
|
'image/x-kodak-dcr',
|
||||||
|
'image/x-kodak-k25',
|
||||||
|
'image/x-kodak-kdc',
|
||||||
|
'image/x-leica-rwl',
|
||||||
|
'image/x-minolta-mrw',
|
||||||
|
'image/x-nikon-nef',
|
||||||
|
'image/x-olympus-orf',
|
||||||
|
'image/x-olympus-ori',
|
||||||
|
'image/x-panasonic-raw',
|
||||||
|
'image/x-pentax-pef',
|
||||||
|
'image/x-phantom-cin',
|
||||||
|
'image/x-phaseone-cap',
|
||||||
|
'image/x-phaseone-iiq',
|
||||||
|
'image/x-samsung-srw',
|
||||||
|
'image/x-sigma-x3f',
|
||||||
|
'image/x-sony-arw',
|
||||||
|
'image/x-sony-sr2',
|
||||||
|
'image/x-sony-srf',
|
||||||
|
'video/3gpp',
|
||||||
|
'video/mp2t',
|
||||||
|
'video/mp4',
|
||||||
|
'video/mpeg',
|
||||||
|
'video/quicktime',
|
||||||
|
'video/webm',
|
||||||
|
'video/x-flv',
|
||||||
|
'video/x-matroska',
|
||||||
|
'video/x-ms-wmv',
|
||||||
|
'video/x-msvideo',
|
||||||
|
];
|
||||||
|
|
||||||
|
export function isSupportedFileType(mimetype: string): boolean {
|
||||||
|
return validMimeTypes.includes(mimetype);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSidecarFileType(mimeType: string): boolean {
|
||||||
|
return ['application/xml', 'text/xml'].includes(mimeType);
|
||||||
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ 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,
|
||||||
},
|
},
|
||||||
user1: {
|
user1: {
|
||||||
email: 'immich@test.com',
|
email: 'immich@test.com',
|
||||||
|
@ -31,6 +32,7 @@ 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,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -194,5 +194,26 @@ describe(StorageTemplateService.name, () => {
|
||||||
['upload/library/user-id/2023/2023-02-23/asset-id.ext', '/original/path.ext'],
|
['upload/library/user-id/2023/2023-02-23/asset-id.ext', '/original/path.ext'],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not move read-only asset', async () => {
|
||||||
|
assetMock.getAll.mockResolvedValue({
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
...assetEntityStub.image,
|
||||||
|
originalPath: 'upload/library/user-id/2023/2023-02-23/asset-id+1.ext',
|
||||||
|
isReadOnly: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
hasNextPage: false,
|
||||||
|
});
|
||||||
|
assetMock.save.mockResolvedValue(assetEntityStub.image);
|
||||||
|
userMock.getList.mockResolvedValue([userEntityStub.user1]);
|
||||||
|
|
||||||
|
await sut.handleMigration();
|
||||||
|
|
||||||
|
expect(assetMock.getAll).toHaveBeenCalled();
|
||||||
|
expect(storageMock.moveFile).not.toHaveBeenCalled();
|
||||||
|
expect(assetMock.save).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -76,6 +76,11 @@ export class StorageTemplateService {
|
||||||
|
|
||||||
// TODO: use asset core (once in domain)
|
// TODO: use asset core (once in domain)
|
||||||
async moveAsset(asset: AssetEntity, metadata: MoveAssetMetadata) {
|
async moveAsset(asset: AssetEntity, metadata: MoveAssetMetadata) {
|
||||||
|
if (asset.isReadOnly) {
|
||||||
|
this.logger.verbose(`Not moving read-only asset: ${asset.originalPath}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const destination = await this.core.getTemplatePath(asset, metadata);
|
const destination = await this.core.getTemplatePath(asset, metadata);
|
||||||
if (asset.originalPath !== destination) {
|
if (asset.originalPath !== destination) {
|
||||||
const source = asset.originalPath;
|
const source = asset.originalPath;
|
||||||
|
|
|
@ -23,6 +23,10 @@ export class CreateUserDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
@Transform(toSanitized)
|
@Transform(toSanitized)
|
||||||
storageLabel?: string | null;
|
storageLabel?: string | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
externalPath?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CreateAdminDto {
|
export class CreateAdminDto {
|
||||||
|
|
|
@ -29,6 +29,10 @@ export class UpdateUserDto {
|
||||||
@Transform(toSanitized)
|
@Transform(toSanitized)
|
||||||
storageLabel?: string;
|
storageLabel?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
externalPath?: string;
|
||||||
|
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsUUID('4')
|
@IsUUID('4')
|
||||||
@ApiProperty({ format: 'uuid' })
|
@ApiProperty({ format: 'uuid' })
|
||||||
|
|
|
@ -6,6 +6,7 @@ export class UserResponseDto {
|
||||||
firstName!: string;
|
firstName!: string;
|
||||||
lastName!: string;
|
lastName!: string;
|
||||||
storageLabel!: string | null;
|
storageLabel!: string | null;
|
||||||
|
externalPath!: string | null;
|
||||||
profileImagePath!: string;
|
profileImagePath!: string;
|
||||||
shouldChangePassword!: boolean;
|
shouldChangePassword!: boolean;
|
||||||
isAdmin!: boolean;
|
isAdmin!: boolean;
|
||||||
|
@ -22,6 +23,7 @@ export function mapUser(entity: UserEntity): UserResponseDto {
|
||||||
firstName: entity.firstName,
|
firstName: entity.firstName,
|
||||||
lastName: entity.lastName,
|
lastName: entity.lastName,
|
||||||
storageLabel: entity.storageLabel,
|
storageLabel: entity.storageLabel,
|
||||||
|
externalPath: entity.externalPath,
|
||||||
profileImagePath: entity.profileImagePath,
|
profileImagePath: entity.profileImagePath,
|
||||||
shouldChangePassword: entity.shouldChangePassword,
|
shouldChangePassword: entity.shouldChangePassword,
|
||||||
isAdmin: entity.isAdmin,
|
isAdmin: entity.isAdmin,
|
||||||
|
|
|
@ -6,7 +6,6 @@ import {
|
||||||
Logger,
|
Logger,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { hash } from 'bcrypt';
|
|
||||||
import { constants, createReadStream, ReadStream } from 'fs';
|
import { constants, createReadStream, ReadStream } from 'fs';
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
import { AuthUserDto } from '../auth';
|
import { AuthUserDto } from '../auth';
|
||||||
|
@ -28,6 +27,7 @@ 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 && authUser.id !== id) {
|
} else if (dto.isAdmin && authUser.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');
|
||||||
|
@ -56,6 +56,10 @@ export class UserCore {
|
||||||
dto.storageLabel = null;
|
dto.storageLabel = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dto.externalPath === '') {
|
||||||
|
dto.externalPath = null;
|
||||||
|
}
|
||||||
|
|
||||||
return this.userRepository.update(id, dto);
|
return this.userRepository.update(id, dto);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logger.error(e, 'Failed to update user info');
|
Logger.error(e, 'Failed to update user info');
|
||||||
|
@ -79,7 +83,7 @@ export class UserCore {
|
||||||
try {
|
try {
|
||||||
const payload: Partial<UserEntity> = { ...createUserDto };
|
const payload: Partial<UserEntity> = { ...createUserDto };
|
||||||
if (payload.password) {
|
if (payload.password) {
|
||||||
payload.password = await hash(payload.password, SALT_ROUNDS);
|
payload.password = await this.cryptoRepository.hashBcrypt(payload.password, SALT_ROUNDS);
|
||||||
}
|
}
|
||||||
return this.userRepository.create(payload);
|
return this.userRepository.create(payload);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -53,6 +53,7 @@ const adminUser: UserEntity = Object.freeze({
|
||||||
tags: [],
|
tags: [],
|
||||||
assets: [],
|
assets: [],
|
||||||
storageLabel: 'admin',
|
storageLabel: 'admin',
|
||||||
|
externalPath: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const immichUser: UserEntity = Object.freeze({
|
const immichUser: UserEntity = Object.freeze({
|
||||||
|
@ -71,6 +72,7 @@ const immichUser: UserEntity = Object.freeze({
|
||||||
tags: [],
|
tags: [],
|
||||||
assets: [],
|
assets: [],
|
||||||
storageLabel: null,
|
storageLabel: null,
|
||||||
|
externalPath: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const updatedImmichUser: UserEntity = Object.freeze({
|
const updatedImmichUser: UserEntity = Object.freeze({
|
||||||
|
@ -89,6 +91,7 @@ const updatedImmichUser: UserEntity = Object.freeze({
|
||||||
tags: [],
|
tags: [],
|
||||||
assets: [],
|
assets: [],
|
||||||
storageLabel: null,
|
storageLabel: null,
|
||||||
|
externalPath: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const adminUserResponse = Object.freeze({
|
const adminUserResponse = Object.freeze({
|
||||||
|
@ -104,6 +107,7 @@ const adminUserResponse = Object.freeze({
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
updatedAt: new Date('2021-01-01'),
|
updatedAt: new Date('2021-01-01'),
|
||||||
storageLabel: 'admin',
|
storageLabel: 'admin',
|
||||||
|
externalPath: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(UserService.name, () => {
|
describe(UserService.name, () => {
|
||||||
|
@ -153,6 +157,7 @@ describe(UserService.name, () => {
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
updatedAt: new Date('2021-01-01'),
|
updatedAt: new Date('2021-01-01'),
|
||||||
storageLabel: 'admin',
|
storageLabel: 'admin',
|
||||||
|
externalPath: null,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
|
@ -32,6 +32,7 @@ describe('Album service', () => {
|
||||||
tags: [],
|
tags: [],
|
||||||
assets: [],
|
assets: [],
|
||||||
storageLabel: null,
|
storageLabel: null,
|
||||||
|
externalPath: null,
|
||||||
});
|
});
|
||||||
const albumId = 'f19ab956-4761-41ea-a5d6-bae948308d58';
|
const albumId = 'f19ab956-4761-41ea-a5d6-bae948308d58';
|
||||||
const sharedAlbumOwnerId = '2222';
|
const sharedAlbumOwnerId = '2222';
|
||||||
|
|
|
@ -20,6 +20,10 @@ export interface AssetCheck {
|
||||||
checksum: Buffer;
|
checksum: Buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AssetOwnerCheck extends AssetCheck {
|
||||||
|
ownerId: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IAssetRepository {
|
export interface IAssetRepository {
|
||||||
get(id: string): Promise<AssetEntity | null>;
|
get(id: string): Promise<AssetEntity | null>;
|
||||||
create(
|
create(
|
||||||
|
@ -39,6 +43,7 @@ export interface IAssetRepository {
|
||||||
getAssetByTimeBucket(userId: string, getAssetByTimeBucketDto: GetAssetByTimeBucketDto): Promise<AssetEntity[]>;
|
getAssetByTimeBucket(userId: string, getAssetByTimeBucketDto: GetAssetByTimeBucketDto): Promise<AssetEntity[]>;
|
||||||
getAssetsByChecksums(userId: string, checksums: Buffer[]): Promise<AssetCheck[]>;
|
getAssetsByChecksums(userId: string, checksums: Buffer[]): Promise<AssetCheck[]>;
|
||||||
getExistingAssets(userId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise<string[]>;
|
getExistingAssets(userId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise<string[]>;
|
||||||
|
getByOriginalPath(originalPath: string): Promise<AssetOwnerCheck | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IAssetRepository = 'IAssetRepository';
|
export const IAssetRepository = 'IAssetRepository';
|
||||||
|
@ -350,4 +355,17 @@ export class AssetRepository implements IAssetRepository {
|
||||||
|
|
||||||
return assetCountByUserId;
|
return assetCountByUserId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getByOriginalPath(originalPath: string): Promise<AssetOwnerCheck | null> {
|
||||||
|
return this.assetRepository.findOne({
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
ownerId: true,
|
||||||
|
checksum: true,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
originalPath,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@ import { AssetBulkUploadCheckDto } from './dto/asset-check.dto';
|
||||||
import { AssetSearchDto } from './dto/asset-search.dto';
|
import { AssetSearchDto } from './dto/asset-search.dto';
|
||||||
import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
|
import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
|
||||||
import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
|
import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
|
||||||
import { CreateAssetDto, mapToUploadFile } from './dto/create-asset.dto';
|
import { CreateAssetDto, ImportAssetDto, mapToUploadFile } from './dto/create-asset.dto';
|
||||||
import { DeleteAssetDto } from './dto/delete-asset.dto';
|
import { DeleteAssetDto } from './dto/delete-asset.dto';
|
||||||
import { DeviceIdDto } from './dto/device-id.dto';
|
import { DeviceIdDto } from './dto/device-id.dto';
|
||||||
import { DownloadFilesDto } from './dto/download-files.dto';
|
import { DownloadFilesDto } from './dto/download-files.dto';
|
||||||
|
@ -114,6 +114,20 @@ export class AssetController {
|
||||||
return responseDto;
|
return responseDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('import')
|
||||||
|
async importFile(
|
||||||
|
@AuthUser() authUser: AuthUserDto,
|
||||||
|
@Body(new ValidationPipe()) dto: ImportAssetDto,
|
||||||
|
@Response({ passthrough: true }) res: Res,
|
||||||
|
): Promise<AssetFileUploadResponseDto> {
|
||||||
|
const responseDto = await this.assetService.importFile(authUser, dto);
|
||||||
|
if (responseDto.duplicate) {
|
||||||
|
res.status(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseDto;
|
||||||
|
}
|
||||||
|
|
||||||
@SharedLinkRoute()
|
@SharedLinkRoute()
|
||||||
@Get('/download/:id')
|
@Get('/download/:id')
|
||||||
@ApiOkResponse({ content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } } })
|
@ApiOkResponse({ content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } } })
|
||||||
|
|
|
@ -2,17 +2,17 @@ import { AuthUserDto, IJobRepository, JobName } from '@app/domain';
|
||||||
import { AssetEntity, AssetType, UserEntity } from '@app/infra/entities';
|
import { AssetEntity, AssetType, UserEntity } from '@app/infra/entities';
|
||||||
import { parse } from 'node:path';
|
import { parse } from 'node:path';
|
||||||
import { IAssetRepository } from './asset-repository';
|
import { IAssetRepository } from './asset-repository';
|
||||||
import { CreateAssetDto, UploadFile } from './dto/create-asset.dto';
|
import { CreateAssetDto, ImportAssetDto, UploadFile } from './dto/create-asset.dto';
|
||||||
|
|
||||||
export class AssetCore {
|
export class AssetCore {
|
||||||
constructor(private repository: IAssetRepository, private jobRepository: IJobRepository) {}
|
constructor(private repository: IAssetRepository, private jobRepository: IJobRepository) {}
|
||||||
|
|
||||||
async create(
|
async create(
|
||||||
authUser: AuthUserDto,
|
authUser: AuthUserDto,
|
||||||
dto: CreateAssetDto,
|
dto: CreateAssetDto | ImportAssetDto,
|
||||||
file: UploadFile,
|
file: UploadFile,
|
||||||
livePhotoAssetId?: string,
|
livePhotoAssetId?: string,
|
||||||
sidecarFile?: UploadFile,
|
sidecarPath?: string,
|
||||||
): Promise<AssetEntity> {
|
): Promise<AssetEntity> {
|
||||||
const asset = await this.repository.create({
|
const asset = await this.repository.create({
|
||||||
owner: { id: authUser.id } as UserEntity,
|
owner: { id: authUser.id } as UserEntity,
|
||||||
|
@ -41,7 +41,8 @@ export class AssetCore {
|
||||||
sharedLinks: [],
|
sharedLinks: [],
|
||||||
originalFileName: parse(file.originalName).name,
|
originalFileName: parse(file.originalName).name,
|
||||||
faces: [],
|
faces: [],
|
||||||
sidecarPath: sidecarFile?.originalPath || null,
|
sidecarPath: sidecarPath || null,
|
||||||
|
isReadOnly: dto.isReadOnly ?? false,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: { id: asset.id, source: 'upload' } });
|
await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: { id: asset.id, source: 'upload' } });
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { IAccessRepository, IJobRepository, IStorageRepository, JobName } from '@app/domain';
|
import { IAccessRepository, ICryptoRepository, IJobRepository, IStorageRepository, JobName } from '@app/domain';
|
||||||
import { AssetEntity, AssetType, ExifEntity } from '@app/infra/entities';
|
import { AssetEntity, AssetType, ExifEntity } from '@app/infra/entities';
|
||||||
import { ForbiddenException } from '@nestjs/common';
|
import { ForbiddenException } from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
|
@ -6,6 +6,7 @@ import {
|
||||||
authStub,
|
authStub,
|
||||||
fileStub,
|
fileStub,
|
||||||
newAccessRepositoryMock,
|
newAccessRepositoryMock,
|
||||||
|
newCryptoRepositoryMock,
|
||||||
newJobRepositoryMock,
|
newJobRepositoryMock,
|
||||||
newStorageRepositoryMock,
|
newStorageRepositoryMock,
|
||||||
} from '@test';
|
} from '@test';
|
||||||
|
@ -121,6 +122,7 @@ describe('AssetService', () => {
|
||||||
let a: Repository<AssetEntity>; // TO BE DELETED AFTER FINISHED REFACTORING
|
let a: Repository<AssetEntity>; // TO BE DELETED AFTER FINISHED REFACTORING
|
||||||
let accessMock: jest.Mocked<IAccessRepository>;
|
let accessMock: jest.Mocked<IAccessRepository>;
|
||||||
let assetRepositoryMock: jest.Mocked<IAssetRepository>;
|
let assetRepositoryMock: jest.Mocked<IAssetRepository>;
|
||||||
|
let cryptoMock: jest.Mocked<ICryptoRepository>;
|
||||||
let downloadServiceMock: jest.Mocked<Partial<DownloadService>>;
|
let downloadServiceMock: jest.Mocked<Partial<DownloadService>>;
|
||||||
let jobMock: jest.Mocked<IJobRepository>;
|
let jobMock: jest.Mocked<IJobRepository>;
|
||||||
let storageMock: jest.Mocked<IStorageRepository>;
|
let storageMock: jest.Mocked<IStorageRepository>;
|
||||||
|
@ -144,13 +146,17 @@ describe('AssetService', () => {
|
||||||
getAssetCountByUserId: jest.fn(),
|
getAssetCountByUserId: jest.fn(),
|
||||||
getArchivedAssetCountByUserId: jest.fn(),
|
getArchivedAssetCountByUserId: jest.fn(),
|
||||||
getExistingAssets: jest.fn(),
|
getExistingAssets: jest.fn(),
|
||||||
|
getByOriginalPath: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
cryptoMock = newCryptoRepositoryMock();
|
||||||
|
|
||||||
downloadServiceMock = {
|
downloadServiceMock = {
|
||||||
downloadArchive: jest.fn(),
|
downloadArchive: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
accessMock = newAccessRepositoryMock();
|
accessMock = newAccessRepositoryMock();
|
||||||
|
cryptoMock = newCryptoRepositoryMock();
|
||||||
jobMock = newJobRepositoryMock();
|
jobMock = newJobRepositoryMock();
|
||||||
storageMock = newStorageRepositoryMock();
|
storageMock = newStorageRepositoryMock();
|
||||||
|
|
||||||
|
@ -158,6 +164,7 @@ describe('AssetService', () => {
|
||||||
accessMock,
|
accessMock,
|
||||||
assetRepositoryMock,
|
assetRepositoryMock,
|
||||||
a,
|
a,
|
||||||
|
cryptoMock,
|
||||||
downloadServiceMock as DownloadService,
|
downloadServiceMock as DownloadService,
|
||||||
jobMock,
|
jobMock,
|
||||||
storageMock,
|
storageMock,
|
||||||
|
@ -439,6 +446,43 @@ describe('AssetService', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('importFile', () => {
|
||||||
|
it('should handle a file import', async () => {
|
||||||
|
assetRepositoryMock.create.mockResolvedValue(assetEntityStub.image);
|
||||||
|
storageMock.checkFileExists.mockResolvedValue(true);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.importFile(authStub.external1, {
|
||||||
|
..._getCreateAssetDto(),
|
||||||
|
assetPath: '/data/user1/fake_path/asset_1.jpeg',
|
||||||
|
isReadOnly: true,
|
||||||
|
}),
|
||||||
|
).resolves.toEqual({ duplicate: false, id: 'asset-id' });
|
||||||
|
|
||||||
|
expect(assetRepositoryMock.create).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle a duplicate if originalPath already exists', async () => {
|
||||||
|
const error = new QueryFailedError('', [], '');
|
||||||
|
(error as any).constraint = 'UQ_userid_checksum';
|
||||||
|
|
||||||
|
assetRepositoryMock.create.mockRejectedValue(error);
|
||||||
|
assetRepositoryMock.getAssetsByChecksums.mockResolvedValue([assetEntityStub.image]);
|
||||||
|
storageMock.checkFileExists.mockResolvedValue(true);
|
||||||
|
cryptoMock.hashFile.mockResolvedValue(Buffer.from('file hash', 'utf8'));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.importFile(authStub.external1, {
|
||||||
|
..._getCreateAssetDto(),
|
||||||
|
assetPath: '/data/user1/fake_path/asset_1.jpeg',
|
||||||
|
isReadOnly: true,
|
||||||
|
}),
|
||||||
|
).resolves.toEqual({ duplicate: true, id: 'asset-id' });
|
||||||
|
|
||||||
|
expect(assetRepositoryMock.create).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('getAssetById', () => {
|
describe('getAssetById', () => {
|
||||||
it('should allow owner access', async () => {
|
it('should allow owner access', async () => {
|
||||||
accessMock.hasOwnerAssetAccess.mockResolvedValue(true);
|
accessMock.hasOwnerAssetAccess.mockResolvedValue(true);
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
import {
|
import {
|
||||||
AssetResponseDto,
|
AssetResponseDto,
|
||||||
|
AuthUserDto,
|
||||||
getLivePhotoMotionFilename,
|
getLivePhotoMotionFilename,
|
||||||
IAccessRepository,
|
IAccessRepository,
|
||||||
|
ICryptoRepository,
|
||||||
IJobRepository,
|
IJobRepository,
|
||||||
ImmichReadStream,
|
ImmichReadStream,
|
||||||
|
isSidecarFileType,
|
||||||
|
isSupportedFileType,
|
||||||
IStorageRepository,
|
IStorageRepository,
|
||||||
JobName,
|
JobName,
|
||||||
mapAsset,
|
mapAsset,
|
||||||
|
@ -21,12 +25,14 @@ import {
|
||||||
StreamableFile,
|
StreamableFile,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { R_OK, W_OK } from 'constants';
|
||||||
import { Response as Res } from 'express';
|
import { Response as Res } from 'express';
|
||||||
import { constants, createReadStream, stat } from 'fs';
|
import { createReadStream, stat } from 'fs';
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
|
import mime from 'mime-types';
|
||||||
|
import path from 'path';
|
||||||
import { QueryFailedError, Repository } from 'typeorm';
|
import { QueryFailedError, Repository } from 'typeorm';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
|
||||||
import { DownloadService } from '../../modules/download/download.service';
|
import { DownloadService } from '../../modules/download/download.service';
|
||||||
import { IAssetRepository } from './asset-repository';
|
import { IAssetRepository } from './asset-repository';
|
||||||
import { AssetCore } from './asset.core';
|
import { AssetCore } from './asset.core';
|
||||||
|
@ -34,7 +40,7 @@ import { AssetBulkUploadCheckDto } from './dto/asset-check.dto';
|
||||||
import { AssetSearchDto } from './dto/asset-search.dto';
|
import { AssetSearchDto } from './dto/asset-search.dto';
|
||||||
import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
|
import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
|
||||||
import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
|
import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
|
||||||
import { CreateAssetDto, UploadFile } from './dto/create-asset.dto';
|
import { CreateAssetDto, ImportAssetDto, UploadFile } from './dto/create-asset.dto';
|
||||||
import { DeleteAssetDto } from './dto/delete-asset.dto';
|
import { DeleteAssetDto } from './dto/delete-asset.dto';
|
||||||
import { DownloadFilesDto } from './dto/download-files.dto';
|
import { DownloadFilesDto } from './dto/download-files.dto';
|
||||||
import { DownloadDto } from './dto/download-library.dto';
|
import { DownloadDto } from './dto/download-library.dto';
|
||||||
|
@ -78,6 +84,7 @@ export class AssetService {
|
||||||
@Inject(IAccessRepository) private accessRepository: IAccessRepository,
|
@Inject(IAccessRepository) private accessRepository: IAccessRepository,
|
||||||
@Inject(IAssetRepository) private _assetRepository: IAssetRepository,
|
@Inject(IAssetRepository) private _assetRepository: IAssetRepository,
|
||||||
@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
|
@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
|
||||||
|
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||||
private downloadService: DownloadService,
|
private downloadService: DownloadService,
|
||||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||||
|
@ -107,7 +114,7 @@ export class AssetService {
|
||||||
livePhotoAsset = await this.assetCore.create(authUser, livePhotoDto, livePhotoFile);
|
livePhotoAsset = await this.assetCore.create(authUser, livePhotoDto, livePhotoFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
const asset = await this.assetCore.create(authUser, dto, file, livePhotoAsset?.id, sidecarFile);
|
const asset = await this.assetCore.create(authUser, dto, file, livePhotoAsset?.id, sidecarFile?.originalPath);
|
||||||
|
|
||||||
return { id: asset.id, duplicate: false };
|
return { id: asset.id, duplicate: false };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
@ -129,6 +136,73 @@ export class AssetService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async importFile(authUser: AuthUserDto, dto: ImportAssetDto): Promise<AssetFileUploadResponseDto> {
|
||||||
|
dto = {
|
||||||
|
...dto,
|
||||||
|
assetPath: path.resolve(dto.assetPath),
|
||||||
|
sidecarPath: dto.sidecarPath ? path.resolve(dto.sidecarPath) : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const assetPathType = mime.lookup(dto.assetPath) as string;
|
||||||
|
if (!isSupportedFileType(assetPathType)) {
|
||||||
|
throw new BadRequestException(`Unsupported file type ${assetPathType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dto.sidecarPath) {
|
||||||
|
const sidecarType = mime.lookup(dto.sidecarPath) as string;
|
||||||
|
if (!isSidecarFileType(sidecarType)) {
|
||||||
|
throw new BadRequestException(`Unsupported sidecar file type ${assetPathType}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const filepath of [dto.assetPath, dto.sidecarPath]) {
|
||||||
|
if (!filepath) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const exists = await this.storageRepository.checkFileExists(filepath, R_OK);
|
||||||
|
if (!exists) {
|
||||||
|
throw new BadRequestException('File does not exist');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!authUser.externalPath || !dto.assetPath.match(new RegExp(`^${authUser.externalPath}`))) {
|
||||||
|
throw new BadRequestException("File does not exist within user's external path");
|
||||||
|
}
|
||||||
|
|
||||||
|
const assetFile: UploadFile = {
|
||||||
|
checksum: await this.cryptoRepository.hashFile(dto.assetPath),
|
||||||
|
mimeType: assetPathType,
|
||||||
|
originalPath: dto.assetPath,
|
||||||
|
originalName: path.parse(dto.assetPath).name,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const asset = await this.assetCore.create(authUser, dto, assetFile, undefined, dto.sidecarPath);
|
||||||
|
return { id: asset.id, duplicate: false };
|
||||||
|
} catch (error: QueryFailedError | Error | any) {
|
||||||
|
// handle duplicates with a success response
|
||||||
|
if (error instanceof QueryFailedError && (error as any).constraint === 'UQ_userid_checksum') {
|
||||||
|
const [duplicate] = await this._assetRepository.getAssetsByChecksums(authUser.id, [assetFile.checksum]);
|
||||||
|
return { id: duplicate.id, duplicate: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof QueryFailedError && (error as any).constraint === 'UQ_4ed4f8052685ff5b1e7ca1058ba') {
|
||||||
|
const duplicate = await this._assetRepository.getByOriginalPath(dto.assetPath);
|
||||||
|
if (duplicate) {
|
||||||
|
if (duplicate.ownerId === authUser.id) {
|
||||||
|
return { id: duplicate.id, duplicate: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BadRequestException('Path in use by another user');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.error(`Error importing file ${error}`, error?.stack);
|
||||||
|
throw new BadRequestException(`Error importing file`, `${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async getUserAssetsByDeviceId(authUser: AuthUserDto, deviceId: string) {
|
public async getUserAssetsByDeviceId(authUser: AuthUserDto, deviceId: string) {
|
||||||
return this._assetRepository.getAllByDeviceId(authUser.id, deviceId);
|
return this._assetRepository.getAllByDeviceId(authUser.id, deviceId);
|
||||||
}
|
}
|
||||||
|
@ -291,7 +365,7 @@ export class AssetService {
|
||||||
let videoPath = asset.originalPath;
|
let videoPath = asset.originalPath;
|
||||||
let mimeType = asset.mimeType;
|
let mimeType = asset.mimeType;
|
||||||
|
|
||||||
await fs.access(videoPath, constants.R_OK | constants.W_OK);
|
await fs.access(videoPath, R_OK | W_OK);
|
||||||
|
|
||||||
if (asset.encodedVideoPath) {
|
if (asset.encodedVideoPath) {
|
||||||
videoPath = asset.encodedVideoPath == '' ? String(asset.originalPath) : String(asset.encodedVideoPath);
|
videoPath = asset.encodedVideoPath == '' ? String(asset.originalPath) : String(asset.encodedVideoPath);
|
||||||
|
@ -373,13 +447,16 @@ export class AssetService {
|
||||||
await this.jobRepository.queue({ name: JobName.SEARCH_REMOVE_ASSET, data: { ids: [id] } });
|
await this.jobRepository.queue({ name: JobName.SEARCH_REMOVE_ASSET, data: { ids: [id] } });
|
||||||
|
|
||||||
result.push({ id, status: DeleteAssetStatusEnum.SUCCESS });
|
result.push({ id, status: DeleteAssetStatusEnum.SUCCESS });
|
||||||
deleteQueue.push(
|
|
||||||
asset.originalPath,
|
if (!asset.isReadOnly) {
|
||||||
asset.webpPath,
|
deleteQueue.push(
|
||||||
asset.resizePath,
|
asset.originalPath,
|
||||||
asset.encodedVideoPath,
|
asset.webpPath,
|
||||||
asset.sidecarPath,
|
asset.resizePath,
|
||||||
);
|
asset.encodedVideoPath,
|
||||||
|
asset.sidecarPath,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// TODO refactor this to use cascades
|
// TODO refactor this to use cascades
|
||||||
if (asset.livePhotoVideoId && !ids.includes(asset.livePhotoVideoId)) {
|
if (asset.livePhotoVideoId && !ids.includes(asset.livePhotoVideoId)) {
|
||||||
|
@ -665,7 +742,7 @@ export class AssetService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await fs.access(filepath, constants.R_OK);
|
await fs.access(filepath, R_OK);
|
||||||
|
|
||||||
return new StreamableFile(createReadStream(filepath));
|
return new StreamableFile(createReadStream(filepath));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import { AssetType } from '@app/infra/entities';
|
import { AssetType } from '@app/infra/entities';
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsBoolean, IsEnum, IsNotEmpty, IsOptional } from 'class-validator';
|
import { Transform } from 'class-transformer';
|
||||||
|
import { IsBoolean, IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||||
import { ImmichFile } from '../../../config/asset-upload.config';
|
import { ImmichFile } from '../../../config/asset-upload.config';
|
||||||
|
import { toSanitized } from '../../../utils/transform.util';
|
||||||
|
|
||||||
export class CreateAssetDto {
|
export class CreateAssetBase {
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
deviceAssetId!: string;
|
deviceAssetId!: string;
|
||||||
|
|
||||||
|
@ -32,11 +34,17 @@ export class CreateAssetDto {
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
isVisible?: boolean;
|
isVisible?: boolean;
|
||||||
|
|
||||||
@IsNotEmpty()
|
|
||||||
fileExtension!: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
duration?: string;
|
duration?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CreateAssetDto extends CreateAssetBase {
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
isReadOnly?: boolean = false;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
fileExtension!: string;
|
||||||
|
|
||||||
// The properties below are added to correctly generate the API docs
|
// The properties below are added to correctly generate the API docs
|
||||||
// and client SDKs. Validation should be handled in the controller.
|
// and client SDKs. Validation should be handled in the controller.
|
||||||
|
@ -50,6 +58,23 @@ export class CreateAssetDto {
|
||||||
sidecarData?: any;
|
sidecarData?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class ImportAssetDto extends CreateAssetBase {
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
isReadOnly?: boolean = true;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@Transform(toSanitized)
|
||||||
|
assetPath!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@Transform(toSanitized)
|
||||||
|
sidecarPath?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface UploadFile {
|
export interface UploadFile {
|
||||||
mimeType: string;
|
mimeType: string;
|
||||||
checksum: Buffer;
|
checksum: Buffer;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { isSidecarFileType, isSupportedFileType } from '@app/domain';
|
||||||
import { StorageCore, StorageFolder } from '@app/domain/storage';
|
import { StorageCore, StorageFolder } from '@app/domain/storage';
|
||||||
import { BadRequestException, Logger, UnauthorizedException } from '@nestjs/common';
|
import { BadRequestException, Logger, UnauthorizedException } from '@nestjs/common';
|
||||||
import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface';
|
import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface';
|
||||||
|
@ -49,67 +50,18 @@ export const multerUtils = { fileFilter, filename, destination };
|
||||||
|
|
||||||
const logger = new Logger('AssetUploadConfig');
|
const logger = new Logger('AssetUploadConfig');
|
||||||
|
|
||||||
const validMimeTypes = [
|
|
||||||
'image/avif',
|
|
||||||
'image/gif',
|
|
||||||
'image/heic',
|
|
||||||
'image/heif',
|
|
||||||
'image/jpeg',
|
|
||||||
'image/jxl',
|
|
||||||
'image/png',
|
|
||||||
'image/tiff',
|
|
||||||
'image/webp',
|
|
||||||
'image/x-adobe-dng',
|
|
||||||
'image/x-arriflex-ari',
|
|
||||||
'image/x-canon-cr2',
|
|
||||||
'image/x-canon-cr3',
|
|
||||||
'image/x-canon-crw',
|
|
||||||
'image/x-epson-erf',
|
|
||||||
'image/x-fuji-raf',
|
|
||||||
'image/x-hasselblad-3fr',
|
|
||||||
'image/x-hasselblad-fff',
|
|
||||||
'image/x-kodak-dcr',
|
|
||||||
'image/x-kodak-k25',
|
|
||||||
'image/x-kodak-kdc',
|
|
||||||
'image/x-leica-rwl',
|
|
||||||
'image/x-minolta-mrw',
|
|
||||||
'image/x-nikon-nef',
|
|
||||||
'image/x-olympus-orf',
|
|
||||||
'image/x-olympus-ori',
|
|
||||||
'image/x-panasonic-raw',
|
|
||||||
'image/x-pentax-pef',
|
|
||||||
'image/x-phantom-cin',
|
|
||||||
'image/x-phaseone-cap',
|
|
||||||
'image/x-phaseone-iiq',
|
|
||||||
'image/x-samsung-srw',
|
|
||||||
'image/x-sigma-x3f',
|
|
||||||
'image/x-sony-arw',
|
|
||||||
'image/x-sony-sr2',
|
|
||||||
'image/x-sony-srf',
|
|
||||||
'video/3gpp',
|
|
||||||
'video/mp2t',
|
|
||||||
'video/mp4',
|
|
||||||
'video/mpeg',
|
|
||||||
'video/quicktime',
|
|
||||||
'video/webm',
|
|
||||||
'video/x-flv',
|
|
||||||
'video/x-matroska',
|
|
||||||
'video/x-ms-wmv',
|
|
||||||
'video/x-msvideo',
|
|
||||||
];
|
|
||||||
|
|
||||||
function fileFilter(req: AuthRequest, file: any, cb: any) {
|
function fileFilter(req: AuthRequest, file: any, cb: any) {
|
||||||
if (!req.user || (req.user.isPublicUser && !req.user.isAllowUpload)) {
|
if (!req.user || (req.user.isPublicUser && !req.user.isAllowUpload)) {
|
||||||
return cb(new UnauthorizedException());
|
return cb(new UnauthorizedException());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (validMimeTypes.includes(file.mimetype)) {
|
if (isSupportedFileType(file.mimetype)) {
|
||||||
cb(null, true);
|
cb(null, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Additionally support XML but only for sidecar files.
|
// Additionally support XML but only for sidecar files.
|
||||||
if (file.fieldname === 'sidecarData' && ['application/xml', 'text/xml'].includes(file.mimetype)) {
|
if (file.fieldname === 'sidecarData' && isSidecarFileType(file.mimetype)) {
|
||||||
return cb(null, true);
|
return cb(null, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,7 @@ export class AssetEntity {
|
||||||
@Column()
|
@Column()
|
||||||
type!: AssetType;
|
type!: AssetType;
|
||||||
|
|
||||||
@Column()
|
@Column({ unique: true })
|
||||||
originalPath!: string;
|
originalPath!: string;
|
||||||
|
|
||||||
@Column({ type: 'varchar', nullable: true })
|
@Column({ type: 'varchar', nullable: true })
|
||||||
|
@ -75,6 +75,9 @@ export class AssetEntity {
|
||||||
@Column({ type: 'boolean', default: false })
|
@Column({ type: 'boolean', default: false })
|
||||||
isArchived!: boolean;
|
isArchived!: boolean;
|
||||||
|
|
||||||
|
@Column({ type: 'boolean', default: false })
|
||||||
|
isReadOnly!: boolean;
|
||||||
|
|
||||||
@Column({ type: 'varchar', nullable: true })
|
@Column({ type: 'varchar', nullable: true })
|
||||||
mimeType!: string | null;
|
mimeType!: string | null;
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,9 @@ 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;
|
||||||
|
|
||||||
|
|
18
server/src/infra/migrations/1686584273471-ImportAsset.ts
Normal file
18
server/src/infra/migrations/1686584273471-ImportAsset.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class ImportAsset1686584273471 implements MigrationInterface {
|
||||||
|
name = 'ImportAsset1686584273471'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "assets" ADD "isReadOnly" boolean NOT NULL DEFAULT false`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "assets" ADD CONSTRAINT "UQ_4ed4f8052685ff5b1e7ca1058ba" UNIQUE ("originalPath")`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "users" ADD "externalPath" character varying`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "assets" DROP CONSTRAINT "UQ_4ed4f8052685ff5b1e7ca1058ba"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "isReadOnly"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "externalPath"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ import { ICryptoRepository } from '@app/domain';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { compareSync, hash } from 'bcrypt';
|
import { compareSync, hash } from 'bcrypt';
|
||||||
import { createHash, randomBytes } from 'crypto';
|
import { createHash, randomBytes } from 'crypto';
|
||||||
|
import { createReadStream } from 'fs';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CryptoRepository implements ICryptoRepository {
|
export class CryptoRepository implements ICryptoRepository {
|
||||||
|
@ -13,4 +14,14 @@ export class CryptoRepository implements ICryptoRepository {
|
||||||
hashSha256(value: string) {
|
hashSha256(value: string) {
|
||||||
return createHash('sha256').update(value).digest('base64');
|
return createHash('sha256').update(value).digest('base64');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hashFile(filepath: string): Promise<Buffer> {
|
||||||
|
return new Promise<Buffer>((resolve, reject) => {
|
||||||
|
const hash = createHash('sha1');
|
||||||
|
const stream = createReadStream(filepath);
|
||||||
|
stream.on('error', (err) => reject(err));
|
||||||
|
stream.on('data', (chunk) => hash.update(chunk));
|
||||||
|
stream.on('end', () => resolve(hash.digest()));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,6 +50,7 @@ export const authStub = {
|
||||||
isAdmin: true,
|
isAdmin: true,
|
||||||
isPublicUser: false,
|
isPublicUser: false,
|
||||||
isAllowUpload: true,
|
isAllowUpload: true,
|
||||||
|
externalPath: null,
|
||||||
}),
|
}),
|
||||||
user1: Object.freeze<AuthUserDto>({
|
user1: Object.freeze<AuthUserDto>({
|
||||||
id: 'user-id',
|
id: 'user-id',
|
||||||
|
@ -60,6 +61,7 @@ export const authStub = {
|
||||||
isAllowDownload: true,
|
isAllowDownload: true,
|
||||||
isShowExif: true,
|
isShowExif: true,
|
||||||
accessTokenId: 'token-id',
|
accessTokenId: 'token-id',
|
||||||
|
externalPath: null,
|
||||||
}),
|
}),
|
||||||
user2: Object.freeze<AuthUserDto>({
|
user2: Object.freeze<AuthUserDto>({
|
||||||
id: 'user-2',
|
id: 'user-2',
|
||||||
|
@ -70,6 +72,18 @@ export const authStub = {
|
||||||
isAllowDownload: true,
|
isAllowDownload: true,
|
||||||
isShowExif: true,
|
isShowExif: true,
|
||||||
accessTokenId: 'token-id',
|
accessTokenId: 'token-id',
|
||||||
|
externalPath: null,
|
||||||
|
}),
|
||||||
|
external1: Object.freeze<AuthUserDto>({
|
||||||
|
id: 'user-id',
|
||||||
|
email: 'immich@test.com',
|
||||||
|
isAdmin: false,
|
||||||
|
isPublicUser: false,
|
||||||
|
isAllowUpload: true,
|
||||||
|
isAllowDownload: true,
|
||||||
|
isShowExif: true,
|
||||||
|
accessTokenId: 'token-id',
|
||||||
|
externalPath: '/data/user1',
|
||||||
}),
|
}),
|
||||||
adminSharedLink: Object.freeze<AuthUserDto>({
|
adminSharedLink: Object.freeze<AuthUserDto>({
|
||||||
id: 'admin_id',
|
id: 'admin_id',
|
||||||
|
@ -111,6 +125,7 @@ export const userEntityStub = {
|
||||||
firstName: 'admin_first_name',
|
firstName: 'admin_first_name',
|
||||||
lastName: 'admin_last_name',
|
lastName: 'admin_last_name',
|
||||||
storageLabel: 'admin',
|
storageLabel: 'admin',
|
||||||
|
externalPath: null,
|
||||||
oauthId: '',
|
oauthId: '',
|
||||||
shouldChangePassword: false,
|
shouldChangePassword: false,
|
||||||
profileImagePath: '',
|
profileImagePath: '',
|
||||||
|
@ -126,6 +141,7 @@ export const userEntityStub = {
|
||||||
firstName: 'immich_first_name',
|
firstName: 'immich_first_name',
|
||||||
lastName: 'immich_last_name',
|
lastName: 'immich_last_name',
|
||||||
storageLabel: null,
|
storageLabel: null,
|
||||||
|
externalPath: null,
|
||||||
oauthId: '',
|
oauthId: '',
|
||||||
shouldChangePassword: false,
|
shouldChangePassword: false,
|
||||||
profileImagePath: '',
|
profileImagePath: '',
|
||||||
|
@ -141,6 +157,7 @@ export const userEntityStub = {
|
||||||
firstName: 'immich_first_name',
|
firstName: 'immich_first_name',
|
||||||
lastName: 'immich_last_name',
|
lastName: 'immich_last_name',
|
||||||
storageLabel: null,
|
storageLabel: null,
|
||||||
|
externalPath: null,
|
||||||
oauthId: '',
|
oauthId: '',
|
||||||
shouldChangePassword: false,
|
shouldChangePassword: false,
|
||||||
profileImagePath: '',
|
profileImagePath: '',
|
||||||
|
@ -156,6 +173,7 @@ export const userEntityStub = {
|
||||||
firstName: 'immich_first_name',
|
firstName: 'immich_first_name',
|
||||||
lastName: 'immich_last_name',
|
lastName: 'immich_last_name',
|
||||||
storageLabel: 'label-1',
|
storageLabel: 'label-1',
|
||||||
|
externalPath: null,
|
||||||
oauthId: '',
|
oauthId: '',
|
||||||
shouldChangePassword: false,
|
shouldChangePassword: false,
|
||||||
profileImagePath: '',
|
profileImagePath: '',
|
||||||
|
@ -212,6 +230,7 @@ export const assetEntityStub = {
|
||||||
sharedLinks: [],
|
sharedLinks: [],
|
||||||
faces: [],
|
faces: [],
|
||||||
sidecarPath: null,
|
sidecarPath: null,
|
||||||
|
isReadOnly: false,
|
||||||
}),
|
}),
|
||||||
noWebpPath: Object.freeze<AssetEntity>({
|
noWebpPath: Object.freeze<AssetEntity>({
|
||||||
id: 'asset-id',
|
id: 'asset-id',
|
||||||
|
@ -242,6 +261,7 @@ export const assetEntityStub = {
|
||||||
originalFileName: 'asset-id.ext',
|
originalFileName: 'asset-id.ext',
|
||||||
faces: [],
|
faces: [],
|
||||||
sidecarPath: null,
|
sidecarPath: null,
|
||||||
|
isReadOnly: false,
|
||||||
}),
|
}),
|
||||||
noThumbhash: Object.freeze<AssetEntity>({
|
noThumbhash: Object.freeze<AssetEntity>({
|
||||||
id: 'asset-id',
|
id: 'asset-id',
|
||||||
|
@ -263,6 +283,7 @@ export const assetEntityStub = {
|
||||||
mimeType: null,
|
mimeType: null,
|
||||||
isFavorite: true,
|
isFavorite: true,
|
||||||
isArchived: false,
|
isArchived: false,
|
||||||
|
isReadOnly: false,
|
||||||
duration: null,
|
duration: null,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
livePhotoVideo: null,
|
livePhotoVideo: null,
|
||||||
|
@ -293,6 +314,7 @@ export const assetEntityStub = {
|
||||||
mimeType: null,
|
mimeType: null,
|
||||||
isFavorite: true,
|
isFavorite: true,
|
||||||
isArchived: false,
|
isArchived: false,
|
||||||
|
isReadOnly: false,
|
||||||
duration: null,
|
duration: null,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
livePhotoVideo: null,
|
livePhotoVideo: null,
|
||||||
|
@ -324,6 +346,7 @@ export const assetEntityStub = {
|
||||||
mimeType: null,
|
mimeType: null,
|
||||||
isFavorite: true,
|
isFavorite: true,
|
||||||
isArchived: false,
|
isArchived: false,
|
||||||
|
isReadOnly: false,
|
||||||
duration: null,
|
duration: null,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
livePhotoVideo: null,
|
livePhotoVideo: null,
|
||||||
|
@ -375,6 +398,7 @@ export const assetEntityStub = {
|
||||||
mimeType: null,
|
mimeType: null,
|
||||||
isFavorite: false,
|
isFavorite: false,
|
||||||
isArchived: false,
|
isArchived: false,
|
||||||
|
isReadOnly: false,
|
||||||
duration: null,
|
duration: null,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
livePhotoVideo: null,
|
livePhotoVideo: null,
|
||||||
|
@ -408,6 +432,7 @@ export const assetEntityStub = {
|
||||||
mimeType: null,
|
mimeType: null,
|
||||||
isFavorite: true,
|
isFavorite: true,
|
||||||
isArchived: false,
|
isArchived: false,
|
||||||
|
isReadOnly: false,
|
||||||
duration: null,
|
duration: null,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
livePhotoVideo: null,
|
livePhotoVideo: null,
|
||||||
|
@ -865,6 +890,7 @@ export const sharedLinkStub = {
|
||||||
updatedAt: today,
|
updatedAt: today,
|
||||||
isFavorite: false,
|
isFavorite: false,
|
||||||
isArchived: false,
|
isArchived: false,
|
||||||
|
isReadOnly: false,
|
||||||
mimeType: 'image/jpeg',
|
mimeType: 'image/jpeg',
|
||||||
smartInfo: {
|
smartInfo: {
|
||||||
assetId: 'id_1',
|
assetId: 'id_1',
|
||||||
|
|
|
@ -6,5 +6,6 @@ export const newCryptoRepositoryMock = (): jest.Mocked<ICryptoRepository> => {
|
||||||
compareBcrypt: jest.fn().mockReturnValue(true),
|
compareBcrypt: jest.fn().mockReturnValue(true),
|
||||||
hashBcrypt: jest.fn().mockImplementation((input) => Promise.resolve(`${input} (hashed)`)),
|
hashBcrypt: jest.fn().mockImplementation((input) => Promise.resolve(`${input} (hashed)`)),
|
||||||
hashSha256: jest.fn().mockImplementation((input) => `${input} (hashed)`),
|
hashSha256: jest.fn().mockImplementation((input) => `${input} (hashed)`),
|
||||||
|
hashFile: jest.fn().mockImplementation((input) => `${input} (file-hashed)`),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
245
web/src/api/open-api/api.ts
generated
245
web/src/api/open-api/api.ts
generated
|
@ -979,6 +979,12 @@ export interface CreateUserDto {
|
||||||
* @memberof CreateUserDto
|
* @memberof CreateUserDto
|
||||||
*/
|
*/
|
||||||
'storageLabel'?: string | null;
|
'storageLabel'?: string | null;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof CreateUserDto
|
||||||
|
*/
|
||||||
|
'externalPath'?: string | null;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -1294,6 +1300,87 @@ export interface GetAssetCountByTimeBucketDto {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @interface ImportAssetDto
|
||||||
|
*/
|
||||||
|
export interface ImportAssetDto {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {AssetTypeEnum}
|
||||||
|
* @memberof ImportAssetDto
|
||||||
|
*/
|
||||||
|
'assetType': AssetTypeEnum;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof ImportAssetDto
|
||||||
|
*/
|
||||||
|
'isReadOnly'?: boolean;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof ImportAssetDto
|
||||||
|
*/
|
||||||
|
'assetPath': string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof ImportAssetDto
|
||||||
|
*/
|
||||||
|
'sidecarPath'?: string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof ImportAssetDto
|
||||||
|
*/
|
||||||
|
'deviceAssetId': string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof ImportAssetDto
|
||||||
|
*/
|
||||||
|
'deviceId': string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof ImportAssetDto
|
||||||
|
*/
|
||||||
|
'fileCreatedAt': string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof ImportAssetDto
|
||||||
|
*/
|
||||||
|
'fileModifiedAt': string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof ImportAssetDto
|
||||||
|
*/
|
||||||
|
'isFavorite': boolean;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof ImportAssetDto
|
||||||
|
*/
|
||||||
|
'isArchived'?: boolean;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof ImportAssetDto
|
||||||
|
*/
|
||||||
|
'isVisible'?: boolean;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof ImportAssetDto
|
||||||
|
*/
|
||||||
|
'duration'?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @export
|
* @export
|
||||||
|
@ -2736,6 +2823,12 @@ export interface UpdateUserDto {
|
||||||
* @memberof UpdateUserDto
|
* @memberof UpdateUserDto
|
||||||
*/
|
*/
|
||||||
'storageLabel'?: string;
|
'storageLabel'?: string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof UpdateUserDto
|
||||||
|
*/
|
||||||
|
'externalPath'?: string;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
|
@ -2841,6 +2934,12 @@ export interface UserResponseDto {
|
||||||
* @memberof UserResponseDto
|
* @memberof UserResponseDto
|
||||||
*/
|
*/
|
||||||
'storageLabel': string | null;
|
'storageLabel': string | null;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof UserResponseDto
|
||||||
|
*/
|
||||||
|
'externalPath': string | null;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
|
@ -5412,6 +5511,50 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
||||||
options: localVarRequestOptions,
|
options: localVarRequestOptions,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {ImportAssetDto} importAssetDto
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
importFile: async (importAssetDto: ImportAssetDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||||
|
// verify required parameter 'importAssetDto' is not null or undefined
|
||||||
|
assertParamExists('importFile', 'importAssetDto', importAssetDto)
|
||||||
|
const localVarPath = `/asset/import`;
|
||||||
|
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||||
|
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||||
|
let baseOptions;
|
||||||
|
if (configuration) {
|
||||||
|
baseOptions = configuration.baseOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
|
||||||
|
const localVarHeaderParameter = {} as any;
|
||||||
|
const localVarQueryParameter = {} as any;
|
||||||
|
|
||||||
|
// authentication cookie required
|
||||||
|
|
||||||
|
// authentication api_key required
|
||||||
|
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||||
|
|
||||||
|
// authentication bearer required
|
||||||
|
// http bearer authentication required
|
||||||
|
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||||
|
|
||||||
|
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||||
|
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||||
|
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||||
|
localVarRequestOptions.data = serializeDataIfNeeded(importAssetDto, localVarRequestOptions, configuration)
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: toPathString(localVarUrlObj),
|
||||||
|
options: localVarRequestOptions,
|
||||||
|
};
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {SearchAssetDto} searchAssetDto
|
* @param {SearchAssetDto} searchAssetDto
|
||||||
|
@ -5565,26 +5708,29 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
||||||
*
|
*
|
||||||
* @param {AssetTypeEnum} assetType
|
* @param {AssetTypeEnum} assetType
|
||||||
* @param {File} assetData
|
* @param {File} assetData
|
||||||
|
* @param {string} fileExtension
|
||||||
* @param {string} deviceAssetId
|
* @param {string} deviceAssetId
|
||||||
* @param {string} deviceId
|
* @param {string} deviceId
|
||||||
* @param {string} fileCreatedAt
|
* @param {string} fileCreatedAt
|
||||||
* @param {string} fileModifiedAt
|
* @param {string} fileModifiedAt
|
||||||
* @param {boolean} isFavorite
|
* @param {boolean} isFavorite
|
||||||
* @param {string} fileExtension
|
|
||||||
* @param {string} [key]
|
* @param {string} [key]
|
||||||
* @param {File} [livePhotoData]
|
* @param {File} [livePhotoData]
|
||||||
* @param {File} [sidecarData]
|
* @param {File} [sidecarData]
|
||||||
|
* @param {boolean} [isReadOnly]
|
||||||
* @param {boolean} [isArchived]
|
* @param {boolean} [isArchived]
|
||||||
* @param {boolean} [isVisible]
|
* @param {boolean} [isVisible]
|
||||||
* @param {string} [duration]
|
* @param {string} [duration]
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
uploadFile: async (assetType: AssetTypeEnum, assetData: File, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, fileExtension: string, key?: string, livePhotoData?: File, sidecarData?: File, isArchived?: boolean, isVisible?: boolean, duration?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
uploadFile: async (assetType: AssetTypeEnum, assetData: File, fileExtension: string, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, key?: string, livePhotoData?: File, sidecarData?: File, isReadOnly?: boolean, isArchived?: boolean, isVisible?: boolean, duration?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||||
// verify required parameter 'assetType' is not null or undefined
|
// verify required parameter 'assetType' is not null or undefined
|
||||||
assertParamExists('uploadFile', 'assetType', assetType)
|
assertParamExists('uploadFile', 'assetType', assetType)
|
||||||
// verify required parameter 'assetData' is not null or undefined
|
// verify required parameter 'assetData' is not null or undefined
|
||||||
assertParamExists('uploadFile', 'assetData', assetData)
|
assertParamExists('uploadFile', 'assetData', assetData)
|
||||||
|
// verify required parameter 'fileExtension' is not null or undefined
|
||||||
|
assertParamExists('uploadFile', 'fileExtension', fileExtension)
|
||||||
// verify required parameter 'deviceAssetId' is not null or undefined
|
// verify required parameter 'deviceAssetId' is not null or undefined
|
||||||
assertParamExists('uploadFile', 'deviceAssetId', deviceAssetId)
|
assertParamExists('uploadFile', 'deviceAssetId', deviceAssetId)
|
||||||
// verify required parameter 'deviceId' is not null or undefined
|
// verify required parameter 'deviceId' is not null or undefined
|
||||||
|
@ -5595,8 +5741,6 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
||||||
assertParamExists('uploadFile', 'fileModifiedAt', fileModifiedAt)
|
assertParamExists('uploadFile', 'fileModifiedAt', fileModifiedAt)
|
||||||
// verify required parameter 'isFavorite' is not null or undefined
|
// verify required parameter 'isFavorite' is not null or undefined
|
||||||
assertParamExists('uploadFile', 'isFavorite', isFavorite)
|
assertParamExists('uploadFile', 'isFavorite', isFavorite)
|
||||||
// verify required parameter 'fileExtension' is not null or undefined
|
|
||||||
assertParamExists('uploadFile', 'fileExtension', fileExtension)
|
|
||||||
const localVarPath = `/asset/upload`;
|
const localVarPath = `/asset/upload`;
|
||||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||||
|
@ -5640,6 +5784,14 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
||||||
localVarFormParams.append('sidecarData', sidecarData as any);
|
localVarFormParams.append('sidecarData', sidecarData as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isReadOnly !== undefined) {
|
||||||
|
localVarFormParams.append('isReadOnly', isReadOnly as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileExtension !== undefined) {
|
||||||
|
localVarFormParams.append('fileExtension', fileExtension as any);
|
||||||
|
}
|
||||||
|
|
||||||
if (deviceAssetId !== undefined) {
|
if (deviceAssetId !== undefined) {
|
||||||
localVarFormParams.append('deviceAssetId', deviceAssetId as any);
|
localVarFormParams.append('deviceAssetId', deviceAssetId as any);
|
||||||
}
|
}
|
||||||
|
@ -5668,10 +5820,6 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
||||||
localVarFormParams.append('isVisible', isVisible as any);
|
localVarFormParams.append('isVisible', isVisible as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fileExtension !== undefined) {
|
|
||||||
localVarFormParams.append('fileExtension', fileExtension as any);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (duration !== undefined) {
|
if (duration !== undefined) {
|
||||||
localVarFormParams.append('duration', duration as any);
|
localVarFormParams.append('duration', duration as any);
|
||||||
}
|
}
|
||||||
|
@ -5909,6 +6057,16 @@ export const AssetApiFp = function(configuration?: Configuration) {
|
||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getUserAssetsByDeviceId(deviceId, options);
|
const localVarAxiosArgs = await localVarAxiosParamCreator.getUserAssetsByDeviceId(deviceId, options);
|
||||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {ImportAssetDto} importAssetDto
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
async importFile(importAssetDto: ImportAssetDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AssetFileUploadResponseDto>> {
|
||||||
|
const localVarAxiosArgs = await localVarAxiosParamCreator.importFile(importAssetDto, options);
|
||||||
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {SearchAssetDto} searchAssetDto
|
* @param {SearchAssetDto} searchAssetDto
|
||||||
|
@ -5947,23 +6105,24 @@ export const AssetApiFp = function(configuration?: Configuration) {
|
||||||
*
|
*
|
||||||
* @param {AssetTypeEnum} assetType
|
* @param {AssetTypeEnum} assetType
|
||||||
* @param {File} assetData
|
* @param {File} assetData
|
||||||
|
* @param {string} fileExtension
|
||||||
* @param {string} deviceAssetId
|
* @param {string} deviceAssetId
|
||||||
* @param {string} deviceId
|
* @param {string} deviceId
|
||||||
* @param {string} fileCreatedAt
|
* @param {string} fileCreatedAt
|
||||||
* @param {string} fileModifiedAt
|
* @param {string} fileModifiedAt
|
||||||
* @param {boolean} isFavorite
|
* @param {boolean} isFavorite
|
||||||
* @param {string} fileExtension
|
|
||||||
* @param {string} [key]
|
* @param {string} [key]
|
||||||
* @param {File} [livePhotoData]
|
* @param {File} [livePhotoData]
|
||||||
* @param {File} [sidecarData]
|
* @param {File} [sidecarData]
|
||||||
|
* @param {boolean} [isReadOnly]
|
||||||
* @param {boolean} [isArchived]
|
* @param {boolean} [isArchived]
|
||||||
* @param {boolean} [isVisible]
|
* @param {boolean} [isVisible]
|
||||||
* @param {string} [duration]
|
* @param {string} [duration]
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
async uploadFile(assetType: AssetTypeEnum, assetData: File, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, fileExtension: string, key?: string, livePhotoData?: File, sidecarData?: File, isArchived?: boolean, isVisible?: boolean, duration?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AssetFileUploadResponseDto>> {
|
async uploadFile(assetType: AssetTypeEnum, assetData: File, fileExtension: string, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, key?: string, livePhotoData?: File, sidecarData?: File, isReadOnly?: boolean, isArchived?: boolean, isVisible?: boolean, duration?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AssetFileUploadResponseDto>> {
|
||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.uploadFile(assetType, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, fileExtension, key, livePhotoData, sidecarData, isArchived, isVisible, duration, options);
|
const localVarAxiosArgs = await localVarAxiosParamCreator.uploadFile(assetType, assetData, fileExtension, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, key, livePhotoData, sidecarData, isReadOnly, isArchived, isVisible, duration, options);
|
||||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -6166,6 +6325,15 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
|
||||||
getUserAssetsByDeviceId(deviceId: string, options?: any): AxiosPromise<Array<string>> {
|
getUserAssetsByDeviceId(deviceId: string, options?: any): AxiosPromise<Array<string>> {
|
||||||
return localVarFp.getUserAssetsByDeviceId(deviceId, options).then((request) => request(axios, basePath));
|
return localVarFp.getUserAssetsByDeviceId(deviceId, options).then((request) => request(axios, basePath));
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {ImportAssetDto} importAssetDto
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
importFile(importAssetDto: ImportAssetDto, options?: any): AxiosPromise<AssetFileUploadResponseDto> {
|
||||||
|
return localVarFp.importFile(importAssetDto, options).then((request) => request(axios, basePath));
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {SearchAssetDto} searchAssetDto
|
* @param {SearchAssetDto} searchAssetDto
|
||||||
|
@ -6201,23 +6369,24 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
|
||||||
*
|
*
|
||||||
* @param {AssetTypeEnum} assetType
|
* @param {AssetTypeEnum} assetType
|
||||||
* @param {File} assetData
|
* @param {File} assetData
|
||||||
|
* @param {string} fileExtension
|
||||||
* @param {string} deviceAssetId
|
* @param {string} deviceAssetId
|
||||||
* @param {string} deviceId
|
* @param {string} deviceId
|
||||||
* @param {string} fileCreatedAt
|
* @param {string} fileCreatedAt
|
||||||
* @param {string} fileModifiedAt
|
* @param {string} fileModifiedAt
|
||||||
* @param {boolean} isFavorite
|
* @param {boolean} isFavorite
|
||||||
* @param {string} fileExtension
|
|
||||||
* @param {string} [key]
|
* @param {string} [key]
|
||||||
* @param {File} [livePhotoData]
|
* @param {File} [livePhotoData]
|
||||||
* @param {File} [sidecarData]
|
* @param {File} [sidecarData]
|
||||||
|
* @param {boolean} [isReadOnly]
|
||||||
* @param {boolean} [isArchived]
|
* @param {boolean} [isArchived]
|
||||||
* @param {boolean} [isVisible]
|
* @param {boolean} [isVisible]
|
||||||
* @param {string} [duration]
|
* @param {string} [duration]
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
uploadFile(assetType: AssetTypeEnum, assetData: File, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, fileExtension: string, key?: string, livePhotoData?: File, sidecarData?: File, isArchived?: boolean, isVisible?: boolean, duration?: string, options?: any): AxiosPromise<AssetFileUploadResponseDto> {
|
uploadFile(assetType: AssetTypeEnum, assetData: File, fileExtension: string, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, key?: string, livePhotoData?: File, sidecarData?: File, isReadOnly?: boolean, isArchived?: boolean, isVisible?: boolean, duration?: string, options?: any): AxiosPromise<AssetFileUploadResponseDto> {
|
||||||
return localVarFp.uploadFile(assetType, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, fileExtension, key, livePhotoData, sidecarData, isArchived, isVisible, duration, options).then((request) => request(axios, basePath));
|
return localVarFp.uploadFile(assetType, assetData, fileExtension, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, key, livePhotoData, sidecarData, isReadOnly, isArchived, isVisible, duration, options).then((request) => request(axios, basePath));
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -6537,6 +6706,20 @@ export interface AssetApiGetUserAssetsByDeviceIdRequest {
|
||||||
readonly deviceId: string
|
readonly deviceId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request parameters for importFile operation in AssetApi.
|
||||||
|
* @export
|
||||||
|
* @interface AssetApiImportFileRequest
|
||||||
|
*/
|
||||||
|
export interface AssetApiImportFileRequest {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {ImportAssetDto}
|
||||||
|
* @memberof AssetApiImportFile
|
||||||
|
*/
|
||||||
|
readonly importAssetDto: ImportAssetDto
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request parameters for searchAsset operation in AssetApi.
|
* Request parameters for searchAsset operation in AssetApi.
|
||||||
* @export
|
* @export
|
||||||
|
@ -6627,6 +6810,13 @@ export interface AssetApiUploadFileRequest {
|
||||||
*/
|
*/
|
||||||
readonly assetData: File
|
readonly assetData: File
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof AssetApiUploadFile
|
||||||
|
*/
|
||||||
|
readonly fileExtension: string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
|
@ -6662,13 +6852,6 @@ export interface AssetApiUploadFileRequest {
|
||||||
*/
|
*/
|
||||||
readonly isFavorite: boolean
|
readonly isFavorite: boolean
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @type {string}
|
|
||||||
* @memberof AssetApiUploadFile
|
|
||||||
*/
|
|
||||||
readonly fileExtension: string
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
|
@ -6690,6 +6873,13 @@ export interface AssetApiUploadFileRequest {
|
||||||
*/
|
*/
|
||||||
readonly sidecarData?: File
|
readonly sidecarData?: File
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof AssetApiUploadFile
|
||||||
|
*/
|
||||||
|
readonly isReadOnly?: boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
|
@ -6934,6 +7124,17 @@ export class AssetApi extends BaseAPI {
|
||||||
return AssetApiFp(this.configuration).getUserAssetsByDeviceId(requestParameters.deviceId, options).then((request) => request(this.axios, this.basePath));
|
return AssetApiFp(this.configuration).getUserAssetsByDeviceId(requestParameters.deviceId, options).then((request) => request(this.axios, this.basePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {AssetApiImportFileRequest} requestParameters Request parameters.
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
* @memberof AssetApi
|
||||||
|
*/
|
||||||
|
public importFile(requestParameters: AssetApiImportFileRequest, options?: AxiosRequestConfig) {
|
||||||
|
return AssetApiFp(this.configuration).importFile(requestParameters.importAssetDto, options).then((request) => request(this.axios, this.basePath));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {AssetApiSearchAssetRequest} requestParameters Request parameters.
|
* @param {AssetApiSearchAssetRequest} requestParameters Request parameters.
|
||||||
|
@ -6975,7 +7176,7 @@ export class AssetApi extends BaseAPI {
|
||||||
* @memberof AssetApi
|
* @memberof AssetApi
|
||||||
*/
|
*/
|
||||||
public uploadFile(requestParameters: AssetApiUploadFileRequest, options?: AxiosRequestConfig) {
|
public uploadFile(requestParameters: AssetApiUploadFileRequest, options?: AxiosRequestConfig) {
|
||||||
return AssetApiFp(this.configuration).uploadFile(requestParameters.assetType, requestParameters.assetData, requestParameters.deviceAssetId, requestParameters.deviceId, requestParameters.fileCreatedAt, requestParameters.fileModifiedAt, requestParameters.isFavorite, requestParameters.fileExtension, requestParameters.key, requestParameters.livePhotoData, requestParameters.sidecarData, requestParameters.isArchived, requestParameters.isVisible, requestParameters.duration, options).then((request) => request(this.axios, this.basePath));
|
return AssetApiFp(this.configuration).uploadFile(requestParameters.assetType, requestParameters.assetData, requestParameters.fileExtension, requestParameters.deviceAssetId, requestParameters.deviceId, requestParameters.fileCreatedAt, requestParameters.fileModifiedAt, requestParameters.isFavorite, requestParameters.key, requestParameters.livePhotoData, requestParameters.sidecarData, requestParameters.isReadOnly, requestParameters.isArchived, requestParameters.isVisible, requestParameters.duration, options).then((request) => request(this.axios, this.basePath));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,14 +19,15 @@
|
||||||
|
|
||||||
const editUser = async () => {
|
const editUser = async () => {
|
||||||
try {
|
try {
|
||||||
const { id, email, firstName, lastName, storageLabel } = user;
|
const { id, email, firstName, lastName, storageLabel, externalPath } = user;
|
||||||
const { status } = await api.userApi.updateUser({
|
const { status } = await api.userApi.updateUser({
|
||||||
updateUserDto: {
|
updateUserDto: {
|
||||||
id,
|
id,
|
||||||
email,
|
email,
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
storageLabel: storageLabel || ''
|
storageLabel: storageLabel || '',
|
||||||
|
externalPath: externalPath || ''
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -131,6 +132,22 @@
|
||||||
</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="text-red-400 ml-4 text-sm">{error}</p>
|
<p class="text-red-400 ml-4 text-sm">{error}</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -75,6 +75,14 @@
|
||||||
required={false}
|
required={false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<SettingInputField
|
||||||
|
inputType={SettingInputFieldType.TEXT}
|
||||||
|
label="EXTERNAL PATH"
|
||||||
|
disabled={true}
|
||||||
|
value={user.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>
|
||||||
|
|
|
@ -5,6 +5,8 @@
|
||||||
import PencilOutline from 'svelte-material-icons/PencilOutline.svelte';
|
import PencilOutline from 'svelte-material-icons/PencilOutline.svelte';
|
||||||
import TrashCanOutline from 'svelte-material-icons/TrashCanOutline.svelte';
|
import TrashCanOutline from 'svelte-material-icons/TrashCanOutline.svelte';
|
||||||
import DeleteRestore from 'svelte-material-icons/DeleteRestore.svelte';
|
import DeleteRestore from 'svelte-material-icons/DeleteRestore.svelte';
|
||||||
|
import Check from 'svelte-material-icons/Check.svelte';
|
||||||
|
import Close from 'svelte-material-icons/Close.svelte';
|
||||||
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||||
import CreateUserForm from '$lib/components/forms/create-user-form.svelte';
|
import CreateUserForm from '$lib/components/forms/create-user-form.svelte';
|
||||||
import EditUserForm from '$lib/components/forms/edit-user-form.svelte';
|
import EditUserForm from '$lib/components/forms/edit-user-form.svelte';
|
||||||
|
@ -171,6 +173,7 @@
|
||||||
<th class="text-center w-1/4 font-medium text-sm">Email</th>
|
<th class="text-center w-1/4 font-medium text-sm">Email</th>
|
||||||
<th class="text-center w-1/4 font-medium text-sm">First name</th>
|
<th class="text-center w-1/4 font-medium text-sm">First name</th>
|
||||||
<th class="text-center w-1/4 font-medium text-sm">Last name</th>
|
<th class="text-center w-1/4 font-medium text-sm">Last name</th>
|
||||||
|
<th class="text-center w-1/4 font-medium text-sm">Can import</th>
|
||||||
<th class="text-center w-1/4 font-medium text-sm">Action</th>
|
<th class="text-center w-1/4 font-medium text-sm">Action</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -191,6 +194,15 @@
|
||||||
<td class="text-sm px-4 w-1/4 text-ellipsis">{user.email}</td>
|
<td class="text-sm px-4 w-1/4 text-ellipsis">{user.email}</td>
|
||||||
<td class="text-sm px-4 w-1/4 text-ellipsis">{user.firstName}</td>
|
<td class="text-sm px-4 w-1/4 text-ellipsis">{user.firstName}</td>
|
||||||
<td class="text-sm px-4 w-1/4 text-ellipsis">{user.lastName}</td>
|
<td class="text-sm px-4 w-1/4 text-ellipsis">{user.lastName}</td>
|
||||||
|
<td class="text-sm px-4 w-1/4 text-ellipsis">
|
||||||
|
<div class="container flex flex-wrap mx-auto justify-center">
|
||||||
|
{#if user.externalPath}
|
||||||
|
<Check size="16" />
|
||||||
|
{:else}
|
||||||
|
<Close size="16" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td class="text-sm px-4 w-1/4 text-ellipsis">
|
<td class="text-sm px-4 w-1/4 text-ellipsis">
|
||||||
{#if !isDeleted(user)}
|
{#if !isDeleted(user)}
|
||||||
<button
|
<button
|
||||||
|
|
|
@ -8,6 +8,7 @@ export const userFactory = Sync.makeFactory<UserResponseDto>({
|
||||||
firstName: Sync.each(() => faker.name.firstName()),
|
firstName: Sync.each(() => faker.name.firstName()),
|
||||||
lastName: Sync.each(() => faker.name.lastName()),
|
lastName: Sync.each(() => faker.name.lastName()),
|
||||||
storageLabel: Sync.each(() => faker.random.alphaNumeric()),
|
storageLabel: Sync.each(() => faker.random.alphaNumeric()),
|
||||||
|
externalPath: Sync.each(() => faker.random.alphaNumeric()),
|
||||||
profileImagePath: '',
|
profileImagePath: '',
|
||||||
shouldChangePassword: Sync.each(() => faker.datatype.boolean()),
|
shouldChangePassword: Sync.each(() => faker.datatype.boolean()),
|
||||||
isAdmin: true,
|
isAdmin: true,
|
||||||
|
|
Loading…
Add table
Reference in a new issue