mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +00:00
feat: asset copy (#23172)
This commit is contained in:
parent
fdfb04d83c
commit
4ae7cadeae
20 changed files with 644 additions and 2 deletions
2
mobile/openapi/README.md
generated
2
mobile/openapi/README.md
generated
|
|
@ -97,6 +97,7 @@ Class | Method | HTTP request | Description
|
||||||
*AlbumsApi* | [**updateAlbumUser**](doc//AlbumsApi.md#updatealbumuser) | **PUT** /albums/{id}/user/{userId} |
|
*AlbumsApi* | [**updateAlbumUser**](doc//AlbumsApi.md#updatealbumuser) | **PUT** /albums/{id}/user/{userId} |
|
||||||
*AssetsApi* | [**checkBulkUpload**](doc//AssetsApi.md#checkbulkupload) | **POST** /assets/bulk-upload-check | checkBulkUpload
|
*AssetsApi* | [**checkBulkUpload**](doc//AssetsApi.md#checkbulkupload) | **POST** /assets/bulk-upload-check | checkBulkUpload
|
||||||
*AssetsApi* | [**checkExistingAssets**](doc//AssetsApi.md#checkexistingassets) | **POST** /assets/exist | checkExistingAssets
|
*AssetsApi* | [**checkExistingAssets**](doc//AssetsApi.md#checkexistingassets) | **POST** /assets/exist | checkExistingAssets
|
||||||
|
*AssetsApi* | [**copyAsset**](doc//AssetsApi.md#copyasset) | **PUT** /assets/copy |
|
||||||
*AssetsApi* | [**deleteAssetMetadata**](doc//AssetsApi.md#deleteassetmetadata) | **DELETE** /assets/{id}/metadata/{key} |
|
*AssetsApi* | [**deleteAssetMetadata**](doc//AssetsApi.md#deleteassetmetadata) | **DELETE** /assets/{id}/metadata/{key} |
|
||||||
*AssetsApi* | [**deleteAssets**](doc//AssetsApi.md#deleteassets) | **DELETE** /assets |
|
*AssetsApi* | [**deleteAssets**](doc//AssetsApi.md#deleteassets) | **DELETE** /assets |
|
||||||
*AssetsApi* | [**downloadAsset**](doc//AssetsApi.md#downloadasset) | **GET** /assets/{id}/original |
|
*AssetsApi* | [**downloadAsset**](doc//AssetsApi.md#downloadasset) | **GET** /assets/{id}/original |
|
||||||
|
|
@ -321,6 +322,7 @@ Class | Method | HTTP request | Description
|
||||||
- [AssetBulkUploadCheckItem](doc//AssetBulkUploadCheckItem.md)
|
- [AssetBulkUploadCheckItem](doc//AssetBulkUploadCheckItem.md)
|
||||||
- [AssetBulkUploadCheckResponseDto](doc//AssetBulkUploadCheckResponseDto.md)
|
- [AssetBulkUploadCheckResponseDto](doc//AssetBulkUploadCheckResponseDto.md)
|
||||||
- [AssetBulkUploadCheckResult](doc//AssetBulkUploadCheckResult.md)
|
- [AssetBulkUploadCheckResult](doc//AssetBulkUploadCheckResult.md)
|
||||||
|
- [AssetCopyDto](doc//AssetCopyDto.md)
|
||||||
- [AssetDeltaSyncDto](doc//AssetDeltaSyncDto.md)
|
- [AssetDeltaSyncDto](doc//AssetDeltaSyncDto.md)
|
||||||
- [AssetDeltaSyncResponseDto](doc//AssetDeltaSyncResponseDto.md)
|
- [AssetDeltaSyncResponseDto](doc//AssetDeltaSyncResponseDto.md)
|
||||||
- [AssetFaceCreateDto](doc//AssetFaceCreateDto.md)
|
- [AssetFaceCreateDto](doc//AssetFaceCreateDto.md)
|
||||||
|
|
|
||||||
1
mobile/openapi/lib/api.dart
generated
1
mobile/openapi/lib/api.dart
generated
|
|
@ -90,6 +90,7 @@ part 'model/asset_bulk_upload_check_dto.dart';
|
||||||
part 'model/asset_bulk_upload_check_item.dart';
|
part 'model/asset_bulk_upload_check_item.dart';
|
||||||
part 'model/asset_bulk_upload_check_response_dto.dart';
|
part 'model/asset_bulk_upload_check_response_dto.dart';
|
||||||
part 'model/asset_bulk_upload_check_result.dart';
|
part 'model/asset_bulk_upload_check_result.dart';
|
||||||
|
part 'model/asset_copy_dto.dart';
|
||||||
part 'model/asset_delta_sync_dto.dart';
|
part 'model/asset_delta_sync_dto.dart';
|
||||||
part 'model/asset_delta_sync_response_dto.dart';
|
part 'model/asset_delta_sync_response_dto.dart';
|
||||||
part 'model/asset_face_create_dto.dart';
|
part 'model/asset_face_create_dto.dart';
|
||||||
|
|
|
||||||
44
mobile/openapi/lib/api/assets_api.dart
generated
44
mobile/openapi/lib/api/assets_api.dart
generated
|
|
@ -128,6 +128,50 @@ class AssetsApi {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This endpoint requires the `asset.copy` permission.
|
||||||
|
///
|
||||||
|
/// Note: This method returns the HTTP [Response].
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [AssetCopyDto] assetCopyDto (required):
|
||||||
|
Future<Response> copyAssetWithHttpInfo(AssetCopyDto assetCopyDto,) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final apiPath = r'/assets/copy';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody = assetCopyDto;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>['application/json'];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
apiPath,
|
||||||
|
'PUT',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This endpoint requires the `asset.copy` permission.
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [AssetCopyDto] assetCopyDto (required):
|
||||||
|
Future<void> copyAsset(AssetCopyDto assetCopyDto,) async {
|
||||||
|
final response = await copyAssetWithHttpInfo(assetCopyDto,);
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// This endpoint requires the `asset.update` permission.
|
/// This endpoint requires the `asset.update` permission.
|
||||||
///
|
///
|
||||||
/// Note: This method returns the HTTP [Response].
|
/// Note: This method returns the HTTP [Response].
|
||||||
|
|
|
||||||
2
mobile/openapi/lib/api_client.dart
generated
2
mobile/openapi/lib/api_client.dart
generated
|
|
@ -234,6 +234,8 @@ class ApiClient {
|
||||||
return AssetBulkUploadCheckResponseDto.fromJson(value);
|
return AssetBulkUploadCheckResponseDto.fromJson(value);
|
||||||
case 'AssetBulkUploadCheckResult':
|
case 'AssetBulkUploadCheckResult':
|
||||||
return AssetBulkUploadCheckResult.fromJson(value);
|
return AssetBulkUploadCheckResult.fromJson(value);
|
||||||
|
case 'AssetCopyDto':
|
||||||
|
return AssetCopyDto.fromJson(value);
|
||||||
case 'AssetDeltaSyncDto':
|
case 'AssetDeltaSyncDto':
|
||||||
return AssetDeltaSyncDto.fromJson(value);
|
return AssetDeltaSyncDto.fromJson(value);
|
||||||
case 'AssetDeltaSyncResponseDto':
|
case 'AssetDeltaSyncResponseDto':
|
||||||
|
|
|
||||||
142
mobile/openapi/lib/model/asset_copy_dto.dart
generated
Normal file
142
mobile/openapi/lib/model/asset_copy_dto.dart
generated
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.18
|
||||||
|
|
||||||
|
// 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 AssetCopyDto {
|
||||||
|
/// Returns a new [AssetCopyDto] instance.
|
||||||
|
AssetCopyDto({
|
||||||
|
this.albums = true,
|
||||||
|
this.favorite = true,
|
||||||
|
this.sharedLinks = true,
|
||||||
|
this.sidecar = true,
|
||||||
|
required this.sourceId,
|
||||||
|
this.stack = true,
|
||||||
|
required this.targetId,
|
||||||
|
});
|
||||||
|
|
||||||
|
bool albums;
|
||||||
|
|
||||||
|
bool favorite;
|
||||||
|
|
||||||
|
bool sharedLinks;
|
||||||
|
|
||||||
|
bool sidecar;
|
||||||
|
|
||||||
|
String sourceId;
|
||||||
|
|
||||||
|
bool stack;
|
||||||
|
|
||||||
|
String targetId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is AssetCopyDto &&
|
||||||
|
other.albums == albums &&
|
||||||
|
other.favorite == favorite &&
|
||||||
|
other.sharedLinks == sharedLinks &&
|
||||||
|
other.sidecar == sidecar &&
|
||||||
|
other.sourceId == sourceId &&
|
||||||
|
other.stack == stack &&
|
||||||
|
other.targetId == targetId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(albums.hashCode) +
|
||||||
|
(favorite.hashCode) +
|
||||||
|
(sharedLinks.hashCode) +
|
||||||
|
(sidecar.hashCode) +
|
||||||
|
(sourceId.hashCode) +
|
||||||
|
(stack.hashCode) +
|
||||||
|
(targetId.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'AssetCopyDto[albums=$albums, favorite=$favorite, sharedLinks=$sharedLinks, sidecar=$sidecar, sourceId=$sourceId, stack=$stack, targetId=$targetId]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'albums'] = this.albums;
|
||||||
|
json[r'favorite'] = this.favorite;
|
||||||
|
json[r'sharedLinks'] = this.sharedLinks;
|
||||||
|
json[r'sidecar'] = this.sidecar;
|
||||||
|
json[r'sourceId'] = this.sourceId;
|
||||||
|
json[r'stack'] = this.stack;
|
||||||
|
json[r'targetId'] = this.targetId;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [AssetCopyDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static AssetCopyDto? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "AssetCopyDto");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return AssetCopyDto(
|
||||||
|
albums: mapValueOfType<bool>(json, r'albums') ?? true,
|
||||||
|
favorite: mapValueOfType<bool>(json, r'favorite') ?? true,
|
||||||
|
sharedLinks: mapValueOfType<bool>(json, r'sharedLinks') ?? true,
|
||||||
|
sidecar: mapValueOfType<bool>(json, r'sidecar') ?? true,
|
||||||
|
sourceId: mapValueOfType<String>(json, r'sourceId')!,
|
||||||
|
stack: mapValueOfType<bool>(json, r'stack') ?? true,
|
||||||
|
targetId: mapValueOfType<String>(json, r'targetId')!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<AssetCopyDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <AssetCopyDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = AssetCopyDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, AssetCopyDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, AssetCopyDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = AssetCopyDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of AssetCopyDto-objects as value to a dart map
|
||||||
|
static Map<String, List<AssetCopyDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<AssetCopyDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = AssetCopyDto.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'sourceId',
|
||||||
|
'targetId',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
3
mobile/openapi/lib/model/permission.dart
generated
3
mobile/openapi/lib/model/permission.dart
generated
|
|
@ -42,6 +42,7 @@ class Permission {
|
||||||
static const assetPeriodDownload = Permission._(r'asset.download');
|
static const assetPeriodDownload = Permission._(r'asset.download');
|
||||||
static const assetPeriodUpload = Permission._(r'asset.upload');
|
static const assetPeriodUpload = Permission._(r'asset.upload');
|
||||||
static const assetPeriodReplace = Permission._(r'asset.replace');
|
static const assetPeriodReplace = Permission._(r'asset.replace');
|
||||||
|
static const assetPeriodCopy = Permission._(r'asset.copy');
|
||||||
static const albumPeriodCreate = Permission._(r'album.create');
|
static const albumPeriodCreate = Permission._(r'album.create');
|
||||||
static const albumPeriodRead = Permission._(r'album.read');
|
static const albumPeriodRead = Permission._(r'album.read');
|
||||||
static const albumPeriodUpdate = Permission._(r'album.update');
|
static const albumPeriodUpdate = Permission._(r'album.update');
|
||||||
|
|
@ -174,6 +175,7 @@ class Permission {
|
||||||
assetPeriodDownload,
|
assetPeriodDownload,
|
||||||
assetPeriodUpload,
|
assetPeriodUpload,
|
||||||
assetPeriodReplace,
|
assetPeriodReplace,
|
||||||
|
assetPeriodCopy,
|
||||||
albumPeriodCreate,
|
albumPeriodCreate,
|
||||||
albumPeriodRead,
|
albumPeriodRead,
|
||||||
albumPeriodUpdate,
|
albumPeriodUpdate,
|
||||||
|
|
@ -341,6 +343,7 @@ class PermissionTypeTransformer {
|
||||||
case r'asset.download': return Permission.assetPeriodDownload;
|
case r'asset.download': return Permission.assetPeriodDownload;
|
||||||
case r'asset.upload': return Permission.assetPeriodUpload;
|
case r'asset.upload': return Permission.assetPeriodUpload;
|
||||||
case r'asset.replace': return Permission.assetPeriodReplace;
|
case r'asset.replace': return Permission.assetPeriodReplace;
|
||||||
|
case r'asset.copy': return Permission.assetPeriodCopy;
|
||||||
case r'album.create': return Permission.albumPeriodCreate;
|
case r'album.create': return Permission.albumPeriodCreate;
|
||||||
case r'album.read': return Permission.albumPeriodRead;
|
case r'album.read': return Permission.albumPeriodRead;
|
||||||
case r'album.update': return Permission.albumPeriodUpdate;
|
case r'album.update': return Permission.albumPeriodUpdate;
|
||||||
|
|
|
||||||
|
|
@ -1946,6 +1946,43 @@
|
||||||
"x-immich-permission": "asset.upload"
|
"x-immich-permission": "asset.upload"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/assets/copy": {
|
||||||
|
"put": {
|
||||||
|
"operationId": "copyAsset",
|
||||||
|
"parameters": [],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/AssetCopyDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Assets"
|
||||||
|
],
|
||||||
|
"x-immich-permission": "asset.copy",
|
||||||
|
"description": "This endpoint requires the `asset.copy` permission."
|
||||||
|
}
|
||||||
|
},
|
||||||
"/assets/device/{deviceId}": {
|
"/assets/device/{deviceId}": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Get all asset of a device that are in the database, ID only.",
|
"description": "Get all asset of a device that are in the database, ID only.",
|
||||||
|
|
@ -10651,6 +10688,43 @@
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"AssetCopyDto": {
|
||||||
|
"properties": {
|
||||||
|
"albums": {
|
||||||
|
"default": true,
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"favorite": {
|
||||||
|
"default": true,
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"sharedLinks": {
|
||||||
|
"default": true,
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"sidecar": {
|
||||||
|
"default": true,
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"sourceId": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"stack": {
|
||||||
|
"default": true,
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"targetId": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"sourceId",
|
||||||
|
"targetId"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"AssetDeltaSyncDto": {
|
"AssetDeltaSyncDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"updatedAfter": {
|
"updatedAfter": {
|
||||||
|
|
@ -13398,6 +13472,7 @@
|
||||||
"asset.download",
|
"asset.download",
|
||||||
"asset.upload",
|
"asset.upload",
|
||||||
"asset.replace",
|
"asset.replace",
|
||||||
|
"asset.copy",
|
||||||
"album.create",
|
"album.create",
|
||||||
"album.read",
|
"album.read",
|
||||||
"album.update",
|
"album.update",
|
||||||
|
|
|
||||||
|
|
@ -517,6 +517,15 @@ export type AssetBulkUploadCheckResult = {
|
||||||
export type AssetBulkUploadCheckResponseDto = {
|
export type AssetBulkUploadCheckResponseDto = {
|
||||||
results: AssetBulkUploadCheckResult[];
|
results: AssetBulkUploadCheckResult[];
|
||||||
};
|
};
|
||||||
|
export type AssetCopyDto = {
|
||||||
|
albums?: boolean;
|
||||||
|
favorite?: boolean;
|
||||||
|
sharedLinks?: boolean;
|
||||||
|
sidecar?: boolean;
|
||||||
|
sourceId: string;
|
||||||
|
stack?: boolean;
|
||||||
|
targetId: string;
|
||||||
|
};
|
||||||
export type CheckExistingAssetsDto = {
|
export type CheckExistingAssetsDto = {
|
||||||
deviceAssetIds: string[];
|
deviceAssetIds: string[];
|
||||||
deviceId: string;
|
deviceId: string;
|
||||||
|
|
@ -2256,6 +2265,18 @@ export function checkBulkUpload({ assetBulkUploadCheckDto }: {
|
||||||
body: assetBulkUploadCheckDto
|
body: assetBulkUploadCheckDto
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This endpoint requires the `asset.copy` permission.
|
||||||
|
*/
|
||||||
|
export function copyAsset({ assetCopyDto }: {
|
||||||
|
assetCopyDto: AssetCopyDto;
|
||||||
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
|
return oazapfts.ok(oazapfts.fetchText("/assets/copy", oazapfts.json({
|
||||||
|
...opts,
|
||||||
|
method: "PUT",
|
||||||
|
body: assetCopyDto
|
||||||
|
})));
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* getAllUserAssetsByDeviceId
|
* getAllUserAssetsByDeviceId
|
||||||
*/
|
*/
|
||||||
|
|
@ -4796,6 +4817,7 @@ export enum Permission {
|
||||||
AssetDownload = "asset.download",
|
AssetDownload = "asset.download",
|
||||||
AssetUpload = "asset.upload",
|
AssetUpload = "asset.upload",
|
||||||
AssetReplace = "asset.replace",
|
AssetReplace = "asset.replace",
|
||||||
|
AssetCopy = "asset.copy",
|
||||||
AlbumCreate = "album.create",
|
AlbumCreate = "album.create",
|
||||||
AlbumRead = "album.read",
|
AlbumRead = "album.read",
|
||||||
AlbumUpdate = "album.update",
|
AlbumUpdate = "album.update",
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||||
import {
|
import {
|
||||||
AssetBulkDeleteDto,
|
AssetBulkDeleteDto,
|
||||||
AssetBulkUpdateDto,
|
AssetBulkUpdateDto,
|
||||||
|
AssetCopyDto,
|
||||||
AssetJobsDto,
|
AssetJobsDto,
|
||||||
AssetMetadataResponseDto,
|
AssetMetadataResponseDto,
|
||||||
AssetMetadataRouteParams,
|
AssetMetadataRouteParams,
|
||||||
|
|
@ -90,6 +91,13 @@ export class AssetController {
|
||||||
return this.service.update(auth, id, dto);
|
return this.service.update(auth, id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Put('copy')
|
||||||
|
@Authenticated({ permission: Permission.AssetCopy })
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
copyAsset(@Auth() auth: AuthDto, @Body() dto: AssetCopyDto): Promise<void> {
|
||||||
|
return this.service.copy(auth, dto);
|
||||||
|
}
|
||||||
|
|
||||||
@Get(':id/metadata')
|
@Get(':id/metadata')
|
||||||
@Authenticated({ permission: Permission.AssetRead })
|
@Authenticated({ permission: Permission.AssetRead })
|
||||||
getAssetMetadata(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<AssetMetadataResponseDto[]> {
|
getAssetMetadata(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<AssetMetadataResponseDto[]> {
|
||||||
|
|
|
||||||
|
|
@ -186,6 +186,29 @@ export class AssetMetadataResponseDto {
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class AssetCopyDto {
|
||||||
|
@ValidateUUID()
|
||||||
|
sourceId!: string;
|
||||||
|
|
||||||
|
@ValidateUUID()
|
||||||
|
targetId!: string;
|
||||||
|
|
||||||
|
@ValidateBoolean({ optional: true, default: true })
|
||||||
|
sharedLinks?: boolean;
|
||||||
|
|
||||||
|
@ValidateBoolean({ optional: true, default: true })
|
||||||
|
albums?: boolean;
|
||||||
|
|
||||||
|
@ValidateBoolean({ optional: true, default: true })
|
||||||
|
sidecar?: boolean;
|
||||||
|
|
||||||
|
@ValidateBoolean({ optional: true, default: true })
|
||||||
|
stack?: boolean;
|
||||||
|
|
||||||
|
@ValidateBoolean({ optional: true, default: true })
|
||||||
|
favorite?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export const mapStats = (stats: AssetStats): AssetStatsResponseDto => {
|
export const mapStats = (stats: AssetStats): AssetStatsResponseDto => {
|
||||||
return {
|
return {
|
||||||
images: stats[AssetType.Image],
|
images: stats[AssetType.Image],
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,7 @@ export enum Permission {
|
||||||
AssetDownload = 'asset.download',
|
AssetDownload = 'asset.download',
|
||||||
AssetUpload = 'asset.upload',
|
AssetUpload = 'asset.upload',
|
||||||
AssetReplace = 'asset.replace',
|
AssetReplace = 'asset.replace',
|
||||||
|
AssetCopy = 'asset.copy',
|
||||||
|
|
||||||
AlbumCreate = 'album.create',
|
AlbumCreate = 'album.create',
|
||||||
AlbumRead = 'album.read',
|
AlbumRead = 'album.read',
|
||||||
|
|
|
||||||
|
|
@ -422,3 +422,15 @@ group by
|
||||||
"asset"."ownerId"
|
"asset"."ownerId"
|
||||||
order by
|
order by
|
||||||
"assetCount" desc
|
"assetCount" desc
|
||||||
|
|
||||||
|
-- AlbumRepository.copyAlbums
|
||||||
|
insert into
|
||||||
|
"album_asset"
|
||||||
|
select
|
||||||
|
"album_asset"."albumsId",
|
||||||
|
$1 as "assetsId"
|
||||||
|
from
|
||||||
|
"album_asset"
|
||||||
|
where
|
||||||
|
"album_asset"."assetsId" = $2
|
||||||
|
on conflict do nothing
|
||||||
|
|
|
||||||
13
server/src/queries/shared.link.asset.repository.sql
Normal file
13
server/src/queries/shared.link.asset.repository.sql
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
-- NOTE: This file is auto generated by ./sql-generator
|
||||||
|
|
||||||
|
-- SharedLinkAssetRepository.copySharedLinks
|
||||||
|
insert into
|
||||||
|
"shared_link_asset"
|
||||||
|
select
|
||||||
|
$1 as "assetsId",
|
||||||
|
"shared_link_asset"."sharedLinksId"
|
||||||
|
from
|
||||||
|
"shared_link_asset"
|
||||||
|
where
|
||||||
|
"shared_link_asset"."assetsId" = $2
|
||||||
|
on conflict do nothing
|
||||||
|
|
@ -153,3 +153,10 @@ from
|
||||||
left join "stack" on "stack"."id" = "asset"."stackId"
|
left join "stack" on "stack"."id" = "asset"."stackId"
|
||||||
where
|
where
|
||||||
"asset"."id" = $1
|
"asset"."id" = $1
|
||||||
|
|
||||||
|
-- StackRepository.merge
|
||||||
|
update "asset"
|
||||||
|
set
|
||||||
|
"stackId" = $1
|
||||||
|
where
|
||||||
|
"asset"."stackId" = $2
|
||||||
|
|
|
||||||
|
|
@ -397,4 +397,18 @@ export class AlbumRepository {
|
||||||
.orderBy('assetCount', 'desc')
|
.orderBy('assetCount', 'desc')
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [{ sourceAssetId: DummyValue.UUID, targetAssetId: DummyValue.UUID }] })
|
||||||
|
async copyAlbums({ sourceAssetId, targetAssetId }: { sourceAssetId: string; targetAssetId: string }) {
|
||||||
|
return this.db
|
||||||
|
.insertInto('album_asset')
|
||||||
|
.expression((eb) =>
|
||||||
|
eb
|
||||||
|
.selectFrom('album_asset')
|
||||||
|
.select((eb) => ['album_asset.albumsId', eb.val(targetAssetId).as('assetsId')])
|
||||||
|
.where('album_asset.assetsId', '=', sourceAssetId),
|
||||||
|
)
|
||||||
|
.onConflict((oc) => oc.doNothing())
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Kysely } from 'kysely';
|
import { Kysely } from 'kysely';
|
||||||
import { InjectKysely } from 'nestjs-kysely';
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
|
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||||
import { DB } from 'src/schema';
|
import { DB } from 'src/schema';
|
||||||
|
|
||||||
export class SharedLinkAssetRepository {
|
export class SharedLinkAssetRepository {
|
||||||
|
|
@ -15,4 +16,18 @@ export class SharedLinkAssetRepository {
|
||||||
|
|
||||||
return deleted.map((row) => row.assetsId);
|
return deleted.map((row) => row.assetsId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [{ sourceAssetId: DummyValue.UUID, targetAssetId: DummyValue.UUID }] })
|
||||||
|
async copySharedLinks({ sourceAssetId, targetAssetId }: { sourceAssetId: string; targetAssetId: string }) {
|
||||||
|
return this.db
|
||||||
|
.insertInto('shared_link_asset')
|
||||||
|
.expression((eb) =>
|
||||||
|
eb
|
||||||
|
.selectFrom('shared_link_asset')
|
||||||
|
.select((eb) => [eb.val(targetAssetId).as('assetsId'), 'shared_link_asset.sharedLinksId'])
|
||||||
|
.where('shared_link_asset.assetsId', '=', sourceAssetId),
|
||||||
|
)
|
||||||
|
.onConflict((oc) => oc.doNothing())
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -162,4 +162,9 @@ export class StackRepository {
|
||||||
.where('asset.id', '=', assetId)
|
.where('asset.id', '=', assetId)
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [{ sourceId: DummyValue.UUID, targetId: DummyValue.UUID }] })
|
||||||
|
merge({ sourceId, targetId }: { sourceId: string; targetId: string }) {
|
||||||
|
return this.db.updateTable('asset').set({ stackId: targetId }).where('asset.stackId', '=', sourceId).execute();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { AssetResponseDto, MapAsset, SanitizedAssetResponseDto, mapAsset } from
|
||||||
import {
|
import {
|
||||||
AssetBulkDeleteDto,
|
AssetBulkDeleteDto,
|
||||||
AssetBulkUpdateDto,
|
AssetBulkUpdateDto,
|
||||||
|
AssetCopyDto,
|
||||||
AssetJobName,
|
AssetJobName,
|
||||||
AssetJobsDto,
|
AssetJobsDto,
|
||||||
AssetMetadataResponseDto,
|
AssetMetadataResponseDto,
|
||||||
|
|
@ -183,6 +184,84 @@ export class AssetService extends BaseService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async copy(
|
||||||
|
auth: AuthDto,
|
||||||
|
{
|
||||||
|
sourceId,
|
||||||
|
targetId,
|
||||||
|
albums = true,
|
||||||
|
sidecar = true,
|
||||||
|
sharedLinks = true,
|
||||||
|
stack = true,
|
||||||
|
favorite = true,
|
||||||
|
}: AssetCopyDto,
|
||||||
|
) {
|
||||||
|
await this.requireAccess({ auth, permission: Permission.AssetCopy, ids: [sourceId, targetId] });
|
||||||
|
const sourceAsset = await this.assetRepository.getById(sourceId);
|
||||||
|
const targetAsset = await this.assetRepository.getById(targetId);
|
||||||
|
|
||||||
|
if (!sourceAsset || !targetAsset) {
|
||||||
|
throw new BadRequestException('Both assets must exist');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceId === targetId) {
|
||||||
|
throw new BadRequestException('Source and target id must be distinct');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (albums) {
|
||||||
|
await this.albumRepository.copyAlbums({ sourceAssetId: sourceId, targetAssetId: targetId });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sharedLinks) {
|
||||||
|
await this.sharedLinkAssetRepository.copySharedLinks({ sourceAssetId: sourceId, targetAssetId: targetId });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stack) {
|
||||||
|
await this.copyStack(sourceAsset, targetAsset);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (favorite) {
|
||||||
|
await this.assetRepository.update({ id: targetId, isFavorite: sourceAsset.isFavorite });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sidecar) {
|
||||||
|
await this.copySidecar(sourceAsset, targetAsset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async copyStack(
|
||||||
|
sourceAsset: { id: string; stackId: string | null },
|
||||||
|
targetAsset: { id: string; stackId: string | null },
|
||||||
|
) {
|
||||||
|
if (!sourceAsset.stackId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetAsset.stackId) {
|
||||||
|
await this.stackRepository.merge({ sourceId: sourceAsset.stackId, targetId: targetAsset.stackId });
|
||||||
|
await this.stackRepository.delete(sourceAsset.stackId);
|
||||||
|
} else {
|
||||||
|
await this.assetRepository.update({ id: targetAsset.id, stackId: sourceAsset.stackId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async copySidecar(
|
||||||
|
targetAsset: { sidecarPath: string | null },
|
||||||
|
sourceAsset: { id: string; sidecarPath: string | null; originalPath: string },
|
||||||
|
) {
|
||||||
|
if (!targetAsset.sidecarPath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceAsset.sidecarPath) {
|
||||||
|
await this.storageRepository.unlink(sourceAsset.sidecarPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.storageRepository.copyFile(targetAsset.sidecarPath, `${sourceAsset.originalPath}.xmp`);
|
||||||
|
await this.assetRepository.update({ id: sourceAsset.id, sidecarPath: `${sourceAsset.originalPath}.xmp` });
|
||||||
|
await this.jobRepository.queue({ name: JobName.AssetExtractMetadata, data: { id: sourceAsset.id } });
|
||||||
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.AssetDeleteCheck, queue: QueueName.BackgroundTask })
|
@OnJob({ name: JobName.AssetDeleteCheck, queue: QueueName.BackgroundTask })
|
||||||
async handleAssetDeletionCheck(): Promise<JobStatus> {
|
async handleAssetDeletionCheck(): Promise<JobStatus> {
|
||||||
const config = await this.getConfig({ withCache: false });
|
const config = await this.getConfig({ withCache: false });
|
||||||
|
|
|
||||||
|
|
@ -153,6 +153,10 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe
|
||||||
return await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission);
|
return await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case Permission.AssetCopy: {
|
||||||
|
return await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission);
|
||||||
|
}
|
||||||
|
|
||||||
case Permission.AlbumRead: {
|
case Permission.AlbumRead: {
|
||||||
const isOwner = await access.album.checkOwnerAccess(auth.user.id, ids);
|
const isOwner = await access.album.checkOwnerAccess(auth.user.id, ids);
|
||||||
const isShared = await access.album.checkSharedAlbumAccess(
|
const isShared = await access.album.checkSharedAlbumAccess(
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,14 @@
|
||||||
import { Kysely } from 'kysely';
|
import { Kysely } from 'kysely';
|
||||||
|
import { JobName, SharedLinkType } from 'src/enum';
|
||||||
|
import { AccessRepository } from 'src/repositories/access.repository';
|
||||||
|
import { AlbumRepository } from 'src/repositories/album.repository';
|
||||||
import { AssetRepository } from 'src/repositories/asset.repository';
|
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||||
|
import { JobRepository } from 'src/repositories/job.repository';
|
||||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
|
import { SharedLinkAssetRepository } from 'src/repositories/shared-link-asset.repository';
|
||||||
|
import { SharedLinkRepository } from 'src/repositories/shared-link.repository';
|
||||||
|
import { StackRepository } from 'src/repositories/stack.repository';
|
||||||
|
import { StorageRepository } from 'src/repositories/storage.repository';
|
||||||
import { DB } from 'src/schema';
|
import { DB } from 'src/schema';
|
||||||
import { AssetService } from 'src/services/asset.service';
|
import { AssetService } from 'src/services/asset.service';
|
||||||
import { newMediumService } from 'test/medium.factory';
|
import { newMediumService } from 'test/medium.factory';
|
||||||
|
|
@ -12,8 +20,8 @@ let defaultDatabase: Kysely<DB>;
|
||||||
const setup = (db?: Kysely<DB>) => {
|
const setup = (db?: Kysely<DB>) => {
|
||||||
return newMediumService(AssetService, {
|
return newMediumService(AssetService, {
|
||||||
database: db || defaultDatabase,
|
database: db || defaultDatabase,
|
||||||
real: [AssetRepository],
|
real: [AssetRepository, AlbumRepository, AccessRepository, SharedLinkAssetRepository, StackRepository],
|
||||||
mock: [LoggingRepository],
|
mock: [LoggingRepository, JobRepository, StorageRepository],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -32,4 +40,166 @@ describe(AssetService.name, () => {
|
||||||
await expect(sut.getStatistics(auth, {})).resolves.toEqual({ images: 1, total: 1, videos: 0 });
|
await expect(sut.getStatistics(auth, {})).resolves.toEqual({ images: 1, total: 1, videos: 0 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('copy', () => {
|
||||||
|
it('should copy albums', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
const albumRepo = ctx.get(AlbumRepository);
|
||||||
|
|
||||||
|
const { user } = await ctx.newUser();
|
||||||
|
const { asset: oldAsset } = await ctx.newAsset({ ownerId: user.id });
|
||||||
|
const { asset: newAsset } = await ctx.newAsset({ ownerId: user.id });
|
||||||
|
|
||||||
|
const { album } = await ctx.newAlbum({ ownerId: user.id });
|
||||||
|
await ctx.newAlbumAsset({ albumId: album.id, assetId: oldAsset.id });
|
||||||
|
|
||||||
|
const auth = factory.auth({ user: { id: user.id } });
|
||||||
|
await sut.copy(auth, { sourceId: oldAsset.id, targetId: newAsset.id });
|
||||||
|
|
||||||
|
await expect(albumRepo.getAssetIds(album.id, [oldAsset.id, newAsset.id])).resolves.toEqual(
|
||||||
|
new Set([oldAsset.id, newAsset.id]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should copy shared links', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
const sharedLinkRepo = ctx.get(SharedLinkRepository);
|
||||||
|
|
||||||
|
const { user } = await ctx.newUser();
|
||||||
|
const { asset: oldAsset } = await ctx.newAsset({ ownerId: user.id });
|
||||||
|
const { asset: newAsset } = await ctx.newAsset({ ownerId: user.id });
|
||||||
|
|
||||||
|
await ctx.newExif({ assetId: oldAsset.id, description: 'foo' });
|
||||||
|
await ctx.newExif({ assetId: newAsset.id, description: 'bar' });
|
||||||
|
|
||||||
|
const { id: sharedLinkId } = await sharedLinkRepo.create({
|
||||||
|
allowUpload: false,
|
||||||
|
key: Buffer.from('123'),
|
||||||
|
type: SharedLinkType.Individual,
|
||||||
|
userId: user.id,
|
||||||
|
assetIds: [oldAsset.id],
|
||||||
|
});
|
||||||
|
|
||||||
|
const auth = factory.auth({ user: { id: user.id } });
|
||||||
|
|
||||||
|
await sut.copy(auth, { sourceId: oldAsset.id, targetId: newAsset.id });
|
||||||
|
await expect(sharedLinkRepo.get(user.id, sharedLinkId)).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
assets: [expect.objectContaining({ id: oldAsset.id }), expect.objectContaining({ id: newAsset.id })],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should merge stacks', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
const stackRepo = ctx.get(StackRepository);
|
||||||
|
|
||||||
|
const { user } = await ctx.newUser();
|
||||||
|
const { asset: oldAsset } = await ctx.newAsset({ ownerId: user.id });
|
||||||
|
const { asset: asset1 } = await ctx.newAsset({ ownerId: user.id });
|
||||||
|
|
||||||
|
const { asset: newAsset } = await ctx.newAsset({ ownerId: user.id });
|
||||||
|
const { asset: asset2 } = await ctx.newAsset({ ownerId: user.id });
|
||||||
|
|
||||||
|
await ctx.newExif({ assetId: oldAsset.id, description: 'foo' });
|
||||||
|
await ctx.newExif({ assetId: asset1.id, description: 'bar' });
|
||||||
|
await ctx.newExif({ assetId: newAsset.id, description: 'bar' });
|
||||||
|
await ctx.newExif({ assetId: asset2.id, description: 'foo' });
|
||||||
|
|
||||||
|
await ctx.newStack({ ownerId: user.id }, [oldAsset.id, asset1.id]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
stack: { id: newStackId },
|
||||||
|
} = await ctx.newStack({ ownerId: user.id }, [newAsset.id, asset2.id]);
|
||||||
|
|
||||||
|
const auth = factory.auth({ user: { id: user.id } });
|
||||||
|
await sut.copy(auth, { sourceId: oldAsset.id, targetId: newAsset.id });
|
||||||
|
|
||||||
|
await expect(stackRepo.getById(oldAsset.id)).resolves.toEqual(undefined);
|
||||||
|
|
||||||
|
const newStack = await stackRepo.getById(newStackId);
|
||||||
|
expect(newStack).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
primaryAssetId: newAsset.id,
|
||||||
|
assets: expect.arrayContaining([expect.objectContaining({ id: asset2.id })]),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(newStack!.assets.length).toEqual(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should copy stack', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
const stackRepo = ctx.get(StackRepository);
|
||||||
|
|
||||||
|
const { user } = await ctx.newUser();
|
||||||
|
const { asset: oldAsset } = await ctx.newAsset({ ownerId: user.id });
|
||||||
|
const { asset: asset1 } = await ctx.newAsset({ ownerId: user.id });
|
||||||
|
|
||||||
|
const { asset: newAsset } = await ctx.newAsset({ ownerId: user.id });
|
||||||
|
|
||||||
|
await ctx.newExif({ assetId: oldAsset.id, description: 'foo' });
|
||||||
|
await ctx.newExif({ assetId: asset1.id, description: 'bar' });
|
||||||
|
await ctx.newExif({ assetId: newAsset.id, description: 'bar' });
|
||||||
|
|
||||||
|
const {
|
||||||
|
stack: { id: stackId },
|
||||||
|
} = await ctx.newStack({ ownerId: user.id }, [oldAsset.id, asset1.id]);
|
||||||
|
|
||||||
|
const auth = factory.auth({ user: { id: user.id } });
|
||||||
|
await sut.copy(auth, { sourceId: oldAsset.id, targetId: newAsset.id });
|
||||||
|
|
||||||
|
const stack = await stackRepo.getById(stackId);
|
||||||
|
expect(stack).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
primaryAssetId: oldAsset.id,
|
||||||
|
assets: expect.arrayContaining([expect.objectContaining({ id: newAsset.id })]),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(stack!.assets.length).toEqual(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should copy favorite status', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
const assetRepo = ctx.get(AssetRepository);
|
||||||
|
|
||||||
|
const { user } = await ctx.newUser();
|
||||||
|
const { asset: oldAsset } = await ctx.newAsset({ ownerId: user.id, isFavorite: true });
|
||||||
|
const { asset: newAsset } = await ctx.newAsset({ ownerId: user.id });
|
||||||
|
|
||||||
|
await ctx.newExif({ assetId: oldAsset.id, description: 'foo' });
|
||||||
|
await ctx.newExif({ assetId: newAsset.id, description: 'bar' });
|
||||||
|
|
||||||
|
const auth = factory.auth({ user: { id: user.id } });
|
||||||
|
await sut.copy(auth, { sourceId: oldAsset.id, targetId: newAsset.id });
|
||||||
|
|
||||||
|
await expect(assetRepo.getById(newAsset.id)).resolves.toEqual(expect.objectContaining({ isFavorite: true }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should copy sidecar file', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
const storageRepo = ctx.getMock(StorageRepository);
|
||||||
|
const jobRepo = ctx.getMock(JobRepository);
|
||||||
|
|
||||||
|
storageRepo.copyFile.mockResolvedValue();
|
||||||
|
jobRepo.queue.mockResolvedValue();
|
||||||
|
|
||||||
|
const { user } = await ctx.newUser();
|
||||||
|
const { asset: oldAsset } = await ctx.newAsset({ ownerId: user.id, sidecarPath: '/path/to/my/sidecar.xmp' });
|
||||||
|
const { asset: newAsset } = await ctx.newAsset({ ownerId: user.id });
|
||||||
|
|
||||||
|
await ctx.newExif({ assetId: oldAsset.id, description: 'foo' });
|
||||||
|
await ctx.newExif({ assetId: newAsset.id, description: 'bar' });
|
||||||
|
|
||||||
|
const auth = factory.auth({ user: { id: user.id } });
|
||||||
|
|
||||||
|
await sut.copy(auth, { sourceId: oldAsset.id, targetId: newAsset.id });
|
||||||
|
|
||||||
|
expect(storageRepo.copyFile).toHaveBeenCalledWith('/path/to/my/sidecar.xmp', `${newAsset.originalPath}.xmp`);
|
||||||
|
|
||||||
|
expect(jobRepo.queue).toHaveBeenCalledWith({
|
||||||
|
name: JobName.AssetExtractMetadata,
|
||||||
|
data: { id: newAsset.id },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue