forked from dark_thunder/immich
feat(server): asset search endpoint (#4931)
* feat(server): GET /assets endpoint * chore: open api * chore: use dumb name * feat: search by make, model, lens, city, state, country * chore: open api * chore: pagination validation and tests * chore: pr feedback
This commit is contained in:
Generated
+624
File diff suppressed because it is too large
Load Diff
Generated
+3
@@ -29,6 +29,7 @@ doc/AssetIdsDto.md
|
||||
doc/AssetIdsResponseDto.md
|
||||
doc/AssetJobName.md
|
||||
doc/AssetJobsDto.md
|
||||
doc/AssetOrder.md
|
||||
doc/AssetResponseDto.md
|
||||
doc/AssetStatsResponseDto.md
|
||||
doc/AssetTypeEnum.md
|
||||
@@ -220,6 +221,7 @@ lib/model/asset_ids_dto.dart
|
||||
lib/model/asset_ids_response_dto.dart
|
||||
lib/model/asset_job_name.dart
|
||||
lib/model/asset_jobs_dto.dart
|
||||
lib/model/asset_order.dart
|
||||
lib/model/asset_response_dto.dart
|
||||
lib/model/asset_stats_response_dto.dart
|
||||
lib/model/asset_type_enum.dart
|
||||
@@ -376,6 +378,7 @@ test/asset_ids_dto_test.dart
|
||||
test/asset_ids_response_dto_test.dart
|
||||
test/asset_job_name_test.dart
|
||||
test/asset_jobs_dto_test.dart
|
||||
test/asset_order_test.dart
|
||||
test/asset_response_dto_test.dart
|
||||
test/asset_stats_response_dto_test.dart
|
||||
test/asset_type_enum_test.dart
|
||||
|
||||
Generated
+2
@@ -116,6 +116,7 @@ Class | Method | HTTP request | Description
|
||||
*AssetApi* | [**restoreTrash**](doc//AssetApi.md#restoretrash) | **POST** /asset/trash/restore |
|
||||
*AssetApi* | [**runAssetJobs**](doc//AssetApi.md#runassetjobs) | **POST** /asset/jobs |
|
||||
*AssetApi* | [**searchAsset**](doc//AssetApi.md#searchasset) | **POST** /asset/search |
|
||||
*AssetApi* | [**searchAssets**](doc//AssetApi.md#searchassets) | **GET** /assets |
|
||||
*AssetApi* | [**serveFile**](doc//AssetApi.md#servefile) | **GET** /asset/file/{id} |
|
||||
*AssetApi* | [**updateAsset**](doc//AssetApi.md#updateasset) | **PUT** /asset/{id} |
|
||||
*AssetApi* | [**updateAssets**](doc//AssetApi.md#updateassets) | **PUT** /asset |
|
||||
@@ -229,6 +230,7 @@ Class | Method | HTTP request | Description
|
||||
- [AssetIdsResponseDto](doc//AssetIdsResponseDto.md)
|
||||
- [AssetJobName](doc//AssetJobName.md)
|
||||
- [AssetJobsDto](doc//AssetJobsDto.md)
|
||||
- [AssetOrder](doc//AssetOrder.md)
|
||||
- [AssetResponseDto](doc//AssetResponseDto.md)
|
||||
- [AssetStatsResponseDto](doc//AssetStatsResponseDto.md)
|
||||
- [AssetTypeEnum](doc//AssetTypeEnum.md)
|
||||
|
||||
Generated
+134
@@ -34,6 +34,7 @@ Method | HTTP request | Description
|
||||
[**restoreTrash**](AssetApi.md#restoretrash) | **POST** /asset/trash/restore |
|
||||
[**runAssetJobs**](AssetApi.md#runassetjobs) | **POST** /asset/jobs |
|
||||
[**searchAsset**](AssetApi.md#searchasset) | **POST** /asset/search |
|
||||
[**searchAssets**](AssetApi.md#searchassets) | **GET** /assets |
|
||||
[**serveFile**](AssetApi.md#servefile) | **GET** /asset/file/{id} |
|
||||
[**updateAsset**](AssetApi.md#updateasset) | **PUT** /asset/{id} |
|
||||
[**updateAssets**](AssetApi.md#updateassets) | **PUT** /asset |
|
||||
@@ -1477,6 +1478,139 @@ 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)
|
||||
|
||||
# **searchAssets**
|
||||
> List<AssetResponseDto> searchAssets(id, libraryId, type, order, deviceAssetId, deviceId, checksum, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, withDeleted, withStacked, withExif, withPeople, createdBefore, createdAfter, updatedBefore, updatedAfter, trashedBefore, trashedAfter, takenBefore, takenAfter, originalFileName, originalPath, resizePath, webpPath, encodedVideoPath, city, state, country, make, model, lensModel, page, size)
|
||||
|
||||
|
||||
|
||||
### 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 id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
|
||||
final libraryId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
|
||||
final type = ; // AssetTypeEnum |
|
||||
final order = ; // AssetOrder |
|
||||
final deviceAssetId = deviceAssetId_example; // String |
|
||||
final deviceId = deviceId_example; // String |
|
||||
final checksum = checksum_example; // String |
|
||||
final isArchived = true; // bool |
|
||||
final isEncoded = true; // bool |
|
||||
final isExternal = true; // bool |
|
||||
final isFavorite = true; // bool |
|
||||
final isMotion = true; // bool |
|
||||
final isOffline = true; // bool |
|
||||
final isReadOnly = true; // bool |
|
||||
final isVisible = true; // bool |
|
||||
final withDeleted = true; // bool |
|
||||
final withStacked = true; // bool |
|
||||
final withExif = true; // bool |
|
||||
final withPeople = true; // bool |
|
||||
final createdBefore = 2013-10-20T19:20:30+01:00; // DateTime |
|
||||
final createdAfter = 2013-10-20T19:20:30+01:00; // DateTime |
|
||||
final updatedBefore = 2013-10-20T19:20:30+01:00; // DateTime |
|
||||
final updatedAfter = 2013-10-20T19:20:30+01:00; // DateTime |
|
||||
final trashedBefore = 2013-10-20T19:20:30+01:00; // DateTime |
|
||||
final trashedAfter = 2013-10-20T19:20:30+01:00; // DateTime |
|
||||
final takenBefore = 2013-10-20T19:20:30+01:00; // DateTime |
|
||||
final takenAfter = 2013-10-20T19:20:30+01:00; // DateTime |
|
||||
final originalFileName = originalFileName_example; // String |
|
||||
final originalPath = originalPath_example; // String |
|
||||
final resizePath = resizePath_example; // String |
|
||||
final webpPath = webpPath_example; // String |
|
||||
final encodedVideoPath = encodedVideoPath_example; // String |
|
||||
final city = city_example; // String |
|
||||
final state = state_example; // String |
|
||||
final country = country_example; // String |
|
||||
final make = make_example; // String |
|
||||
final model = model_example; // String |
|
||||
final lensModel = lensModel_example; // String |
|
||||
final page = 8.14; // num |
|
||||
final size = 8.14; // num |
|
||||
|
||||
try {
|
||||
final result = api_instance.searchAssets(id, libraryId, type, order, deviceAssetId, deviceId, checksum, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, withDeleted, withStacked, withExif, withPeople, createdBefore, createdAfter, updatedBefore, updatedAfter, trashedBefore, trashedAfter, takenBefore, takenAfter, originalFileName, originalPath, resizePath, webpPath, encodedVideoPath, city, state, country, make, model, lensModel, page, size);
|
||||
print(result);
|
||||
} catch (e) {
|
||||
print('Exception when calling AssetApi->searchAssets: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**id** | **String**| | [optional]
|
||||
**libraryId** | **String**| | [optional]
|
||||
**type** | [**AssetTypeEnum**](.md)| | [optional]
|
||||
**order** | [**AssetOrder**](.md)| | [optional]
|
||||
**deviceAssetId** | **String**| | [optional]
|
||||
**deviceId** | **String**| | [optional]
|
||||
**checksum** | **String**| | [optional]
|
||||
**isArchived** | **bool**| | [optional]
|
||||
**isEncoded** | **bool**| | [optional]
|
||||
**isExternal** | **bool**| | [optional]
|
||||
**isFavorite** | **bool**| | [optional]
|
||||
**isMotion** | **bool**| | [optional]
|
||||
**isOffline** | **bool**| | [optional]
|
||||
**isReadOnly** | **bool**| | [optional]
|
||||
**isVisible** | **bool**| | [optional]
|
||||
**withDeleted** | **bool**| | [optional]
|
||||
**withStacked** | **bool**| | [optional]
|
||||
**withExif** | **bool**| | [optional]
|
||||
**withPeople** | **bool**| | [optional]
|
||||
**createdBefore** | **DateTime**| | [optional]
|
||||
**createdAfter** | **DateTime**| | [optional]
|
||||
**updatedBefore** | **DateTime**| | [optional]
|
||||
**updatedAfter** | **DateTime**| | [optional]
|
||||
**trashedBefore** | **DateTime**| | [optional]
|
||||
**trashedAfter** | **DateTime**| | [optional]
|
||||
**takenBefore** | **DateTime**| | [optional]
|
||||
**takenAfter** | **DateTime**| | [optional]
|
||||
**originalFileName** | **String**| | [optional]
|
||||
**originalPath** | **String**| | [optional]
|
||||
**resizePath** | **String**| | [optional]
|
||||
**webpPath** | **String**| | [optional]
|
||||
**encodedVideoPath** | **String**| | [optional]
|
||||
**city** | **String**| | [optional]
|
||||
**state** | **String**| | [optional]
|
||||
**country** | **String**| | [optional]
|
||||
**make** | **String**| | [optional]
|
||||
**model** | **String**| | [optional]
|
||||
**lensModel** | **String**| | [optional]
|
||||
**page** | **num**| | [optional]
|
||||
**size** | **num**| | [optional]
|
||||
|
||||
### Return type
|
||||
|
||||
[**List<AssetResponseDto>**](AssetResponseDto.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **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)
|
||||
|
||||
# **serveFile**
|
||||
> MultipartFile serveFile(id, isThumb, isWeb, key)
|
||||
|
||||
|
||||
Generated
+14
@@ -0,0 +1,14 @@
|
||||
# openapi.model.AssetOrder
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
Generated
+1
@@ -68,6 +68,7 @@ part 'model/asset_ids_dto.dart';
|
||||
part 'model/asset_ids_response_dto.dart';
|
||||
part 'model/asset_job_name.dart';
|
||||
part 'model/asset_jobs_dto.dart';
|
||||
part 'model/asset_order.dart';
|
||||
part 'model/asset_response_dto.dart';
|
||||
part 'model/asset_stats_response_dto.dart';
|
||||
part 'model/asset_type_enum.dart';
|
||||
|
||||
Generated
+327
File diff suppressed because it is too large
Load Diff
Generated
+2
@@ -225,6 +225,8 @@ class ApiClient {
|
||||
return AssetJobNameTypeTransformer().decode(value);
|
||||
case 'AssetJobsDto':
|
||||
return AssetJobsDto.fromJson(value);
|
||||
case 'AssetOrder':
|
||||
return AssetOrderTypeTransformer().decode(value);
|
||||
case 'AssetResponseDto':
|
||||
return AssetResponseDto.fromJson(value);
|
||||
case 'AssetStatsResponseDto':
|
||||
|
||||
Generated
+3
@@ -58,6 +58,9 @@ String parameterToString(dynamic value) {
|
||||
if (value is AssetJobName) {
|
||||
return AssetJobNameTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is AssetOrder) {
|
||||
return AssetOrderTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is AssetTypeEnum) {
|
||||
return AssetTypeEnumTypeTransformer().encode(value).toString();
|
||||
}
|
||||
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
//
|
||||
// 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 AssetOrder {
|
||||
/// Instantiate a new enum with the provided [value].
|
||||
const AssetOrder._(this.value);
|
||||
|
||||
/// The underlying value of this enum member.
|
||||
final String value;
|
||||
|
||||
@override
|
||||
String toString() => value;
|
||||
|
||||
String toJson() => value;
|
||||
|
||||
static const asc = AssetOrder._(r'asc');
|
||||
static const desc = AssetOrder._(r'desc');
|
||||
|
||||
/// List of all possible values in this [enum][AssetOrder].
|
||||
static const values = <AssetOrder>[
|
||||
asc,
|
||||
desc,
|
||||
];
|
||||
|
||||
static AssetOrder? fromJson(dynamic value) => AssetOrderTypeTransformer().decode(value);
|
||||
|
||||
static List<AssetOrder>? listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <AssetOrder>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = AssetOrder.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
}
|
||||
|
||||
/// Transformation class that can [encode] an instance of [AssetOrder] to String,
|
||||
/// and [decode] dynamic data back to [AssetOrder].
|
||||
class AssetOrderTypeTransformer {
|
||||
factory AssetOrderTypeTransformer() => _instance ??= const AssetOrderTypeTransformer._();
|
||||
|
||||
const AssetOrderTypeTransformer._();
|
||||
|
||||
String encode(AssetOrder data) => data.value;
|
||||
|
||||
/// Decodes a [dynamic value][data] to a AssetOrder.
|
||||
///
|
||||
/// 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.
|
||||
AssetOrder? decode(dynamic data, {bool allowNull = true}) {
|
||||
if (data != null) {
|
||||
switch (data) {
|
||||
case r'asc': return AssetOrder.asc;
|
||||
case r'desc': return AssetOrder.desc;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Singleton [AssetOrderTypeTransformer] instance.
|
||||
static AssetOrderTypeTransformer? _instance;
|
||||
}
|
||||
|
||||
Generated
+5
@@ -152,6 +152,11 @@ void main() {
|
||||
// TODO
|
||||
});
|
||||
|
||||
//Future<List<AssetResponseDto>> searchAssets({ String id, String libraryId, AssetTypeEnum type, AssetOrder order, String deviceAssetId, String deviceId, String checksum, bool isArchived, bool isEncoded, bool isExternal, bool isFavorite, bool isMotion, bool isOffline, bool isReadOnly, bool isVisible, bool withDeleted, bool withStacked, bool withExif, bool withPeople, DateTime createdBefore, DateTime createdAfter, DateTime updatedBefore, DateTime updatedAfter, DateTime trashedBefore, DateTime trashedAfter, DateTime takenBefore, DateTime takenAfter, String originalFileName, String originalPath, String resizePath, String webpPath, String encodedVideoPath, String city, String state, String country, String make, String model, String lensModel, num page, num size }) async
|
||||
test('test searchAssets', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
//Future<MultipartFile> serveFile(String id, { bool isThumb, bool isWeb, String key }) async
|
||||
test('test serveFile', () async {
|
||||
// TODO
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// 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 AssetOrder
|
||||
void main() {
|
||||
|
||||
group('test AssetOrder', () {
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -30,6 +30,8 @@ import {
|
||||
AssetIdsDto,
|
||||
AssetJobName,
|
||||
AssetJobsDto,
|
||||
AssetOrder,
|
||||
AssetSearchDto,
|
||||
AssetStatsDto,
|
||||
DownloadArchiveInfo,
|
||||
DownloadInfoDto,
|
||||
@@ -91,6 +93,34 @@ export class AssetService {
|
||||
this.configCore = SystemConfigCore.create(configRepository);
|
||||
}
|
||||
|
||||
search(authUser: AuthUserDto, dto: AssetSearchDto) {
|
||||
let checksum: Buffer | undefined = undefined;
|
||||
|
||||
if (dto.checksum) {
|
||||
const encoding = dto.checksum.length === 28 ? 'base64' : 'hex';
|
||||
checksum = Buffer.from(dto.checksum, encoding);
|
||||
}
|
||||
|
||||
const enumToOrder = { [AssetOrder.ASC]: 'ASC', [AssetOrder.DESC]: 'DESC' } as const;
|
||||
const order = dto.order ? enumToOrder[dto.order] : undefined;
|
||||
|
||||
return this.assetRepository
|
||||
.search({
|
||||
...dto,
|
||||
order,
|
||||
checksum,
|
||||
ownerId: authUser.id,
|
||||
})
|
||||
.then((assets) =>
|
||||
assets.map((asset) =>
|
||||
mapAsset(asset, {
|
||||
stripMetadata: false,
|
||||
withStack: true,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
canUploadFile({ authUser, fieldName, file }: UploadRequest): true {
|
||||
this.access.requireUploadAccess(authUser);
|
||||
|
||||
|
||||
@@ -1,8 +1,161 @@
|
||||
import { AssetType } from '@app/infra/entities';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsBoolean, IsInt, IsPositive, IsString } from 'class-validator';
|
||||
import { Optional, ValidateUUID } from '../../domain.util';
|
||||
import { IsBoolean, IsEnum, IsInt, IsPositive, IsString, Min } from 'class-validator';
|
||||
import { Optional, QueryBoolean, QueryDate, ValidateUUID } from '../../domain.util';
|
||||
import { BulkIdsDto } from '../response-dto';
|
||||
|
||||
export enum AssetOrder {
|
||||
ASC = 'asc',
|
||||
DESC = 'desc',
|
||||
}
|
||||
|
||||
export class AssetSearchDto {
|
||||
@ValidateUUID({ optional: true })
|
||||
id?: string;
|
||||
|
||||
@ValidateUUID({ optional: true })
|
||||
libraryId?: string;
|
||||
|
||||
@IsString()
|
||||
@Optional()
|
||||
deviceAssetId?: string;
|
||||
|
||||
@IsString()
|
||||
@Optional()
|
||||
deviceId?: string;
|
||||
|
||||
@IsEnum(AssetType)
|
||||
@Optional()
|
||||
@ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType })
|
||||
type?: AssetType;
|
||||
|
||||
@IsString()
|
||||
@Optional()
|
||||
checksum?: string;
|
||||
|
||||
@QueryBoolean({ optional: true })
|
||||
isArchived?: boolean;
|
||||
|
||||
@QueryBoolean({ optional: true })
|
||||
isEncoded?: boolean;
|
||||
|
||||
@QueryBoolean({ optional: true })
|
||||
isExternal?: boolean;
|
||||
|
||||
@QueryBoolean({ optional: true })
|
||||
isFavorite?: boolean;
|
||||
|
||||
@QueryBoolean({ optional: true })
|
||||
isMotion?: boolean;
|
||||
|
||||
@QueryBoolean({ optional: true })
|
||||
isOffline?: boolean;
|
||||
|
||||
@QueryBoolean({ optional: true })
|
||||
isReadOnly?: boolean;
|
||||
|
||||
@QueryBoolean({ optional: true })
|
||||
isVisible?: boolean;
|
||||
|
||||
@QueryBoolean({ optional: true })
|
||||
withDeleted?: boolean;
|
||||
|
||||
@QueryBoolean({ optional: true })
|
||||
withStacked?: boolean;
|
||||
|
||||
@QueryBoolean({ optional: true })
|
||||
withExif?: boolean;
|
||||
|
||||
@QueryBoolean({ optional: true })
|
||||
withPeople?: boolean;
|
||||
|
||||
@QueryDate({ optional: true })
|
||||
createdBefore?: Date;
|
||||
|
||||
@QueryDate({ optional: true })
|
||||
createdAfter?: Date;
|
||||
|
||||
@QueryDate({ optional: true })
|
||||
updatedBefore?: Date;
|
||||
|
||||
@QueryDate({ optional: true })
|
||||
updatedAfter?: Date;
|
||||
|
||||
@QueryDate({ optional: true })
|
||||
trashedBefore?: Date;
|
||||
|
||||
@QueryDate({ optional: true })
|
||||
trashedAfter?: Date;
|
||||
|
||||
@QueryDate({ optional: true })
|
||||
takenBefore?: Date;
|
||||
|
||||
@QueryDate({ optional: true })
|
||||
takenAfter?: Date;
|
||||
|
||||
@IsString()
|
||||
@Optional()
|
||||
originalFileName?: string;
|
||||
|
||||
@IsString()
|
||||
@Optional()
|
||||
originalPath?: string;
|
||||
|
||||
@IsString()
|
||||
@Optional()
|
||||
resizePath?: string;
|
||||
|
||||
@IsString()
|
||||
@Optional()
|
||||
webpPath?: string;
|
||||
|
||||
@IsString()
|
||||
@Optional()
|
||||
encodedVideoPath?: string;
|
||||
|
||||
@IsString()
|
||||
@Optional()
|
||||
city?: string;
|
||||
|
||||
@IsString()
|
||||
@Optional()
|
||||
state?: string;
|
||||
|
||||
@IsString()
|
||||
@Optional()
|
||||
country?: string;
|
||||
|
||||
@IsString()
|
||||
@Optional()
|
||||
make?: string;
|
||||
|
||||
@IsString()
|
||||
@Optional()
|
||||
model?: string;
|
||||
|
||||
@IsString()
|
||||
@Optional()
|
||||
lensModel?: string;
|
||||
|
||||
@IsEnum(AssetOrder)
|
||||
@Optional()
|
||||
@ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder })
|
||||
order?: AssetOrder;
|
||||
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Type(() => Number)
|
||||
@Optional()
|
||||
page?: number;
|
||||
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Type(() => Number)
|
||||
@Optional()
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export class AssetBulkUpdateDto extends BulkIdsDto {
|
||||
@Optional()
|
||||
@IsBoolean()
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import { applyDecorators } from '@nestjs/common';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsArray, IsNotEmpty, IsOptional, IsString, IsUUID, ValidateIf, ValidationOptions } from 'class-validator';
|
||||
import { Transform, Type } from 'class-transformer';
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsDate,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
ValidateIf,
|
||||
ValidationOptions,
|
||||
} from 'class-validator';
|
||||
import { CronJob } from 'cron';
|
||||
import { basename, extname } from 'node:path';
|
||||
import sanitize from 'sanitize-filename';
|
||||
@@ -33,6 +44,22 @@ interface IValue {
|
||||
value?: string;
|
||||
}
|
||||
|
||||
export const QueryBoolean = ({ optional }: { optional?: boolean }) => {
|
||||
const decorators = [IsBoolean(), Transform(toBoolean)];
|
||||
if (optional) {
|
||||
decorators.push(Optional());
|
||||
}
|
||||
return applyDecorators(...decorators);
|
||||
};
|
||||
|
||||
export const QueryDate = ({ optional }: { optional?: boolean }) => {
|
||||
const decorators = [IsDate(), Type(() => Date)];
|
||||
if (optional) {
|
||||
decorators.push(Optional());
|
||||
}
|
||||
return applyDecorators(...decorators);
|
||||
};
|
||||
|
||||
export const toBoolean = ({ value }: IValue) => {
|
||||
if (value == 'true') {
|
||||
return true;
|
||||
|
||||
@@ -11,11 +11,58 @@ export interface AssetStatsOptions {
|
||||
}
|
||||
|
||||
export interface AssetSearchOptions {
|
||||
isVisible?: boolean;
|
||||
trashedBefore?: Date;
|
||||
id?: string;
|
||||
libraryId?: string;
|
||||
deviceAssetId?: string;
|
||||
deviceId?: string;
|
||||
ownerId?: string;
|
||||
type?: AssetType;
|
||||
order?: 'ASC' | 'DESC';
|
||||
checksum?: Buffer;
|
||||
|
||||
isArchived?: boolean;
|
||||
isEncoded?: boolean;
|
||||
isExternal?: boolean;
|
||||
isFavorite?: boolean;
|
||||
isMotion?: boolean;
|
||||
isOffline?: boolean;
|
||||
isReadOnly?: boolean;
|
||||
isVisible?: boolean;
|
||||
|
||||
withDeleted?: boolean;
|
||||
withStacked?: boolean;
|
||||
withExif?: boolean;
|
||||
withPeople?: boolean;
|
||||
|
||||
createdBefore?: Date;
|
||||
createdAfter?: Date;
|
||||
updatedBefore?: Date;
|
||||
updatedAfter?: Date;
|
||||
trashedBefore?: Date;
|
||||
trashedAfter?: Date;
|
||||
takenBefore?: Date;
|
||||
takenAfter?: Date;
|
||||
|
||||
originalFileName?: string;
|
||||
originalPath?: string;
|
||||
resizePath?: string;
|
||||
webpPath?: string;
|
||||
encodedVideoPath?: string;
|
||||
|
||||
city?: string;
|
||||
state?: string;
|
||||
country?: string;
|
||||
make?: string;
|
||||
model?: string;
|
||||
lensModel?: string;
|
||||
|
||||
/** defaults to 'DESC' */
|
||||
order?: 'ASC' | 'DESC';
|
||||
|
||||
/** defaults to 1 */
|
||||
page?: number;
|
||||
|
||||
/** defaults to 250 */
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export interface LivePhotoSearchOptions {
|
||||
@@ -127,4 +174,5 @@ export interface IAssetRepository {
|
||||
getTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise<AssetEntity[]>;
|
||||
upsertExif(exif: Partial<ExifEntity>): Promise<void>;
|
||||
upsertJobStatus(jobStatus: Partial<AssetJobStatusEntity>): Promise<void>;
|
||||
search(options: AssetSearchOptions): Promise<AssetEntity[]>;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
AlbumController,
|
||||
AppController,
|
||||
AssetController,
|
||||
AssetsController,
|
||||
AuditController,
|
||||
AuthController,
|
||||
JobController,
|
||||
@@ -41,6 +42,7 @@ import { ErrorInterceptor, FileUploadInterceptor } from './interceptors';
|
||||
],
|
||||
controllers: [
|
||||
ActivityController,
|
||||
AssetsController,
|
||||
AssetController,
|
||||
AssetControllerV1,
|
||||
AppController,
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
AssetIdsDto,
|
||||
AssetJobsDto,
|
||||
AssetResponseDto,
|
||||
AssetSearchDto,
|
||||
AssetService,
|
||||
AssetStatsDto,
|
||||
AssetStatsResponseDto,
|
||||
@@ -42,6 +43,19 @@ import { UseValidation, asStreamableFile } from '../app.utils';
|
||||
import { Route } from '../interceptors';
|
||||
import { UUIDParamDto } from './dto/uuid-param.dto';
|
||||
|
||||
@ApiTags('Asset')
|
||||
@Controller('assets')
|
||||
@Authenticated()
|
||||
@UseValidation()
|
||||
export class AssetsController {
|
||||
constructor(private service: AssetService) {}
|
||||
|
||||
@Get()
|
||||
searchAssets(@AuthUser() authUser: AuthUserDto, @Query() dto: AssetSearchDto): Promise<AssetResponseDto[]> {
|
||||
return this.service.search(authUser, dto);
|
||||
}
|
||||
}
|
||||
|
||||
@ApiTags('Asset')
|
||||
@Controller(Route.ASSET)
|
||||
@Authenticated()
|
||||
|
||||
@@ -18,12 +18,15 @@ import {
|
||||
} from '@app/domain';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import _ from 'lodash';
|
||||
import { DateTime } from 'luxon';
|
||||
import { And, FindOptionsRelations, FindOptionsWhere, In, IsNull, LessThan, Not, Repository } from 'typeorm';
|
||||
import { AssetEntity, AssetJobStatusEntity, AssetType, ExifEntity } from '../entities';
|
||||
import OptionalBetween from '../utils/optional-between.util';
|
||||
import { paginate } from '../utils/pagination.util';
|
||||
|
||||
const DEFAULT_SEARCH_SIZE = 250;
|
||||
|
||||
const truncateMap: Record<TimeBucketSize, string> = {
|
||||
[TimeBucketSize.DAY]: 'day',
|
||||
[TimeBucketSize.MONTH]: 'month',
|
||||
@@ -50,6 +53,134 @@ export class AssetRepository implements IAssetRepository {
|
||||
await this.jobStatusRepository.upsert(jobStatus, { conflictPaths: ['assetId'] });
|
||||
}
|
||||
|
||||
search(options: AssetSearchOptions): Promise<AssetEntity[]> {
|
||||
const {
|
||||
id,
|
||||
libraryId,
|
||||
deviceAssetId,
|
||||
type,
|
||||
checksum,
|
||||
ownerId,
|
||||
|
||||
isVisible,
|
||||
isFavorite,
|
||||
isExternal,
|
||||
isReadOnly,
|
||||
isOffline,
|
||||
isArchived,
|
||||
isMotion,
|
||||
isEncoded,
|
||||
|
||||
createdBefore,
|
||||
createdAfter,
|
||||
updatedBefore,
|
||||
updatedAfter,
|
||||
trashedBefore,
|
||||
trashedAfter,
|
||||
takenBefore,
|
||||
takenAfter,
|
||||
|
||||
originalFileName,
|
||||
originalPath,
|
||||
resizePath,
|
||||
webpPath,
|
||||
encodedVideoPath,
|
||||
|
||||
city,
|
||||
state,
|
||||
country,
|
||||
make,
|
||||
model,
|
||||
lensModel,
|
||||
|
||||
withDeleted: _withDeleted,
|
||||
withExif: _withExif,
|
||||
withStacked,
|
||||
withPeople,
|
||||
|
||||
order,
|
||||
} = options;
|
||||
|
||||
const withDeleted = _withDeleted ?? (trashedAfter !== undefined || trashedBefore !== undefined);
|
||||
|
||||
const page = Math.max(options.page || 1, 1);
|
||||
const size = Math.min(options.size || DEFAULT_SEARCH_SIZE, DEFAULT_SEARCH_SIZE);
|
||||
|
||||
const exifWhere = _.omitBy(
|
||||
{
|
||||
city,
|
||||
state,
|
||||
country,
|
||||
make,
|
||||
model,
|
||||
lensModel,
|
||||
},
|
||||
_.isUndefined,
|
||||
);
|
||||
|
||||
const withExif = Object.keys(exifWhere).length > 0 || _withExif;
|
||||
|
||||
const where = _.omitBy(
|
||||
{
|
||||
ownerId,
|
||||
id,
|
||||
libraryId,
|
||||
deviceAssetId,
|
||||
type,
|
||||
checksum,
|
||||
isVisible,
|
||||
isFavorite,
|
||||
isExternal,
|
||||
isReadOnly,
|
||||
isOffline,
|
||||
isArchived,
|
||||
livePhotoVideoId: isMotion && Not(IsNull()),
|
||||
originalFileName,
|
||||
originalPath,
|
||||
resizePath,
|
||||
webpPath,
|
||||
encodedVideoPath: encodedVideoPath ?? (isEncoded && Not(IsNull())),
|
||||
createdAt: OptionalBetween(createdAfter, createdBefore),
|
||||
updatedAt: OptionalBetween(updatedAfter, updatedBefore),
|
||||
deletedAt: OptionalBetween(trashedAfter, trashedBefore),
|
||||
fileCreatedAt: OptionalBetween(takenAfter, takenBefore),
|
||||
exifInfo: Object.keys(exifWhere).length > 0 ? exifWhere : undefined,
|
||||
},
|
||||
_.isUndefined,
|
||||
);
|
||||
|
||||
const builder = this.repository.createQueryBuilder('asset');
|
||||
|
||||
if (withExif) {
|
||||
if (_withExif) {
|
||||
builder.leftJoinAndSelect('asset.exifInfo', 'exifInfo');
|
||||
} else {
|
||||
builder.leftJoin('asset.exifInfo', 'exifInfo');
|
||||
}
|
||||
}
|
||||
|
||||
if (withPeople) {
|
||||
builder.leftJoinAndSelect('asset.faces', 'faces');
|
||||
builder.leftJoinAndSelect('faces.person', 'person');
|
||||
}
|
||||
|
||||
if (withStacked) {
|
||||
builder.leftJoinAndSelect('asset.stack', 'stack');
|
||||
}
|
||||
|
||||
if (withDeleted) {
|
||||
builder.withDeleted();
|
||||
}
|
||||
|
||||
builder
|
||||
.where(where)
|
||||
.skip(size * (page - 1))
|
||||
.take(size)
|
||||
.orderBy('asset.fileCreatedAt', order ?? 'DESC');
|
||||
|
||||
return builder.getMany();
|
||||
}
|
||||
|
||||
create(asset: AssetCreate): Promise<AssetEntity> {
|
||||
return this.repository.save(asset);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -31,5 +31,6 @@ export const newAssetRepositoryMock = (): jest.Mocked<IAssetRepository> => {
|
||||
getTimeBuckets: jest.fn(),
|
||||
restoreAll: jest.fn(),
|
||||
softDeleteAll: jest.fn(),
|
||||
search: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -5,25 +5,39 @@ import { INestApplication } from '@nestjs/common';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import * as fs from 'fs';
|
||||
import path from 'path';
|
||||
import { EntityTarget, ObjectLiteral } from 'typeorm';
|
||||
import { AppService } from '../src/microservices/app.service';
|
||||
|
||||
export const IMMICH_TEST_ASSET_PATH = process.env.IMMICH_TEST_ASSET_PATH;
|
||||
export const IMMICH_TEST_ASSET_TEMP_PATH = path.normalize(`${IMMICH_TEST_ASSET_PATH}/temp/`);
|
||||
|
||||
export interface ResetOptions {
|
||||
entities?: EntityTarget<ObjectLiteral>[];
|
||||
}
|
||||
export const db = {
|
||||
reset: async () => {
|
||||
reset: async (options?: ResetOptions) => {
|
||||
if (!dataSource.isInitialized) {
|
||||
await dataSource.initialize();
|
||||
}
|
||||
|
||||
await dataSource.transaction(async (em) => {
|
||||
for (const entity of dataSource.entityMetadatas) {
|
||||
if (entity.tableName === 'users') {
|
||||
const entities = options?.entities || [];
|
||||
const tableNames =
|
||||
entities.length > 0
|
||||
? entities.map((entity) => em.getRepository(entity).metadata.tableName)
|
||||
: dataSource.entityMetadatas.map((entity) => entity.tableName);
|
||||
|
||||
let deleteUsers = false;
|
||||
for (const tableName of tableNames) {
|
||||
if (tableName === 'users') {
|
||||
deleteUsers = true;
|
||||
continue;
|
||||
}
|
||||
await em.query(`DELETE FROM ${entity.tableName} CASCADE;`);
|
||||
await em.query(`DELETE FROM ${tableName} CASCADE;`);
|
||||
}
|
||||
if (deleteUsers) {
|
||||
await em.query(`DELETE FROM "users" CASCADE;`);
|
||||
}
|
||||
await em.query(`DELETE FROM "users" CASCADE;`);
|
||||
});
|
||||
},
|
||||
disconnect: async () => {
|
||||
|
||||
Generated
+624
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user