1
0
mirror of https://github.com/immich-app/immich.git synced 2025-08-09 23:17:29 +02:00

feat(server,web) Semantic import path validation (#7076)

* add library validation api

* chore: open api

* show warning i UI

* add flex row

* fix e2e

* tests

* fix tests

* enforce path validation

* enforce validation on refresh

* return 400 on bad import path

* add limits to import paths

* set response code to 200

* fix e2e

* fix lint

* fix test

* restore e2e folder

* fix import

* use startsWith

* icon color

* notify user of failed validation

* add parent div to validation

* add docs to the import validation

* improve library troubleshooting docs

* fix button alignment

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
Jonathan Jogenfors
2024-02-20 16:53:12 +01:00
committed by GitHub
parent e7a875eadd
commit b3c7bebbd4
32 changed files with 1472 additions and 75 deletions

View File

@@ -182,6 +182,9 @@ doc/UserAvatarColor.md
doc/UserDto.md
doc/UserResponseDto.md
doc/ValidateAccessTokenResponseDto.md
doc/ValidateLibraryDto.md
doc/ValidateLibraryImportPathResponseDto.md
doc/ValidateLibraryResponseDto.md
doc/VideoCodec.md
git_push.sh
lib/api.dart
@@ -372,6 +375,9 @@ lib/model/user_avatar_color.dart
lib/model/user_dto.dart
lib/model/user_response_dto.dart
lib/model/validate_access_token_response_dto.dart
lib/model/validate_library_dto.dart
lib/model/validate_library_import_path_response_dto.dart
lib/model/validate_library_response_dto.dart
lib/model/video_codec.dart
pubspec.yaml
test/activity_api_test.dart
@@ -553,4 +559,7 @@ test/user_avatar_color_test.dart
test/user_dto_test.dart
test/user_response_dto_test.dart
test/validate_access_token_response_dto_test.dart
test/validate_library_dto_test.dart
test/validate_library_import_path_response_dto_test.dart
test/validate_library_response_dto_test.dart
test/video_codec_test.dart

View File

@@ -141,6 +141,7 @@ Class | Method | HTTP request | Description
*LibraryApi* | [**removeOfflineFiles**](doc//LibraryApi.md#removeofflinefiles) | **POST** /library/{id}/removeOffline |
*LibraryApi* | [**scanLibrary**](doc//LibraryApi.md#scanlibrary) | **POST** /library/{id}/scan |
*LibraryApi* | [**updateLibrary**](doc//LibraryApi.md#updatelibrary) | **PUT** /library/{id} |
*LibraryApi* | [**validate**](doc//LibraryApi.md#validate) | **POST** /library/{id}/validate |
*OAuthApi* | [**finishOAuth**](doc//OAuthApi.md#finishoauth) | **POST** /oauth/callback |
*OAuthApi* | [**linkOAuthAccount**](doc//OAuthApi.md#linkoauthaccount) | **POST** /oauth/link |
*OAuthApi* | [**redirectOAuthToMobile**](doc//OAuthApi.md#redirectoauthtomobile) | **GET** /oauth/mobile-redirect |
@@ -372,6 +373,9 @@ Class | Method | HTTP request | Description
- [UserDto](doc//UserDto.md)
- [UserResponseDto](doc//UserResponseDto.md)
- [ValidateAccessTokenResponseDto](doc//ValidateAccessTokenResponseDto.md)
- [ValidateLibraryDto](doc//ValidateLibraryDto.md)
- [ValidateLibraryImportPathResponseDto](doc//ValidateLibraryImportPathResponseDto.md)
- [ValidateLibraryResponseDto](doc//ValidateLibraryResponseDto.md)
- [VideoCodec](doc//VideoCodec.md)

View File

@@ -17,6 +17,7 @@ Method | HTTP request | Description
[**removeOfflineFiles**](LibraryApi.md#removeofflinefiles) | **POST** /library/{id}/removeOffline |
[**scanLibrary**](LibraryApi.md#scanlibrary) | **POST** /library/{id}/scan |
[**updateLibrary**](LibraryApi.md#updatelibrary) | **PUT** /library/{id} |
[**validate**](LibraryApi.md#validate) | **POST** /library/{id}/validate |
# **createLibrary**
@@ -456,3 +457,60 @@ 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)
# **validate**
> ValidateLibraryResponseDto validate(id, validateLibraryDto)
### 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 = LibraryApi();
final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final validateLibraryDto = ValidateLibraryDto(); // ValidateLibraryDto |
try {
final result = api_instance.validate(id, validateLibraryDto);
print(result);
} catch (e) {
print('Exception when calling LibraryApi->validate: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**id** | **String**| |
**validateLibraryDto** | [**ValidateLibraryDto**](ValidateLibraryDto.md)| |
### Return type
[**ValidateLibraryResponseDto**](ValidateLibraryResponseDto.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)

16
mobile/openapi/doc/ValidateLibraryDto.md generated Normal file
View File

@@ -0,0 +1,16 @@
# openapi.model.ValidateLibraryDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**exclusionPatterns** | **List<String>** | | [optional] [default to const []]
**importPaths** | **List<String>** | | [optional] [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)

View File

@@ -0,0 +1,17 @@
# openapi.model.ValidateLibraryImportPathResponseDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**importPath** | **String** | |
**isValid** | **bool** | | [optional] [default to false]
**message** | **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)

View File

@@ -0,0 +1,15 @@
# openapi.model.ValidateLibraryResponseDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**importPaths** | [**List<ValidateLibraryImportPathResponseDto>**](ValidateLibraryImportPathResponseDto.md) | | [optional] [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)

View File

@@ -209,6 +209,9 @@ part 'model/user_avatar_color.dart';
part 'model/user_dto.dart';
part 'model/user_response_dto.dart';
part 'model/validate_access_token_response_dto.dart';
part 'model/validate_library_dto.dart';
part 'model/validate_library_import_path_response_dto.dart';
part 'model/validate_library_response_dto.dart';
part 'model/video_codec.dart';

View File

@@ -378,4 +378,56 @@ class LibraryApi {
}
return null;
}
/// Performs an HTTP 'POST /library/{id}/validate' operation and returns the [Response].
/// Parameters:
///
/// * [String] id (required):
///
/// * [ValidateLibraryDto] validateLibraryDto (required):
Future<Response> validateWithHttpInfo(String id, ValidateLibraryDto validateLibraryDto,) async {
// ignore: prefer_const_declarations
final path = r'/library/{id}/validate'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody = validateLibraryDto;
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:
///
/// * [String] id (required):
///
/// * [ValidateLibraryDto] validateLibraryDto (required):
Future<ValidateLibraryResponseDto?> validate(String id, ValidateLibraryDto validateLibraryDto,) async {
final response = await validateWithHttpInfo(id, validateLibraryDto,);
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), 'ValidateLibraryResponseDto',) as ValidateLibraryResponseDto;
}
return null;
}
}

View File

@@ -500,6 +500,12 @@ class ApiClient {
return UserResponseDto.fromJson(value);
case 'ValidateAccessTokenResponseDto':
return ValidateAccessTokenResponseDto.fromJson(value);
case 'ValidateLibraryDto':
return ValidateLibraryDto.fromJson(value);
case 'ValidateLibraryImportPathResponseDto':
return ValidateLibraryImportPathResponseDto.fromJson(value);
case 'ValidateLibraryResponseDto':
return ValidateLibraryResponseDto.fromJson(value);
case 'VideoCodec':
return VideoCodecTypeTransformer().decode(value);
default:

View File

@@ -0,0 +1,108 @@
//
// 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 ValidateLibraryDto {
/// Returns a new [ValidateLibraryDto] instance.
ValidateLibraryDto({
this.exclusionPatterns = const [],
this.importPaths = const [],
});
List<String> exclusionPatterns;
List<String> importPaths;
@override
bool operator ==(Object other) => identical(this, other) || other is ValidateLibraryDto &&
_deepEquality.equals(other.exclusionPatterns, exclusionPatterns) &&
_deepEquality.equals(other.importPaths, importPaths);
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(exclusionPatterns.hashCode) +
(importPaths.hashCode);
@override
String toString() => 'ValidateLibraryDto[exclusionPatterns=$exclusionPatterns, importPaths=$importPaths]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'exclusionPatterns'] = this.exclusionPatterns;
json[r'importPaths'] = this.importPaths;
return json;
}
/// Returns a new [ValidateLibraryDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static ValidateLibraryDto? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return ValidateLibraryDto(
exclusionPatterns: json[r'exclusionPatterns'] is Iterable
? (json[r'exclusionPatterns'] as Iterable).cast<String>().toList(growable: false)
: const [],
importPaths: json[r'importPaths'] is Iterable
? (json[r'importPaths'] as Iterable).cast<String>().toList(growable: false)
: const [],
);
}
return null;
}
static List<ValidateLibraryDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <ValidateLibraryDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = ValidateLibraryDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, ValidateLibraryDto> mapFromJson(dynamic json) {
final map = <String, ValidateLibraryDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = ValidateLibraryDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of ValidateLibraryDto-objects as value to a dart map
static Map<String, List<ValidateLibraryDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<ValidateLibraryDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = ValidateLibraryDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
};
}

View File

@@ -0,0 +1,122 @@
//
// 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 ValidateLibraryImportPathResponseDto {
/// Returns a new [ValidateLibraryImportPathResponseDto] instance.
ValidateLibraryImportPathResponseDto({
required this.importPath,
this.isValid = false,
this.message,
});
String importPath;
bool isValid;
///
/// 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? message;
@override
bool operator ==(Object other) => identical(this, other) || other is ValidateLibraryImportPathResponseDto &&
other.importPath == importPath &&
other.isValid == isValid &&
other.message == message;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(importPath.hashCode) +
(isValid.hashCode) +
(message == null ? 0 : message!.hashCode);
@override
String toString() => 'ValidateLibraryImportPathResponseDto[importPath=$importPath, isValid=$isValid, message=$message]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'importPath'] = this.importPath;
json[r'isValid'] = this.isValid;
if (this.message != null) {
json[r'message'] = this.message;
} else {
// json[r'message'] = null;
}
return json;
}
/// Returns a new [ValidateLibraryImportPathResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static ValidateLibraryImportPathResponseDto? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return ValidateLibraryImportPathResponseDto(
importPath: mapValueOfType<String>(json, r'importPath')!,
isValid: mapValueOfType<bool>(json, r'isValid') ?? false,
message: mapValueOfType<String>(json, r'message'),
);
}
return null;
}
static List<ValidateLibraryImportPathResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <ValidateLibraryImportPathResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = ValidateLibraryImportPathResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, ValidateLibraryImportPathResponseDto> mapFromJson(dynamic json) {
final map = <String, ValidateLibraryImportPathResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = ValidateLibraryImportPathResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of ValidateLibraryImportPathResponseDto-objects as value to a dart map
static Map<String, List<ValidateLibraryImportPathResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<ValidateLibraryImportPathResponseDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = ValidateLibraryImportPathResponseDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'importPath',
};
}

View File

@@ -0,0 +1,97 @@
//
// 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 ValidateLibraryResponseDto {
/// Returns a new [ValidateLibraryResponseDto] instance.
ValidateLibraryResponseDto({
this.importPaths = const [],
});
List<ValidateLibraryImportPathResponseDto> importPaths;
@override
bool operator ==(Object other) => identical(this, other) || other is ValidateLibraryResponseDto &&
_deepEquality.equals(other.importPaths, importPaths);
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(importPaths.hashCode);
@override
String toString() => 'ValidateLibraryResponseDto[importPaths=$importPaths]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'importPaths'] = this.importPaths;
return json;
}
/// Returns a new [ValidateLibraryResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static ValidateLibraryResponseDto? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return ValidateLibraryResponseDto(
importPaths: ValidateLibraryImportPathResponseDto.listFromJson(json[r'importPaths']),
);
}
return null;
}
static List<ValidateLibraryResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <ValidateLibraryResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = ValidateLibraryResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, ValidateLibraryResponseDto> mapFromJson(dynamic json) {
final map = <String, ValidateLibraryResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = ValidateLibraryResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of ValidateLibraryResponseDto-objects as value to a dart map
static Map<String, List<ValidateLibraryResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<ValidateLibraryResponseDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = ValidateLibraryResponseDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
};
}

View File

@@ -57,5 +57,10 @@ void main() {
// TODO
});
//Future<ValidateLibraryResponseDto> validate(String id, ValidateLibraryDto validateLibraryDto) async
test('test validate', () async {
// TODO
});
});
}

View File

@@ -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 ValidateLibraryDto
void main() {
// final instance = ValidateLibraryDto();
group('test ValidateLibraryDto', () {
// List<String> exclusionPatterns (default value: const [])
test('to test the property `exclusionPatterns`', () async {
// TODO
});
// List<String> importPaths (default value: const [])
test('to test the property `importPaths`', () async {
// TODO
});
});
}

View File

@@ -0,0 +1,37 @@
//
// 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 ValidateLibraryImportPathResponseDto
void main() {
// final instance = ValidateLibraryImportPathResponseDto();
group('test ValidateLibraryImportPathResponseDto', () {
// String importPath
test('to test the property `importPath`', () async {
// TODO
});
// bool isValid (default value: false)
test('to test the property `isValid`', () async {
// TODO
});
// String message
test('to test the property `message`', () async {
// TODO
});
});
}

View File

@@ -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 ValidateLibraryResponseDto
void main() {
// final instance = ValidateLibraryResponseDto();
group('test ValidateLibraryResponseDto', () {
// List<ValidateLibraryImportPathResponseDto> importPaths (default value: const [])
test('to test the property `importPaths`', () async {
// TODO
});
});
}