diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index 094bcbe40a157..eafeb6851f1eb 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -17,6 +17,10 @@ doc/AlbumCountResponseDto.md doc/AlbumResponseDto.md doc/AllJobStatusResponseDto.md doc/AssetApi.md +doc/AssetBulkUploadCheckDto.md +doc/AssetBulkUploadCheckItem.md +doc/AssetBulkUploadCheckResponseDto.md +doc/AssetBulkUploadCheckResult.md doc/AssetCountByTimeBucket.md doc/AssetCountByTimeBucketResponseDto.md doc/AssetCountByUserIdResponseDto.md @@ -142,6 +146,10 @@ lib/model/api_key_create_dto.dart lib/model/api_key_create_response_dto.dart lib/model/api_key_response_dto.dart lib/model/api_key_update_dto.dart +lib/model/asset_bulk_upload_check_dto.dart +lib/model/asset_bulk_upload_check_item.dart +lib/model/asset_bulk_upload_check_response_dto.dart +lib/model/asset_bulk_upload_check_result.dart lib/model/asset_count_by_time_bucket.dart lib/model/asset_count_by_time_bucket_response_dto.dart lib/model/asset_count_by_user_id_response_dto.dart @@ -236,6 +244,10 @@ test/api_key_create_response_dto_test.dart test/api_key_response_dto_test.dart test/api_key_update_dto_test.dart test/asset_api_test.dart +test/asset_bulk_upload_check_dto_test.dart +test/asset_bulk_upload_check_item_test.dart +test/asset_bulk_upload_check_response_dto_test.dart +test/asset_bulk_upload_check_result_test.dart test/asset_count_by_time_bucket_response_dto_test.dart test/asset_count_by_time_bucket_test.dart test/asset_count_by_user_id_response_dto_test.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 2c317643255b0..b6b3ea18df60a 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -90,6 +90,7 @@ Class | Method | HTTP request | Description *AlbumApi* | [**removeUserFromAlbum**](doc//AlbumApi.md#removeuserfromalbum) | **DELETE** /album/{id}/user/{userId} | *AlbumApi* | [**updateAlbumInfo**](doc//AlbumApi.md#updatealbuminfo) | **PATCH** /album/{id} | *AssetApi* | [**addAssetsToSharedLink**](doc//AssetApi.md#addassetstosharedlink) | **PATCH** /asset/shared-link/add | +*AssetApi* | [**bulkUploadCheck**](doc//AssetApi.md#bulkuploadcheck) | **POST** /asset/bulk-upload-check | *AssetApi* | [**checkDuplicateAsset**](doc//AssetApi.md#checkduplicateasset) | **POST** /asset/check | *AssetApi* | [**checkExistingAssets**](doc//AssetApi.md#checkexistingassets) | **POST** /asset/exist | *AssetApi* | [**createAssetsSharedLink**](doc//AssetApi.md#createassetssharedlink) | **POST** /asset/shared-link | @@ -183,6 +184,10 @@ Class | Method | HTTP request | Description - [AlbumCountResponseDto](doc//AlbumCountResponseDto.md) - [AlbumResponseDto](doc//AlbumResponseDto.md) - [AllJobStatusResponseDto](doc//AllJobStatusResponseDto.md) + - [AssetBulkUploadCheckDto](doc//AssetBulkUploadCheckDto.md) + - [AssetBulkUploadCheckItem](doc//AssetBulkUploadCheckItem.md) + - [AssetBulkUploadCheckResponseDto](doc//AssetBulkUploadCheckResponseDto.md) + - [AssetBulkUploadCheckResult](doc//AssetBulkUploadCheckResult.md) - [AssetCountByTimeBucket](doc//AssetCountByTimeBucket.md) - [AssetCountByTimeBucketResponseDto](doc//AssetCountByTimeBucketResponseDto.md) - [AssetCountByUserIdResponseDto](doc//AssetCountByUserIdResponseDto.md) diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md index 2e5a4641fdd18..fbe6d02a70cda 100644 --- a/mobile/openapi/doc/AssetApi.md +++ b/mobile/openapi/doc/AssetApi.md @@ -10,6 +10,7 @@ All URIs are relative to */api* Method | HTTP request | Description ------------- | ------------- | ------------- [**addAssetsToSharedLink**](AssetApi.md#addassetstosharedlink) | **PATCH** /asset/shared-link/add | +[**bulkUploadCheck**](AssetApi.md#bulkuploadcheck) | **POST** /asset/bulk-upload-check | [**checkDuplicateAsset**](AssetApi.md#checkduplicateasset) | **POST** /asset/check | [**checkExistingAssets**](AssetApi.md#checkexistingassets) | **POST** /asset/exist | [**createAssetsSharedLink**](AssetApi.md#createassetssharedlink) | **POST** /asset/shared-link | @@ -93,6 +94,63 @@ 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) +# **bulkUploadCheck** +> AssetBulkUploadCheckResponseDto bulkUploadCheck(assetBulkUploadCheckDto) + + + +Checks if assets exist by checksums + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = AssetApi(); +final assetBulkUploadCheckDto = AssetBulkUploadCheckDto(); // AssetBulkUploadCheckDto | + +try { + final result = api_instance.bulkUploadCheck(assetBulkUploadCheckDto); + print(result); +} catch (e) { + print('Exception when calling AssetApi->bulkUploadCheck: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **assetBulkUploadCheckDto** | [**AssetBulkUploadCheckDto**](AssetBulkUploadCheckDto.md)| | + +### Return type + +[**AssetBulkUploadCheckResponseDto**](AssetBulkUploadCheckResponseDto.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) + # **checkDuplicateAsset** > CheckDuplicateAssetResponseDto checkDuplicateAsset(checkDuplicateAssetDto, key) diff --git a/mobile/openapi/doc/AssetBulkUploadCheckDto.md b/mobile/openapi/doc/AssetBulkUploadCheckDto.md new file mode 100644 index 0000000000000..e3d8419ec758a --- /dev/null +++ b/mobile/openapi/doc/AssetBulkUploadCheckDto.md @@ -0,0 +1,15 @@ +# openapi.model.AssetBulkUploadCheckDto + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**assets** | [**List**](AssetBulkUploadCheckItem.md) | | [default to const []] + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/doc/AssetBulkUploadCheckItem.md b/mobile/openapi/doc/AssetBulkUploadCheckItem.md new file mode 100644 index 0000000000000..d0cb9983207b8 --- /dev/null +++ b/mobile/openapi/doc/AssetBulkUploadCheckItem.md @@ -0,0 +1,16 @@ +# openapi.model.AssetBulkUploadCheckItem + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**id** | **String** | | +**checksum** | **String** | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/doc/AssetBulkUploadCheckResponseDto.md b/mobile/openapi/doc/AssetBulkUploadCheckResponseDto.md new file mode 100644 index 0000000000000..5cdea7d3b8c65 --- /dev/null +++ b/mobile/openapi/doc/AssetBulkUploadCheckResponseDto.md @@ -0,0 +1,15 @@ +# openapi.model.AssetBulkUploadCheckResponseDto + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**results** | [**List**](AssetBulkUploadCheckResult.md) | | [default to const []] + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/doc/AssetBulkUploadCheckResult.md b/mobile/openapi/doc/AssetBulkUploadCheckResult.md new file mode 100644 index 0000000000000..670d1d9fa4151 --- /dev/null +++ b/mobile/openapi/doc/AssetBulkUploadCheckResult.md @@ -0,0 +1,18 @@ +# openapi.model.AssetBulkUploadCheckResult + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**id** | **String** | | +**action** | **String** | | +**reason** | **String** | | [optional] +**assetId** | **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) + + diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index e22e9ac694e10..833628d62e01c 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -54,6 +54,10 @@ part 'model/admin_signup_response_dto.dart'; part 'model/album_count_response_dto.dart'; part 'model/album_response_dto.dart'; part 'model/all_job_status_response_dto.dart'; +part 'model/asset_bulk_upload_check_dto.dart'; +part 'model/asset_bulk_upload_check_item.dart'; +part 'model/asset_bulk_upload_check_response_dto.dart'; +part 'model/asset_bulk_upload_check_result.dart'; part 'model/asset_count_by_time_bucket.dart'; part 'model/asset_count_by_time_bucket_response_dto.dart'; part 'model/asset_count_by_user_id_response_dto.dart'; diff --git a/mobile/openapi/lib/api/asset_api.dart b/mobile/openapi/lib/api/asset_api.dart index e6cde800db0cc..44acdb29197e4 100644 --- a/mobile/openapi/lib/api/asset_api.dart +++ b/mobile/openapi/lib/api/asset_api.dart @@ -71,6 +71,58 @@ class AssetApi { return null; } + /// Checks if assets exist by checksums + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [AssetBulkUploadCheckDto] assetBulkUploadCheckDto (required): + Future bulkUploadCheckWithHttpInfo(AssetBulkUploadCheckDto assetBulkUploadCheckDto,) async { + // ignore: prefer_const_declarations + final path = r'/asset/bulk-upload-check'; + + // ignore: prefer_final_locals + Object? postBody = assetBulkUploadCheckDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Checks if assets exist by checksums + /// + /// Parameters: + /// + /// * [AssetBulkUploadCheckDto] assetBulkUploadCheckDto (required): + Future bulkUploadCheck(AssetBulkUploadCheckDto assetBulkUploadCheckDto,) async { + final response = await bulkUploadCheckWithHttpInfo(assetBulkUploadCheckDto,); + 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), 'AssetBulkUploadCheckResponseDto',) as AssetBulkUploadCheckResponseDto; + + } + return null; + } + /// Check duplicated asset before uploading - for Web upload used /// /// Note: This method returns the HTTP [Response]. diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index e159f6057899c..7b46ea15f1ec2 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -203,6 +203,14 @@ class ApiClient { return AlbumResponseDto.fromJson(value); case 'AllJobStatusResponseDto': return AllJobStatusResponseDto.fromJson(value); + case 'AssetBulkUploadCheckDto': + return AssetBulkUploadCheckDto.fromJson(value); + case 'AssetBulkUploadCheckItem': + return AssetBulkUploadCheckItem.fromJson(value); + case 'AssetBulkUploadCheckResponseDto': + return AssetBulkUploadCheckResponseDto.fromJson(value); + case 'AssetBulkUploadCheckResult': + return AssetBulkUploadCheckResult.fromJson(value); case 'AssetCountByTimeBucket': return AssetCountByTimeBucket.fromJson(value); case 'AssetCountByTimeBucketResponseDto': diff --git a/mobile/openapi/lib/model/asset_bulk_upload_check_dto.dart b/mobile/openapi/lib/model/asset_bulk_upload_check_dto.dart new file mode 100644 index 0000000000000..6d5fdf5142b9c --- /dev/null +++ b/mobile/openapi/lib/model/asset_bulk_upload_check_dto.dart @@ -0,0 +1,109 @@ +// +// 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 AssetBulkUploadCheckDto { + /// Returns a new [AssetBulkUploadCheckDto] instance. + AssetBulkUploadCheckDto({ + this.assets = const [], + }); + + List assets; + + @override + bool operator ==(Object other) => identical(this, other) || other is AssetBulkUploadCheckDto && + other.assets == assets; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (assets.hashCode); + + @override + String toString() => 'AssetBulkUploadCheckDto[assets=$assets]'; + + Map toJson() { + final json = {}; + json[r'assets'] = this.assets; + return json; + } + + /// Returns a new [AssetBulkUploadCheckDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static AssetBulkUploadCheckDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + // 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 "AssetBulkUploadCheckDto[$key]" is missing from JSON.'); + assert(json[key] != null, 'Required key "AssetBulkUploadCheckDto[$key]" has a null value in JSON.'); + }); + return true; + }()); + + return AssetBulkUploadCheckDto( + assets: AssetBulkUploadCheckItem.listFromJson(json[r'assets']), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AssetBulkUploadCheckDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = AssetBulkUploadCheckDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of AssetBulkUploadCheckDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = AssetBulkUploadCheckDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'assets', + }; +} + diff --git a/mobile/openapi/lib/model/asset_bulk_upload_check_item.dart b/mobile/openapi/lib/model/asset_bulk_upload_check_item.dart new file mode 100644 index 0000000000000..89ee3cf341e66 --- /dev/null +++ b/mobile/openapi/lib/model/asset_bulk_upload_check_item.dart @@ -0,0 +1,117 @@ +// +// 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 AssetBulkUploadCheckItem { + /// Returns a new [AssetBulkUploadCheckItem] instance. + AssetBulkUploadCheckItem({ + required this.id, + required this.checksum, + }); + + String id; + + String checksum; + + @override + bool operator ==(Object other) => identical(this, other) || other is AssetBulkUploadCheckItem && + other.id == id && + other.checksum == checksum; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (id.hashCode) + + (checksum.hashCode); + + @override + String toString() => 'AssetBulkUploadCheckItem[id=$id, checksum=$checksum]'; + + Map toJson() { + final json = {}; + json[r'id'] = this.id; + json[r'checksum'] = this.checksum; + return json; + } + + /// Returns a new [AssetBulkUploadCheckItem] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static AssetBulkUploadCheckItem? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + // 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 "AssetBulkUploadCheckItem[$key]" is missing from JSON.'); + assert(json[key] != null, 'Required key "AssetBulkUploadCheckItem[$key]" has a null value in JSON.'); + }); + return true; + }()); + + return AssetBulkUploadCheckItem( + id: mapValueOfType(json, r'id')!, + checksum: mapValueOfType(json, r'checksum')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AssetBulkUploadCheckItem.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = AssetBulkUploadCheckItem.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of AssetBulkUploadCheckItem-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = AssetBulkUploadCheckItem.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'id', + 'checksum', + }; +} + diff --git a/mobile/openapi/lib/model/asset_bulk_upload_check_response_dto.dart b/mobile/openapi/lib/model/asset_bulk_upload_check_response_dto.dart new file mode 100644 index 0000000000000..9a0ed965f40c8 --- /dev/null +++ b/mobile/openapi/lib/model/asset_bulk_upload_check_response_dto.dart @@ -0,0 +1,109 @@ +// +// 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 AssetBulkUploadCheckResponseDto { + /// Returns a new [AssetBulkUploadCheckResponseDto] instance. + AssetBulkUploadCheckResponseDto({ + this.results = const [], + }); + + List results; + + @override + bool operator ==(Object other) => identical(this, other) || other is AssetBulkUploadCheckResponseDto && + other.results == results; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (results.hashCode); + + @override + String toString() => 'AssetBulkUploadCheckResponseDto[results=$results]'; + + Map toJson() { + final json = {}; + json[r'results'] = this.results; + return json; + } + + /// Returns a new [AssetBulkUploadCheckResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static AssetBulkUploadCheckResponseDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + // 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 "AssetBulkUploadCheckResponseDto[$key]" is missing from JSON.'); + assert(json[key] != null, 'Required key "AssetBulkUploadCheckResponseDto[$key]" has a null value in JSON.'); + }); + return true; + }()); + + return AssetBulkUploadCheckResponseDto( + results: AssetBulkUploadCheckResult.listFromJson(json[r'results']), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AssetBulkUploadCheckResponseDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = AssetBulkUploadCheckResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of AssetBulkUploadCheckResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = AssetBulkUploadCheckResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'results', + }; +} + diff --git a/mobile/openapi/lib/model/asset_bulk_upload_check_result.dart b/mobile/openapi/lib/model/asset_bulk_upload_check_result.dart new file mode 100644 index 0000000000000..016342de0a293 --- /dev/null +++ b/mobile/openapi/lib/model/asset_bulk_upload_check_result.dart @@ -0,0 +1,293 @@ +// +// 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 AssetBulkUploadCheckResult { + /// Returns a new [AssetBulkUploadCheckResult] instance. + AssetBulkUploadCheckResult({ + required this.id, + required this.action, + this.reason, + this.assetId, + }); + + String id; + + AssetBulkUploadCheckResultActionEnum action; + + AssetBulkUploadCheckResultReasonEnum? reason; + + /// + /// 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? assetId; + + @override + bool operator ==(Object other) => identical(this, other) || other is AssetBulkUploadCheckResult && + other.id == id && + other.action == action && + other.reason == reason && + other.assetId == assetId; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (id.hashCode) + + (action.hashCode) + + (reason == null ? 0 : reason!.hashCode) + + (assetId == null ? 0 : assetId!.hashCode); + + @override + String toString() => 'AssetBulkUploadCheckResult[id=$id, action=$action, reason=$reason, assetId=$assetId]'; + + Map toJson() { + final json = {}; + json[r'id'] = this.id; + json[r'action'] = this.action; + if (this.reason != null) { + json[r'reason'] = this.reason; + } else { + // json[r'reason'] = null; + } + if (this.assetId != null) { + json[r'assetId'] = this.assetId; + } else { + // json[r'assetId'] = null; + } + return json; + } + + /// Returns a new [AssetBulkUploadCheckResult] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static AssetBulkUploadCheckResult? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + // 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 "AssetBulkUploadCheckResult[$key]" is missing from JSON.'); + assert(json[key] != null, 'Required key "AssetBulkUploadCheckResult[$key]" has a null value in JSON.'); + }); + return true; + }()); + + return AssetBulkUploadCheckResult( + id: mapValueOfType(json, r'id')!, + action: AssetBulkUploadCheckResultActionEnum.fromJson(json[r'action'])!, + reason: AssetBulkUploadCheckResultReasonEnum.fromJson(json[r'reason']), + assetId: mapValueOfType(json, r'assetId'), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AssetBulkUploadCheckResult.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = AssetBulkUploadCheckResult.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of AssetBulkUploadCheckResult-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = AssetBulkUploadCheckResult.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'id', + 'action', + }; +} + + +class AssetBulkUploadCheckResultActionEnum { + /// Instantiate a new enum with the provided [value]. + const AssetBulkUploadCheckResultActionEnum._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const accept = AssetBulkUploadCheckResultActionEnum._(r'accept'); + static const reject = AssetBulkUploadCheckResultActionEnum._(r'reject'); + + /// List of all possible values in this [enum][AssetBulkUploadCheckResultActionEnum]. + static const values = [ + accept, + reject, + ]; + + static AssetBulkUploadCheckResultActionEnum? fromJson(dynamic value) => AssetBulkUploadCheckResultActionEnumTypeTransformer().decode(value); + + static List? listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AssetBulkUploadCheckResultActionEnum.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [AssetBulkUploadCheckResultActionEnum] to String, +/// and [decode] dynamic data back to [AssetBulkUploadCheckResultActionEnum]. +class AssetBulkUploadCheckResultActionEnumTypeTransformer { + factory AssetBulkUploadCheckResultActionEnumTypeTransformer() => _instance ??= const AssetBulkUploadCheckResultActionEnumTypeTransformer._(); + + const AssetBulkUploadCheckResultActionEnumTypeTransformer._(); + + String encode(AssetBulkUploadCheckResultActionEnum data) => data.value; + + /// Decodes a [dynamic value][data] to a AssetBulkUploadCheckResultActionEnum. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + AssetBulkUploadCheckResultActionEnum? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'accept': return AssetBulkUploadCheckResultActionEnum.accept; + case r'reject': return AssetBulkUploadCheckResultActionEnum.reject; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [AssetBulkUploadCheckResultActionEnumTypeTransformer] instance. + static AssetBulkUploadCheckResultActionEnumTypeTransformer? _instance; +} + + + +class AssetBulkUploadCheckResultReasonEnum { + /// Instantiate a new enum with the provided [value]. + const AssetBulkUploadCheckResultReasonEnum._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const duplicate = AssetBulkUploadCheckResultReasonEnum._(r'duplicate'); + static const unsupportedFormat = AssetBulkUploadCheckResultReasonEnum._(r'unsupported-format'); + + /// List of all possible values in this [enum][AssetBulkUploadCheckResultReasonEnum]. + static const values = [ + duplicate, + unsupportedFormat, + ]; + + static AssetBulkUploadCheckResultReasonEnum? fromJson(dynamic value) => AssetBulkUploadCheckResultReasonEnumTypeTransformer().decode(value); + + static List? listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AssetBulkUploadCheckResultReasonEnum.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [AssetBulkUploadCheckResultReasonEnum] to String, +/// and [decode] dynamic data back to [AssetBulkUploadCheckResultReasonEnum]. +class AssetBulkUploadCheckResultReasonEnumTypeTransformer { + factory AssetBulkUploadCheckResultReasonEnumTypeTransformer() => _instance ??= const AssetBulkUploadCheckResultReasonEnumTypeTransformer._(); + + const AssetBulkUploadCheckResultReasonEnumTypeTransformer._(); + + String encode(AssetBulkUploadCheckResultReasonEnum data) => data.value; + + /// Decodes a [dynamic value][data] to a AssetBulkUploadCheckResultReasonEnum. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + AssetBulkUploadCheckResultReasonEnum? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'duplicate': return AssetBulkUploadCheckResultReasonEnum.duplicate; + case r'unsupported-format': return AssetBulkUploadCheckResultReasonEnum.unsupportedFormat; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [AssetBulkUploadCheckResultReasonEnumTypeTransformer] instance. + static AssetBulkUploadCheckResultReasonEnumTypeTransformer? _instance; +} + + diff --git a/mobile/openapi/test/asset_api_test.dart b/mobile/openapi/test/asset_api_test.dart index 085f1560f822c..af0bc44c25e1a 100644 --- a/mobile/openapi/test/asset_api_test.dart +++ b/mobile/openapi/test/asset_api_test.dart @@ -22,6 +22,13 @@ void main() { // TODO }); + // Checks if assets exist by checksums + // + //Future bulkUploadCheck(AssetBulkUploadCheckDto assetBulkUploadCheckDto) async + test('test bulkUploadCheck', () async { + // TODO + }); + // Check duplicated asset before uploading - for Web upload used // //Future checkDuplicateAsset(CheckDuplicateAssetDto checkDuplicateAssetDto, { String key }) async diff --git a/mobile/openapi/test/asset_bulk_upload_check_dto_test.dart b/mobile/openapi/test/asset_bulk_upload_check_dto_test.dart new file mode 100644 index 0000000000000..830cf2e29e551 --- /dev/null +++ b/mobile/openapi/test/asset_bulk_upload_check_dto_test.dart @@ -0,0 +1,27 @@ +// +// 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 AssetBulkUploadCheckDto +void main() { + // final instance = AssetBulkUploadCheckDto(); + + group('test AssetBulkUploadCheckDto', () { + // List assets (default value: const []) + test('to test the property `assets`', () async { + // TODO + }); + + + }); + +} diff --git a/mobile/openapi/test/asset_bulk_upload_check_item_test.dart b/mobile/openapi/test/asset_bulk_upload_check_item_test.dart new file mode 100644 index 0000000000000..688e5b1fcd8e2 --- /dev/null +++ b/mobile/openapi/test/asset_bulk_upload_check_item_test.dart @@ -0,0 +1,32 @@ +// +// 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 AssetBulkUploadCheckItem +void main() { + // final instance = AssetBulkUploadCheckItem(); + + group('test AssetBulkUploadCheckItem', () { + // String id + test('to test the property `id`', () async { + // TODO + }); + + // String checksum + test('to test the property `checksum`', () async { + // TODO + }); + + + }); + +} diff --git a/mobile/openapi/test/asset_bulk_upload_check_response_dto_test.dart b/mobile/openapi/test/asset_bulk_upload_check_response_dto_test.dart new file mode 100644 index 0000000000000..1af1fede086cb --- /dev/null +++ b/mobile/openapi/test/asset_bulk_upload_check_response_dto_test.dart @@ -0,0 +1,27 @@ +// +// 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 AssetBulkUploadCheckResponseDto +void main() { + // final instance = AssetBulkUploadCheckResponseDto(); + + group('test AssetBulkUploadCheckResponseDto', () { + // List results (default value: const []) + test('to test the property `results`', () async { + // TODO + }); + + + }); + +} diff --git a/mobile/openapi/test/asset_bulk_upload_check_result_test.dart b/mobile/openapi/test/asset_bulk_upload_check_result_test.dart new file mode 100644 index 0000000000000..dc1bf68a529e9 --- /dev/null +++ b/mobile/openapi/test/asset_bulk_upload_check_result_test.dart @@ -0,0 +1,42 @@ +// +// 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 AssetBulkUploadCheckResult +void main() { + // final instance = AssetBulkUploadCheckResult(); + + group('test AssetBulkUploadCheckResult', () { + // String id + test('to test the property `id`', () async { + // TODO + }); + + // String action + test('to test the property `action`', () async { + // TODO + }); + + // String reason + test('to test the property `reason`', () async { + // TODO + }); + + // String assetId + test('to test the property `assetId`', () async { + // TODO + }); + + + }); + +} diff --git a/server/apps/immich/src/api-v1/asset/asset-repository.ts b/server/apps/immich/src/api-v1/asset/asset-repository.ts index a5500a020c84f..cf033c783a257 100644 --- a/server/apps/immich/src/api-v1/asset/asset-repository.ts +++ b/server/apps/immich/src/api-v1/asset/asset-repository.ts @@ -10,13 +10,17 @@ import { TimeGroupEnum } from './dto/get-asset-count-by-time-bucket.dto'; import { GetAssetByTimeBucketDto } from './dto/get-asset-by-time-bucket.dto'; import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto'; import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto'; -import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-assets-response.dto'; import { In } from 'typeorm/find-options/operator/In'; import { UpdateAssetDto } from './dto/update-asset.dto'; import { ITagRepository } from '../tag/tag.repository'; import { IsNull, Not } from 'typeorm'; import { AssetSearchDto } from './dto/asset-search.dto'; +export interface AssetCheck { + id: string; + checksum: Buffer; +} + export interface IAssetRepository { get(id: string): Promise; create( @@ -38,11 +42,8 @@ export interface IAssetRepository { getAssetCountByUserId(userId: string): Promise; getArchivedAssetCountByUserId(userId: string): Promise; getAssetByTimeBucket(userId: string, getAssetByTimeBucketDto: GetAssetByTimeBucketDto): Promise; - getAssetByChecksum(userId: string, checksum: Buffer): Promise; - getExistingAssets( - userId: string, - checkDuplicateAssetDto: CheckExistingAssetsDto, - ): Promise; + getAssetsByChecksums(userId: string, checksums: Buffer[]): Promise; + getExistingAssets(userId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise; countByIdAndUser(assetId: string, userId: string): Promise; } @@ -310,41 +311,39 @@ export class AssetRepository implements IAssetRepository { * @returns Promise - Array of assetIds belong to the device */ async getAllByDeviceId(ownerId: string, deviceId: string): Promise { - const rows = await this.assetRepository.find({ + const items = await this.assetRepository.find({ + select: { deviceAssetId: true }, where: { ownerId, deviceId, isVisible: true, }, - select: ['deviceAssetId'], }); - const res: string[] = []; - rows.forEach((v) => res.push(v.deviceAssetId)); - return res; + return items.map((asset) => asset.deviceAssetId); } /** - * Get asset by checksum on the database + * Get assets by checksums on the database * @param ownerId - * @param checksum + * @param checksums * */ - getAssetByChecksum(ownerId: string, checksum: Buffer): Promise { - return this.assetRepository.findOneOrFail({ + async getAssetsByChecksums(ownerId: string, checksums: Buffer[]): Promise { + return this.assetRepository.find({ + select: { + id: true, + checksum: true, + }, where: { ownerId, - checksum, + checksum: In(checksums), }, - relations: ['exifInfo'], }); } - async getExistingAssets( - ownerId: string, - checkDuplicateAssetDto: CheckExistingAssetsDto, - ): Promise { - const existingAssets = await this.assetRepository.find({ + async getExistingAssets(ownerId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise { + const assets = await this.assetRepository.find({ select: { deviceAssetId: true }, where: { deviceAssetId: In(checkDuplicateAssetDto.deviceAssetIds), @@ -352,7 +351,7 @@ export class AssetRepository implements IAssetRepository { ownerId, }, }); - return new CheckExistingAssetsResponseDto(existingAssets.map((a) => a.deviceAssetId)); + return assets.map((asset) => asset.deviceAssetId); } async countByIdAndUser(assetId: string, ownerId: string): Promise { diff --git a/server/apps/immich/src/api-v1/asset/asset.controller.ts b/server/apps/immich/src/api-v1/asset/asset.controller.ts index 6088b8122217a..774a72ea9be76 100644 --- a/server/apps/immich/src/api-v1/asset/asset.controller.ts +++ b/server/apps/immich/src/api-v1/asset/asset.controller.ts @@ -57,6 +57,8 @@ import { AssetSearchDto } from './dto/asset-search.dto'; import { assetUploadOption, ImmichFile } from '../../config/asset-upload.config'; import FileNotEmptyValidator from '../validation/file-not-empty-validator'; import { RemoveAssetsDto } from '../album/dto/remove-assets.dto'; +import { AssetBulkUploadCheckDto } from './dto/asset-check.dto'; +import { AssetBulkUploadCheckResponseDto } from './response-dto/asset-check-response.dto'; import { AssetIdDto } from './dto/asset-id.dto'; import { DeviceIdDto } from './dto/device-id.dto'; @@ -332,6 +334,19 @@ export class AssetController { return await this.assetService.checkExistingAssets(authUser, checkExistingAssetsDto); } + /** + * Checks if assets exist by checksums + */ + @Authenticated() + @Post('/bulk-upload-check') + @HttpCode(200) + bulkUploadCheck( + @GetAuthUser() authUser: AuthUserDto, + @Body(ValidationPipe) dto: AssetBulkUploadCheckDto, + ): Promise { + return this.assetService.bulkUploadCheck(authUser, dto); + } + @Authenticated() @Post('/shared-link') async createAssetsSharedLink( diff --git a/server/apps/immich/src/api-v1/asset/asset.core.ts b/server/apps/immich/src/api-v1/asset/asset.core.ts index 8d3992d389cff..34e014fc724b1 100644 --- a/server/apps/immich/src/api-v1/asset/asset.core.ts +++ b/server/apps/immich/src/api-v1/asset/asset.core.ts @@ -17,7 +17,7 @@ export class AssetCore { owner: { id: authUser.id } as UserEntity, mimeType: file.mimeType, - checksum: file.checksum || null, + checksum: file.checksum, originalPath: file.originalPath, deviceAssetId: dto.deviceAssetId, diff --git a/server/apps/immich/src/api-v1/asset/asset.service.spec.ts b/server/apps/immich/src/api-v1/asset/asset.service.spec.ts index 80d5cc91da94c..cacacc606a0c4 100644 --- a/server/apps/immich/src/api-v1/asset/asset.service.spec.ts +++ b/server/apps/immich/src/api-v1/asset/asset.service.spec.ts @@ -157,7 +157,7 @@ describe('AssetService', () => { getLocationsByUserId: jest.fn(), getSearchPropertiesByUserId: jest.fn(), getAssetByTimeBucket: jest.fn(), - getAssetByChecksum: jest.fn(), + getAssetsByChecksums: jest.fn(), getAssetCountByUserId: jest.fn(), getArchivedAssetCountByUserId: jest.fn(), getExistingAssets: jest.fn(), @@ -299,7 +299,7 @@ describe('AssetService', () => { (error as any).constraint = 'UQ_userid_checksum'; assetRepositoryMock.create.mockRejectedValue(error); - assetRepositoryMock.getAssetByChecksum.mockResolvedValue(_getAsset_1()); + assetRepositoryMock.getAssetsByChecksums.mockResolvedValue([_getAsset_1()]); await expect(sut.uploadFile(authStub.user1, dto, file)).resolves.toEqual({ duplicate: true, id: 'id_1' }); diff --git a/server/apps/immich/src/api-v1/asset/asset.service.ts b/server/apps/immich/src/api-v1/asset/asset.service.ts index e130b6b07b7d9..145cd717dea85 100644 --- a/server/apps/immich/src/api-v1/asset/asset.service.ts +++ b/server/apps/immich/src/api-v1/asset/asset.service.ts @@ -63,6 +63,12 @@ import { mapSharedLink, SharedLinkResponseDto } from '@app/domain'; import { AssetSearchDto } from './dto/asset-search.dto'; import { AddAssetsDto } from '../album/dto/add-assets.dto'; import { RemoveAssetsDto } from '../album/dto/remove-assets.dto'; +import { AssetBulkUploadCheckDto } from './dto/asset-check.dto'; +import { + AssetUploadAction, + AssetRejectReason, + AssetBulkUploadCheckResponseDto, +} from './response-dto/asset-check-response.dto'; const fileInfo = promisify(stat); @@ -128,7 +134,8 @@ export class AssetService { // handle duplicates with a success response if (error instanceof QueryFailedError && (error as any).constraint === 'UQ_userid_checksum') { - const duplicate = await this.getAssetByChecksum(authUser.id, file.checksum); + const checksums = [file.checksum, livePhotoFile?.checksum].filter((checksum): checksum is Buffer => !!checksum); + const [duplicate] = await this._assetRepository.getAssetsByChecksums(authUser.id, checksums); return { id: duplicate.id, duplicate: true }; } @@ -463,7 +470,40 @@ export class AssetService { authUser: AuthUserDto, checkExistingAssetsDto: CheckExistingAssetsDto, ): Promise { - return this._assetRepository.getExistingAssets(authUser.id, checkExistingAssetsDto); + return { + existingIds: await this._assetRepository.getExistingAssets(authUser.id, checkExistingAssetsDto), + }; + } + + async bulkUploadCheck(authUser: AuthUserDto, dto: AssetBulkUploadCheckDto): Promise { + const checksums: Buffer[] = dto.assets.map((asset) => Buffer.from(asset.checksum, 'hex')); + const results = await this._assetRepository.getAssetsByChecksums(authUser.id, checksums); + const resultsMap: Record = {}; + + for (const { id, checksum } of results) { + resultsMap[checksum.toString('hex')] = id; + } + + return { + results: dto.assets.map(({ id, checksum }) => { + const duplicate = resultsMap[checksum]; + if (duplicate) { + return { + id, + assetId: duplicate, + action: AssetUploadAction.REJECT, + reason: AssetRejectReason.DUPLICATE, + }; + } + + // TODO mime-check + + return { + id, + action: AssetUploadAction.ACCEPT, + }; + }), + }; } async getAssetCountByTimeBucket( @@ -482,10 +522,6 @@ export class AssetService { return mapAssetCountByTimeBucket(result); } - getAssetByChecksum(userId: string, checksum: Buffer) { - return this._assetRepository.getAssetByChecksum(userId, checksum); - } - getAssetCountByUserId(authUser: AuthUserDto): Promise { return this._assetRepository.getAssetCountByUserId(authUser.id); } diff --git a/server/apps/immich/src/api-v1/asset/dto/asset-check.dto.ts b/server/apps/immich/src/api-v1/asset/dto/asset-check.dto.ts new file mode 100644 index 0000000000000..6fab46d63130f --- /dev/null +++ b/server/apps/immich/src/api-v1/asset/dto/asset-check.dto.ts @@ -0,0 +1,19 @@ +import { Type } from 'class-transformer'; +import { IsArray, IsNotEmpty, IsString, ValidateNested } from 'class-validator'; + +export class AssetBulkUploadCheckItem { + @IsString() + @IsNotEmpty() + id!: string; + + @IsString() + @IsNotEmpty() + checksum!: string; +} + +export class AssetBulkUploadCheckDto { + @IsArray() + @ValidateNested({ each: true }) + @Type(() => AssetBulkUploadCheckItem) + assets!: AssetBulkUploadCheckItem[]; +} diff --git a/server/apps/immich/src/api-v1/asset/response-dto/asset-check-response.dto.ts b/server/apps/immich/src/api-v1/asset/response-dto/asset-check-response.dto.ts new file mode 100644 index 0000000000000..1a51dc53f2bb3 --- /dev/null +++ b/server/apps/immich/src/api-v1/asset/response-dto/asset-check-response.dto.ts @@ -0,0 +1,20 @@ +export class AssetBulkUploadCheckResult { + id!: string; + action!: AssetUploadAction; + reason?: AssetRejectReason; + assetId?: string; +} + +export class AssetBulkUploadCheckResponseDto { + results!: AssetBulkUploadCheckResult[]; +} + +export enum AssetUploadAction { + ACCEPT = 'accept', + REJECT = 'reject', +} + +export enum AssetRejectReason { + DUPLICATE = 'duplicate', + UNSUPPORTED_FORMAT = 'unsupported-format', +} diff --git a/server/apps/immich/src/api-v1/asset/response-dto/check-existing-assets-response.dto.ts b/server/apps/immich/src/api-v1/asset/response-dto/check-existing-assets-response.dto.ts index 9a159c308a10c..c39a79606be10 100644 --- a/server/apps/immich/src/api-v1/asset/response-dto/check-existing-assets-response.dto.ts +++ b/server/apps/immich/src/api-v1/asset/response-dto/check-existing-assets-response.dto.ts @@ -1,6 +1,3 @@ export class CheckExistingAssetsResponseDto { - constructor(existingIds: string[]) { - this.existingIds = existingIds; - } - existingIds: string[]; + existingIds!: string[]; } diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index e0c021f4b86db..10704b9bd3f7b 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -3251,6 +3251,49 @@ ] } }, + "/asset/bulk-upload-check": { + "post": { + "operationId": "bulkUploadCheck", + "description": "Checks if assets exist by checksums", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssetBulkUploadCheckDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssetBulkUploadCheckResponseDto" + } + } + } + } + }, + "tags": [ + "Asset" + ], + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ] + } + }, "/asset/shared-link": { "post": { "operationId": "createAssetsSharedLink", @@ -6046,6 +6089,78 @@ "existingIds" ] }, + "AssetBulkUploadCheckItem": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "checksum": { + "type": "string" + } + }, + "required": [ + "id", + "checksum" + ] + }, + "AssetBulkUploadCheckDto": { + "type": "object", + "properties": { + "assets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AssetBulkUploadCheckItem" + } + } + }, + "required": [ + "assets" + ] + }, + "AssetBulkUploadCheckResult": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "action": { + "type": "string", + "enum": [ + "accept", + "reject" + ] + }, + "reason": { + "type": "string", + "enum": [ + "duplicate", + "unsupported-format" + ] + }, + "assetId": { + "type": "string" + } + }, + "required": [ + "id", + "action" + ] + }, + "AssetBulkUploadCheckResponseDto": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AssetBulkUploadCheckResult" + } + } + }, + "required": [ + "results" + ] + }, "CreateAssetsShareLinkDto": { "type": "object", "properties": { diff --git a/server/libs/domain/test/fixtures.ts b/server/libs/domain/test/fixtures.ts index c963709e7ffe2..664347da0cbc6 100644 --- a/server/libs/domain/test/fixtures.ts +++ b/server/libs/domain/test/fixtures.ts @@ -147,6 +147,7 @@ export const assetEntityStub = { deviceId: 'device-id', originalPath: 'upload/upload/path.ext', resizePath: null, + checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, webpPath: null, encodedVideoPath: null, @@ -173,6 +174,7 @@ export const assetEntityStub = { deviceId: 'device-id', originalPath: '/original/path.ext', resizePath: '/uploads/user-id/thumbs/path.ext', + checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, webpPath: null, encodedVideoPath: null, @@ -201,6 +203,7 @@ export const assetEntityStub = { deviceId: 'device-id', originalPath: '/original/path.ext', resizePath: '/uploads/user-id/thumbs/path.ext', + checksum: Buffer.from('file hash', 'utf8'), type: AssetType.VIDEO, webpPath: null, encodedVideoPath: null, @@ -246,6 +249,7 @@ export const assetEntityStub = { owner: userEntityStub.user1, ownerId: 'user-id', deviceId: 'device-id', + checksum: Buffer.from('file hash', 'utf8'), originalPath: '/original/path.ext', resizePath: '/uploads/user-id/thumbs/path.ext', type: AssetType.IMAGE, @@ -663,6 +667,7 @@ export const sharedLinkStub = { type: AssetType.VIDEO, originalPath: 'fake_path/jpeg', resizePath: '', + checksum: Buffer.from('file hash', 'utf8'), fileModifiedAt: today.toISOString(), fileCreatedAt: today.toISOString(), createdAt: today.toISOString(), diff --git a/server/libs/infra/src/entities/asset.entity.ts b/server/libs/infra/src/entities/asset.entity.ts index cba5518ea9080..3e6356e2c6b06 100644 --- a/server/libs/infra/src/entities/asset.entity.ts +++ b/server/libs/infra/src/entities/asset.entity.ts @@ -75,9 +75,9 @@ export class AssetEntity { @Column({ type: 'varchar', nullable: true }) mimeType!: string | null; - @Column({ type: 'bytea', nullable: true, select: false }) - @Index({ where: `'checksum' IS NOT NULL` }) // avoid null index - checksum?: Buffer | null; // sha1 checksum + @Column({ type: 'bytea' }) + @Index() + checksum!: Buffer; // sha1 checksum @Column({ type: 'varchar', nullable: true }) duration!: string | null; diff --git a/server/libs/infra/src/migrations/1684328185099-RequireChecksumNotNull.ts b/server/libs/infra/src/migrations/1684328185099-RequireChecksumNotNull.ts new file mode 100644 index 0000000000000..6da8f326220f0 --- /dev/null +++ b/server/libs/infra/src/migrations/1684328185099-RequireChecksumNotNull.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RequireChecksumNotNull1684328185099 implements MigrationInterface { + name = 'removeNotNullFromChecksumIndex1684328185099'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "public"."IDX_64c507300988dd1764f9a6530c"`); + await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "checksum" SET NOT NULL`); + await queryRunner.query(`CREATE INDEX "IDX_8d3efe36c0755849395e6ea866" ON "assets" ("checksum") `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "public"."IDX_8d3efe36c0755849395e6ea866"`); + await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "checksum" DROP NOT NULL`); + await queryRunner.query( + `CREATE INDEX "IDX_64c507300988dd1764f9a6530c" ON "assets" ("checksum") WHERE ('checksum' IS NOT NULL)`, + ); + } +} diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 685964b2914bf..37d1ce449145a 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -346,6 +346,96 @@ export interface AllJobStatusResponseDto { */ 'recognize-faces-queue': JobStatusDto; } +/** + * + * @export + * @interface AssetBulkUploadCheckDto + */ +export interface AssetBulkUploadCheckDto { + /** + * + * @type {Array} + * @memberof AssetBulkUploadCheckDto + */ + 'assets': Array; +} +/** + * + * @export + * @interface AssetBulkUploadCheckItem + */ +export interface AssetBulkUploadCheckItem { + /** + * + * @type {string} + * @memberof AssetBulkUploadCheckItem + */ + 'id': string; + /** + * + * @type {string} + * @memberof AssetBulkUploadCheckItem + */ + 'checksum': string; +} +/** + * + * @export + * @interface AssetBulkUploadCheckResponseDto + */ +export interface AssetBulkUploadCheckResponseDto { + /** + * + * @type {Array} + * @memberof AssetBulkUploadCheckResponseDto + */ + 'results': Array; +} +/** + * + * @export + * @interface AssetBulkUploadCheckResult + */ +export interface AssetBulkUploadCheckResult { + /** + * + * @type {string} + * @memberof AssetBulkUploadCheckResult + */ + 'id': string; + /** + * + * @type {string} + * @memberof AssetBulkUploadCheckResult + */ + 'action': AssetBulkUploadCheckResultActionEnum; + /** + * + * @type {string} + * @memberof AssetBulkUploadCheckResult + */ + 'reason'?: AssetBulkUploadCheckResultReasonEnum; + /** + * + * @type {string} + * @memberof AssetBulkUploadCheckResult + */ + 'assetId'?: string; +} + +export const AssetBulkUploadCheckResultActionEnum = { + Accept: 'accept', + Reject: 'reject' +} as const; + +export type AssetBulkUploadCheckResultActionEnum = typeof AssetBulkUploadCheckResultActionEnum[keyof typeof AssetBulkUploadCheckResultActionEnum]; +export const AssetBulkUploadCheckResultReasonEnum = { + Duplicate: 'duplicate', + UnsupportedFormat: 'unsupported-format' +} as const; + +export type AssetBulkUploadCheckResultReasonEnum = typeof AssetBulkUploadCheckResultReasonEnum[keyof typeof AssetBulkUploadCheckResultReasonEnum]; + /** * * @export @@ -4120,6 +4210,50 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration options: localVarRequestOptions, }; }, + /** + * Checks if assets exist by checksums + * @param {AssetBulkUploadCheckDto} assetBulkUploadCheckDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + bulkUploadCheck: async (assetBulkUploadCheckDto: AssetBulkUploadCheckDto, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'assetBulkUploadCheckDto' is not null or undefined + assertParamExists('bulkUploadCheck', 'assetBulkUploadCheckDto', assetBulkUploadCheckDto) + const localVarPath = `/asset/bulk-upload-check`; + // 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(assetBulkUploadCheckDto, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * Check duplicated asset before uploading - for Web upload used * @param {CheckDuplicateAssetDto} checkDuplicateAssetDto @@ -5312,6 +5446,16 @@ export const AssetApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.addAssetsToSharedLink(addAssetsDto, key, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * Checks if assets exist by checksums + * @param {AssetBulkUploadCheckDto} assetBulkUploadCheckDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async bulkUploadCheck(assetBulkUploadCheckDto: AssetBulkUploadCheckDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.bulkUploadCheck(assetBulkUploadCheckDto, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * Check duplicated asset before uploading - for Web upload used * @param {CheckDuplicateAssetDto} checkDuplicateAssetDto @@ -5595,6 +5739,15 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath addAssetsToSharedLink(addAssetsDto: AddAssetsDto, key?: string, options?: any): AxiosPromise { return localVarFp.addAssetsToSharedLink(addAssetsDto, key, options).then((request) => request(axios, basePath)); }, + /** + * Checks if assets exist by checksums + * @param {AssetBulkUploadCheckDto} assetBulkUploadCheckDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + bulkUploadCheck(assetBulkUploadCheckDto: AssetBulkUploadCheckDto, options?: any): AxiosPromise { + return localVarFp.bulkUploadCheck(assetBulkUploadCheckDto, options).then((request) => request(axios, basePath)); + }, /** * Check duplicated asset before uploading - for Web upload used * @param {CheckDuplicateAssetDto} checkDuplicateAssetDto @@ -5856,6 +6009,17 @@ export class AssetApi extends BaseAPI { return AssetApiFp(this.configuration).addAssetsToSharedLink(addAssetsDto, key, options).then((request) => request(this.axios, this.basePath)); } + /** + * Checks if assets exist by checksums + * @param {AssetBulkUploadCheckDto} assetBulkUploadCheckDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AssetApi + */ + public bulkUploadCheck(assetBulkUploadCheckDto: AssetBulkUploadCheckDto, options?: AxiosRequestConfig) { + return AssetApiFp(this.configuration).bulkUploadCheck(assetBulkUploadCheckDto, options).then((request) => request(this.axios, this.basePath)); + } + /** * Check duplicated asset before uploading - for Web upload used * @param {CheckDuplicateAssetDto} checkDuplicateAssetDto diff --git a/web/src/lib/utils/file-uploader.ts b/web/src/lib/utils/file-uploader.ts index bad793e5dadb9..e825d0eb74927 100644 --- a/web/src/lib/utils/file-uploader.ts +++ b/web/src/lib/utils/file-uploader.ts @@ -4,7 +4,7 @@ import { } from './../components/shared-components/notification/notification'; import { uploadAssetsStore } from '$lib/stores/upload'; import type { UploadAsset } from '../models/upload-asset'; -import { api, AssetFileUploadResponseDto } from '@api'; +import { AssetFileUploadResponseDto } from '@api'; import { addAssetsToAlbum, getFileMimeType, getFilenameExtension } from '$lib/utils/asset-utils'; import { mergeMap, filter, firstValueFrom, from, of, combineLatestAll } from 'rxjs'; import axios from 'axios'; @@ -73,7 +73,7 @@ async function fileUploader( const deviceAssetId = 'web' + '-' + asset.name + '-' + asset.lastModified; try { - // Create and add Unique ID of asset on the device + // Create and add pseudo-unique ID of asset on the device formData.append('deviceAssetId', deviceAssetId); // Get device id - for web -> use WEB @@ -102,23 +102,6 @@ async function fileUploader( // failed uploads. formData.append('assetData', new File([asset], asset.name, { type: mimeType })); - // Check if asset upload on server before performing upload - const { data, status } = await api.assetApi.checkDuplicateAsset( - { - deviceAssetId: String(deviceAssetId), - deviceId: 'WEB' - }, - sharedKey - ); - - if (status === 200 && data.isExist && data.id) { - if (albumId) { - await addAssetsToAlbum(albumId, [data.id], sharedKey); - } - - return data.id; - } - const newUploadAsset: UploadAsset = { id: deviceAssetId, file: asset,