diff --git a/README.md b/README.md index fce1a0e6d..53488e0ff 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,7 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM | Global Map | No | Yes | | Partner Sharing | Yes | Yes | | Facial recognition and clustering | No | Yes | +| Offline support | Yes | No | # Support the project diff --git a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart index 7f9697d9a..38a79abd7 100644 --- a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart +++ b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart @@ -52,7 +52,7 @@ class GalleryViewerPage extends HookConsumerWidget { final showAppBar = useState(true); final isPlayingMotionVideo = useState(false); final isPlayingVideo = useState(false); - late Offset localPosition; + Offset? localPosition; final authToken = 'Bearer ${Store.get(StoreKey.accessToken)}'; final currentIndex = useState(initialIndex); final currentAsset = loadAsset(currentIndex.value); @@ -246,8 +246,13 @@ class GalleryViewerPage extends HookConsumerWidget { return; } + // Guard [localPosition] null + if (localPosition == null) { + return; + } + // Check for delta from initial down point - final d = details.localPosition - localPosition; + final d = details.localPosition - localPosition!; // If the magnitude of the dx swipe is large, we probably didn't mean to go down if (d.dx.abs() > dxThreshold) { return; @@ -367,6 +372,26 @@ class GalleryViewerPage extends HookConsumerWidget { ); } + ImageProvider imageProvider(Asset asset) { + if (asset.isLocal) { + return localImageProvider(asset); + } else { + if (isLoadOriginal.value) { + return originalImageProvider(asset); + } else if (isLoadPreview.value) { + return remoteThumbnailImageProvider( + asset, + api.ThumbnailFormat.JPEG, + ); + } else { + return remoteThumbnailImageProvider( + asset, + api.ThumbnailFormat.WEBP, + ); + } + } + } + return Scaffold( backgroundColor: Colors.black, body: WillPopScope( @@ -460,26 +485,9 @@ class GalleryViewerPage extends HookConsumerWidget { : null, builder: (context, index) { final asset = loadAsset(index); + final ImageProvider provider = imageProvider(asset); + if (asset.isImage && !isPlayingMotionVideo.value) { - // Show photo - final ImageProvider provider; - if (asset.isLocal) { - provider = localImageProvider(asset); - } else { - if (isLoadOriginal.value) { - provider = originalImageProvider(asset); - } else if (isLoadPreview.value) { - provider = remoteThumbnailImageProvider( - asset, - api.ThumbnailFormat.JPEG, - ); - } else { - provider = remoteThumbnailImageProvider( - asset, - api.ThumbnailFormat.WEBP, - ); - } - } return PhotoViewGalleryPageOptions( onDragStart: (_, details, __) => localPosition = details.localPosition, @@ -512,18 +520,23 @@ class GalleryViewerPage extends HookConsumerWidget { maxScale: 1.0, minScale: 1.0, basePosition: Alignment.bottomCenter, - child: SafeArea( - child: VideoViewerPage( - onPlaying: () => isPlayingVideo.value = true, - onPaused: () => isPlayingVideo.value = false, - asset: asset, - isMotionVideo: isPlayingMotionVideo.value, - onVideoEnded: () { - if (isPlayingMotionVideo.value) { - isPlayingMotionVideo.value = false; - } - }, + child: VideoViewerPage( + onPlaying: () => isPlayingVideo.value = true, + onPaused: () => isPlayingVideo.value = false, + asset: asset, + isMotionVideo: isPlayingMotionVideo.value, + placeholder: Image( + image: provider, + fit: BoxFit.fitWidth, + height: MediaQuery.of(context).size.height, + width: MediaQuery.of(context).size.width, + alignment: Alignment.center, ), + onVideoEnded: () { + if (isPlayingMotionVideo.value) { + isPlayingMotionVideo.value = false; + } + }, ), ); } diff --git a/mobile/lib/modules/asset_viewer/views/video_viewer_page.dart b/mobile/lib/modules/asset_viewer/views/video_viewer_page.dart index 37f6f9877..fb23fd3ef 100644 --- a/mobile/lib/modules/asset_viewer/views/video_viewer_page.dart +++ b/mobile/lib/modules/asset_viewer/views/video_viewer_page.dart @@ -15,6 +15,7 @@ import 'package:video_player/video_player.dart'; class VideoViewerPage extends HookConsumerWidget { final Asset asset; final bool isMotionVideo; + final Widget? placeholder; final VoidCallback onVideoEnded; final VoidCallback? onPlaying; final VoidCallback? onPaused; @@ -26,6 +27,7 @@ class VideoViewerPage extends HookConsumerWidget { required this.onVideoEnded, this.onPlaying, this.onPaused, + this.placeholder, }) : super(key: key); @override @@ -66,6 +68,7 @@ class VideoViewerPage extends HookConsumerWidget { onVideoEnded: onVideoEnded, onPaused: onPaused, onPlaying: onPlaying, + placeholder: placeholder, ), if (downloadAssetStatus == DownloadAssetStatus.loading) const Center( @@ -95,6 +98,10 @@ class VideoPlayer extends StatefulWidget { final Function()? onPlaying; final Function()? onPaused; + /// The placeholder to show while the video is loading + /// usually, a thumbnail of the video + final Widget? placeholder; + const VideoPlayer({ Key? key, this.url, @@ -104,6 +111,7 @@ class VideoPlayer extends StatefulWidget { required this.isMotionVideo, this.onPlaying, this.onPaused, + this.placeholder, }) : super(key: key); @override @@ -186,12 +194,18 @@ class _VideoPlayerState extends State { ), ); } else { - return const Center( - child: SizedBox( - width: 75, - height: 75, - child: CircularProgressIndicator.adaptive( - strokeWidth: 2, + return SizedBox( + height: MediaQuery.of(context).size.height, + width: MediaQuery.of(context).size.width, + child: Center( + child: Stack( + children: [ + if (widget.placeholder != null) + widget.placeholder!, + const Center( + child: ImmichLoadingIndicator(), + ), + ], ), ), ); diff --git a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart index 45f5b2ad4..b628273bf 100644 --- a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart +++ b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart @@ -1,3 +1,6 @@ +import 'dart:math'; + +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -51,6 +54,12 @@ class ImmichAssetGrid extends HookConsumerWidget { final enableHeroAnimations = useState(false); final transitionDuration = ModalRoute.of(context)?.transitionDuration; + final perRow = useState( + assetsPerRow ?? settings.getSetting(AppSettingsEnum.tilesPerRow)!, + ); + final scaleFactor = useState(7.0 - perRow.value); + final baseScaleFactor = useState(7.0 - perRow.value); + useEffect( () { // Wait for transition to complete, then re-enable @@ -80,22 +89,43 @@ class ImmichAssetGrid extends HookConsumerWidget { onWillPop: onWillPop, child: HeroMode( enabled: enableHeroAnimations.value, - child: ImmichAssetGridView( - onRefresh: onRefresh, - assetsPerRow: assetsPerRow ?? - settings.getSetting(AppSettingsEnum.tilesPerRow), - listener: listener, - showStorageIndicator: showStorageIndicator ?? - settings.getSetting(AppSettingsEnum.storageIndicator), - renderList: renderList, - margin: margin, - selectionActive: selectionActive, - preselectedAssets: preselectedAssets, - canDeselect: canDeselect, - dynamicLayout: dynamicLayout ?? - settings.getSetting(AppSettingsEnum.dynamicLayout), - showMultiSelectIndicator: showMultiSelectIndicator, - visibleItemsListener: visibleItemsListener, + child: RawGestureDetector( + gestures: { + CustomScaleGestureRecognizer: + GestureRecognizerFactoryWithHandlers< + CustomScaleGestureRecognizer>( + () => CustomScaleGestureRecognizer(), + (CustomScaleGestureRecognizer scale) { + scale.onStart = (details) { + baseScaleFactor.value = scaleFactor.value; + }; + + scale.onUpdate = (details) { + scaleFactor.value = + max(min(5.0, baseScaleFactor.value * details.scale), 1.0); + if (7 - scaleFactor.value.toInt() != perRow.value) { + perRow.value = 7 - scaleFactor.value.toInt(); + } + }; + scale.onEnd = (details) {}; + }) + }, + child: ImmichAssetGridView( + onRefresh: onRefresh, + assetsPerRow: perRow.value, + listener: listener, + showStorageIndicator: showStorageIndicator ?? + settings.getSetting(AppSettingsEnum.storageIndicator), + renderList: renderList, + margin: margin, + selectionActive: selectionActive, + preselectedAssets: preselectedAssets, + canDeselect: canDeselect, + dynamicLayout: dynamicLayout ?? + settings.getSetting(AppSettingsEnum.dynamicLayout), + showMultiSelectIndicator: showMultiSelectIndicator, + visibleItemsListener: visibleItemsListener, + ), ), ), ); @@ -113,3 +143,11 @@ class ImmichAssetGrid extends HookConsumerWidget { ); } } + +/// accepts a gesture even though it should reject it (because child won) +class CustomScaleGestureRecognizer extends ScaleGestureRecognizer { + @override + void rejectGesture(int pointer) { + acceptGesture(pointer); + } +} diff --git a/mobile/lib/utils/files_helper.dart b/mobile/lib/utils/files_helper.dart index a379bfc92..ec93cd097 100644 --- a/mobile/lib/utils/files_helper.dart +++ b/mobile/lib/utils/files_helper.dart @@ -134,6 +134,15 @@ class FileHelper { case 'cin': return {"type": "image", "subType": "x-phantom-cin"}; + case 'jxl': + return {"type": "image", "subType": "jxl"}; + + case 'mts': + return {"type": "video", "subType": "mp2t"}; + + case 'm2ts': + return {"type": "video", "subType": "mp2t"}; + default: return {"type": "unsupport", "subType": "unsupport"}; } diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index 0d0d73276..e351e3c65 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -37,8 +37,6 @@ doc/CheckDuplicateAssetResponseDto.md doc/CheckExistingAssetsDto.md doc/CheckExistingAssetsResponseDto.md doc/CreateAlbumDto.md -doc/CreateAlbumShareLinkDto.md -doc/CreateAssetsShareLinkDto.md doc/CreateProfileImageResponseDto.md doc/CreateTagDto.md doc/CreateUserDto.md @@ -48,10 +46,10 @@ doc/DeleteAssetDto.md doc/DeleteAssetResponseDto.md doc/DeleteAssetStatus.md doc/DownloadFilesDto.md -doc/EditSharedLinkDto.md doc/ExifResponseDto.md doc/GetAssetByTimeBucketDto.md doc/GetAssetCountByTimeBucketDto.md +doc/ImportAssetDto.md doc/JobApi.md doc/JobCommand.md doc/JobCommandDto.md @@ -89,7 +87,9 @@ doc/ServerInfoResponseDto.md doc/ServerPingResponse.md doc/ServerStatsResponseDto.md doc/ServerVersionReponseDto.md -doc/ShareApi.md +doc/SharedLinkApi.md +doc/SharedLinkCreateDto.md +doc/SharedLinkEditDto.md doc/SharedLinkResponseDto.md doc/SharedLinkType.md doc/SignUpDto.md @@ -128,7 +128,7 @@ lib/api/partner_api.dart lib/api/person_api.dart lib/api/search_api.dart lib/api/server_info_api.dart -lib/api/share_api.dart +lib/api/shared_link_api.dart lib/api/system_config_api.dart lib/api/tag_api.dart lib/api/user_api.dart @@ -170,8 +170,6 @@ lib/model/check_duplicate_asset_response_dto.dart lib/model/check_existing_assets_dto.dart lib/model/check_existing_assets_response_dto.dart lib/model/create_album_dto.dart -lib/model/create_album_share_link_dto.dart -lib/model/create_assets_share_link_dto.dart lib/model/create_profile_image_response_dto.dart lib/model/create_tag_dto.dart lib/model/create_user_dto.dart @@ -181,10 +179,10 @@ lib/model/delete_asset_dto.dart lib/model/delete_asset_response_dto.dart lib/model/delete_asset_status.dart lib/model/download_files_dto.dart -lib/model/edit_shared_link_dto.dart lib/model/exif_response_dto.dart lib/model/get_asset_by_time_bucket_dto.dart lib/model/get_asset_count_by_time_bucket_dto.dart +lib/model/import_asset_dto.dart lib/model/job_command.dart lib/model/job_command_dto.dart lib/model/job_counts_dto.dart @@ -216,6 +214,8 @@ lib/model/server_info_response_dto.dart lib/model/server_ping_response.dart lib/model/server_stats_response_dto.dart lib/model/server_version_reponse_dto.dart +lib/model/shared_link_create_dto.dart +lib/model/shared_link_edit_dto.dart lib/model/shared_link_response_dto.dart lib/model/shared_link_type.dart lib/model/sign_up_dto.dart @@ -274,8 +274,6 @@ test/check_duplicate_asset_response_dto_test.dart test/check_existing_assets_dto_test.dart test/check_existing_assets_response_dto_test.dart test/create_album_dto_test.dart -test/create_album_share_link_dto_test.dart -test/create_assets_share_link_dto_test.dart test/create_profile_image_response_dto_test.dart test/create_tag_dto_test.dart test/create_user_dto_test.dart @@ -285,10 +283,10 @@ test/delete_asset_dto_test.dart test/delete_asset_response_dto_test.dart test/delete_asset_status_test.dart test/download_files_dto_test.dart -test/edit_shared_link_dto_test.dart test/exif_response_dto_test.dart test/get_asset_by_time_bucket_dto_test.dart test/get_asset_count_by_time_bucket_dto_test.dart +test/import_asset_dto_test.dart test/job_api_test.dart test/job_command_dto_test.dart test/job_command_test.dart @@ -326,7 +324,9 @@ test/server_info_response_dto_test.dart test/server_ping_response_test.dart test/server_stats_response_dto_test.dart test/server_version_reponse_dto_test.dart -test/share_api_test.dart +test/shared_link_api_test.dart +test/shared_link_create_dto_test.dart +test/shared_link_edit_dto_test.dart test/shared_link_response_dto_test.dart test/shared_link_type_test.dart test/sign_up_dto_test.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 695db07bf..1e78809c3 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -80,7 +80,6 @@ Class | Method | HTTP request | Description *AlbumApi* | [**addAssetsToAlbum**](doc//AlbumApi.md#addassetstoalbum) | **PUT** /album/{id}/assets | *AlbumApi* | [**addUsersToAlbum**](doc//AlbumApi.md#adduserstoalbum) | **PUT** /album/{id}/users | *AlbumApi* | [**createAlbum**](doc//AlbumApi.md#createalbum) | **POST** /album | -*AlbumApi* | [**createAlbumSharedLink**](doc//AlbumApi.md#createalbumsharedlink) | **POST** /album/create-shared-link | *AlbumApi* | [**deleteAlbum**](doc//AlbumApi.md#deletealbum) | **DELETE** /album/{id} | *AlbumApi* | [**downloadArchive**](doc//AlbumApi.md#downloadarchive) | **GET** /album/{id}/download | *AlbumApi* | [**getAlbumCount**](doc//AlbumApi.md#getalbumcount) | **GET** /album/count | @@ -89,11 +88,9 @@ Class | Method | HTTP request | Description *AlbumApi* | [**removeAssetFromAlbum**](doc//AlbumApi.md#removeassetfromalbum) | **DELETE** /album/{id}/assets | *AlbumApi* | [**removeUserFromAlbum**](doc//AlbumApi.md#removeuserfromalbum) | **DELETE** /album/{id}/user/{userId} | *AlbumApi* | [**updateAlbumInfo**](doc//AlbumApi.md#updatealbuminfo) | **PATCH** /album/{id} | -*AssetApi* | [**addAssetsToSharedLink**](doc//AssetApi.md#addassetstosharedlink) | **PATCH** /asset/shared-link/add | *AssetApi* | [**bulkUploadCheck**](doc//AssetApi.md#bulkuploadcheck) | **POST** /asset/bulk-upload-check | *AssetApi* | [**checkDuplicateAsset**](doc//AssetApi.md#checkduplicateasset) | **POST** /asset/check | *AssetApi* | [**checkExistingAssets**](doc//AssetApi.md#checkexistingassets) | **POST** /asset/exist | -*AssetApi* | [**createAssetsSharedLink**](doc//AssetApi.md#createassetssharedlink) | **POST** /asset/shared-link | *AssetApi* | [**deleteAsset**](doc//AssetApi.md#deleteasset) | **DELETE** /asset | *AssetApi* | [**downloadFile**](doc//AssetApi.md#downloadfile) | **GET** /asset/download/{id} | *AssetApi* | [**downloadFiles**](doc//AssetApi.md#downloadfiles) | **POST** /asset/download-files | @@ -111,7 +108,7 @@ Class | Method | HTTP request | Description *AssetApi* | [**getMapMarkers**](doc//AssetApi.md#getmapmarkers) | **GET** /asset/map-marker | *AssetApi* | [**getMemoryLane**](doc//AssetApi.md#getmemorylane) | **GET** /asset/memory-lane | *AssetApi* | [**getUserAssetsByDeviceId**](doc//AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} | -*AssetApi* | [**removeAssetsFromSharedLink**](doc//AssetApi.md#removeassetsfromsharedlink) | **PATCH** /asset/shared-link/remove | +*AssetApi* | [**importFile**](doc//AssetApi.md#importfile) | **POST** /asset/import | *AssetApi* | [**searchAsset**](doc//AssetApi.md#searchasset) | **POST** /asset/search | *AssetApi* | [**serveFile**](doc//AssetApi.md#servefile) | **GET** /asset/file/{id} | *AssetApi* | [**updateAsset**](doc//AssetApi.md#updateasset) | **PUT** /asset/{id} | @@ -146,11 +143,14 @@ Class | Method | HTTP request | Description *ServerInfoApi* | [**getServerVersion**](doc//ServerInfoApi.md#getserverversion) | **GET** /server-info/version | *ServerInfoApi* | [**getStats**](doc//ServerInfoApi.md#getstats) | **GET** /server-info/stats | *ServerInfoApi* | [**pingServer**](doc//ServerInfoApi.md#pingserver) | **GET** /server-info/ping | -*ShareApi* | [**getAllSharedLinks**](doc//ShareApi.md#getallsharedlinks) | **GET** /share | -*ShareApi* | [**getMySharedLink**](doc//ShareApi.md#getmysharedlink) | **GET** /share/me | -*ShareApi* | [**getSharedLinkById**](doc//ShareApi.md#getsharedlinkbyid) | **GET** /share/{id} | -*ShareApi* | [**removeSharedLink**](doc//ShareApi.md#removesharedlink) | **DELETE** /share/{id} | -*ShareApi* | [**updateSharedLink**](doc//ShareApi.md#updatesharedlink) | **PATCH** /share/{id} | +*SharedLinkApi* | [**addSharedLinkAssets**](doc//SharedLinkApi.md#addsharedlinkassets) | **PUT** /shared-link/{id}/assets | +*SharedLinkApi* | [**createSharedLink**](doc//SharedLinkApi.md#createsharedlink) | **POST** /shared-link | +*SharedLinkApi* | [**getAllSharedLinks**](doc//SharedLinkApi.md#getallsharedlinks) | **GET** /shared-link | +*SharedLinkApi* | [**getMySharedLink**](doc//SharedLinkApi.md#getmysharedlink) | **GET** /shared-link/me | +*SharedLinkApi* | [**getSharedLinkById**](doc//SharedLinkApi.md#getsharedlinkbyid) | **GET** /shared-link/{id} | +*SharedLinkApi* | [**removeSharedLink**](doc//SharedLinkApi.md#removesharedlink) | **DELETE** /shared-link/{id} | +*SharedLinkApi* | [**removeSharedLinkAssets**](doc//SharedLinkApi.md#removesharedlinkassets) | **DELETE** /shared-link/{id}/assets | +*SharedLinkApi* | [**updateSharedLink**](doc//SharedLinkApi.md#updatesharedlink) | **PATCH** /shared-link/{id} | *SystemConfigApi* | [**getConfig**](doc//SystemConfigApi.md#getconfig) | **GET** /system-config | *SystemConfigApi* | [**getDefaults**](doc//SystemConfigApi.md#getdefaults) | **GET** /system-config/defaults | *SystemConfigApi* | [**getStorageTemplateOptions**](doc//SystemConfigApi.md#getstoragetemplateoptions) | **GET** /system-config/storage-template-options | @@ -207,8 +207,6 @@ Class | Method | HTTP request | Description - [CheckExistingAssetsDto](doc//CheckExistingAssetsDto.md) - [CheckExistingAssetsResponseDto](doc//CheckExistingAssetsResponseDto.md) - [CreateAlbumDto](doc//CreateAlbumDto.md) - - [CreateAlbumShareLinkDto](doc//CreateAlbumShareLinkDto.md) - - [CreateAssetsShareLinkDto](doc//CreateAssetsShareLinkDto.md) - [CreateProfileImageResponseDto](doc//CreateProfileImageResponseDto.md) - [CreateTagDto](doc//CreateTagDto.md) - [CreateUserDto](doc//CreateUserDto.md) @@ -218,10 +216,10 @@ Class | Method | HTTP request | Description - [DeleteAssetResponseDto](doc//DeleteAssetResponseDto.md) - [DeleteAssetStatus](doc//DeleteAssetStatus.md) - [DownloadFilesDto](doc//DownloadFilesDto.md) - - [EditSharedLinkDto](doc//EditSharedLinkDto.md) - [ExifResponseDto](doc//ExifResponseDto.md) - [GetAssetByTimeBucketDto](doc//GetAssetByTimeBucketDto.md) - [GetAssetCountByTimeBucketDto](doc//GetAssetCountByTimeBucketDto.md) + - [ImportAssetDto](doc//ImportAssetDto.md) - [JobCommand](doc//JobCommand.md) - [JobCommandDto](doc//JobCommandDto.md) - [JobCountsDto](doc//JobCountsDto.md) @@ -253,6 +251,8 @@ Class | Method | HTTP request | Description - [ServerPingResponse](doc//ServerPingResponse.md) - [ServerStatsResponseDto](doc//ServerStatsResponseDto.md) - [ServerVersionReponseDto](doc//ServerVersionReponseDto.md) + - [SharedLinkCreateDto](doc//SharedLinkCreateDto.md) + - [SharedLinkEditDto](doc//SharedLinkEditDto.md) - [SharedLinkResponseDto](doc//SharedLinkResponseDto.md) - [SharedLinkType](doc//SharedLinkType.md) - [SignUpDto](doc//SignUpDto.md) diff --git a/mobile/openapi/doc/AlbumApi.md b/mobile/openapi/doc/AlbumApi.md index 172cd1416..b4eba7916 100644 --- a/mobile/openapi/doc/AlbumApi.md +++ b/mobile/openapi/doc/AlbumApi.md @@ -12,7 +12,6 @@ Method | HTTP request | Description [**addAssetsToAlbum**](AlbumApi.md#addassetstoalbum) | **PUT** /album/{id}/assets | [**addUsersToAlbum**](AlbumApi.md#adduserstoalbum) | **PUT** /album/{id}/users | [**createAlbum**](AlbumApi.md#createalbum) | **POST** /album | -[**createAlbumSharedLink**](AlbumApi.md#createalbumsharedlink) | **POST** /album/create-shared-link | [**deleteAlbum**](AlbumApi.md#deletealbum) | **DELETE** /album/{id} | [**downloadArchive**](AlbumApi.md#downloadarchive) | **GET** /album/{id}/download | [**getAlbumCount**](AlbumApi.md#getalbumcount) | **GET** /album/count | @@ -194,61 +193,6 @@ 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) -# **createAlbumSharedLink** -> SharedLinkResponseDto createAlbumSharedLink(createAlbumShareLinkDto) - - - -### Example -```dart -import 'package:openapi/api.dart'; -// TODO Configure API key authorization: cookie -//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; -// TODO Configure API key authorization: api_key -//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; -// TODO Configure HTTP Bearer authorization: bearer -// Case 1. Use String Token -//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); -// Case 2. Use Function which generate token. -// String yourTokenGeneratorFunction() { ... } -//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); - -final api_instance = AlbumApi(); -final createAlbumShareLinkDto = CreateAlbumShareLinkDto(); // CreateAlbumShareLinkDto | - -try { - final result = api_instance.createAlbumSharedLink(createAlbumShareLinkDto); - print(result); -} catch (e) { - print('Exception when calling AlbumApi->createAlbumSharedLink: $e\n'); -} -``` - -### Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **createAlbumShareLinkDto** | [**CreateAlbumShareLinkDto**](CreateAlbumShareLinkDto.md)| | - -### Return type - -[**SharedLinkResponseDto**](SharedLinkResponseDto.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) - # **deleteAlbum** > deleteAlbum(id) diff --git a/mobile/openapi/doc/AlbumResponseDto.md b/mobile/openapi/doc/AlbumResponseDto.md index d257dea2e..08425de5c 100644 --- a/mobile/openapi/doc/AlbumResponseDto.md +++ b/mobile/openapi/doc/AlbumResponseDto.md @@ -19,6 +19,7 @@ Name | Type | Description | Notes **sharedUsers** | [**List**](UserResponseDto.md) | | [default to const []] **assets** | [**List**](AssetResponseDto.md) | | [default to const []] **owner** | [**UserResponseDto**](UserResponseDto.md) | | +**lastModifiedAssetTimestamp** | [**DateTime**](DateTime.md) | | [optional] [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md index b40916e1f..ef3610b00 100644 --- a/mobile/openapi/doc/AssetApi.md +++ b/mobile/openapi/doc/AssetApi.md @@ -9,11 +9,9 @@ All URIs are relative to */api* Method | HTTP request | Description ------------- | ------------- | ------------- -[**addAssetsToSharedLink**](AssetApi.md#addassetstosharedlink) | **PATCH** /asset/shared-link/add | [**bulkUploadCheck**](AssetApi.md#bulkuploadcheck) | **POST** /asset/bulk-upload-check | [**checkDuplicateAsset**](AssetApi.md#checkduplicateasset) | **POST** /asset/check | [**checkExistingAssets**](AssetApi.md#checkexistingassets) | **POST** /asset/exist | -[**createAssetsSharedLink**](AssetApi.md#createassetssharedlink) | **POST** /asset/shared-link | [**deleteAsset**](AssetApi.md#deleteasset) | **DELETE** /asset | [**downloadFile**](AssetApi.md#downloadfile) | **GET** /asset/download/{id} | [**downloadFiles**](AssetApi.md#downloadfiles) | **POST** /asset/download-files | @@ -31,70 +29,13 @@ Method | HTTP request | Description [**getMapMarkers**](AssetApi.md#getmapmarkers) | **GET** /asset/map-marker | [**getMemoryLane**](AssetApi.md#getmemorylane) | **GET** /asset/memory-lane | [**getUserAssetsByDeviceId**](AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} | -[**removeAssetsFromSharedLink**](AssetApi.md#removeassetsfromsharedlink) | **PATCH** /asset/shared-link/remove | +[**importFile**](AssetApi.md#importfile) | **POST** /asset/import | [**searchAsset**](AssetApi.md#searchasset) | **POST** /asset/search | [**serveFile**](AssetApi.md#servefile) | **GET** /asset/file/{id} | [**updateAsset**](AssetApi.md#updateasset) | **PUT** /asset/{id} | [**uploadFile**](AssetApi.md#uploadfile) | **POST** /asset/upload | -# **addAssetsToSharedLink** -> SharedLinkResponseDto addAssetsToSharedLink(addAssetsDto, key) - - - -### Example -```dart -import 'package:openapi/api.dart'; -// TODO Configure API key authorization: cookie -//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; -// TODO Configure API key authorization: api_key -//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; -// TODO Configure HTTP Bearer authorization: bearer -// Case 1. Use String Token -//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); -// Case 2. Use Function which generate token. -// String yourTokenGeneratorFunction() { ... } -//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); - -final api_instance = AssetApi(); -final addAssetsDto = AddAssetsDto(); // AddAssetsDto | -final key = key_example; // String | - -try { - final result = api_instance.addAssetsToSharedLink(addAssetsDto, key); - print(result); -} catch (e) { - print('Exception when calling AssetApi->addAssetsToSharedLink: $e\n'); -} -``` - -### Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **addAssetsDto** | [**AddAssetsDto**](AddAssetsDto.md)| | - **key** | **String**| | [optional] - -### Return type - -[**SharedLinkResponseDto**](SharedLinkResponseDto.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) - # **bulkUploadCheck** > AssetBulkUploadCheckResponseDto bulkUploadCheck(assetBulkUploadCheckDto) @@ -268,61 +209,6 @@ 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) -# **createAssetsSharedLink** -> SharedLinkResponseDto createAssetsSharedLink(createAssetsShareLinkDto) - - - -### Example -```dart -import 'package:openapi/api.dart'; -// TODO Configure API key authorization: cookie -//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; -// TODO Configure API key authorization: api_key -//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; -// TODO Configure HTTP Bearer authorization: bearer -// Case 1. Use String Token -//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); -// Case 2. Use Function which generate token. -// String yourTokenGeneratorFunction() { ... } -//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); - -final api_instance = AssetApi(); -final createAssetsShareLinkDto = CreateAssetsShareLinkDto(); // CreateAssetsShareLinkDto | - -try { - final result = api_instance.createAssetsSharedLink(createAssetsShareLinkDto); - print(result); -} catch (e) { - print('Exception when calling AssetApi->createAssetsSharedLink: $e\n'); -} -``` - -### Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **createAssetsShareLinkDto** | [**CreateAssetsShareLinkDto**](CreateAssetsShareLinkDto.md)| | - -### Return type - -[**SharedLinkResponseDto**](SharedLinkResponseDto.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) - # **deleteAsset** > List deleteAsset(deleteAssetDto) @@ -1274,8 +1160,8 @@ 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) -# **removeAssetsFromSharedLink** -> SharedLinkResponseDto removeAssetsFromSharedLink(removeAssetsDto, key) +# **importFile** +> AssetFileUploadResponseDto importFile(importAssetDto) @@ -1298,14 +1184,13 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); final api_instance = AssetApi(); -final removeAssetsDto = RemoveAssetsDto(); // RemoveAssetsDto | -final key = key_example; // String | +final importAssetDto = ImportAssetDto(); // ImportAssetDto | try { - final result = api_instance.removeAssetsFromSharedLink(removeAssetsDto, key); + final result = api_instance.importFile(importAssetDto); print(result); } catch (e) { - print('Exception when calling AssetApi->removeAssetsFromSharedLink: $e\n'); + print('Exception when calling AssetApi->importFile: $e\n'); } ``` @@ -1313,12 +1198,11 @@ try { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - **removeAssetsDto** | [**RemoveAssetsDto**](RemoveAssetsDto.md)| | - **key** | **String**| | [optional] + **importAssetDto** | [**ImportAssetDto**](ImportAssetDto.md)| | ### Return type -[**SharedLinkResponseDto**](SharedLinkResponseDto.md) +[**AssetFileUploadResponseDto**](AssetFileUploadResponseDto.md) ### Authorization @@ -1507,7 +1391,7 @@ Name | Type | Description | Notes [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) # **uploadFile** -> AssetFileUploadResponseDto uploadFile(assetType, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, fileExtension, key, livePhotoData, sidecarData, isArchived, isVisible, duration) +> AssetFileUploadResponseDto uploadFile(assetType, assetData, fileExtension, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, key, livePhotoData, sidecarData, isReadOnly, isArchived, isVisible, duration) @@ -1532,21 +1416,22 @@ import 'package:openapi/api.dart'; final api_instance = AssetApi(); final assetType = ; // AssetTypeEnum | final assetData = BINARY_DATA_HERE; // MultipartFile | +final fileExtension = fileExtension_example; // String | final deviceAssetId = deviceAssetId_example; // String | final deviceId = deviceId_example; // String | final fileCreatedAt = 2013-10-20T19:20:30+01:00; // DateTime | final fileModifiedAt = 2013-10-20T19:20:30+01:00; // DateTime | final isFavorite = true; // bool | -final fileExtension = fileExtension_example; // String | final key = key_example; // String | final livePhotoData = BINARY_DATA_HERE; // MultipartFile | final sidecarData = BINARY_DATA_HERE; // MultipartFile | +final isReadOnly = true; // bool | final isArchived = true; // bool | final isVisible = true; // bool | final duration = duration_example; // String | try { - final result = api_instance.uploadFile(assetType, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, fileExtension, key, livePhotoData, sidecarData, isArchived, isVisible, duration); + final result = api_instance.uploadFile(assetType, assetData, fileExtension, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, key, livePhotoData, sidecarData, isReadOnly, isArchived, isVisible, duration); print(result); } catch (e) { print('Exception when calling AssetApi->uploadFile: $e\n'); @@ -1559,15 +1444,16 @@ Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **assetType** | [**AssetTypeEnum**](AssetTypeEnum.md)| | **assetData** | **MultipartFile**| | + **fileExtension** | **String**| | **deviceAssetId** | **String**| | **deviceId** | **String**| | **fileCreatedAt** | **DateTime**| | **fileModifiedAt** | **DateTime**| | **isFavorite** | **bool**| | - **fileExtension** | **String**| | **key** | **String**| | [optional] **livePhotoData** | **MultipartFile**| | [optional] **sidecarData** | **MultipartFile**| | [optional] + **isReadOnly** | **bool**| | [optional] [default to false] **isArchived** | **bool**| | [optional] **isVisible** | **bool**| | [optional] **duration** | **String**| | [optional] diff --git a/mobile/openapi/doc/CreateAssetsShareLinkDto.md b/mobile/openapi/doc/CreateAssetsShareLinkDto.md deleted file mode 100644 index 59a6f02f3..000000000 --- a/mobile/openapi/doc/CreateAssetsShareLinkDto.md +++ /dev/null @@ -1,20 +0,0 @@ -# openapi.model.CreateAssetsShareLinkDto - -## Load the model package -```dart -import 'package:openapi/api.dart'; -``` - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**assetIds** | **List** | | [default to const []] -**expiresAt** | [**DateTime**](DateTime.md) | | [optional] -**allowUpload** | **bool** | | [optional] -**allowDownload** | **bool** | | [optional] -**showExif** | **bool** | | [optional] -**description** | **String** | | [optional] - -[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) - - diff --git a/mobile/openapi/doc/CreateUserDto.md b/mobile/openapi/doc/CreateUserDto.md index 647d7f0ff..09b963b92 100644 --- a/mobile/openapi/doc/CreateUserDto.md +++ b/mobile/openapi/doc/CreateUserDto.md @@ -13,6 +13,7 @@ Name | Type | Description | Notes **firstName** | **String** | | **lastName** | **String** | | **storageLabel** | **String** | | [optional] +**externalPath** | **String** | | [optional] [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/mobile/openapi/doc/ImportAssetDto.md b/mobile/openapi/doc/ImportAssetDto.md new file mode 100644 index 000000000..da612b0ab --- /dev/null +++ b/mobile/openapi/doc/ImportAssetDto.md @@ -0,0 +1,26 @@ +# openapi.model.ImportAssetDto + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**assetType** | [**AssetTypeEnum**](AssetTypeEnum.md) | | +**isReadOnly** | **bool** | | [optional] [default to true] +**assetPath** | **String** | | +**sidecarPath** | **String** | | [optional] +**deviceAssetId** | **String** | | +**deviceId** | **String** | | +**fileCreatedAt** | [**DateTime**](DateTime.md) | | +**fileModifiedAt** | [**DateTime**](DateTime.md) | | +**isFavorite** | **bool** | | +**isArchived** | **bool** | | [optional] +**isVisible** | **bool** | | [optional] +**duration** | **String** | | [optional] + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/doc/ShareApi.md b/mobile/openapi/doc/SharedLinkApi.md similarity index 52% rename from mobile/openapi/doc/ShareApi.md rename to mobile/openapi/doc/SharedLinkApi.md index 90dde98cb..34b8e1e71 100644 --- a/mobile/openapi/doc/ShareApi.md +++ b/mobile/openapi/doc/SharedLinkApi.md @@ -1,4 +1,4 @@ -# openapi.api.ShareApi +# openapi.api.SharedLinkApi ## Load the API package ```dart @@ -9,13 +9,130 @@ All URIs are relative to */api* Method | HTTP request | Description ------------- | ------------- | ------------- -[**getAllSharedLinks**](ShareApi.md#getallsharedlinks) | **GET** /share | -[**getMySharedLink**](ShareApi.md#getmysharedlink) | **GET** /share/me | -[**getSharedLinkById**](ShareApi.md#getsharedlinkbyid) | **GET** /share/{id} | -[**removeSharedLink**](ShareApi.md#removesharedlink) | **DELETE** /share/{id} | -[**updateSharedLink**](ShareApi.md#updatesharedlink) | **PATCH** /share/{id} | +[**addSharedLinkAssets**](SharedLinkApi.md#addsharedlinkassets) | **PUT** /shared-link/{id}/assets | +[**createSharedLink**](SharedLinkApi.md#createsharedlink) | **POST** /shared-link | +[**getAllSharedLinks**](SharedLinkApi.md#getallsharedlinks) | **GET** /shared-link | +[**getMySharedLink**](SharedLinkApi.md#getmysharedlink) | **GET** /shared-link/me | +[**getSharedLinkById**](SharedLinkApi.md#getsharedlinkbyid) | **GET** /shared-link/{id} | +[**removeSharedLink**](SharedLinkApi.md#removesharedlink) | **DELETE** /shared-link/{id} | +[**removeSharedLinkAssets**](SharedLinkApi.md#removesharedlinkassets) | **DELETE** /shared-link/{id}/assets | +[**updateSharedLink**](SharedLinkApi.md#updatesharedlink) | **PATCH** /shared-link/{id} | +# **addSharedLinkAssets** +> List addSharedLinkAssets(id, assetIdsDto, key) + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = SharedLinkApi(); +final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | +final assetIdsDto = AssetIdsDto(); // AssetIdsDto | +final key = key_example; // String | + +try { + final result = api_instance.addSharedLinkAssets(id, assetIdsDto, key); + print(result); +} catch (e) { + print('Exception when calling SharedLinkApi->addSharedLinkAssets: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **id** | **String**| | + **assetIdsDto** | [**AssetIdsDto**](AssetIdsDto.md)| | + **key** | **String**| | [optional] + +### Return type + +[**List**](AssetIdsResponseDto.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) + +# **createSharedLink** +> SharedLinkResponseDto createSharedLink(sharedLinkCreateDto) + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = SharedLinkApi(); +final sharedLinkCreateDto = SharedLinkCreateDto(); // SharedLinkCreateDto | + +try { + final result = api_instance.createSharedLink(sharedLinkCreateDto); + print(result); +} catch (e) { + print('Exception when calling SharedLinkApi->createSharedLink: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **sharedLinkCreateDto** | [**SharedLinkCreateDto**](SharedLinkCreateDto.md)| | + +### Return type + +[**SharedLinkResponseDto**](SharedLinkResponseDto.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) + # **getAllSharedLinks** > List getAllSharedLinks() @@ -39,13 +156,13 @@ import 'package:openapi/api.dart'; // String yourTokenGeneratorFunction() { ... } //defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); -final api_instance = ShareApi(); +final api_instance = SharedLinkApi(); try { final result = api_instance.getAllSharedLinks(); print(result); } catch (e) { - print('Exception when calling ShareApi->getAllSharedLinks: $e\n'); + print('Exception when calling SharedLinkApi->getAllSharedLinks: $e\n'); } ``` @@ -90,14 +207,14 @@ import 'package:openapi/api.dart'; // String yourTokenGeneratorFunction() { ... } //defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); -final api_instance = ShareApi(); +final api_instance = SharedLinkApi(); final key = key_example; // String | try { final result = api_instance.getMySharedLink(key); print(result); } catch (e) { - print('Exception when calling ShareApi->getMySharedLink: $e\n'); + print('Exception when calling SharedLinkApi->getMySharedLink: $e\n'); } ``` @@ -145,14 +262,14 @@ import 'package:openapi/api.dart'; // String yourTokenGeneratorFunction() { ... } //defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); -final api_instance = ShareApi(); +final api_instance = SharedLinkApi(); final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | try { final result = api_instance.getSharedLinkById(id); print(result); } catch (e) { - print('Exception when calling ShareApi->getSharedLinkById: $e\n'); + print('Exception when calling SharedLinkApi->getSharedLinkById: $e\n'); } ``` @@ -200,13 +317,13 @@ import 'package:openapi/api.dart'; // String yourTokenGeneratorFunction() { ... } //defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); -final api_instance = ShareApi(); +final api_instance = SharedLinkApi(); final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | try { api_instance.removeSharedLink(id); } catch (e) { - print('Exception when calling ShareApi->removeSharedLink: $e\n'); + print('Exception when calling SharedLinkApi->removeSharedLink: $e\n'); } ``` @@ -231,8 +348,8 @@ void (empty response body) [[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) -# **updateSharedLink** -> SharedLinkResponseDto updateSharedLink(id, editSharedLinkDto) +# **removeSharedLinkAssets** +> List removeSharedLinkAssets(id, assetIdsDto, key) @@ -254,15 +371,16 @@ import 'package:openapi/api.dart'; // String yourTokenGeneratorFunction() { ... } //defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); -final api_instance = ShareApi(); +final api_instance = SharedLinkApi(); final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | -final editSharedLinkDto = EditSharedLinkDto(); // EditSharedLinkDto | +final assetIdsDto = AssetIdsDto(); // AssetIdsDto | +final key = key_example; // String | try { - final result = api_instance.updateSharedLink(id, editSharedLinkDto); + final result = api_instance.removeSharedLinkAssets(id, assetIdsDto, key); print(result); } catch (e) { - print('Exception when calling ShareApi->updateSharedLink: $e\n'); + print('Exception when calling SharedLinkApi->removeSharedLinkAssets: $e\n'); } ``` @@ -271,7 +389,65 @@ try { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **id** | **String**| | - **editSharedLinkDto** | [**EditSharedLinkDto**](EditSharedLinkDto.md)| | + **assetIdsDto** | [**AssetIdsDto**](AssetIdsDto.md)| | + **key** | **String**| | [optional] + +### Return type + +[**List**](AssetIdsResponseDto.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) + +# **updateSharedLink** +> SharedLinkResponseDto updateSharedLink(id, sharedLinkEditDto) + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = SharedLinkApi(); +final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | +final sharedLinkEditDto = SharedLinkEditDto(); // SharedLinkEditDto | + +try { + final result = api_instance.updateSharedLink(id, sharedLinkEditDto); + print(result); +} catch (e) { + print('Exception when calling SharedLinkApi->updateSharedLink: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **id** | **String**| | + **sharedLinkEditDto** | [**SharedLinkEditDto**](SharedLinkEditDto.md)| | ### Return type diff --git a/mobile/openapi/doc/CreateAlbumShareLinkDto.md b/mobile/openapi/doc/SharedLinkCreateDto.md similarity index 54% rename from mobile/openapi/doc/CreateAlbumShareLinkDto.md rename to mobile/openapi/doc/SharedLinkCreateDto.md index 522314932..fbed9ef71 100644 --- a/mobile/openapi/doc/CreateAlbumShareLinkDto.md +++ b/mobile/openapi/doc/SharedLinkCreateDto.md @@ -1,4 +1,4 @@ -# openapi.model.CreateAlbumShareLinkDto +# openapi.model.SharedLinkCreateDto ## Load the model package ```dart @@ -8,12 +8,14 @@ import 'package:openapi/api.dart'; ## Properties Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**albumId** | **String** | | -**expiresAt** | [**DateTime**](DateTime.md) | | [optional] -**allowUpload** | **bool** | | [optional] -**allowDownload** | **bool** | | [optional] -**showExif** | **bool** | | [optional] +**type** | [**SharedLinkType**](SharedLinkType.md) | | +**assetIds** | **List** | | [optional] [default to const []] +**albumId** | **String** | | [optional] **description** | **String** | | [optional] +**expiresAt** | [**DateTime**](DateTime.md) | | [optional] +**allowUpload** | **bool** | | [optional] [default to false] +**allowDownload** | **bool** | | [optional] [default to true] +**showExif** | **bool** | | [optional] [default to true] [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/mobile/openapi/doc/EditSharedLinkDto.md b/mobile/openapi/doc/SharedLinkEditDto.md similarity index 94% rename from mobile/openapi/doc/EditSharedLinkDto.md rename to mobile/openapi/doc/SharedLinkEditDto.md index 8097bb118..5105726cb 100644 --- a/mobile/openapi/doc/EditSharedLinkDto.md +++ b/mobile/openapi/doc/SharedLinkEditDto.md @@ -1,4 +1,4 @@ -# openapi.model.EditSharedLinkDto +# openapi.model.SharedLinkEditDto ## Load the model package ```dart diff --git a/mobile/openapi/doc/SharedLinkResponseDto.md b/mobile/openapi/doc/SharedLinkResponseDto.md index 8ffe0787a..f649807f4 100644 --- a/mobile/openapi/doc/SharedLinkResponseDto.md +++ b/mobile/openapi/doc/SharedLinkResponseDto.md @@ -10,7 +10,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **type** | [**SharedLinkType**](SharedLinkType.md) | | **id** | **String** | | -**description** | **String** | | [optional] +**description** | **String** | | **userId** | **String** | | **key** | **String** | | **createdAt** | [**DateTime**](DateTime.md) | | diff --git a/mobile/openapi/doc/UpdateUserDto.md b/mobile/openapi/doc/UpdateUserDto.md index a7422c53b..6376c18b1 100644 --- a/mobile/openapi/doc/UpdateUserDto.md +++ b/mobile/openapi/doc/UpdateUserDto.md @@ -14,6 +14,7 @@ Name | Type | Description | Notes **firstName** | **String** | | [optional] **lastName** | **String** | | [optional] **storageLabel** | **String** | | [optional] +**externalPath** | **String** | | [optional] **isAdmin** | **bool** | | [optional] **shouldChangePassword** | **bool** | | [optional] diff --git a/mobile/openapi/doc/UserResponseDto.md b/mobile/openapi/doc/UserResponseDto.md index c551dd5c7..1c7557b96 100644 --- a/mobile/openapi/doc/UserResponseDto.md +++ b/mobile/openapi/doc/UserResponseDto.md @@ -13,6 +13,7 @@ Name | Type | Description | Notes **firstName** | **String** | | **lastName** | **String** | | **storageLabel** | **String** | | +**externalPath** | **String** | | **profileImagePath** | **String** | | **shouldChangePassword** | **bool** | | **isAdmin** | **bool** | | diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 393854e74..9363e99b1 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -38,7 +38,7 @@ part 'api/partner_api.dart'; part 'api/person_api.dart'; part 'api/search_api.dart'; part 'api/server_info_api.dart'; -part 'api/share_api.dart'; +part 'api/shared_link_api.dart'; part 'api/system_config_api.dart'; part 'api/tag_api.dart'; part 'api/user_api.dart'; @@ -73,8 +73,6 @@ part 'model/check_duplicate_asset_response_dto.dart'; part 'model/check_existing_assets_dto.dart'; part 'model/check_existing_assets_response_dto.dart'; part 'model/create_album_dto.dart'; -part 'model/create_album_share_link_dto.dart'; -part 'model/create_assets_share_link_dto.dart'; part 'model/create_profile_image_response_dto.dart'; part 'model/create_tag_dto.dart'; part 'model/create_user_dto.dart'; @@ -84,10 +82,10 @@ part 'model/delete_asset_dto.dart'; part 'model/delete_asset_response_dto.dart'; part 'model/delete_asset_status.dart'; part 'model/download_files_dto.dart'; -part 'model/edit_shared_link_dto.dart'; part 'model/exif_response_dto.dart'; part 'model/get_asset_by_time_bucket_dto.dart'; part 'model/get_asset_count_by_time_bucket_dto.dart'; +part 'model/import_asset_dto.dart'; part 'model/job_command.dart'; part 'model/job_command_dto.dart'; part 'model/job_counts_dto.dart'; @@ -119,6 +117,8 @@ part 'model/server_info_response_dto.dart'; part 'model/server_ping_response.dart'; part 'model/server_stats_response_dto.dart'; part 'model/server_version_reponse_dto.dart'; +part 'model/shared_link_create_dto.dart'; +part 'model/shared_link_edit_dto.dart'; part 'model/shared_link_response_dto.dart'; part 'model/shared_link_type.dart'; part 'model/sign_up_dto.dart'; diff --git a/mobile/openapi/lib/api/album_api.dart b/mobile/openapi/lib/api/album_api.dart index b17c838f5..37490881d 100644 --- a/mobile/openapi/lib/api/album_api.dart +++ b/mobile/openapi/lib/api/album_api.dart @@ -175,53 +175,6 @@ class AlbumApi { return null; } - /// Performs an HTTP 'POST /album/create-shared-link' operation and returns the [Response]. - /// Parameters: - /// - /// * [CreateAlbumShareLinkDto] createAlbumShareLinkDto (required): - Future createAlbumSharedLinkWithHttpInfo(CreateAlbumShareLinkDto createAlbumShareLinkDto,) async { - // ignore: prefer_const_declarations - final path = r'/album/create-shared-link'; - - // ignore: prefer_final_locals - Object? postBody = createAlbumShareLinkDto; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = ['application/json']; - - - return apiClient.invokeAPI( - path, - 'POST', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Parameters: - /// - /// * [CreateAlbumShareLinkDto] createAlbumShareLinkDto (required): - Future createAlbumSharedLink(CreateAlbumShareLinkDto createAlbumShareLinkDto,) async { - final response = await createAlbumSharedLinkWithHttpInfo(createAlbumShareLinkDto,); - 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), 'SharedLinkResponseDto',) as SharedLinkResponseDto; - - } - return null; - } - /// Performs an HTTP 'DELETE /album/{id}' operation and returns the [Response]. /// Parameters: /// diff --git a/mobile/openapi/lib/api/asset_api.dart b/mobile/openapi/lib/api/asset_api.dart index 188d239b1..d8d03ca53 100644 --- a/mobile/openapi/lib/api/asset_api.dart +++ b/mobile/openapi/lib/api/asset_api.dart @@ -16,61 +16,6 @@ class AssetApi { final ApiClient apiClient; - /// Performs an HTTP 'PATCH /asset/shared-link/add' operation and returns the [Response]. - /// Parameters: - /// - /// * [AddAssetsDto] addAssetsDto (required): - /// - /// * [String] key: - Future addAssetsToSharedLinkWithHttpInfo(AddAssetsDto addAssetsDto, { String? key, }) async { - // ignore: prefer_const_declarations - final path = r'/asset/shared-link/add'; - - // ignore: prefer_final_locals - Object? postBody = addAssetsDto; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - if (key != null) { - queryParams.addAll(_queryParams('', 'key', key)); - } - - const contentTypes = ['application/json']; - - - return apiClient.invokeAPI( - path, - 'PATCH', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Parameters: - /// - /// * [AddAssetsDto] addAssetsDto (required): - /// - /// * [String] key: - Future addAssetsToSharedLink(AddAssetsDto addAssetsDto, { String? key, }) async { - final response = await addAssetsToSharedLinkWithHttpInfo(addAssetsDto, key: key, ); - 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), 'SharedLinkResponseDto',) as SharedLinkResponseDto; - - } - return null; - } - /// Checks if assets exist by checksums /// /// Note: This method returns the HTTP [Response]. @@ -235,53 +180,6 @@ class AssetApi { return null; } - /// Performs an HTTP 'POST /asset/shared-link' operation and returns the [Response]. - /// Parameters: - /// - /// * [CreateAssetsShareLinkDto] createAssetsShareLinkDto (required): - Future createAssetsSharedLinkWithHttpInfo(CreateAssetsShareLinkDto createAssetsShareLinkDto,) async { - // ignore: prefer_const_declarations - final path = r'/asset/shared-link'; - - // ignore: prefer_final_locals - Object? postBody = createAssetsShareLinkDto; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = ['application/json']; - - - return apiClient.invokeAPI( - path, - 'POST', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Parameters: - /// - /// * [CreateAssetsShareLinkDto] createAssetsShareLinkDto (required): - Future createAssetsSharedLink(CreateAssetsShareLinkDto createAssetsShareLinkDto,) async { - final response = await createAssetsSharedLinkWithHttpInfo(createAssetsShareLinkDto,); - 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), 'SharedLinkResponseDto',) as SharedLinkResponseDto; - - } - return null; - } - /// Performs an HTTP 'DELETE /asset' operation and returns the [Response]. /// Parameters: /// @@ -1225,33 +1123,27 @@ class AssetApi { return null; } - /// Performs an HTTP 'PATCH /asset/shared-link/remove' operation and returns the [Response]. + /// Performs an HTTP 'POST /asset/import' operation and returns the [Response]. /// Parameters: /// - /// * [RemoveAssetsDto] removeAssetsDto (required): - /// - /// * [String] key: - Future removeAssetsFromSharedLinkWithHttpInfo(RemoveAssetsDto removeAssetsDto, { String? key, }) async { + /// * [ImportAssetDto] importAssetDto (required): + Future importFileWithHttpInfo(ImportAssetDto importAssetDto,) async { // ignore: prefer_const_declarations - final path = r'/asset/shared-link/remove'; + final path = r'/asset/import'; // ignore: prefer_final_locals - Object? postBody = removeAssetsDto; + Object? postBody = importAssetDto; final queryParams = []; final headerParams = {}; final formParams = {}; - if (key != null) { - queryParams.addAll(_queryParams('', 'key', key)); - } - const contentTypes = ['application/json']; return apiClient.invokeAPI( path, - 'PATCH', + 'POST', queryParams, postBody, headerParams, @@ -1262,11 +1154,9 @@ class AssetApi { /// Parameters: /// - /// * [RemoveAssetsDto] removeAssetsDto (required): - /// - /// * [String] key: - Future removeAssetsFromSharedLink(RemoveAssetsDto removeAssetsDto, { String? key, }) async { - final response = await removeAssetsFromSharedLinkWithHttpInfo(removeAssetsDto, key: key, ); + /// * [ImportAssetDto] importAssetDto (required): + Future importFile(ImportAssetDto importAssetDto,) async { + final response = await importFileWithHttpInfo(importAssetDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -1274,7 +1164,7 @@ class AssetApi { // 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), 'SharedLinkResponseDto',) as SharedLinkResponseDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AssetFileUploadResponseDto',) as AssetFileUploadResponseDto; } return null; @@ -1464,6 +1354,8 @@ class AssetApi { /// /// * [MultipartFile] assetData (required): /// + /// * [String] fileExtension (required): + /// /// * [String] deviceAssetId (required): /// /// * [String] deviceId (required): @@ -1474,20 +1366,20 @@ class AssetApi { /// /// * [bool] isFavorite (required): /// - /// * [String] fileExtension (required): - /// /// * [String] key: /// /// * [MultipartFile] livePhotoData: /// /// * [MultipartFile] sidecarData: /// + /// * [bool] isReadOnly: + /// /// * [bool] isArchived: /// /// * [bool] isVisible: /// /// * [String] duration: - Future uploadFileWithHttpInfo(AssetTypeEnum assetType, MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, bool isFavorite, String fileExtension, { String? key, MultipartFile? livePhotoData, MultipartFile? sidecarData, bool? isArchived, bool? isVisible, String? duration, }) async { + Future uploadFileWithHttpInfo(AssetTypeEnum assetType, MultipartFile assetData, String fileExtension, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, bool isFavorite, { String? key, MultipartFile? livePhotoData, MultipartFile? sidecarData, bool? isReadOnly, bool? isArchived, bool? isVisible, String? duration, }) async { // ignore: prefer_const_declarations final path = r'/asset/upload'; @@ -1525,6 +1417,14 @@ class AssetApi { mp.fields[r'sidecarData'] = sidecarData.field; mp.files.add(sidecarData); } + if (isReadOnly != null) { + hasFields = true; + mp.fields[r'isReadOnly'] = parameterToString(isReadOnly); + } + if (fileExtension != null) { + hasFields = true; + mp.fields[r'fileExtension'] = parameterToString(fileExtension); + } if (deviceAssetId != null) { hasFields = true; mp.fields[r'deviceAssetId'] = parameterToString(deviceAssetId); @@ -1553,10 +1453,6 @@ class AssetApi { hasFields = true; mp.fields[r'isVisible'] = parameterToString(isVisible); } - if (fileExtension != null) { - hasFields = true; - mp.fields[r'fileExtension'] = parameterToString(fileExtension); - } if (duration != null) { hasFields = true; mp.fields[r'duration'] = parameterToString(duration); @@ -1582,6 +1478,8 @@ class AssetApi { /// /// * [MultipartFile] assetData (required): /// + /// * [String] fileExtension (required): + /// /// * [String] deviceAssetId (required): /// /// * [String] deviceId (required): @@ -1592,21 +1490,21 @@ class AssetApi { /// /// * [bool] isFavorite (required): /// - /// * [String] fileExtension (required): - /// /// * [String] key: /// /// * [MultipartFile] livePhotoData: /// /// * [MultipartFile] sidecarData: /// + /// * [bool] isReadOnly: + /// /// * [bool] isArchived: /// /// * [bool] isVisible: /// /// * [String] duration: - Future uploadFile(AssetTypeEnum assetType, MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, bool isFavorite, String fileExtension, { String? key, MultipartFile? livePhotoData, MultipartFile? sidecarData, bool? isArchived, bool? isVisible, String? duration, }) async { - final response = await uploadFileWithHttpInfo(assetType, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, fileExtension, key: key, livePhotoData: livePhotoData, sidecarData: sidecarData, isArchived: isArchived, isVisible: isVisible, duration: duration, ); + Future uploadFile(AssetTypeEnum assetType, MultipartFile assetData, String fileExtension, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, bool isFavorite, { String? key, MultipartFile? livePhotoData, MultipartFile? sidecarData, bool? isReadOnly, bool? isArchived, bool? isVisible, String? duration, }) async { + final response = await uploadFileWithHttpInfo(assetType, assetData, fileExtension, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, key: key, livePhotoData: livePhotoData, sidecarData: sidecarData, isReadOnly: isReadOnly, isArchived: isArchived, isVisible: isVisible, duration: duration, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/share_api.dart b/mobile/openapi/lib/api/share_api.dart deleted file mode 100644 index 59dd1f890..000000000 --- a/mobile/openapi/lib/api/share_api.dart +++ /dev/null @@ -1,253 +0,0 @@ -// -// 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 ShareApi { - ShareApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient; - - final ApiClient apiClient; - - /// Performs an HTTP 'GET /share' operation and returns the [Response]. - Future getAllSharedLinksWithHttpInfo() async { - // ignore: prefer_const_declarations - final path = r'/share'; - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = []; - - - return apiClient.invokeAPI( - path, - 'GET', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - Future?> getAllSharedLinks() async { - final response = await getAllSharedLinksWithHttpInfo(); - 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) { - final responseBody = await _decodeBodyBytes(response); - return (await apiClient.deserializeAsync(responseBody, 'List') as List) - .cast() - .toList(); - - } - return null; - } - - /// Performs an HTTP 'GET /share/me' operation and returns the [Response]. - /// Parameters: - /// - /// * [String] key: - Future getMySharedLinkWithHttpInfo({ String? key, }) async { - // ignore: prefer_const_declarations - final path = r'/share/me'; - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - if (key != null) { - queryParams.addAll(_queryParams('', 'key', key)); - } - - const contentTypes = []; - - - return apiClient.invokeAPI( - path, - 'GET', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Parameters: - /// - /// * [String] key: - Future getMySharedLink({ String? key, }) async { - final response = await getMySharedLinkWithHttpInfo( key: key, ); - 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), 'SharedLinkResponseDto',) as SharedLinkResponseDto; - - } - return null; - } - - /// Performs an HTTP 'GET /share/{id}' operation and returns the [Response]. - /// Parameters: - /// - /// * [String] id (required): - Future getSharedLinkByIdWithHttpInfo(String id,) async { - // ignore: prefer_const_declarations - final path = r'/share/{id}' - .replaceAll('{id}', id); - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = []; - - - return apiClient.invokeAPI( - path, - 'GET', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Parameters: - /// - /// * [String] id (required): - Future getSharedLinkById(String id,) async { - final response = await getSharedLinkByIdWithHttpInfo(id,); - 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), 'SharedLinkResponseDto',) as SharedLinkResponseDto; - - } - return null; - } - - /// Performs an HTTP 'DELETE /share/{id}' operation and returns the [Response]. - /// Parameters: - /// - /// * [String] id (required): - Future removeSharedLinkWithHttpInfo(String id,) async { - // ignore: prefer_const_declarations - final path = r'/share/{id}' - .replaceAll('{id}', id); - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = []; - - - return apiClient.invokeAPI( - path, - 'DELETE', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Parameters: - /// - /// * [String] id (required): - Future removeSharedLink(String id,) async { - final response = await removeSharedLinkWithHttpInfo(id,); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - } - - /// Performs an HTTP 'PATCH /share/{id}' operation and returns the [Response]. - /// Parameters: - /// - /// * [String] id (required): - /// - /// * [EditSharedLinkDto] editSharedLinkDto (required): - Future updateSharedLinkWithHttpInfo(String id, EditSharedLinkDto editSharedLinkDto,) async { - // ignore: prefer_const_declarations - final path = r'/share/{id}' - .replaceAll('{id}', id); - - // ignore: prefer_final_locals - Object? postBody = editSharedLinkDto; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = ['application/json']; - - - return apiClient.invokeAPI( - path, - 'PATCH', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Parameters: - /// - /// * [String] id (required): - /// - /// * [EditSharedLinkDto] editSharedLinkDto (required): - Future updateSharedLink(String id, EditSharedLinkDto editSharedLinkDto,) async { - final response = await updateSharedLinkWithHttpInfo(id, editSharedLinkDto,); - 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), 'SharedLinkResponseDto',) as SharedLinkResponseDto; - - } - return null; - } -} diff --git a/mobile/openapi/lib/api/shared_link_api.dart b/mobile/openapi/lib/api/shared_link_api.dart new file mode 100644 index 000000000..029f7bc8a --- /dev/null +++ b/mobile/openapi/lib/api/shared_link_api.dart @@ -0,0 +1,426 @@ +// +// 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 SharedLinkApi { + SharedLinkApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient; + + final ApiClient apiClient; + + /// Performs an HTTP 'PUT /shared-link/{id}/assets' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [AssetIdsDto] assetIdsDto (required): + /// + /// * [String] key: + Future addSharedLinkAssetsWithHttpInfo(String id, AssetIdsDto assetIdsDto, { String? key, }) async { + // ignore: prefer_const_declarations + final path = r'/shared-link/{id}/assets' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody = assetIdsDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + if (key != null) { + queryParams.addAll(_queryParams('', 'key', key)); + } + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'PUT', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [AssetIdsDto] assetIdsDto (required): + /// + /// * [String] key: + Future?> addSharedLinkAssets(String id, AssetIdsDto assetIdsDto, { String? key, }) async { + final response = await addSharedLinkAssetsWithHttpInfo(id, assetIdsDto, key: key, ); + 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) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(); + + } + return null; + } + + /// Performs an HTTP 'POST /shared-link' operation and returns the [Response]. + /// Parameters: + /// + /// * [SharedLinkCreateDto] sharedLinkCreateDto (required): + Future createSharedLinkWithHttpInfo(SharedLinkCreateDto sharedLinkCreateDto,) async { + // ignore: prefer_const_declarations + final path = r'/shared-link'; + + // ignore: prefer_final_locals + Object? postBody = sharedLinkCreateDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [SharedLinkCreateDto] sharedLinkCreateDto (required): + Future createSharedLink(SharedLinkCreateDto sharedLinkCreateDto,) async { + final response = await createSharedLinkWithHttpInfo(sharedLinkCreateDto,); + 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), 'SharedLinkResponseDto',) as SharedLinkResponseDto; + + } + return null; + } + + /// Performs an HTTP 'GET /shared-link' operation and returns the [Response]. + Future getAllSharedLinksWithHttpInfo() async { + // ignore: prefer_const_declarations + final path = r'/shared-link'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + Future?> getAllSharedLinks() async { + final response = await getAllSharedLinksWithHttpInfo(); + 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) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(); + + } + return null; + } + + /// Performs an HTTP 'GET /shared-link/me' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] key: + Future getMySharedLinkWithHttpInfo({ String? key, }) async { + // ignore: prefer_const_declarations + final path = r'/shared-link/me'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + if (key != null) { + queryParams.addAll(_queryParams('', 'key', key)); + } + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] key: + Future getMySharedLink({ String? key, }) async { + final response = await getMySharedLinkWithHttpInfo( key: key, ); + 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), 'SharedLinkResponseDto',) as SharedLinkResponseDto; + + } + return null; + } + + /// Performs an HTTP 'GET /shared-link/{id}' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] id (required): + Future getSharedLinkByIdWithHttpInfo(String id,) async { + // ignore: prefer_const_declarations + final path = r'/shared-link/{id}' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] id (required): + Future getSharedLinkById(String id,) async { + final response = await getSharedLinkByIdWithHttpInfo(id,); + 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), 'SharedLinkResponseDto',) as SharedLinkResponseDto; + + } + return null; + } + + /// Performs an HTTP 'DELETE /shared-link/{id}' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] id (required): + Future removeSharedLinkWithHttpInfo(String id,) async { + // ignore: prefer_const_declarations + final path = r'/shared-link/{id}' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'DELETE', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] id (required): + Future removeSharedLink(String id,) async { + final response = await removeSharedLinkWithHttpInfo(id,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + + /// Performs an HTTP 'DELETE /shared-link/{id}/assets' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [AssetIdsDto] assetIdsDto (required): + /// + /// * [String] key: + Future removeSharedLinkAssetsWithHttpInfo(String id, AssetIdsDto assetIdsDto, { String? key, }) async { + // ignore: prefer_const_declarations + final path = r'/shared-link/{id}/assets' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody = assetIdsDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + if (key != null) { + queryParams.addAll(_queryParams('', 'key', key)); + } + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'DELETE', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [AssetIdsDto] assetIdsDto (required): + /// + /// * [String] key: + Future?> removeSharedLinkAssets(String id, AssetIdsDto assetIdsDto, { String? key, }) async { + final response = await removeSharedLinkAssetsWithHttpInfo(id, assetIdsDto, key: key, ); + 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) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(); + + } + return null; + } + + /// Performs an HTTP 'PATCH /shared-link/{id}' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [SharedLinkEditDto] sharedLinkEditDto (required): + Future updateSharedLinkWithHttpInfo(String id, SharedLinkEditDto sharedLinkEditDto,) async { + // ignore: prefer_const_declarations + final path = r'/shared-link/{id}' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody = sharedLinkEditDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'PATCH', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [SharedLinkEditDto] sharedLinkEditDto (required): + Future updateSharedLink(String id, SharedLinkEditDto sharedLinkEditDto,) async { + final response = await updateSharedLinkWithHttpInfo(id, sharedLinkEditDto,); + 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), 'SharedLinkResponseDto',) as SharedLinkResponseDto; + + } + return null; + } +} diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index bf0e60bc7..9deee81b7 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -241,10 +241,6 @@ class ApiClient { return CheckExistingAssetsResponseDto.fromJson(value); case 'CreateAlbumDto': return CreateAlbumDto.fromJson(value); - case 'CreateAlbumShareLinkDto': - return CreateAlbumShareLinkDto.fromJson(value); - case 'CreateAssetsShareLinkDto': - return CreateAssetsShareLinkDto.fromJson(value); case 'CreateProfileImageResponseDto': return CreateProfileImageResponseDto.fromJson(value); case 'CreateTagDto': @@ -263,14 +259,14 @@ class ApiClient { return DeleteAssetStatusTypeTransformer().decode(value); case 'DownloadFilesDto': return DownloadFilesDto.fromJson(value); - case 'EditSharedLinkDto': - return EditSharedLinkDto.fromJson(value); case 'ExifResponseDto': return ExifResponseDto.fromJson(value); case 'GetAssetByTimeBucketDto': return GetAssetByTimeBucketDto.fromJson(value); case 'GetAssetCountByTimeBucketDto': return GetAssetCountByTimeBucketDto.fromJson(value); + case 'ImportAssetDto': + return ImportAssetDto.fromJson(value); case 'JobCommand': return JobCommandTypeTransformer().decode(value); case 'JobCommandDto': @@ -333,6 +329,10 @@ class ApiClient { return ServerStatsResponseDto.fromJson(value); case 'ServerVersionReponseDto': return ServerVersionReponseDto.fromJson(value); + case 'SharedLinkCreateDto': + return SharedLinkCreateDto.fromJson(value); + case 'SharedLinkEditDto': + return SharedLinkEditDto.fromJson(value); case 'SharedLinkResponseDto': return SharedLinkResponseDto.fromJson(value); case 'SharedLinkType': diff --git a/mobile/openapi/lib/model/album_response_dto.dart b/mobile/openapi/lib/model/album_response_dto.dart index 4661d5f28..3ef986cd6 100644 --- a/mobile/openapi/lib/model/album_response_dto.dart +++ b/mobile/openapi/lib/model/album_response_dto.dart @@ -24,6 +24,7 @@ class AlbumResponseDto { this.sharedUsers = const [], this.assets = const [], required this.owner, + this.lastModifiedAssetTimestamp, }); int assetCount; @@ -48,6 +49,14 @@ class AlbumResponseDto { UserResponseDto owner; + /// + /// 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. + /// + DateTime? lastModifiedAssetTimestamp; + @override bool operator ==(Object other) => identical(this, other) || other is AlbumResponseDto && other.assetCount == assetCount && @@ -60,7 +69,8 @@ class AlbumResponseDto { other.shared == shared && other.sharedUsers == sharedUsers && other.assets == assets && - other.owner == owner; + other.owner == owner && + other.lastModifiedAssetTimestamp == lastModifiedAssetTimestamp; @override int get hashCode => @@ -75,10 +85,11 @@ class AlbumResponseDto { (shared.hashCode) + (sharedUsers.hashCode) + (assets.hashCode) + - (owner.hashCode); + (owner.hashCode) + + (lastModifiedAssetTimestamp == null ? 0 : lastModifiedAssetTimestamp!.hashCode); @override - String toString() => 'AlbumResponseDto[assetCount=$assetCount, id=$id, ownerId=$ownerId, albumName=$albumName, createdAt=$createdAt, updatedAt=$updatedAt, albumThumbnailAssetId=$albumThumbnailAssetId, shared=$shared, sharedUsers=$sharedUsers, assets=$assets, owner=$owner]'; + String toString() => 'AlbumResponseDto[assetCount=$assetCount, id=$id, ownerId=$ownerId, albumName=$albumName, createdAt=$createdAt, updatedAt=$updatedAt, albumThumbnailAssetId=$albumThumbnailAssetId, shared=$shared, sharedUsers=$sharedUsers, assets=$assets, owner=$owner, lastModifiedAssetTimestamp=$lastModifiedAssetTimestamp]'; Map toJson() { final json = {}; @@ -97,6 +108,11 @@ class AlbumResponseDto { json[r'sharedUsers'] = this.sharedUsers; json[r'assets'] = this.assets; json[r'owner'] = this.owner; + if (this.lastModifiedAssetTimestamp != null) { + json[r'lastModifiedAssetTimestamp'] = this.lastModifiedAssetTimestamp!.toUtc().toIso8601String(); + } else { + // json[r'lastModifiedAssetTimestamp'] = null; + } return json; } @@ -130,6 +146,7 @@ class AlbumResponseDto { sharedUsers: UserResponseDto.listFromJson(json[r'sharedUsers']), assets: AssetResponseDto.listFromJson(json[r'assets']), owner: UserResponseDto.fromJson(json[r'owner'])!, + lastModifiedAssetTimestamp: mapDateTime(json, r'lastModifiedAssetTimestamp', ''), ); } return null; diff --git a/mobile/openapi/lib/model/create_album_share_link_dto.dart b/mobile/openapi/lib/model/create_album_share_link_dto.dart deleted file mode 100644 index f296d8047..000000000 --- a/mobile/openapi/lib/model/create_album_share_link_dto.dart +++ /dev/null @@ -1,194 +0,0 @@ -// -// 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 CreateAlbumShareLinkDto { - /// Returns a new [CreateAlbumShareLinkDto] instance. - CreateAlbumShareLinkDto({ - required this.albumId, - this.expiresAt, - this.allowUpload, - this.allowDownload, - this.showExif, - this.description, - }); - - String albumId; - - /// - /// 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. - /// - DateTime? expiresAt; - - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - bool? allowUpload; - - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - bool? allowDownload; - - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - bool? showExif; - - /// - /// 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? description; - - @override - bool operator ==(Object other) => identical(this, other) || other is CreateAlbumShareLinkDto && - other.albumId == albumId && - other.expiresAt == expiresAt && - other.allowUpload == allowUpload && - other.allowDownload == allowDownload && - other.showExif == showExif && - other.description == description; - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (albumId.hashCode) + - (expiresAt == null ? 0 : expiresAt!.hashCode) + - (allowUpload == null ? 0 : allowUpload!.hashCode) + - (allowDownload == null ? 0 : allowDownload!.hashCode) + - (showExif == null ? 0 : showExif!.hashCode) + - (description == null ? 0 : description!.hashCode); - - @override - String toString() => 'CreateAlbumShareLinkDto[albumId=$albumId, expiresAt=$expiresAt, allowUpload=$allowUpload, allowDownload=$allowDownload, showExif=$showExif, description=$description]'; - - Map toJson() { - final json = {}; - json[r'albumId'] = this.albumId; - if (this.expiresAt != null) { - json[r'expiresAt'] = this.expiresAt!.toUtc().toIso8601String(); - } else { - // json[r'expiresAt'] = null; - } - if (this.allowUpload != null) { - json[r'allowUpload'] = this.allowUpload; - } else { - // json[r'allowUpload'] = null; - } - if (this.allowDownload != null) { - json[r'allowDownload'] = this.allowDownload; - } else { - // json[r'allowDownload'] = null; - } - if (this.showExif != null) { - json[r'showExif'] = this.showExif; - } else { - // json[r'showExif'] = null; - } - if (this.description != null) { - json[r'description'] = this.description; - } else { - // json[r'description'] = null; - } - return json; - } - - /// Returns a new [CreateAlbumShareLinkDto] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static CreateAlbumShareLinkDto? fromJson(dynamic value) { - if (value is Map) { - final json = value.cast(); - - // Ensure that the map contains the required keys. - // Note 1: the values aren't checked for validity beyond being non-null. - // Note 2: this code is stripped in release mode! - assert(() { - requiredKeys.forEach((key) { - assert(json.containsKey(key), 'Required key "CreateAlbumShareLinkDto[$key]" is missing from JSON.'); - assert(json[key] != null, 'Required key "CreateAlbumShareLinkDto[$key]" has a null value in JSON.'); - }); - return true; - }()); - - return CreateAlbumShareLinkDto( - albumId: mapValueOfType(json, r'albumId')!, - expiresAt: mapDateTime(json, r'expiresAt', ''), - allowUpload: mapValueOfType(json, r'allowUpload'), - allowDownload: mapValueOfType(json, r'allowDownload'), - showExif: mapValueOfType(json, r'showExif'), - description: mapValueOfType(json, r'description'), - ); - } - return null; - } - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = CreateAlbumShareLinkDto.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } - - static Map mapFromJson(dynamic json) { - final map = {}; - if (json is Map && json.isNotEmpty) { - json = json.cast(); // ignore: parameter_assignments - for (final entry in json.entries) { - final value = CreateAlbumShareLinkDto.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of CreateAlbumShareLinkDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; - if (json is Map && json.isNotEmpty) { - // ignore: parameter_assignments - json = json.cast(); - for (final entry in json.entries) { - map[entry.key] = CreateAlbumShareLinkDto.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'albumId', - }; -} - diff --git a/mobile/openapi/lib/model/create_user_dto.dart b/mobile/openapi/lib/model/create_user_dto.dart index cd5c2a2d6..870847aea 100644 --- a/mobile/openapi/lib/model/create_user_dto.dart +++ b/mobile/openapi/lib/model/create_user_dto.dart @@ -18,6 +18,7 @@ class CreateUserDto { required this.firstName, required this.lastName, this.storageLabel, + this.externalPath, }); String email; @@ -30,13 +31,16 @@ class CreateUserDto { String? storageLabel; + String? externalPath; + @override bool operator ==(Object other) => identical(this, other) || other is CreateUserDto && other.email == email && other.password == password && other.firstName == firstName && other.lastName == lastName && - other.storageLabel == storageLabel; + other.storageLabel == storageLabel && + other.externalPath == externalPath; @override int get hashCode => @@ -45,10 +49,11 @@ class CreateUserDto { (password.hashCode) + (firstName.hashCode) + (lastName.hashCode) + - (storageLabel == null ? 0 : storageLabel!.hashCode); + (storageLabel == null ? 0 : storageLabel!.hashCode) + + (externalPath == null ? 0 : externalPath!.hashCode); @override - String toString() => 'CreateUserDto[email=$email, password=$password, firstName=$firstName, lastName=$lastName, storageLabel=$storageLabel]'; + String toString() => 'CreateUserDto[email=$email, password=$password, firstName=$firstName, lastName=$lastName, storageLabel=$storageLabel, externalPath=$externalPath]'; Map toJson() { final json = {}; @@ -61,6 +66,11 @@ class CreateUserDto { } else { // json[r'storageLabel'] = null; } + if (this.externalPath != null) { + json[r'externalPath'] = this.externalPath; + } else { + // json[r'externalPath'] = null; + } return json; } @@ -88,6 +98,7 @@ class CreateUserDto { firstName: mapValueOfType(json, r'firstName')!, lastName: mapValueOfType(json, r'lastName')!, storageLabel: mapValueOfType(json, r'storageLabel'), + externalPath: mapValueOfType(json, r'externalPath'), ); } return null; diff --git a/mobile/openapi/lib/model/import_asset_dto.dart b/mobile/openapi/lib/model/import_asset_dto.dart new file mode 100644 index 000000000..bb322ba81 --- /dev/null +++ b/mobile/openapi/lib/model/import_asset_dto.dart @@ -0,0 +1,232 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class ImportAssetDto { + /// Returns a new [ImportAssetDto] instance. + ImportAssetDto({ + required this.assetType, + this.isReadOnly = true, + required this.assetPath, + this.sidecarPath, + required this.deviceAssetId, + required this.deviceId, + required this.fileCreatedAt, + required this.fileModifiedAt, + required this.isFavorite, + this.isArchived, + this.isVisible, + this.duration, + }); + + AssetTypeEnum assetType; + + bool isReadOnly; + + String assetPath; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? sidecarPath; + + String deviceAssetId; + + String deviceId; + + DateTime fileCreatedAt; + + DateTime fileModifiedAt; + + bool isFavorite; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? isArchived; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? isVisible; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? duration; + + @override + bool operator ==(Object other) => identical(this, other) || other is ImportAssetDto && + other.assetType == assetType && + other.isReadOnly == isReadOnly && + other.assetPath == assetPath && + other.sidecarPath == sidecarPath && + other.deviceAssetId == deviceAssetId && + other.deviceId == deviceId && + other.fileCreatedAt == fileCreatedAt && + other.fileModifiedAt == fileModifiedAt && + other.isFavorite == isFavorite && + other.isArchived == isArchived && + other.isVisible == isVisible && + other.duration == duration; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (assetType.hashCode) + + (isReadOnly.hashCode) + + (assetPath.hashCode) + + (sidecarPath == null ? 0 : sidecarPath!.hashCode) + + (deviceAssetId.hashCode) + + (deviceId.hashCode) + + (fileCreatedAt.hashCode) + + (fileModifiedAt.hashCode) + + (isFavorite.hashCode) + + (isArchived == null ? 0 : isArchived!.hashCode) + + (isVisible == null ? 0 : isVisible!.hashCode) + + (duration == null ? 0 : duration!.hashCode); + + @override + String toString() => 'ImportAssetDto[assetType=$assetType, isReadOnly=$isReadOnly, assetPath=$assetPath, sidecarPath=$sidecarPath, deviceAssetId=$deviceAssetId, deviceId=$deviceId, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, isFavorite=$isFavorite, isArchived=$isArchived, isVisible=$isVisible, duration=$duration]'; + + Map toJson() { + final json = {}; + json[r'assetType'] = this.assetType; + json[r'isReadOnly'] = this.isReadOnly; + json[r'assetPath'] = this.assetPath; + if (this.sidecarPath != null) { + json[r'sidecarPath'] = this.sidecarPath; + } else { + // json[r'sidecarPath'] = null; + } + json[r'deviceAssetId'] = this.deviceAssetId; + json[r'deviceId'] = this.deviceId; + json[r'fileCreatedAt'] = this.fileCreatedAt.toUtc().toIso8601String(); + json[r'fileModifiedAt'] = this.fileModifiedAt.toUtc().toIso8601String(); + json[r'isFavorite'] = this.isFavorite; + if (this.isArchived != null) { + json[r'isArchived'] = this.isArchived; + } else { + // json[r'isArchived'] = null; + } + if (this.isVisible != null) { + json[r'isVisible'] = this.isVisible; + } else { + // json[r'isVisible'] = null; + } + if (this.duration != null) { + json[r'duration'] = this.duration; + } else { + // json[r'duration'] = null; + } + return json; + } + + /// Returns a new [ImportAssetDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static ImportAssetDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + // Ensure that the map contains the required keys. + // Note 1: the values aren't checked for validity beyond being non-null. + // Note 2: this code is stripped in release mode! + assert(() { + requiredKeys.forEach((key) { + assert(json.containsKey(key), 'Required key "ImportAssetDto[$key]" is missing from JSON.'); + assert(json[key] != null, 'Required key "ImportAssetDto[$key]" has a null value in JSON.'); + }); + return true; + }()); + + return ImportAssetDto( + assetType: AssetTypeEnum.fromJson(json[r'assetType'])!, + isReadOnly: mapValueOfType(json, r'isReadOnly') ?? true, + assetPath: mapValueOfType(json, r'assetPath')!, + sidecarPath: mapValueOfType(json, r'sidecarPath'), + deviceAssetId: mapValueOfType(json, r'deviceAssetId')!, + deviceId: mapValueOfType(json, r'deviceId')!, + fileCreatedAt: mapDateTime(json, r'fileCreatedAt', '')!, + fileModifiedAt: mapDateTime(json, r'fileModifiedAt', '')!, + isFavorite: mapValueOfType(json, r'isFavorite')!, + isArchived: mapValueOfType(json, r'isArchived'), + isVisible: mapValueOfType(json, r'isVisible'), + duration: mapValueOfType(json, r'duration'), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = ImportAssetDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = ImportAssetDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of ImportAssetDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = ImportAssetDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'assetType', + 'assetPath', + 'deviceAssetId', + 'deviceId', + 'fileCreatedAt', + 'fileModifiedAt', + 'isFavorite', + }; +} + diff --git a/mobile/openapi/lib/model/create_assets_share_link_dto.dart b/mobile/openapi/lib/model/shared_link_create_dto.dart similarity index 53% rename from mobile/openapi/lib/model/create_assets_share_link_dto.dart rename to mobile/openapi/lib/model/shared_link_create_dto.dart index c8391c1db..c8d1c547a 100644 --- a/mobile/openapi/lib/model/create_assets_share_link_dto.dart +++ b/mobile/openapi/lib/model/shared_link_create_dto.dart @@ -10,17 +10,21 @@ part of openapi.api; -class CreateAssetsShareLinkDto { - /// Returns a new [CreateAssetsShareLinkDto] instance. - CreateAssetsShareLinkDto({ +class SharedLinkCreateDto { + /// Returns a new [SharedLinkCreateDto] instance. + SharedLinkCreateDto({ + required this.type, this.assetIds = const [], - this.expiresAt, - this.allowUpload, - this.allowDownload, - this.showExif, + this.albumId, this.description, + this.expiresAt, + this.allowUpload = false, + this.allowDownload = true, + this.showExif = true, }); + SharedLinkType type; + List assetIds; /// @@ -29,31 +33,7 @@ class CreateAssetsShareLinkDto { /// source code must fall back to having a nullable type. /// Consider adding a "default:" property in the specification file to hide this note. /// - DateTime? expiresAt; - - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - bool? allowUpload; - - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - bool? allowDownload; - - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - bool? showExif; + String? albumId; /// /// Please note: This property should have been non-nullable! Since the specification file @@ -63,63 +43,69 @@ class CreateAssetsShareLinkDto { /// String? description; + DateTime? expiresAt; + + bool allowUpload; + + bool allowDownload; + + bool showExif; + @override - bool operator ==(Object other) => identical(this, other) || other is CreateAssetsShareLinkDto && + bool operator ==(Object other) => identical(this, other) || other is SharedLinkCreateDto && + other.type == type && other.assetIds == assetIds && + other.albumId == albumId && + other.description == description && other.expiresAt == expiresAt && other.allowUpload == allowUpload && other.allowDownload == allowDownload && - other.showExif == showExif && - other.description == description; + other.showExif == showExif; @override int get hashCode => // ignore: unnecessary_parenthesis + (type.hashCode) + (assetIds.hashCode) + + (albumId == null ? 0 : albumId!.hashCode) + + (description == null ? 0 : description!.hashCode) + (expiresAt == null ? 0 : expiresAt!.hashCode) + - (allowUpload == null ? 0 : allowUpload!.hashCode) + - (allowDownload == null ? 0 : allowDownload!.hashCode) + - (showExif == null ? 0 : showExif!.hashCode) + - (description == null ? 0 : description!.hashCode); + (allowUpload.hashCode) + + (allowDownload.hashCode) + + (showExif.hashCode); @override - String toString() => 'CreateAssetsShareLinkDto[assetIds=$assetIds, expiresAt=$expiresAt, allowUpload=$allowUpload, allowDownload=$allowDownload, showExif=$showExif, description=$description]'; + String toString() => 'SharedLinkCreateDto[type=$type, assetIds=$assetIds, albumId=$albumId, description=$description, expiresAt=$expiresAt, allowUpload=$allowUpload, allowDownload=$allowDownload, showExif=$showExif]'; Map toJson() { final json = {}; + json[r'type'] = this.type; json[r'assetIds'] = this.assetIds; - if (this.expiresAt != null) { - json[r'expiresAt'] = this.expiresAt!.toUtc().toIso8601String(); + if (this.albumId != null) { + json[r'albumId'] = this.albumId; } else { - // json[r'expiresAt'] = null; - } - if (this.allowUpload != null) { - json[r'allowUpload'] = this.allowUpload; - } else { - // json[r'allowUpload'] = null; - } - if (this.allowDownload != null) { - json[r'allowDownload'] = this.allowDownload; - } else { - // json[r'allowDownload'] = null; - } - if (this.showExif != null) { - json[r'showExif'] = this.showExif; - } else { - // json[r'showExif'] = null; + // json[r'albumId'] = null; } if (this.description != null) { json[r'description'] = this.description; } else { // json[r'description'] = null; } + if (this.expiresAt != null) { + json[r'expiresAt'] = this.expiresAt!.toUtc().toIso8601String(); + } else { + // json[r'expiresAt'] = null; + } + json[r'allowUpload'] = this.allowUpload; + json[r'allowDownload'] = this.allowDownload; + json[r'showExif'] = this.showExif; return json; } - /// Returns a new [CreateAssetsShareLinkDto] instance and imports its values from + /// Returns a new [SharedLinkCreateDto] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static CreateAssetsShareLinkDto? fromJson(dynamic value) { + static SharedLinkCreateDto? fromJson(dynamic value) { if (value is Map) { final json = value.cast(); @@ -128,31 +114,33 @@ class CreateAssetsShareLinkDto { // Note 2: this code is stripped in release mode! assert(() { requiredKeys.forEach((key) { - assert(json.containsKey(key), 'Required key "CreateAssetsShareLinkDto[$key]" is missing from JSON.'); - assert(json[key] != null, 'Required key "CreateAssetsShareLinkDto[$key]" has a null value in JSON.'); + assert(json.containsKey(key), 'Required key "SharedLinkCreateDto[$key]" is missing from JSON.'); + assert(json[key] != null, 'Required key "SharedLinkCreateDto[$key]" has a null value in JSON.'); }); return true; }()); - return CreateAssetsShareLinkDto( + return SharedLinkCreateDto( + type: SharedLinkType.fromJson(json[r'type'])!, assetIds: json[r'assetIds'] is Iterable ? (json[r'assetIds'] as Iterable).cast().toList(growable: false) : const [], - expiresAt: mapDateTime(json, r'expiresAt', ''), - allowUpload: mapValueOfType(json, r'allowUpload'), - allowDownload: mapValueOfType(json, r'allowDownload'), - showExif: mapValueOfType(json, r'showExif'), + albumId: mapValueOfType(json, r'albumId'), description: mapValueOfType(json, r'description'), + expiresAt: mapDateTime(json, r'expiresAt', ''), + allowUpload: mapValueOfType(json, r'allowUpload') ?? false, + allowDownload: mapValueOfType(json, r'allowDownload') ?? true, + showExif: mapValueOfType(json, r'showExif') ?? true, ); } return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = CreateAssetsShareLinkDto.fromJson(row); + final value = SharedLinkCreateDto.fromJson(row); if (value != null) { result.add(value); } @@ -161,12 +149,12 @@ class CreateAssetsShareLinkDto { return result.toList(growable: growable); } - static Map mapFromJson(dynamic json) { - final map = {}; + static Map mapFromJson(dynamic json) { + final map = {}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = CreateAssetsShareLinkDto.fromJson(entry.value); + final value = SharedLinkCreateDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -175,14 +163,14 @@ class CreateAssetsShareLinkDto { return map; } - // maps a json object with a list of CreateAssetsShareLinkDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; + // maps a json object with a list of SharedLinkCreateDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = CreateAssetsShareLinkDto.listFromJson(entry.value, growable: growable,); + map[entry.key] = SharedLinkCreateDto.listFromJson(entry.value, growable: growable,); } } return map; @@ -190,7 +178,7 @@ class CreateAssetsShareLinkDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { - 'assetIds', + 'type', }; } diff --git a/mobile/openapi/lib/model/edit_shared_link_dto.dart b/mobile/openapi/lib/model/shared_link_edit_dto.dart similarity index 82% rename from mobile/openapi/lib/model/edit_shared_link_dto.dart rename to mobile/openapi/lib/model/shared_link_edit_dto.dart index e6a38994f..5f031ca79 100644 --- a/mobile/openapi/lib/model/edit_shared_link_dto.dart +++ b/mobile/openapi/lib/model/shared_link_edit_dto.dart @@ -10,9 +10,9 @@ part of openapi.api; -class EditSharedLinkDto { - /// Returns a new [EditSharedLinkDto] instance. - EditSharedLinkDto({ +class SharedLinkEditDto { + /// Returns a new [SharedLinkEditDto] instance. + SharedLinkEditDto({ this.description, this.expiresAt, this.allowUpload, @@ -55,7 +55,7 @@ class EditSharedLinkDto { bool? showExif; @override - bool operator ==(Object other) => identical(this, other) || other is EditSharedLinkDto && + bool operator ==(Object other) => identical(this, other) || other is SharedLinkEditDto && other.description == description && other.expiresAt == expiresAt && other.allowUpload == allowUpload && @@ -72,7 +72,7 @@ class EditSharedLinkDto { (showExif == null ? 0 : showExif!.hashCode); @override - String toString() => 'EditSharedLinkDto[description=$description, expiresAt=$expiresAt, allowUpload=$allowUpload, allowDownload=$allowDownload, showExif=$showExif]'; + String toString() => 'SharedLinkEditDto[description=$description, expiresAt=$expiresAt, allowUpload=$allowUpload, allowDownload=$allowDownload, showExif=$showExif]'; Map toJson() { final json = {}; @@ -104,10 +104,10 @@ class EditSharedLinkDto { return json; } - /// Returns a new [EditSharedLinkDto] instance and imports its values from + /// Returns a new [SharedLinkEditDto] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static EditSharedLinkDto? fromJson(dynamic value) { + static SharedLinkEditDto? fromJson(dynamic value) { if (value is Map) { final json = value.cast(); @@ -116,13 +116,13 @@ class EditSharedLinkDto { // Note 2: this code is stripped in release mode! assert(() { requiredKeys.forEach((key) { - assert(json.containsKey(key), 'Required key "EditSharedLinkDto[$key]" is missing from JSON.'); - assert(json[key] != null, 'Required key "EditSharedLinkDto[$key]" has a null value in JSON.'); + assert(json.containsKey(key), 'Required key "SharedLinkEditDto[$key]" is missing from JSON.'); + assert(json[key] != null, 'Required key "SharedLinkEditDto[$key]" has a null value in JSON.'); }); return true; }()); - return EditSharedLinkDto( + return SharedLinkEditDto( description: mapValueOfType(json, r'description'), expiresAt: mapDateTime(json, r'expiresAt', ''), allowUpload: mapValueOfType(json, r'allowUpload'), @@ -133,11 +133,11 @@ class EditSharedLinkDto { return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = EditSharedLinkDto.fromJson(row); + final value = SharedLinkEditDto.fromJson(row); if (value != null) { result.add(value); } @@ -146,12 +146,12 @@ class EditSharedLinkDto { return result.toList(growable: growable); } - static Map mapFromJson(dynamic json) { - final map = {}; + static Map mapFromJson(dynamic json) { + final map = {}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = EditSharedLinkDto.fromJson(entry.value); + final value = SharedLinkEditDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -160,14 +160,14 @@ class EditSharedLinkDto { return map; } - // maps a json object with a list of EditSharedLinkDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; + // maps a json object with a list of SharedLinkEditDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = EditSharedLinkDto.listFromJson(entry.value, growable: growable,); + map[entry.key] = SharedLinkEditDto.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/mobile/openapi/lib/model/shared_link_response_dto.dart b/mobile/openapi/lib/model/shared_link_response_dto.dart index e7f2f9bf5..77b4a5016 100644 --- a/mobile/openapi/lib/model/shared_link_response_dto.dart +++ b/mobile/openapi/lib/model/shared_link_response_dto.dart @@ -15,7 +15,7 @@ class SharedLinkResponseDto { SharedLinkResponseDto({ required this.type, required this.id, - this.description, + required this.description, required this.userId, required this.key, required this.createdAt, @@ -31,12 +31,6 @@ class SharedLinkResponseDto { String id; - /// - /// 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? description; String userId; @@ -206,6 +200,7 @@ class SharedLinkResponseDto { static const requiredKeys = { 'type', 'id', + 'description', 'userId', 'key', 'createdAt', diff --git a/mobile/openapi/lib/model/update_user_dto.dart b/mobile/openapi/lib/model/update_user_dto.dart index 570eaaa7c..1a77bd909 100644 --- a/mobile/openapi/lib/model/update_user_dto.dart +++ b/mobile/openapi/lib/model/update_user_dto.dart @@ -19,6 +19,7 @@ class UpdateUserDto { this.firstName, this.lastName, this.storageLabel, + this.externalPath, this.isAdmin, this.shouldChangePassword, }); @@ -65,6 +66,14 @@ class UpdateUserDto { /// String? storageLabel; + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? externalPath; + /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -89,6 +98,7 @@ class UpdateUserDto { other.firstName == firstName && other.lastName == lastName && other.storageLabel == storageLabel && + other.externalPath == externalPath && other.isAdmin == isAdmin && other.shouldChangePassword == shouldChangePassword; @@ -101,11 +111,12 @@ class UpdateUserDto { (firstName == null ? 0 : firstName!.hashCode) + (lastName == null ? 0 : lastName!.hashCode) + (storageLabel == null ? 0 : storageLabel!.hashCode) + + (externalPath == null ? 0 : externalPath!.hashCode) + (isAdmin == null ? 0 : isAdmin!.hashCode) + (shouldChangePassword == null ? 0 : shouldChangePassword!.hashCode); @override - String toString() => 'UpdateUserDto[id=$id, email=$email, password=$password, firstName=$firstName, lastName=$lastName, storageLabel=$storageLabel, isAdmin=$isAdmin, shouldChangePassword=$shouldChangePassword]'; + String toString() => 'UpdateUserDto[id=$id, email=$email, password=$password, firstName=$firstName, lastName=$lastName, storageLabel=$storageLabel, externalPath=$externalPath, isAdmin=$isAdmin, shouldChangePassword=$shouldChangePassword]'; Map toJson() { final json = {}; @@ -135,6 +146,11 @@ class UpdateUserDto { } else { // json[r'storageLabel'] = null; } + if (this.externalPath != null) { + json[r'externalPath'] = this.externalPath; + } else { + // json[r'externalPath'] = null; + } if (this.isAdmin != null) { json[r'isAdmin'] = this.isAdmin; } else { @@ -173,6 +189,7 @@ class UpdateUserDto { firstName: mapValueOfType(json, r'firstName'), lastName: mapValueOfType(json, r'lastName'), storageLabel: mapValueOfType(json, r'storageLabel'), + externalPath: mapValueOfType(json, r'externalPath'), isAdmin: mapValueOfType(json, r'isAdmin'), shouldChangePassword: mapValueOfType(json, r'shouldChangePassword'), ); diff --git a/mobile/openapi/test/album_api_test.dart b/mobile/openapi/test/album_api_test.dart index 77f92590e..28c93deb4 100644 --- a/mobile/openapi/test/album_api_test.dart +++ b/mobile/openapi/test/album_api_test.dart @@ -32,11 +32,6 @@ void main() { // TODO }); - //Future createAlbumSharedLink(CreateAlbumShareLinkDto createAlbumShareLinkDto) async - test('test createAlbumSharedLink', () async { - // TODO - }); - //Future deleteAlbum(String id) async test('test deleteAlbum', () async { // TODO diff --git a/mobile/openapi/test/album_response_dto_test.dart b/mobile/openapi/test/album_response_dto_test.dart index bc8c15395..b55b0eb21 100644 --- a/mobile/openapi/test/album_response_dto_test.dart +++ b/mobile/openapi/test/album_response_dto_test.dart @@ -71,6 +71,11 @@ void main() { // TODO }); + // DateTime lastModifiedAssetTimestamp + test('to test the property `lastModifiedAssetTimestamp`', () async { + // TODO + }); + }); diff --git a/mobile/openapi/test/asset_api_test.dart b/mobile/openapi/test/asset_api_test.dart index 9a77c292b..1a2e510cf 100644 --- a/mobile/openapi/test/asset_api_test.dart +++ b/mobile/openapi/test/asset_api_test.dart @@ -17,11 +17,6 @@ void main() { // final instance = AssetApi(); group('tests for AssetApi', () { - //Future addAssetsToSharedLink(AddAssetsDto addAssetsDto, { String key }) async - test('test addAssetsToSharedLink', () async { - // TODO - }); - // Checks if assets exist by checksums // //Future bulkUploadCheck(AssetBulkUploadCheckDto assetBulkUploadCheckDto) async @@ -43,11 +38,6 @@ void main() { // TODO }); - //Future createAssetsSharedLink(CreateAssetsShareLinkDto createAssetsShareLinkDto) async - test('test createAssetsSharedLink', () async { - // TODO - }); - //Future> deleteAsset(DeleteAssetDto deleteAssetDto) async test('test deleteAsset', () async { // TODO @@ -141,8 +131,8 @@ void main() { // TODO }); - //Future removeAssetsFromSharedLink(RemoveAssetsDto removeAssetsDto, { String key }) async - test('test removeAssetsFromSharedLink', () async { + //Future importFile(ImportAssetDto importAssetDto) async + test('test importFile', () async { // TODO }); @@ -163,7 +153,7 @@ void main() { // TODO }); - //Future uploadFile(AssetTypeEnum assetType, MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, bool isFavorite, String fileExtension, { String key, MultipartFile livePhotoData, MultipartFile sidecarData, bool isArchived, bool isVisible, String duration }) async + //Future uploadFile(AssetTypeEnum assetType, MultipartFile assetData, String fileExtension, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, bool isFavorite, { String key, MultipartFile livePhotoData, MultipartFile sidecarData, bool isReadOnly, bool isArchived, bool isVisible, String duration }) async test('test uploadFile', () async { // TODO }); diff --git a/mobile/openapi/test/create_album_share_link_dto_test.dart b/mobile/openapi/test/create_album_share_link_dto_test.dart deleted file mode 100644 index c9dfc6376..000000000 --- a/mobile/openapi/test/create_album_share_link_dto_test.dart +++ /dev/null @@ -1,52 +0,0 @@ -// -// 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 CreateAlbumShareLinkDto -void main() { - // final instance = CreateAlbumShareLinkDto(); - - group('test CreateAlbumShareLinkDto', () { - // String albumId - test('to test the property `albumId`', () async { - // TODO - }); - - // DateTime expiresAt - test('to test the property `expiresAt`', () async { - // TODO - }); - - // bool allowUpload - test('to test the property `allowUpload`', () async { - // TODO - }); - - // bool allowDownload - test('to test the property `allowDownload`', () async { - // TODO - }); - - // bool showExif - test('to test the property `showExif`', () async { - // TODO - }); - - // String description - test('to test the property `description`', () async { - // TODO - }); - - - }); - -} diff --git a/mobile/openapi/test/create_user_dto_test.dart b/mobile/openapi/test/create_user_dto_test.dart index b38665fd3..327acbbe3 100644 --- a/mobile/openapi/test/create_user_dto_test.dart +++ b/mobile/openapi/test/create_user_dto_test.dart @@ -41,6 +41,11 @@ void main() { // TODO }); + // String externalPath + test('to test the property `externalPath`', () async { + // TODO + }); + }); diff --git a/mobile/openapi/test/import_asset_dto_test.dart b/mobile/openapi/test/import_asset_dto_test.dart new file mode 100644 index 000000000..ca7526cc2 --- /dev/null +++ b/mobile/openapi/test/import_asset_dto_test.dart @@ -0,0 +1,82 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + +// tests for ImportAssetDto +void main() { + // final instance = ImportAssetDto(); + + group('test ImportAssetDto', () { + // AssetTypeEnum assetType + test('to test the property `assetType`', () async { + // TODO + }); + + // bool isReadOnly (default value: true) + test('to test the property `isReadOnly`', () async { + // TODO + }); + + // String assetPath + test('to test the property `assetPath`', () async { + // TODO + }); + + // String sidecarPath + test('to test the property `sidecarPath`', () async { + // TODO + }); + + // String deviceAssetId + test('to test the property `deviceAssetId`', () async { + // TODO + }); + + // String deviceId + test('to test the property `deviceId`', () async { + // TODO + }); + + // DateTime fileCreatedAt + test('to test the property `fileCreatedAt`', () async { + // TODO + }); + + // DateTime fileModifiedAt + test('to test the property `fileModifiedAt`', () async { + // TODO + }); + + // bool isFavorite + test('to test the property `isFavorite`', () async { + // TODO + }); + + // bool isArchived + test('to test the property `isArchived`', () async { + // TODO + }); + + // bool isVisible + test('to test the property `isVisible`', () async { + // TODO + }); + + // String duration + test('to test the property `duration`', () async { + // TODO + }); + + + }); + +} diff --git a/mobile/openapi/test/share_api_test.dart b/mobile/openapi/test/shared_link_api_test.dart similarity index 59% rename from mobile/openapi/test/share_api_test.dart rename to mobile/openapi/test/shared_link_api_test.dart index 4c715826b..05843bad7 100644 --- a/mobile/openapi/test/share_api_test.dart +++ b/mobile/openapi/test/shared_link_api_test.dart @@ -12,11 +12,21 @@ import 'package:openapi/api.dart'; import 'package:test/test.dart'; -/// tests for ShareApi +/// tests for SharedLinkApi void main() { - // final instance = ShareApi(); + // final instance = SharedLinkApi(); + + group('tests for SharedLinkApi', () { + //Future> addSharedLinkAssets(String id, AssetIdsDto assetIdsDto, { String key }) async + test('test addSharedLinkAssets', () async { + // TODO + }); + + //Future createSharedLink(SharedLinkCreateDto sharedLinkCreateDto) async + test('test createSharedLink', () async { + // TODO + }); - group('tests for ShareApi', () { //Future> getAllSharedLinks() async test('test getAllSharedLinks', () async { // TODO @@ -37,7 +47,12 @@ void main() { // TODO }); - //Future updateSharedLink(String id, EditSharedLinkDto editSharedLinkDto) async + //Future> removeSharedLinkAssets(String id, AssetIdsDto assetIdsDto, { String key }) async + test('test removeSharedLinkAssets', () async { + // TODO + }); + + //Future updateSharedLink(String id, SharedLinkEditDto sharedLinkEditDto) async test('test updateSharedLink', () async { // TODO }); diff --git a/mobile/openapi/test/create_assets_share_link_dto_test.dart b/mobile/openapi/test/shared_link_create_dto_test.dart similarity index 67% rename from mobile/openapi/test/create_assets_share_link_dto_test.dart rename to mobile/openapi/test/shared_link_create_dto_test.dart index 832e26904..0a2a6a7f9 100644 --- a/mobile/openapi/test/create_assets_share_link_dto_test.dart +++ b/mobile/openapi/test/shared_link_create_dto_test.dart @@ -11,33 +11,23 @@ import 'package:openapi/api.dart'; import 'package:test/test.dart'; -// tests for CreateAssetsShareLinkDto +// tests for SharedLinkCreateDto void main() { - // final instance = CreateAssetsShareLinkDto(); + // final instance = SharedLinkCreateDto(); + + group('test SharedLinkCreateDto', () { + // SharedLinkType type + test('to test the property `type`', () async { + // TODO + }); - group('test CreateAssetsShareLinkDto', () { // List assetIds (default value: const []) test('to test the property `assetIds`', () async { // TODO }); - // DateTime expiresAt - test('to test the property `expiresAt`', () async { - // TODO - }); - - // bool allowUpload - test('to test the property `allowUpload`', () async { - // TODO - }); - - // bool allowDownload - test('to test the property `allowDownload`', () async { - // TODO - }); - - // bool showExif - test('to test the property `showExif`', () async { + // String albumId + test('to test the property `albumId`', () async { // TODO }); @@ -46,6 +36,26 @@ void main() { // TODO }); + // DateTime expiresAt + test('to test the property `expiresAt`', () async { + // TODO + }); + + // bool allowUpload (default value: false) + test('to test the property `allowUpload`', () async { + // TODO + }); + + // bool allowDownload (default value: true) + test('to test the property `allowDownload`', () async { + // TODO + }); + + // bool showExif (default value: true) + test('to test the property `showExif`', () async { + // TODO + }); + }); diff --git a/mobile/openapi/test/edit_shared_link_dto_test.dart b/mobile/openapi/test/shared_link_edit_dto_test.dart similarity index 88% rename from mobile/openapi/test/edit_shared_link_dto_test.dart rename to mobile/openapi/test/shared_link_edit_dto_test.dart index 1308ee71d..575892f1e 100644 --- a/mobile/openapi/test/edit_shared_link_dto_test.dart +++ b/mobile/openapi/test/shared_link_edit_dto_test.dart @@ -11,11 +11,11 @@ import 'package:openapi/api.dart'; import 'package:test/test.dart'; -// tests for EditSharedLinkDto +// tests for SharedLinkEditDto void main() { - // final instance = EditSharedLinkDto(); + // final instance = SharedLinkEditDto(); - group('test EditSharedLinkDto', () { + group('test SharedLinkEditDto', () { // String description test('to test the property `description`', () async { // TODO diff --git a/mobile/openapi/test/update_user_dto_test.dart b/mobile/openapi/test/update_user_dto_test.dart index 4ff0bc8b6..5e89eca18 100644 --- a/mobile/openapi/test/update_user_dto_test.dart +++ b/mobile/openapi/test/update_user_dto_test.dart @@ -46,6 +46,11 @@ void main() { // TODO }); + // String externalPath + test('to test the property `externalPath`', () async { + // TODO + }); + // bool isAdmin test('to test the property `isAdmin`', () async { // TODO diff --git a/mobile/openapi/test/user_response_dto_test.dart b/mobile/openapi/test/user_response_dto_test.dart index ae05daf60..f5c70f21f 100644 --- a/mobile/openapi/test/user_response_dto_test.dart +++ b/mobile/openapi/test/user_response_dto_test.dart @@ -41,6 +41,11 @@ void main() { // TODO }); + // String externalPath + test('to test the property `externalPath`', () async { + // TODO + }); + // String profileImagePath test('to test the property `profileImagePath`', () async { // TODO diff --git a/server/Dockerfile b/server/Dockerfile index c8b807993..87511fde2 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -2,7 +2,7 @@ FROM node:18.16.0-alpine3.18@sha256:f41850f74ff16a33daff988e2ea06ef8f5daeb6fb849 WORKDIR /usr/src/app -RUN apk add --update-cache build-base imagemagick-dev python3 ffmpeg libraw-dev perl vips-dev vips-heif vips-magick +RUN apk add --update-cache build-base imagemagick-dev python3 ffmpeg libraw-dev perl vips-dev vips-heif vips-jxl vips-magick COPY package.json package-lock.json ./ @@ -23,7 +23,7 @@ ENV NODE_ENV=production WORKDIR /usr/src/app -RUN apk add --no-cache ffmpeg imagemagick-dev libraw-dev perl vips-dev vips-heif vips-magick +RUN apk add --no-cache ffmpeg imagemagick-dev libraw-dev perl vips-dev vips-heif vips-jxl vips-magick COPY --from=prod /usr/src/app/node_modules ./node_modules COPY --from=prod /usr/src/app/dist ./dist diff --git a/server/e2e/album.e2e-spec.ts b/server/e2e/album.e2e-spec.ts index 7f2396b27..c1a877e4c 100644 --- a/server/e2e/album.e2e-spec.ts +++ b/server/e2e/album.e2e-spec.ts @@ -1,7 +1,14 @@ -import { AlbumResponseDto, AuthService, CreateAlbumDto, SharedLinkResponseDto, UserService } from '@app/domain'; -import { CreateAlbumShareLinkDto } from '@app/immich/api-v1/album/dto/create-album-shared-link.dto'; +import { + AlbumResponseDto, + AuthService, + CreateAlbumDto, + SharedLinkCreateDto, + SharedLinkResponseDto, + UserService, +} from '@app/domain'; import { AppModule } from '@app/immich/app.module'; import { AuthUserDto } from '@app/immich/decorators/auth-user.decorator'; +import { SharedLinkType } from '@app/infra/entities'; import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import request from 'supertest'; @@ -14,8 +21,10 @@ async function _createAlbum(app: INestApplication, data: CreateAlbumDto) { return res.body as AlbumResponseDto; } -async function _createAlbumSharedLink(app: INestApplication, data: CreateAlbumShareLinkDto) { - const res = await request(app.getHttpServer()).post('/album/create-shared-link').send(data); +async function _createAlbumSharedLink(app: INestApplication, data: Omit) { + const res = await request(app.getHttpServer()) + .post('/shared-link') + .send({ ...data, type: SharedLinkType.ALBUM }); expect(res.status).toEqual(201); return res.body as SharedLinkResponseDto; } diff --git a/server/e2e/user.e2e-spec.ts b/server/e2e/user.e2e-spec.ts index d74626cb3..2f3ead59a 100644 --- a/server/e2e/user.e2e-spec.ts +++ b/server/e2e/user.e2e-spec.ts @@ -105,6 +105,7 @@ describe('User', () => { updatedAt: expect.anything(), oauthId: '', storageLabel: null, + externalPath: null, }, { email: userTwoEmail, @@ -119,6 +120,7 @@ describe('User', () => { updatedAt: expect.anything(), oauthId: '', storageLabel: null, + externalPath: null, }, { email: authUserEmail, @@ -133,6 +135,7 @@ describe('User', () => { updatedAt: expect.anything(), oauthId: '', storageLabel: 'admin', + externalPath: null, }, ]), ); diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 12fa55b59..bd87580b2 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -127,48 +127,6 @@ ] } }, - "/album/create-shared-link": { - "post": { - "operationId": "createAlbumSharedLink", - "parameters": [], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateAlbumShareLinkDto" - } - } - } - }, - "responses": { - "201": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SharedLinkResponseDto" - } - } - } - } - }, - "tags": [ - "Album" - ], - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ] - } - }, "/album/{id}": { "patch": { "operationId": "updateAlbumInfo", @@ -1472,6 +1430,48 @@ ] } }, + "/asset/import": { + "post": { + "operationId": "importFile", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportAssetDto" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssetFileUploadResponseDto" + } + } + } + } + }, + "tags": [ + "Asset" + ], + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ] + } + }, "/asset/map-marker": { "get": { "operationId": "getMapMarkers", @@ -1660,150 +1660,6 @@ ] } }, - "/asset/shared-link": { - "post": { - "operationId": "createAssetsSharedLink", - "parameters": [], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateAssetsShareLinkDto" - } - } - } - }, - "responses": { - "201": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SharedLinkResponseDto" - } - } - } - } - }, - "tags": [ - "Asset" - ], - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ] - } - }, - "/asset/shared-link/add": { - "patch": { - "operationId": "addAssetsToSharedLink", - "parameters": [ - { - "name": "key", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AddAssetsDto" - } - } - } - }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SharedLinkResponseDto" - } - } - } - } - }, - "tags": [ - "Asset" - ], - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ] - } - }, - "/asset/shared-link/remove": { - "patch": { - "operationId": "removeAssetsFromSharedLink", - "parameters": [ - { - "name": "key", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RemoveAssetsDto" - } - } - } - }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SharedLinkResponseDto" - } - } - } - } - }, - "tags": [ - "Asset" - ], - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ] - } - }, "/asset/stat/archive": { "get": { "operationId": "getArchivedAssetCountByUserId", @@ -3264,7 +3120,7 @@ ] } }, - "/share": { + "/shared-link": { "get": { "operationId": "getAllSharedLinks", "parameters": [], @@ -3284,7 +3140,47 @@ } }, "tags": [ - "share" + "Shared Link" + ], + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ] + }, + "post": { + "operationId": "createSharedLink", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SharedLinkCreateDto" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SharedLinkResponseDto" + } + } + } + } + }, + "tags": [ + "Shared Link" ], "security": [ { @@ -3299,7 +3195,7 @@ ] } }, - "/share/me": { + "/shared-link/me": { "get": { "operationId": "getMySharedLink", "parameters": [ @@ -3325,7 +3221,7 @@ } }, "tags": [ - "share" + "Shared Link" ], "security": [ { @@ -3340,7 +3236,7 @@ ] } }, - "/share/{id}": { + "/shared-link/{id}": { "get": { "operationId": "getSharedLinkById", "parameters": [ @@ -3367,7 +3263,7 @@ } }, "tags": [ - "share" + "Shared Link" ], "security": [ { @@ -3399,7 +3295,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EditSharedLinkDto" + "$ref": "#/components/schemas/SharedLinkEditDto" } } } @@ -3417,7 +3313,7 @@ } }, "tags": [ - "share" + "Shared Link" ], "security": [ { @@ -3450,7 +3346,131 @@ } }, "tags": [ - "share" + "Shared Link" + ], + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ] + } + }, + "/shared-link/{id}/assets": { + "put": { + "operationId": "addSharedLinkAssets", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "name": "key", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssetIdsDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AssetIdsResponseDto" + } + } + } + } + } + }, + "tags": [ + "Shared Link" + ], + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ] + }, + "delete": { + "operationId": "removeSharedLinkAssets", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "name": "key", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssetIdsDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AssetIdsResponseDto" + } + } + } + } + } + }, + "tags": [ + "Shared Link" ], "security": [ { @@ -4584,6 +4604,10 @@ }, "owner": { "$ref": "#/components/schemas/UserResponseDto" + }, + "lastModifiedAssetTimestamp": { + "format": "date-time", + "type": "string" } }, "required": [ @@ -5085,34 +5109,6 @@ "albumName" ] }, - "CreateAlbumShareLinkDto": { - "type": "object", - "properties": { - "albumId": { - "type": "string", - "format": "uuid" - }, - "expiresAt": { - "format": "date-time", - "type": "string" - }, - "allowUpload": { - "type": "boolean" - }, - "allowDownload": { - "type": "boolean" - }, - "showExif": { - "type": "boolean" - }, - "description": { - "type": "string" - } - }, - "required": [ - "albumId" - ] - }, "CreateAssetDto": { "type": "object", "properties": { @@ -5131,6 +5127,13 @@ "type": "string", "format": "binary" }, + "isReadOnly": { + "type": "boolean", + "default": false + }, + "fileExtension": { + "type": "string" + }, "deviceAssetId": { "type": "string" }, @@ -5154,9 +5157,6 @@ "isVisible": { "type": "boolean" }, - "fileExtension": { - "type": "string" - }, "duration": { "type": "string" } @@ -5164,48 +5164,12 @@ "required": [ "assetType", "assetData", + "fileExtension", "deviceAssetId", "deviceId", "fileCreatedAt", "fileModifiedAt", - "isFavorite", - "fileExtension" - ] - }, - "CreateAssetsShareLinkDto": { - "type": "object", - "properties": { - "assetIds": { - "title": "Array asset IDs to be shared", - "example": [ - "bf973405-3f2a-48d2-a687-2ed4167164be", - "dd41870b-5d00-46d2-924e-1d8489a0aa0f", - "fad77c3f-deef-4e7e-9608-14c1aa4e559a" - ], - "type": "array", - "items": { - "type": "string" - } - }, - "expiresAt": { - "format": "date-time", - "type": "string" - }, - "allowUpload": { - "type": "boolean" - }, - "allowDownload": { - "type": "boolean" - }, - "showExif": { - "type": "boolean" - }, - "description": { - "type": "string" - } - }, - "required": [ - "assetIds" + "isFavorite" ] }, "CreateProfileImageDto": { @@ -5268,6 +5232,10 @@ "storageLabel": { "type": "string", "nullable": true + }, + "externalPath": { + "type": "string", + "nullable": true } }, "required": [ @@ -5388,28 +5356,6 @@ "assetIds" ] }, - "EditSharedLinkDto": { - "type": "object", - "properties": { - "description": { - "type": "string" - }, - "expiresAt": { - "format": "date-time", - "type": "string", - "nullable": true - }, - "allowUpload": { - "type": "boolean" - }, - "allowDownload": { - "type": "boolean" - }, - "showExif": { - "type": "boolean" - } - } - }, "ExifResponseDto": { "type": "object", "properties": { @@ -5565,6 +5511,59 @@ "timeGroup" ] }, + "ImportAssetDto": { + "type": "object", + "properties": { + "assetType": { + "$ref": "#/components/schemas/AssetTypeEnum" + }, + "isReadOnly": { + "type": "boolean", + "default": true + }, + "assetPath": { + "type": "string" + }, + "sidecarPath": { + "type": "string" + }, + "deviceAssetId": { + "type": "string" + }, + "deviceId": { + "type": "string" + }, + "fileCreatedAt": { + "format": "date-time", + "type": "string" + }, + "fileModifiedAt": { + "format": "date-time", + "type": "string" + }, + "isFavorite": { + "type": "boolean" + }, + "isArchived": { + "type": "boolean" + }, + "isVisible": { + "type": "boolean" + }, + "duration": { + "type": "string" + } + }, + "required": [ + "assetType", + "assetPath", + "deviceAssetId", + "deviceId", + "fileCreatedAt", + "fileModifiedAt", + "isFavorite" + ] + }, "JobCommand": { "type": "string", "enum": [ @@ -6156,6 +6155,71 @@ "patch" ] }, + "SharedLinkCreateDto": { + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/SharedLinkType" + }, + "assetIds": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + }, + "albumId": { + "type": "string", + "format": "uuid" + }, + "description": { + "type": "string" + }, + "expiresAt": { + "format": "date-time", + "type": "string", + "nullable": true, + "default": null + }, + "allowUpload": { + "type": "boolean", + "default": false + }, + "allowDownload": { + "type": "boolean", + "default": true + }, + "showExif": { + "type": "boolean", + "default": true + } + }, + "required": [ + "type" + ] + }, + "SharedLinkEditDto": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "expiresAt": { + "format": "date-time", + "type": "string", + "nullable": true + }, + "allowUpload": { + "type": "boolean" + }, + "allowDownload": { + "type": "boolean" + }, + "showExif": { + "type": "boolean" + } + } + }, "SharedLinkResponseDto": { "type": "object", "properties": { @@ -6166,7 +6230,8 @@ "type": "string" }, "description": { - "type": "string" + "type": "string", + "nullable": true }, "userId": { "type": "string" @@ -6205,6 +6270,7 @@ "required": [ "type", "id", + "description", "userId", "key", "createdAt", @@ -6629,6 +6695,9 @@ "storageLabel": { "type": "string" }, + "externalPath": { + "type": "string" + }, "isAdmin": { "type": "boolean" }, @@ -6702,6 +6771,10 @@ "type": "string", "nullable": true }, + "externalPath": { + "type": "string", + "nullable": true + }, "profileImagePath": { "type": "string" }, @@ -6734,6 +6807,7 @@ "firstName", "lastName", "storageLabel", + "externalPath", "profileImagePath", "shouldChangePassword", "isAdmin", diff --git a/server/package-lock.json b/server/package-lock.json index 664345d08..2bb83a422 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -21,6 +21,7 @@ "@nestjs/typeorm": "^9.0.1", "@nestjs/websockets": "^9.2.1", "@socket.io/redis-adapter": "^8.0.1", + "@types/mime-types": "^2.1.1", "archiver": "^5.3.1", "axios": "^0.26.0", "bcrypt": "^5.0.1", @@ -38,6 +39,7 @@ "local-reverse-geocoder": "0.12.5", "lodash": "^4.17.21", "luxon": "^3.0.3", + "mime-types": "^2.1.35", "mv": "^2.1.1", "nest-commander": "^3.3.0", "openid-client": "^5.2.1", @@ -3018,6 +3020,11 @@ "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", "dev": true }, + "node_modules/@types/mime-types": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.1.tgz", + "integrity": "sha512-vXOTGVSLR2jMw440moWTC7H19iUyLtP3Z1YTj7cSsubOICinjMxFeb/V57v9QdyyPGbbWolUFSSmSiRSn94tFw==" + }, "node_modules/@types/multer": { "version": "1.4.7", "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.7.tgz", @@ -14296,6 +14303,11 @@ "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", "dev": true }, + "@types/mime-types": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.1.tgz", + "integrity": "sha512-vXOTGVSLR2jMw440moWTC7H19iUyLtP3Z1YTj7cSsubOICinjMxFeb/V57v9QdyyPGbbWolUFSSmSiRSn94tFw==" + }, "@types/multer": { "version": "1.4.7", "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.7.tgz", diff --git a/server/package.json b/server/package.json index de89b46ef..235caffdf 100644 --- a/server/package.json +++ b/server/package.json @@ -50,6 +50,7 @@ "@nestjs/typeorm": "^9.0.1", "@nestjs/websockets": "^9.2.1", "@socket.io/redis-adapter": "^8.0.1", + "@types/mime-types": "^2.1.1", "archiver": "^5.3.1", "axios": "^0.26.0", "bcrypt": "^5.0.1", @@ -67,6 +68,7 @@ "local-reverse-geocoder": "0.12.5", "lodash": "^4.17.21", "luxon": "^3.0.3", + "mime-types": "^2.1.35", "mv": "^2.1.1", "nest-commander": "^3.3.0", "openid-client": "^5.2.1", diff --git a/server/src/domain/access/access.repository.ts b/server/src/domain/access/access.repository.ts index 628647c5e..f9949e46c 100644 --- a/server/src/domain/access/access.repository.ts +++ b/server/src/domain/access/access.repository.ts @@ -2,8 +2,11 @@ export const IAccessRepository = 'IAccessRepository'; export interface IAccessRepository { hasPartnerAccess(userId: string, partnerId: string): Promise; + hasAlbumAssetAccess(userId: string, assetId: string): Promise; hasOwnerAssetAccess(userId: string, assetId: string): Promise; hasPartnerAssetAccess(userId: string, assetId: string): Promise; hasSharedLinkAssetAccess(userId: string, assetId: string): Promise; + + hasAlbumOwnerAccess(userId: string, albumId: string): Promise; } diff --git a/server/src/domain/album/album-response.dto.ts b/server/src/domain/album/album-response.dto.ts index 2af7810db..e50c8aa16 100644 --- a/server/src/domain/album/album-response.dto.ts +++ b/server/src/domain/album/album-response.dto.ts @@ -16,6 +16,7 @@ export class AlbumResponseDto { owner!: UserResponseDto; @ApiProperty({ type: 'integer' }) assetCount!: number; + lastModifiedAssetTimestamp?: Date; } export function mapAlbum(entity: AlbumEntity): AlbumResponseDto { diff --git a/server/src/domain/album/album.service.spec.ts b/server/src/domain/album/album.service.spec.ts index 67e031db5..0b4f42a43 100644 --- a/server/src/domain/album/album.service.spec.ts +++ b/server/src/domain/album/album.service.spec.ts @@ -169,6 +169,7 @@ describe(AlbumService.name, () => { createdAt: new Date('2021-01-01'), deletedAt: null, updatedAt: new Date('2021-01-01'), + externalPath: null, }, ownerId: 'admin_id', shared: false, diff --git a/server/src/domain/album/album.service.ts b/server/src/domain/album/album.service.ts index 21461fe27..ba350db2c 100644 --- a/server/src/domain/album/album.service.ts +++ b/server/src/domain/album/album.service.ts @@ -53,15 +53,19 @@ export class AlbumService { return obj; }, {}); - return albums.map((album) => { - return { - ...album, - assets: album?.assets?.map(mapAsset), - sharedLinks: undefined, // Don't return shared links - shared: album.sharedLinks?.length > 0 || album.sharedUsers?.length > 0, - assetCount: albumsAssetCountObj[album.id], - } as AlbumResponseDto; - }); + return Promise.all( + albums.map(async (album) => { + const lastModifiedAsset = await this.assetRepository.getLastUpdatedAssetForAlbumId(album.id); + return { + ...album, + assets: album?.assets?.map(mapAsset), + sharedLinks: undefined, // Don't return shared links + shared: album.sharedLinks?.length > 0 || album.sharedUsers?.length > 0, + assetCount: albumsAssetCountObj[album.id], + lastModifiedAssetTimestamp: lastModifiedAsset?.fileModifiedAt, + } as AlbumResponseDto; + }), + ); } private async updateInvalidThumbnails(): Promise { diff --git a/server/src/domain/api-key/api-key.core.ts b/server/src/domain/api-key/api-key.core.ts index f70b3fee5..1b075a9c0 100644 --- a/server/src/domain/api-key/api-key.core.ts +++ b/server/src/domain/api-key/api-key.core.ts @@ -19,6 +19,7 @@ export class APIKeyCore { isAdmin: user.isAdmin, isPublicUser: false, isAllowUpload: true, + externalPath: user.externalPath, }; } diff --git a/server/src/domain/asset/asset.repository.ts b/server/src/domain/asset/asset.repository.ts index 16214931a..9479d3c12 100644 --- a/server/src/domain/asset/asset.repository.ts +++ b/server/src/domain/asset/asset.repository.ts @@ -47,6 +47,7 @@ export interface IAssetRepository { getWithout(pagination: PaginationOptions, property: WithoutProperty): Paginated; getWith(pagination: PaginationOptions, property: WithProperty): Paginated; getFirstAssetForAlbumId(albumId: string): Promise; + getLastUpdatedAssetForAlbumId(albumId: string): Promise; deleteAll(ownerId: string): Promise; getAll(pagination: PaginationOptions, options?: AssetSearchOptions): Paginated; save(asset: Partial): Promise; diff --git a/server/src/domain/auth/auth.service.ts b/server/src/domain/auth/auth.service.ts index ebc5ddefb..87df987d9 100644 --- a/server/src/domain/auth/auth.service.ts +++ b/server/src/domain/auth/auth.service.ts @@ -13,7 +13,7 @@ import { IKeyRepository } from '../api-key'; import { APIKeyCore } from '../api-key/api-key.core'; import { ICryptoRepository } from '../crypto/crypto.repository'; import { OAuthCore } from '../oauth/oauth.core'; -import { ISharedLinkRepository, SharedLinkCore } from '../shared-link'; +import { ISharedLinkRepository } from '../shared-link'; import { INITIAL_SYSTEM_CONFIG, ISystemConfigRepository } from '../system-config'; import { IUserRepository, UserCore } from '../user'; import { IUserTokenRepository, UserTokenCore } from '../user-token'; @@ -35,7 +35,6 @@ export class AuthService { private authCore: AuthCore; private oauthCore: OAuthCore; private userCore: UserCore; - private shareCore: SharedLinkCore; private keyCore: APIKeyCore; private logger = new Logger(AuthService.name); @@ -45,7 +44,7 @@ export class AuthService { @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository, @Inject(IUserRepository) userRepository: IUserRepository, @Inject(IUserTokenRepository) userTokenRepository: IUserTokenRepository, - @Inject(ISharedLinkRepository) shareRepository: ISharedLinkRepository, + @Inject(ISharedLinkRepository) private sharedLinkRepository: ISharedLinkRepository, @Inject(IKeyRepository) keyRepository: IKeyRepository, @Inject(INITIAL_SYSTEM_CONFIG) initialConfig: SystemConfig, @@ -54,7 +53,6 @@ export class AuthService { this.authCore = new AuthCore(cryptoRepository, configRepository, userTokenRepository, initialConfig); this.oauthCore = new OAuthCore(configRepository, initialConfig); this.userCore = new UserCore(userRepository, cryptoRepository); - this.shareCore = new SharedLinkCore(shareRepository, cryptoRepository); this.keyCore = new APIKeyCore(cryptoRepository, keyRepository); } @@ -147,7 +145,7 @@ export class AuthService { const apiKey = (headers[IMMICH_API_KEY_HEADER] || params.apiKey) as string; if (shareKey) { - return this.shareCore.validate(shareKey); + return this.validateSharedLink(shareKey); } if (userToken) { @@ -193,4 +191,29 @@ export class AuthService { const cookies = cookieParser.parse(headers.cookie || ''); return cookies[IMMICH_ACCESS_COOKIE] || null; } + + async validateSharedLink(key: string | string[]): Promise { + key = Array.isArray(key) ? key[0] : key; + + const bytes = Buffer.from(key, key.length === 100 ? 'hex' : 'base64url'); + const link = await this.sharedLinkRepository.getByKey(bytes); + if (link) { + if (!link.expiresAt || new Date(link.expiresAt) > new Date()) { + const user = link.user; + if (user) { + return { + id: user.id, + email: user.email, + isAdmin: user.isAdmin, + isPublicUser: true, + sharedLinkId: link.id, + isAllowUpload: link.allowUpload, + isAllowDownload: link.allowDownload, + isShowExif: link.showExif, + }; + } + } + } + throw new UnauthorizedException('Invalid share key'); + } } diff --git a/server/src/domain/auth/dto/auth-user.dto.ts b/server/src/domain/auth/dto/auth-user.dto.ts index 9af777e7b..0f2c9e41d 100644 --- a/server/src/domain/auth/dto/auth-user.dto.ts +++ b/server/src/domain/auth/dto/auth-user.dto.ts @@ -8,4 +8,5 @@ export class AuthUserDto { isAllowDownload?: boolean; isShowExif?: boolean; accessTokenId?: string; + externalPath?: string | null; } diff --git a/server/src/domain/crypto/crypto.repository.ts b/server/src/domain/crypto/crypto.repository.ts index d400b017d..67bacfb1e 100644 --- a/server/src/domain/crypto/crypto.repository.ts +++ b/server/src/domain/crypto/crypto.repository.ts @@ -2,6 +2,7 @@ export const ICryptoRepository = 'ICryptoRepository'; export interface ICryptoRepository { randomBytes(size: number): Buffer; + hashFile(filePath: string): Promise; hashSha256(data: string): string; hashBcrypt(data: string | Buffer, saltOrRounds: string | number): Promise; compareBcrypt(data: string | Buffer, encrypted: string): boolean; diff --git a/server/src/domain/domain.constant.ts b/server/src/domain/domain.constant.ts index 3aec785d7..a2bb8238a 100644 --- a/server/src/domain/domain.constant.ts +++ b/server/src/domain/domain.constant.ts @@ -27,3 +27,60 @@ export function assertMachineLearningEnabled() { throw new BadRequestException('Machine learning is not enabled.'); } } + +const validMimeTypes = [ + 'image/avif', + 'image/gif', + 'image/heic', + 'image/heif', + 'image/jpeg', + 'image/jxl', + 'image/png', + 'image/tiff', + 'image/webp', + 'image/x-adobe-dng', + 'image/x-arriflex-ari', + 'image/x-canon-cr2', + 'image/x-canon-cr3', + 'image/x-canon-crw', + 'image/x-epson-erf', + 'image/x-fuji-raf', + 'image/x-hasselblad-3fr', + 'image/x-hasselblad-fff', + 'image/x-kodak-dcr', + 'image/x-kodak-k25', + 'image/x-kodak-kdc', + 'image/x-leica-rwl', + 'image/x-minolta-mrw', + 'image/x-nikon-nef', + 'image/x-olympus-orf', + 'image/x-olympus-ori', + 'image/x-panasonic-raw', + 'image/x-pentax-pef', + 'image/x-phantom-cin', + 'image/x-phaseone-cap', + 'image/x-phaseone-iiq', + 'image/x-samsung-srw', + 'image/x-sigma-x3f', + 'image/x-sony-arw', + 'image/x-sony-sr2', + 'image/x-sony-srf', + 'video/3gpp', + 'video/mp2t', + 'video/mp4', + 'video/mpeg', + 'video/quicktime', + 'video/webm', + 'video/x-flv', + 'video/x-matroska', + 'video/x-ms-wmv', + 'video/x-msvideo', +]; + +export function isSupportedFileType(mimetype: string): boolean { + return validMimeTypes.includes(mimetype); +} + +export function isSidecarFileType(mimeType: string): boolean { + return ['application/xml', 'text/xml'].includes(mimeType); +} diff --git a/server/src/domain/partner/partner.service.spec.ts b/server/src/domain/partner/partner.service.spec.ts index 2422d20ce..c8e048969 100644 --- a/server/src/domain/partner/partner.service.spec.ts +++ b/server/src/domain/partner/partner.service.spec.ts @@ -17,6 +17,7 @@ const responseDto = { createdAt: new Date('2021-01-01'), deletedAt: null, updatedAt: new Date('2021-01-01'), + externalPath: null, }, user1: { email: 'immich@test.com', @@ -31,6 +32,7 @@ const responseDto = { createdAt: new Date('2021-01-01'), deletedAt: null, updatedAt: new Date('2021-01-01'), + externalPath: null, }, }; diff --git a/server/src/domain/shared-link/dto/create-shared-link.dto.ts b/server/src/domain/shared-link/dto/create-shared-link.dto.ts deleted file mode 100644 index db82021c6..000000000 --- a/server/src/domain/shared-link/dto/create-shared-link.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { AlbumEntity, AssetEntity, SharedLinkType } from '@app/infra/entities'; - -export class CreateSharedLinkDto { - description?: string; - expiresAt?: Date; - type!: SharedLinkType; - assets!: AssetEntity[]; - album?: AlbumEntity; - allowUpload?: boolean; - allowDownload?: boolean; - showExif?: boolean; -} diff --git a/server/src/domain/shared-link/dto/edit-shared-link.dto.ts b/server/src/domain/shared-link/dto/edit-shared-link.dto.ts deleted file mode 100644 index f35f5ed9a..000000000 --- a/server/src/domain/shared-link/dto/edit-shared-link.dto.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { IsOptional } from 'class-validator'; - -export class EditSharedLinkDto { - @IsOptional() - description?: string; - - @IsOptional() - expiresAt?: Date | null; - - @IsOptional() - allowUpload?: boolean; - - @IsOptional() - allowDownload?: boolean; - - @IsOptional() - showExif?: boolean; -} diff --git a/server/src/domain/shared-link/dto/index.ts b/server/src/domain/shared-link/dto/index.ts deleted file mode 100644 index 8f29f0ca7..000000000 --- a/server/src/domain/shared-link/dto/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './create-shared-link.dto'; -export * from './edit-shared-link.dto'; diff --git a/server/src/domain/shared-link/index.ts b/server/src/domain/shared-link/index.ts index d97d10330..78f903346 100644 --- a/server/src/domain/shared-link/index.ts +++ b/server/src/domain/shared-link/index.ts @@ -1,5 +1,4 @@ -export * from './dto'; -export * from './response-dto'; -export * from './shared-link.core'; +export * from './shared-link-response.dto'; +export * from './shared-link.dto'; export * from './shared-link.repository'; export * from './shared-link.service'; diff --git a/server/src/domain/shared-link/response-dto/index.ts b/server/src/domain/shared-link/response-dto/index.ts deleted file mode 100644 index 008ee5c1a..000000000 --- a/server/src/domain/shared-link/response-dto/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './shared-link-response.dto'; diff --git a/server/src/domain/shared-link/response-dto/shared-link-response.dto.ts b/server/src/domain/shared-link/shared-link-response.dto.ts similarity index 95% rename from server/src/domain/shared-link/response-dto/shared-link-response.dto.ts rename to server/src/domain/shared-link/shared-link-response.dto.ts index f4ccabcfe..6c9832e09 100644 --- a/server/src/domain/shared-link/response-dto/shared-link-response.dto.ts +++ b/server/src/domain/shared-link/shared-link-response.dto.ts @@ -1,12 +1,12 @@ import { SharedLinkEntity, SharedLinkType } from '@app/infra/entities'; import { ApiProperty } from '@nestjs/swagger'; import _ from 'lodash'; -import { AlbumResponseDto, mapAlbumExcludeAssetInfo } from '../../album'; -import { AssetResponseDto, mapAsset, mapAssetWithoutExif } from '../../asset'; +import { AlbumResponseDto, mapAlbumExcludeAssetInfo } from '../album'; +import { AssetResponseDto, mapAsset, mapAssetWithoutExif } from '../asset'; export class SharedLinkResponseDto { id!: string; - description?: string; + description!: string | null; userId!: string; key!: string; diff --git a/server/src/domain/shared-link/shared-link.core.ts b/server/src/domain/shared-link/shared-link.core.ts deleted file mode 100644 index c64256d79..000000000 --- a/server/src/domain/shared-link/shared-link.core.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { AssetEntity, SharedLinkEntity } from '@app/infra/entities'; -import { BadRequestException, ForbiddenException, Logger, UnauthorizedException } from '@nestjs/common'; -import { AuthUserDto } from '../auth'; -import { ICryptoRepository } from '../crypto'; -import { CreateSharedLinkDto } from './dto'; -import { ISharedLinkRepository } from './shared-link.repository'; - -export class SharedLinkCore { - readonly logger = new Logger(SharedLinkCore.name); - - constructor(private repository: ISharedLinkRepository, private cryptoRepository: ICryptoRepository) {} - - // TODO: move to SharedLinkController/SharedLinkService - create(userId: string, dto: CreateSharedLinkDto): Promise { - return this.repository.create({ - key: Buffer.from(this.cryptoRepository.randomBytes(50)), - description: dto.description, - userId, - createdAt: new Date(), - expiresAt: dto.expiresAt ?? null, - type: dto.type, - assets: dto.assets, - album: dto.album, - allowUpload: dto.allowUpload ?? false, - allowDownload: dto.allowDownload ?? true, - showExif: dto.showExif ?? true, - }); - } - - async addAssets(userId: string, id: string, assets: AssetEntity[]) { - const link = await this.repository.get(userId, id); - if (!link) { - throw new BadRequestException('Shared link not found'); - } - - return this.repository.update({ ...link, assets: [...link.assets, ...assets] }); - } - - async removeAssets(userId: string, id: string, assets: AssetEntity[]) { - const link = await this.repository.get(userId, id); - if (!link) { - throw new BadRequestException('Shared link not found'); - } - - const newAssets = link.assets.filter((asset) => assets.find((a) => a.id === asset.id)); - - return this.repository.update({ ...link, assets: newAssets }); - } - - checkDownloadAccess(user: AuthUserDto) { - if (user.isPublicUser && !user.isAllowDownload) { - throw new ForbiddenException(); - } - } - - async validate(key: string | string[]): Promise { - key = Array.isArray(key) ? key[0] : key; - - const bytes = Buffer.from(key, key.length === 100 ? 'hex' : 'base64url'); - const link = await this.repository.getByKey(bytes); - if (link) { - if (!link.expiresAt || new Date(link.expiresAt) > new Date()) { - const user = link.user; - if (user) { - return { - id: user.id, - email: user.email, - isAdmin: user.isAdmin, - isPublicUser: true, - sharedLinkId: link.id, - isAllowUpload: link.allowUpload, - isAllowDownload: link.allowDownload, - isShowExif: link.showExif, - }; - } - } - } - throw new UnauthorizedException('Invalid share key'); - } -} diff --git a/server/src/domain/shared-link/shared-link.dto.ts b/server/src/domain/shared-link/shared-link.dto.ts new file mode 100644 index 000000000..0695ad8c5 --- /dev/null +++ b/server/src/domain/shared-link/shared-link.dto.ts @@ -0,0 +1,53 @@ +import { SharedLinkType } from '@app/infra/entities'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsBoolean, IsDate, IsEnum, IsOptional, IsString } from 'class-validator'; +import { ValidateUUID } from '../../immich/decorators/validate-uuid.decorator'; + +export class SharedLinkCreateDto { + @IsEnum(SharedLinkType) + @ApiProperty({ enum: SharedLinkType, enumName: 'SharedLinkType' }) + type!: SharedLinkType; + + @ValidateUUID({ each: true, optional: true }) + assetIds?: string[]; + + @ValidateUUID({ optional: true }) + albumId?: string; + + @IsString() + @IsOptional() + description?: string; + + @IsDate() + @IsOptional() + expiresAt?: Date | null = null; + + @IsOptional() + @IsBoolean() + allowUpload?: boolean = false; + + @IsOptional() + @IsBoolean() + allowDownload?: boolean = true; + + @IsOptional() + @IsBoolean() + showExif?: boolean = true; +} + +export class SharedLinkEditDto { + @IsOptional() + description?: string; + + @IsOptional() + expiresAt?: Date | null; + + @IsOptional() + allowUpload?: boolean; + + @IsOptional() + allowDownload?: boolean; + + @IsOptional() + showExif?: boolean; +} diff --git a/server/src/domain/shared-link/shared-link.repository.ts b/server/src/domain/shared-link/shared-link.repository.ts index 7b6dcb6fe..0f0255d0a 100644 --- a/server/src/domain/shared-link/shared-link.repository.ts +++ b/server/src/domain/shared-link/shared-link.repository.ts @@ -6,7 +6,7 @@ export interface ISharedLinkRepository { getAll(userId: string): Promise; get(userId: string, id: string): Promise; getByKey(key: Buffer): Promise; - create(entity: Omit): Promise; + create(entity: Partial): Promise; update(entity: Partial): Promise; remove(entity: SharedLinkEntity): Promise; } diff --git a/server/src/domain/shared-link/shared-link.service.spec.ts b/server/src/domain/shared-link/shared-link.service.spec.ts index 1c269784c..74fe479bf 100644 --- a/server/src/domain/shared-link/shared-link.service.spec.ts +++ b/server/src/domain/shared-link/shared-link.service.spec.ts @@ -1,16 +1,33 @@ import { BadRequestException, ForbiddenException } from '@nestjs/common'; -import { authStub, newSharedLinkRepositoryMock, sharedLinkResponseStub, sharedLinkStub } from '@test'; +import { + albumStub, + assetEntityStub, + authStub, + newAccessRepositoryMock, + newCryptoRepositoryMock, + newSharedLinkRepositoryMock, + sharedLinkResponseStub, + sharedLinkStub, +} from '@test'; +import { when } from 'jest-when'; +import _ from 'lodash'; +import { SharedLinkType } from '../../infra/entities/shared-link.entity'; +import { AssetIdErrorReason, IAccessRepository, ICryptoRepository } from '../index'; import { ISharedLinkRepository } from './shared-link.repository'; import { SharedLinkService } from './shared-link.service'; describe(SharedLinkService.name, () => { let sut: SharedLinkService; + let accessMock: jest.Mocked; + let cryptoMock: jest.Mocked; let shareMock: jest.Mocked; beforeEach(async () => { + accessMock = newAccessRepositoryMock(); + cryptoMock = newCryptoRepositoryMock(); shareMock = newSharedLinkRepositoryMock(); - sut = new SharedLinkService(shareMock); + sut = new SharedLinkService(accessMock, cryptoMock, shareMock); }); it('should work', () => { @@ -64,6 +81,82 @@ describe(SharedLinkService.name, () => { }); }); + describe('create', () => { + it('should not allow an album shared link without an albumId', async () => { + await expect(sut.create(authStub.admin, { type: SharedLinkType.ALBUM, assetIds: [] })).rejects.toBeInstanceOf( + BadRequestException, + ); + }); + + it('should not allow non-owners to create album shared links', async () => { + accessMock.hasAlbumOwnerAccess.mockResolvedValue(false); + await expect( + sut.create(authStub.admin, { type: SharedLinkType.ALBUM, assetIds: [], albumId: 'album-1' }), + ).rejects.toBeInstanceOf(BadRequestException); + }); + + it('should not allow individual shared links with no assets', async () => { + await expect( + sut.create(authStub.admin, { type: SharedLinkType.INDIVIDUAL, assetIds: [] }), + ).rejects.toBeInstanceOf(BadRequestException); + }); + + it('should require asset ownership to make an individual shared link', async () => { + accessMock.hasOwnerAssetAccess.mockResolvedValue(false); + await expect( + sut.create(authStub.admin, { type: SharedLinkType.INDIVIDUAL, assetIds: ['asset-1'] }), + ).rejects.toBeInstanceOf(BadRequestException); + }); + + it('should create an album shared link', async () => { + accessMock.hasAlbumOwnerAccess.mockResolvedValue(true); + shareMock.create.mockResolvedValue(sharedLinkStub.valid); + + await sut.create(authStub.admin, { type: SharedLinkType.ALBUM, albumId: albumStub.oneAsset.id }); + + expect(accessMock.hasAlbumOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, albumStub.oneAsset.id); + expect(shareMock.create).toHaveBeenCalledWith({ + type: SharedLinkType.ALBUM, + userId: authStub.admin.id, + albumId: albumStub.oneAsset.id, + allowDownload: true, + allowUpload: true, + assets: [], + description: null, + expiresAt: null, + showExif: true, + key: Buffer.from('random-bytes', 'utf8'), + }); + }); + + it('should create an individual shared link', async () => { + accessMock.hasOwnerAssetAccess.mockResolvedValue(true); + shareMock.create.mockResolvedValue(sharedLinkStub.individual); + + await sut.create(authStub.admin, { + type: SharedLinkType.INDIVIDUAL, + assetIds: [assetEntityStub.image.id], + showExif: true, + allowDownload: true, + allowUpload: true, + }); + + expect(accessMock.hasOwnerAssetAccess).toHaveBeenCalledWith(authStub.admin.id, assetEntityStub.image.id); + expect(shareMock.create).toHaveBeenCalledWith({ + type: SharedLinkType.INDIVIDUAL, + userId: authStub.admin.id, + albumId: null, + allowDownload: true, + allowUpload: true, + assets: [{ id: assetEntityStub.image.id }], + description: null, + expiresAt: null, + showExif: true, + key: Buffer.from('random-bytes', 'utf8'), + }); + }); + }); + describe('update', () => { it('should throw an error for an invalid shared link', async () => { shareMock.get.mockResolvedValue(null); @@ -100,4 +193,58 @@ describe(SharedLinkService.name, () => { expect(shareMock.remove).toHaveBeenCalledWith(sharedLinkStub.valid); }); }); + + describe('addAssets', () => { + it('should not work on album shared links', async () => { + shareMock.get.mockResolvedValue(sharedLinkStub.valid); + await expect(sut.addAssets(authStub.admin, 'link-1', { assetIds: ['asset-1'] })).rejects.toBeInstanceOf( + BadRequestException, + ); + }); + + it('should add assets to a shared link', async () => { + shareMock.get.mockResolvedValue(_.cloneDeep(sharedLinkStub.individual)); + shareMock.create.mockResolvedValue(sharedLinkStub.individual); + + when(accessMock.hasOwnerAssetAccess).calledWith(authStub.admin.id, 'asset-2').mockResolvedValue(false); + when(accessMock.hasOwnerAssetAccess).calledWith(authStub.admin.id, 'asset-3').mockResolvedValue(true); + + await expect( + sut.addAssets(authStub.admin, 'link-1', { assetIds: [assetEntityStub.image.id, 'asset-2', 'asset-3'] }), + ).resolves.toEqual([ + { assetId: assetEntityStub.image.id, success: false, error: AssetIdErrorReason.DUPLICATE }, + { assetId: 'asset-2', success: false, error: AssetIdErrorReason.NO_PERMISSION }, + { assetId: 'asset-3', success: true }, + ]); + + expect(accessMock.hasOwnerAssetAccess).toHaveBeenCalledTimes(2); + expect(shareMock.update).toHaveBeenCalledWith({ + ...sharedLinkStub.individual, + assets: [assetEntityStub.image, { id: 'asset-3' }], + }); + }); + }); + + describe('removeAssets', () => { + it('should not work on album shared links', async () => { + shareMock.get.mockResolvedValue(sharedLinkStub.valid); + await expect(sut.removeAssets(authStub.admin, 'link-1', { assetIds: ['asset-1'] })).rejects.toBeInstanceOf( + BadRequestException, + ); + }); + + it('should remove assets from a shared link', async () => { + shareMock.get.mockResolvedValue(_.cloneDeep(sharedLinkStub.individual)); + shareMock.create.mockResolvedValue(sharedLinkStub.individual); + + await expect( + sut.removeAssets(authStub.admin, 'link-1', { assetIds: [assetEntityStub.image.id, 'asset-2'] }), + ).resolves.toEqual([ + { assetId: assetEntityStub.image.id, success: true }, + { assetId: 'asset-2', success: false, error: AssetIdErrorReason.NOT_FOUND }, + ]); + + expect(shareMock.update).toHaveBeenCalledWith({ ...sharedLinkStub.individual, assets: [] }); + }); + }); }); diff --git a/server/src/domain/shared-link/shared-link.service.ts b/server/src/domain/shared-link/shared-link.service.ts index e82e16b22..2e30b629e 100644 --- a/server/src/domain/shared-link/shared-link.service.ts +++ b/server/src/domain/shared-link/shared-link.service.ts @@ -1,15 +1,22 @@ -import { SharedLinkEntity } from '@app/infra/entities'; +import { AssetEntity, SharedLinkEntity, SharedLinkType } from '@app/infra/entities'; import { BadRequestException, ForbiddenException, Inject, Injectable } from '@nestjs/common'; +import { IAccessRepository } from '../access'; +import { AssetIdErrorReason, AssetIdsDto, AssetIdsResponseDto } from '../asset'; import { AuthUserDto } from '../auth'; -import { EditSharedLinkDto } from './dto'; -import { mapSharedLink, mapSharedLinkWithNoExif, SharedLinkResponseDto } from './response-dto'; +import { ICryptoRepository } from '../crypto'; +import { mapSharedLink, mapSharedLinkWithNoExif, SharedLinkResponseDto } from './shared-link-response.dto'; +import { SharedLinkCreateDto, SharedLinkEditDto } from './shared-link.dto'; import { ISharedLinkRepository } from './shared-link.repository'; @Injectable() export class SharedLinkService { - constructor(@Inject(ISharedLinkRepository) private repository: ISharedLinkRepository) {} + constructor( + @Inject(IAccessRepository) private accessRepository: IAccessRepository, + @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, + @Inject(ISharedLinkRepository) private repository: ISharedLinkRepository, + ) {} - async getAll(authUser: AuthUserDto): Promise { + getAll(authUser: AuthUserDto): Promise { return this.repository.getAll(authUser.id).then((links) => links.map(mapSharedLink)); } @@ -30,7 +37,52 @@ export class SharedLinkService { return this.map(sharedLink, { withExif: true }); } - async update(authUser: AuthUserDto, id: string, dto: EditSharedLinkDto) { + async create(authUser: AuthUserDto, dto: SharedLinkCreateDto): Promise { + switch (dto.type) { + case SharedLinkType.ALBUM: + if (!dto.albumId) { + throw new BadRequestException('Invalid albumId'); + } + + const isAlbumOwner = await this.accessRepository.hasAlbumOwnerAccess(authUser.id, dto.albumId); + if (!isAlbumOwner) { + throw new BadRequestException('Invalid albumId'); + } + + break; + + case SharedLinkType.INDIVIDUAL: + if (!dto.assetIds || dto.assetIds.length === 0) { + throw new BadRequestException('Invalid assetIds'); + } + + for (const assetId of dto.assetIds) { + const hasAccess = await this.accessRepository.hasOwnerAssetAccess(authUser.id, assetId); + if (!hasAccess) { + throw new BadRequestException(`No access to assetId: ${assetId}`); + } + } + + break; + } + + const sharedLink = await this.repository.create({ + key: this.cryptoRepository.randomBytes(50), + userId: authUser.id, + type: dto.type, + albumId: dto.albumId || null, + assets: (dto.assetIds || []).map((id) => ({ id } as AssetEntity)), + description: dto.description || null, + expiresAt: dto.expiresAt || null, + allowUpload: dto.allowUpload ?? true, + allowDownload: dto.allowDownload ?? true, + showExif: dto.showExif ?? true, + }); + + return this.map(sharedLink, { withExif: true }); + } + + async update(authUser: AuthUserDto, id: string, dto: SharedLinkEditDto) { await this.findOrFail(authUser, id); const sharedLink = await this.repository.update({ id, @@ -57,6 +109,60 @@ export class SharedLinkService { return sharedLink; } + async addAssets(authUser: AuthUserDto, id: string, dto: AssetIdsDto): Promise { + const sharedLink = await this.findOrFail(authUser, id); + + if (sharedLink.type !== SharedLinkType.INDIVIDUAL) { + throw new BadRequestException('Invalid shared link type'); + } + + const results: AssetIdsResponseDto[] = []; + for (const assetId of dto.assetIds) { + const hasAsset = sharedLink.assets.find((asset) => asset.id === assetId); + if (hasAsset) { + results.push({ assetId, success: false, error: AssetIdErrorReason.DUPLICATE }); + continue; + } + + const hasAccess = await this.accessRepository.hasOwnerAssetAccess(authUser.id, assetId); + if (!hasAccess) { + results.push({ assetId, success: false, error: AssetIdErrorReason.NO_PERMISSION }); + continue; + } + + results.push({ assetId, success: true }); + sharedLink.assets.push({ id: assetId } as AssetEntity); + } + + await this.repository.update(sharedLink); + + return results; + } + + async removeAssets(authUser: AuthUserDto, id: string, dto: AssetIdsDto): Promise { + const sharedLink = await this.findOrFail(authUser, id); + + if (sharedLink.type !== SharedLinkType.INDIVIDUAL) { + throw new BadRequestException('Invalid shared link type'); + } + + const results: AssetIdsResponseDto[] = []; + for (const assetId of dto.assetIds) { + const hasAsset = sharedLink.assets.find((asset) => asset.id === assetId); + if (!hasAsset) { + results.push({ assetId, success: false, error: AssetIdErrorReason.NOT_FOUND }); + continue; + } + + results.push({ assetId, success: true }); + sharedLink.assets = sharedLink.assets.filter((asset) => asset.id !== assetId); + } + + await this.repository.update(sharedLink); + + return results; + } + private map(sharedLink: SharedLinkEntity, { withExif }: { withExif: boolean }) { return withExif ? mapSharedLink(sharedLink) : mapSharedLinkWithNoExif(sharedLink); } diff --git a/server/src/domain/storage-template/storage-template.service.spec.ts b/server/src/domain/storage-template/storage-template.service.spec.ts index 812ccc7e3..8c6a8ebc5 100644 --- a/server/src/domain/storage-template/storage-template.service.spec.ts +++ b/server/src/domain/storage-template/storage-template.service.spec.ts @@ -194,5 +194,26 @@ describe(StorageTemplateService.name, () => { ['upload/library/user-id/2023/2023-02-23/asset-id.ext', '/original/path.ext'], ]); }); + + it('should not move read-only asset', async () => { + assetMock.getAll.mockResolvedValue({ + items: [ + { + ...assetEntityStub.image, + originalPath: 'upload/library/user-id/2023/2023-02-23/asset-id+1.ext', + isReadOnly: true, + }, + ], + hasNextPage: false, + }); + assetMock.save.mockResolvedValue(assetEntityStub.image); + userMock.getList.mockResolvedValue([userEntityStub.user1]); + + await sut.handleMigration(); + + expect(assetMock.getAll).toHaveBeenCalled(); + expect(storageMock.moveFile).not.toHaveBeenCalled(); + expect(assetMock.save).not.toHaveBeenCalled(); + }); }); }); diff --git a/server/src/domain/storage-template/storage-template.service.ts b/server/src/domain/storage-template/storage-template.service.ts index ae0ba9b0e..a38dbb633 100644 --- a/server/src/domain/storage-template/storage-template.service.ts +++ b/server/src/domain/storage-template/storage-template.service.ts @@ -76,6 +76,11 @@ export class StorageTemplateService { // TODO: use asset core (once in domain) async moveAsset(asset: AssetEntity, metadata: MoveAssetMetadata) { + if (asset.isReadOnly) { + this.logger.verbose(`Not moving read-only asset: ${asset.originalPath}`); + return; + } + const destination = await this.core.getTemplatePath(asset, metadata); if (asset.originalPath !== destination) { const source = asset.originalPath; @@ -96,7 +101,10 @@ export class StorageTemplateService { asset.originalPath = destination; asset.sidecarPath = sidecarDestination || null; } catch (error: any) { - this.logger.warn('Unable to save new originalPath to database, undoing move', error?.stack); + this.logger.warn( + `Unable to save new originalPath to database, undoing move for path ${asset.originalPath} - filename ${asset.originalFileName} - id ${asset.id}`, + error?.stack, + ); // Either sidecar move failed or the save failed. Eithr way, move media back await this.storageRepository.moveFile(destination, source); diff --git a/server/src/domain/user/dto/create-user.dto.ts b/server/src/domain/user/dto/create-user.dto.ts index 3927ffe2e..5951be831 100644 --- a/server/src/domain/user/dto/create-user.dto.ts +++ b/server/src/domain/user/dto/create-user.dto.ts @@ -23,6 +23,10 @@ export class CreateUserDto { @IsString() @Transform(toSanitized) storageLabel?: string | null; + + @IsOptional() + @IsString() + externalPath?: string | null; } export class CreateAdminDto { diff --git a/server/src/domain/user/dto/update-user.dto.ts b/server/src/domain/user/dto/update-user.dto.ts index 14c16acf1..fca200ab2 100644 --- a/server/src/domain/user/dto/update-user.dto.ts +++ b/server/src/domain/user/dto/update-user.dto.ts @@ -29,6 +29,10 @@ export class UpdateUserDto { @Transform(toSanitized) storageLabel?: string; + @IsOptional() + @IsString() + externalPath?: string; + @IsNotEmpty() @IsUUID('4') @ApiProperty({ format: 'uuid' }) diff --git a/server/src/domain/user/response-dto/user-response.dto.ts b/server/src/domain/user/response-dto/user-response.dto.ts index 6ad8e848c..a2bd50883 100644 --- a/server/src/domain/user/response-dto/user-response.dto.ts +++ b/server/src/domain/user/response-dto/user-response.dto.ts @@ -6,6 +6,7 @@ export class UserResponseDto { firstName!: string; lastName!: string; storageLabel!: string | null; + externalPath!: string | null; profileImagePath!: string; shouldChangePassword!: boolean; isAdmin!: boolean; @@ -22,6 +23,7 @@ export function mapUser(entity: UserEntity): UserResponseDto { firstName: entity.firstName, lastName: entity.lastName, storageLabel: entity.storageLabel, + externalPath: entity.externalPath, profileImagePath: entity.profileImagePath, shouldChangePassword: entity.shouldChangePassword, isAdmin: entity.isAdmin, diff --git a/server/src/domain/user/user.core.ts b/server/src/domain/user/user.core.ts index 6a986e4cb..2b3ed4074 100644 --- a/server/src/domain/user/user.core.ts +++ b/server/src/domain/user/user.core.ts @@ -6,7 +6,6 @@ import { Logger, NotFoundException, } from '@nestjs/common'; -import { hash } from 'bcrypt'; import { constants, createReadStream, ReadStream } from 'fs'; import fs from 'fs/promises'; import { AuthUserDto } from '../auth'; @@ -28,6 +27,7 @@ export class UserCore { // Users can never update the isAdmin property. delete dto.isAdmin; delete dto.storageLabel; + delete dto.externalPath; } else if (dto.isAdmin && authUser.id !== id) { // Admin cannot create another admin. throw new BadRequestException('The server already has an admin'); @@ -56,6 +56,10 @@ export class UserCore { dto.storageLabel = null; } + if (dto.externalPath === '') { + dto.externalPath = null; + } + return this.userRepository.update(id, dto); } catch (e) { Logger.error(e, 'Failed to update user info'); @@ -79,7 +83,7 @@ export class UserCore { try { const payload: Partial = { ...createUserDto }; if (payload.password) { - payload.password = await hash(payload.password, SALT_ROUNDS); + payload.password = await this.cryptoRepository.hashBcrypt(payload.password, SALT_ROUNDS); } return this.userRepository.create(payload); } catch (e) { diff --git a/server/src/domain/user/user.service.spec.ts b/server/src/domain/user/user.service.spec.ts index d4229847d..efb5e3d5d 100644 --- a/server/src/domain/user/user.service.spec.ts +++ b/server/src/domain/user/user.service.spec.ts @@ -53,6 +53,7 @@ const adminUser: UserEntity = Object.freeze({ tags: [], assets: [], storageLabel: 'admin', + externalPath: null, }); const immichUser: UserEntity = Object.freeze({ @@ -71,6 +72,7 @@ const immichUser: UserEntity = Object.freeze({ tags: [], assets: [], storageLabel: null, + externalPath: null, }); const updatedImmichUser: UserEntity = Object.freeze({ @@ -89,6 +91,7 @@ const updatedImmichUser: UserEntity = Object.freeze({ tags: [], assets: [], storageLabel: null, + externalPath: null, }); const adminUserResponse = Object.freeze({ @@ -104,6 +107,7 @@ const adminUserResponse = Object.freeze({ deletedAt: null, updatedAt: new Date('2021-01-01'), storageLabel: 'admin', + externalPath: null, }); describe(UserService.name, () => { @@ -153,6 +157,7 @@ describe(UserService.name, () => { deletedAt: null, updatedAt: new Date('2021-01-01'), storageLabel: 'admin', + externalPath: null, }, ]); }); diff --git a/server/src/immich/api-v1/album/album.controller.ts b/server/src/immich/api-v1/album/album.controller.ts index e15a89e33..5349f5d65 100644 --- a/server/src/immich/api-v1/album/album.controller.ts +++ b/server/src/immich/api-v1/album/album.controller.ts @@ -1,5 +1,5 @@ import { AlbumResponseDto } from '@app/domain'; -import { Body, Controller, Delete, Get, Param, Post, Put, Query, Response } from '@nestjs/common'; +import { Body, Controller, Delete, Get, Param, Put, Query, Response } from '@nestjs/common'; import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; import { Response as Res } from 'express'; import { handleDownload } from '../../app.utils'; @@ -10,7 +10,6 @@ import { UseValidation } from '../../decorators/use-validation.decorator'; import { DownloadDto } from '../asset/dto/download-library.dto'; import { AlbumService } from './album.service'; import { AddAssetsDto } from './dto/add-assets.dto'; -import { CreateAlbumShareLinkDto as CreateAlbumSharedLinkDto } from './dto/create-album-shared-link.dto'; import { RemoveAssetsDto } from './dto/remove-assets.dto'; import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto'; @@ -59,9 +58,4 @@ export class AlbumController { ) { return this.service.downloadArchive(authUser, id, dto).then((download) => handleDownload(download, res)); } - - @Post('create-shared-link') - createAlbumSharedLink(@AuthUser() authUser: AuthUserDto, @Body() dto: CreateAlbumSharedLinkDto) { - return this.service.createSharedLink(authUser, dto); - } } diff --git a/server/src/immich/api-v1/album/album.service.spec.ts b/server/src/immich/api-v1/album/album.service.spec.ts index 69b38021b..77ccbb67a 100644 --- a/server/src/immich/api-v1/album/album.service.spec.ts +++ b/server/src/immich/api-v1/album/album.service.spec.ts @@ -1,7 +1,7 @@ -import { AlbumResponseDto, ICryptoRepository, ISharedLinkRepository, mapUser } from '@app/domain'; +import { AlbumResponseDto, mapUser } from '@app/domain'; import { AlbumEntity, UserEntity } from '@app/infra/entities'; import { ForbiddenException, NotFoundException } from '@nestjs/common'; -import { newCryptoRepositoryMock, newSharedLinkRepositoryMock, userEntityStub } from '@test'; +import { userEntityStub } from '@test'; import { AuthUserDto } from '../../decorators/auth-user.decorator'; import { DownloadService } from '../../modules/download/download.service'; import { IAlbumRepository } from './album-repository'; @@ -11,9 +11,7 @@ import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto'; describe('Album service', () => { let sut: AlbumService; let albumRepositoryMock: jest.Mocked; - let sharedLinkRepositoryMock: jest.Mocked; let downloadServiceMock: jest.Mocked>; - let cryptoMock: jest.Mocked; const authUser: AuthUserDto = Object.freeze({ id: '1111', @@ -34,6 +32,7 @@ describe('Album service', () => { tags: [], assets: [], storageLabel: null, + externalPath: null, }); const albumId = 'f19ab956-4761-41ea-a5d6-bae948308d58'; const sharedAlbumOwnerId = '2222'; @@ -99,20 +98,11 @@ describe('Album service', () => { updateThumbnails: jest.fn(), }; - sharedLinkRepositoryMock = newSharedLinkRepositoryMock(); - downloadServiceMock = { downloadArchive: jest.fn(), }; - cryptoMock = newCryptoRepositoryMock(); - - sut = new AlbumService( - albumRepositoryMock, - sharedLinkRepositoryMock, - downloadServiceMock as DownloadService, - cryptoMock, - ); + sut = new AlbumService(albumRepositoryMock, downloadServiceMock as DownloadService); }); it('gets an owned album', async () => { diff --git a/server/src/immich/api-v1/album/album.service.ts b/server/src/immich/api-v1/album/album.service.ts index 75df03a13..7e5e551e0 100644 --- a/server/src/immich/api-v1/album/album.service.ts +++ b/server/src/immich/api-v1/album/album.service.ts @@ -1,36 +1,22 @@ -import { - AlbumResponseDto, - ICryptoRepository, - ISharedLinkRepository, - mapAlbum, - mapSharedLink, - SharedLinkCore, - SharedLinkResponseDto, -} from '@app/domain'; -import { AlbumEntity, SharedLinkType } from '@app/infra/entities'; +import { AlbumResponseDto, mapAlbum } from '@app/domain'; +import { AlbumEntity } from '@app/infra/entities'; import { BadRequestException, ForbiddenException, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common'; import { AuthUserDto } from '../../decorators/auth-user.decorator'; import { DownloadService } from '../../modules/download/download.service'; import { DownloadDto } from '../asset/dto/download-library.dto'; import { IAlbumRepository } from './album-repository'; import { AddAssetsDto } from './dto/add-assets.dto'; -import { CreateAlbumShareLinkDto } from './dto/create-album-shared-link.dto'; import { RemoveAssetsDto } from './dto/remove-assets.dto'; import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto'; @Injectable() export class AlbumService { - readonly logger = new Logger(AlbumService.name); - private shareCore: SharedLinkCore; + private logger = new Logger(AlbumService.name); constructor( @Inject(IAlbumRepository) private albumRepository: IAlbumRepository, - @Inject(ISharedLinkRepository) sharedLinkRepository: ISharedLinkRepository, private downloadService: DownloadService, - @Inject(ICryptoRepository) cryptoRepository: ICryptoRepository, - ) { - this.shareCore = new SharedLinkCore(sharedLinkRepository, cryptoRepository); - } + ) {} private async _getAlbum({ authUser, @@ -91,7 +77,7 @@ export class AlbumService { } async downloadArchive(authUser: AuthUserDto, albumId: string, dto: DownloadDto) { - this.shareCore.checkDownloadAccess(authUser); + this.checkDownloadAccess(authUser); const album = await this._getAlbum({ authUser, albumId, validateIsOwner: false }); const assets = (album.assets || []).map((asset) => asset).slice(dto.skip || 0); @@ -99,20 +85,9 @@ export class AlbumService { return this.downloadService.downloadArchive(album.albumName, assets); } - async createSharedLink(authUser: AuthUserDto, dto: CreateAlbumShareLinkDto): Promise { - const album = await this._getAlbum({ authUser, albumId: dto.albumId }); - - const sharedLink = await this.shareCore.create(authUser.id, { - type: SharedLinkType.ALBUM, - expiresAt: dto.expiresAt, - allowUpload: dto.allowUpload, - album, - assets: [], - description: dto.description, - allowDownload: dto.allowDownload, - showExif: dto.showExif, - }); - - return mapSharedLink(sharedLink); + private checkDownloadAccess(authUser: AuthUserDto) { + if (authUser.isPublicUser && !authUser.isAllowDownload) { + throw new ForbiddenException(); + } } } diff --git a/server/src/immich/api-v1/album/dto/create-album-shared-link.dto.ts b/server/src/immich/api-v1/album/dto/create-album-shared-link.dto.ts deleted file mode 100644 index eedf7a207..000000000 --- a/server/src/immich/api-v1/album/dto/create-album-shared-link.dto.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ValidateUUID } from '@app/immich/decorators/validate-uuid.decorator'; -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsBoolean, IsDate, IsOptional, IsString } from 'class-validator'; - -export class CreateAlbumShareLinkDto { - @ValidateUUID() - albumId!: string; - - @IsOptional() - @IsDate() - @Type(() => Date) - @ApiProperty() - expiresAt?: Date; - - @IsBoolean() - @IsOptional() - @ApiProperty() - allowUpload?: boolean; - - @IsBoolean() - @IsOptional() - @ApiProperty() - allowDownload?: boolean; - - @IsBoolean() - @IsOptional() - @ApiProperty() - showExif?: boolean; - - @IsString() - @IsOptional() - @ApiProperty() - description?: string; -} diff --git a/server/src/immich/api-v1/asset/asset-repository.ts b/server/src/immich/api-v1/asset/asset-repository.ts index ec3e39cf8..4fd2f3c06 100644 --- a/server/src/immich/api-v1/asset/asset-repository.ts +++ b/server/src/immich/api-v1/asset/asset-repository.ts @@ -20,6 +20,10 @@ export interface AssetCheck { checksum: Buffer; } +export interface AssetOwnerCheck extends AssetCheck { + ownerId: string; +} + export interface IAssetRepository { get(id: string): Promise; create( @@ -39,6 +43,7 @@ export interface IAssetRepository { getAssetByTimeBucket(userId: string, getAssetByTimeBucketDto: GetAssetByTimeBucketDto): Promise; getAssetsByChecksums(userId: string, checksums: Buffer[]): Promise; getExistingAssets(userId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise; + getByOriginalPath(originalPath: string): Promise; } export const IAssetRepository = 'IAssetRepository'; @@ -350,4 +355,17 @@ export class AssetRepository implements IAssetRepository { return assetCountByUserId; } + + getByOriginalPath(originalPath: string): Promise { + return this.assetRepository.findOne({ + select: { + id: true, + ownerId: true, + checksum: true, + }, + where: { + originalPath, + }, + }); + } } diff --git a/server/src/immich/api-v1/asset/asset.controller.ts b/server/src/immich/api-v1/asset/asset.controller.ts index d57ab0a9e..6b5fc488a 100644 --- a/server/src/immich/api-v1/asset/asset.controller.ts +++ b/server/src/immich/api-v1/asset/asset.controller.ts @@ -1,4 +1,4 @@ -import { AssetResponseDto, ImmichReadStream, SharedLinkResponseDto } from '@app/domain'; +import { AssetResponseDto, ImmichReadStream } from '@app/domain'; import { Body, Controller, @@ -10,7 +10,6 @@ import { HttpStatus, Param, ParseFilePipe, - Patch, Post, Put, Query, @@ -28,16 +27,13 @@ import { assetUploadOption, ImmichFile } from '../../config/asset-upload.config' import { UUIDParamDto } from '../../controllers/dto/uuid-param.dto'; import { AuthUser, AuthUserDto } from '../../decorators/auth-user.decorator'; import { Authenticated, SharedLinkRoute } from '../../decorators/authenticated.decorator'; -import { AddAssetsDto } from '../album/dto/add-assets.dto'; -import { RemoveAssetsDto } from '../album/dto/remove-assets.dto'; import FileNotEmptyValidator from '../validation/file-not-empty-validator'; import { AssetService } from './asset.service'; import { AssetBulkUploadCheckDto } from './dto/asset-check.dto'; import { AssetSearchDto } from './dto/asset-search.dto'; import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto'; import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto'; -import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto'; -import { CreateAssetDto, mapToUploadFile } from './dto/create-asset.dto'; +import { CreateAssetDto, ImportAssetDto, mapToUploadFile } from './dto/create-asset.dto'; import { DeleteAssetDto } from './dto/delete-asset.dto'; import { DeviceIdDto } from './dto/device-id.dto'; import { DownloadFilesDto } from './dto/download-files.dto'; @@ -118,6 +114,20 @@ export class AssetController { return responseDto; } + @Post('import') + async importFile( + @AuthUser() authUser: AuthUserDto, + @Body(new ValidationPipe()) dto: ImportAssetDto, + @Response({ passthrough: true }) res: Res, + ): Promise { + const responseDto = await this.assetService.importFile(authUser, dto); + if (responseDto.duplicate) { + res.status(200); + } + + return responseDto; + } + @SharedLinkRoute() @Get('/download/:id') @ApiOkResponse({ content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } } }) @@ -319,30 +329,4 @@ export class AssetController { ): Promise { return this.assetService.bulkUploadCheck(authUser, dto); } - - @Post('/shared-link') - createAssetsSharedLink( - @AuthUser() authUser: AuthUserDto, - @Body(ValidationPipe) dto: CreateAssetsShareLinkDto, - ): Promise { - return this.assetService.createAssetsSharedLink(authUser, dto); - } - - @SharedLinkRoute() - @Patch('/shared-link/add') - addAssetsToSharedLink( - @AuthUser() authUser: AuthUserDto, - @Body(ValidationPipe) dto: AddAssetsDto, - ): Promise { - return this.assetService.addAssetsToSharedLink(authUser, dto); - } - - @SharedLinkRoute() - @Patch('/shared-link/remove') - removeAssetsFromSharedLink( - @AuthUser() authUser: AuthUserDto, - @Body(ValidationPipe) dto: RemoveAssetsDto, - ): Promise { - return this.assetService.removeAssetsFromSharedLink(authUser, dto); - } } diff --git a/server/src/immich/api-v1/asset/asset.core.ts b/server/src/immich/api-v1/asset/asset.core.ts index 031ab58d4..b68f6234c 100644 --- a/server/src/immich/api-v1/asset/asset.core.ts +++ b/server/src/immich/api-v1/asset/asset.core.ts @@ -2,17 +2,17 @@ import { AuthUserDto, IJobRepository, JobName } from '@app/domain'; import { AssetEntity, AssetType, UserEntity } from '@app/infra/entities'; import { parse } from 'node:path'; import { IAssetRepository } from './asset-repository'; -import { CreateAssetDto, UploadFile } from './dto/create-asset.dto'; +import { CreateAssetDto, ImportAssetDto, UploadFile } from './dto/create-asset.dto'; export class AssetCore { constructor(private repository: IAssetRepository, private jobRepository: IJobRepository) {} async create( authUser: AuthUserDto, - dto: CreateAssetDto, + dto: CreateAssetDto | ImportAssetDto, file: UploadFile, livePhotoAssetId?: string, - sidecarFile?: UploadFile, + sidecarPath?: string, ): Promise { const asset = await this.repository.create({ owner: { id: authUser.id } as UserEntity, @@ -41,7 +41,8 @@ export class AssetCore { sharedLinks: [], originalFileName: parse(file.originalName).name, faces: [], - sidecarPath: sidecarFile?.originalPath || null, + sidecarPath: sidecarPath || null, + isReadOnly: dto.isReadOnly ?? false, }); await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: { id: asset.id, source: 'upload' } }); diff --git a/server/src/immich/api-v1/asset/asset.service.spec.ts b/server/src/immich/api-v1/asset/asset.service.spec.ts index fcd739c9a..4f51ad23a 100644 --- a/server/src/immich/api-v1/asset/asset.service.spec.ts +++ b/server/src/immich/api-v1/asset/asset.service.spec.ts @@ -1,13 +1,6 @@ -import { - IAccessRepository, - ICryptoRepository, - IJobRepository, - ISharedLinkRepository, - IStorageRepository, - JobName, -} from '@app/domain'; +import { IAccessRepository, ICryptoRepository, IJobRepository, IStorageRepository, JobName } from '@app/domain'; import { AssetEntity, AssetType, ExifEntity } from '@app/infra/entities'; -import { BadRequestException, ForbiddenException } from '@nestjs/common'; +import { ForbiddenException } from '@nestjs/common'; import { assetEntityStub, authStub, @@ -15,17 +8,13 @@ import { newAccessRepositoryMock, newCryptoRepositoryMock, newJobRepositoryMock, - newSharedLinkRepositoryMock, newStorageRepositoryMock, - sharedLinkResponseStub, - sharedLinkStub, } from '@test'; import { when } from 'jest-when'; import { QueryFailedError, Repository } from 'typeorm'; import { DownloadService } from '../../modules/download/download.service'; import { IAssetRepository } from './asset-repository'; import { AssetService } from './asset.service'; -import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto'; import { CreateAssetDto } from './dto/create-asset.dto'; import { TimeGroupEnum } from './dto/get-asset-count-by-time-bucket.dto'; import { AssetRejectReason, AssetUploadAction } from './response-dto/asset-check-response.dto'; @@ -133,9 +122,8 @@ describe('AssetService', () => { let a: Repository; // TO BE DELETED AFTER FINISHED REFACTORING let accessMock: jest.Mocked; let assetRepositoryMock: jest.Mocked; - let downloadServiceMock: jest.Mocked>; - let sharedLinkRepositoryMock: jest.Mocked; let cryptoMock: jest.Mocked; + let downloadServiceMock: jest.Mocked>; let jobMock: jest.Mocked; let storageMock: jest.Mocked; @@ -158,26 +146,27 @@ describe('AssetService', () => { getAssetCountByUserId: jest.fn(), getArchivedAssetCountByUserId: jest.fn(), getExistingAssets: jest.fn(), + getByOriginalPath: jest.fn(), }; + cryptoMock = newCryptoRepositoryMock(); + downloadServiceMock = { downloadArchive: jest.fn(), }; accessMock = newAccessRepositoryMock(); - sharedLinkRepositoryMock = newSharedLinkRepositoryMock(); - jobMock = newJobRepositoryMock(); cryptoMock = newCryptoRepositoryMock(); + jobMock = newJobRepositoryMock(); storageMock = newStorageRepositoryMock(); sut = new AssetService( accessMock, assetRepositoryMock, a, - downloadServiceMock as DownloadService, - sharedLinkRepositoryMock, - jobMock, cryptoMock, + downloadServiceMock as DownloadService, + jobMock, storageMock, ); @@ -189,77 +178,6 @@ describe('AssetService', () => { .mockResolvedValue(assetEntityStub.livePhotoMotionAsset); }); - describe('createAssetsSharedLink', () => { - it('should create an individual share link', async () => { - const asset1 = _getAsset_1(); - const dto: CreateAssetsShareLinkDto = { assetIds: [asset1.id] }; - - assetRepositoryMock.getById.mockResolvedValue(asset1); - accessMock.hasOwnerAssetAccess.mockResolvedValue(true); - sharedLinkRepositoryMock.create.mockResolvedValue(sharedLinkStub.valid); - - await expect(sut.createAssetsSharedLink(authStub.user1, dto)).resolves.toEqual(sharedLinkResponseStub.valid); - - expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id); - expect(accessMock.hasOwnerAssetAccess).toHaveBeenCalledWith(authStub.user1.id, asset1.id); - }); - }); - - describe('updateAssetsInSharedLink', () => { - it('should require a valid shared link', async () => { - const asset1 = _getAsset_1(); - - const authDto = authStub.adminSharedLink; - const dto = { assetIds: [asset1.id] }; - - assetRepositoryMock.getById.mockResolvedValue(asset1); - sharedLinkRepositoryMock.get.mockResolvedValue(null); - accessMock.hasSharedLinkAssetAccess.mockResolvedValue(true); - - await expect(sut.addAssetsToSharedLink(authDto, dto)).rejects.toBeInstanceOf(BadRequestException); - - expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id); - expect(sharedLinkRepositoryMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId); - expect(sharedLinkRepositoryMock.update).not.toHaveBeenCalled(); - }); - - it('should add assets to a shared link', async () => { - const asset1 = _getAsset_1(); - - const authDto = authStub.adminSharedLink; - const dto = { assetIds: [asset1.id] }; - - assetRepositoryMock.getById.mockResolvedValue(asset1); - sharedLinkRepositoryMock.get.mockResolvedValue(sharedLinkStub.valid); - accessMock.hasSharedLinkAssetAccess.mockResolvedValue(true); - sharedLinkRepositoryMock.update.mockResolvedValue(sharedLinkStub.valid); - - await expect(sut.addAssetsToSharedLink(authDto, dto)).resolves.toEqual(sharedLinkResponseStub.valid); - - expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id); - expect(sharedLinkRepositoryMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId); - expect(sharedLinkRepositoryMock.update).toHaveBeenCalled(); - }); - - it('should remove assets from a shared link', async () => { - const asset1 = _getAsset_1(); - - const authDto = authStub.adminSharedLink; - const dto = { assetIds: [asset1.id] }; - - assetRepositoryMock.getById.mockResolvedValue(asset1); - sharedLinkRepositoryMock.get.mockResolvedValue(sharedLinkStub.valid); - accessMock.hasSharedLinkAssetAccess.mockResolvedValue(true); - sharedLinkRepositoryMock.update.mockResolvedValue(sharedLinkStub.valid); - - await expect(sut.removeAssetsFromSharedLink(authDto, dto)).resolves.toEqual(sharedLinkResponseStub.valid); - - expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id); - expect(sharedLinkRepositoryMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId); - expect(sharedLinkRepositoryMock.update).toHaveBeenCalled(); - }); - }); - describe('uploadFile', () => { it('should handle a file upload', async () => { const assetEntity = _getAsset_1(); @@ -528,6 +446,43 @@ describe('AssetService', () => { }); }); + describe('importFile', () => { + it('should handle a file import', async () => { + assetRepositoryMock.create.mockResolvedValue(assetEntityStub.image); + storageMock.checkFileExists.mockResolvedValue(true); + + await expect( + sut.importFile(authStub.external1, { + ..._getCreateAssetDto(), + assetPath: '/data/user1/fake_path/asset_1.jpeg', + isReadOnly: true, + }), + ).resolves.toEqual({ duplicate: false, id: 'asset-id' }); + + expect(assetRepositoryMock.create).toHaveBeenCalled(); + }); + + it('should handle a duplicate if originalPath already exists', async () => { + const error = new QueryFailedError('', [], ''); + (error as any).constraint = 'UQ_userid_checksum'; + + assetRepositoryMock.create.mockRejectedValue(error); + assetRepositoryMock.getAssetsByChecksums.mockResolvedValue([assetEntityStub.image]); + storageMock.checkFileExists.mockResolvedValue(true); + cryptoMock.hashFile.mockResolvedValue(Buffer.from('file hash', 'utf8')); + + await expect( + sut.importFile(authStub.external1, { + ..._getCreateAssetDto(), + assetPath: '/data/user1/fake_path/asset_1.jpeg', + isReadOnly: true, + }), + ).resolves.toEqual({ duplicate: true, id: 'asset-id' }); + + expect(assetRepositoryMock.create).toHaveBeenCalled(); + }); + }); + describe('getAssetById', () => { it('should allow owner access', async () => { accessMock.hasOwnerAssetAccess.mockResolvedValue(true); diff --git a/server/src/immich/api-v1/asset/asset.service.ts b/server/src/immich/api-v1/asset/asset.service.ts index 53d8922c7..671ab74b1 100644 --- a/server/src/immich/api-v1/asset/asset.service.ts +++ b/server/src/immich/api-v1/asset/asset.service.ts @@ -1,20 +1,19 @@ import { AssetResponseDto, + AuthUserDto, getLivePhotoMotionFilename, IAccessRepository, ICryptoRepository, IJobRepository, ImmichReadStream, - ISharedLinkRepository, + isSidecarFileType, + isSupportedFileType, IStorageRepository, JobName, mapAsset, mapAssetWithoutExif, - mapSharedLink, - SharedLinkCore, - SharedLinkResponseDto, } from '@app/domain'; -import { AssetEntity, AssetType, SharedLinkType } from '@app/infra/entities'; +import { AssetEntity, AssetType } from '@app/infra/entities'; import { BadRequestException, ForbiddenException, @@ -26,23 +25,22 @@ import { StreamableFile, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { R_OK, W_OK } from 'constants'; import { Response as Res } from 'express'; -import { constants, createReadStream, stat } from 'fs'; +import { createReadStream, stat } from 'fs'; import fs from 'fs/promises'; +import mime from 'mime-types'; +import path from 'path'; import { QueryFailedError, Repository } from 'typeorm'; import { promisify } from 'util'; -import { AuthUserDto } from '../../decorators/auth-user.decorator'; import { DownloadService } from '../../modules/download/download.service'; -import { AddAssetsDto } from '../album/dto/add-assets.dto'; -import { RemoveAssetsDto } from '../album/dto/remove-assets.dto'; import { IAssetRepository } from './asset-repository'; import { AssetCore } from './asset.core'; import { AssetBulkUploadCheckDto } from './dto/asset-check.dto'; import { AssetSearchDto } from './dto/asset-search.dto'; import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto'; import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto'; -import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto'; -import { CreateAssetDto, UploadFile } from './dto/create-asset.dto'; +import { CreateAssetDto, ImportAssetDto, UploadFile } from './dto/create-asset.dto'; import { DeleteAssetDto } from './dto/delete-asset.dto'; import { DownloadFilesDto } from './dto/download-files.dto'; import { DownloadDto } from './dto/download-library.dto'; @@ -80,22 +78,18 @@ interface ServableFile { @Injectable() export class AssetService { readonly logger = new Logger(AssetService.name); - private shareCore: SharedLinkCore; private assetCore: AssetCore; constructor( @Inject(IAccessRepository) private accessRepository: IAccessRepository, @Inject(IAssetRepository) private _assetRepository: IAssetRepository, - @InjectRepository(AssetEntity) - private assetRepository: Repository, + @InjectRepository(AssetEntity) private assetRepository: Repository, + @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, private downloadService: DownloadService, - @Inject(ISharedLinkRepository) sharedLinkRepository: ISharedLinkRepository, @Inject(IJobRepository) private jobRepository: IJobRepository, - @Inject(ICryptoRepository) cryptoRepository: ICryptoRepository, @Inject(IStorageRepository) private storageRepository: IStorageRepository, ) { this.assetCore = new AssetCore(_assetRepository, jobRepository); - this.shareCore = new SharedLinkCore(sharedLinkRepository, cryptoRepository); } public async uploadFile( @@ -120,7 +114,7 @@ export class AssetService { livePhotoAsset = await this.assetCore.create(authUser, livePhotoDto, livePhotoFile); } - const asset = await this.assetCore.create(authUser, dto, file, livePhotoAsset?.id, sidecarFile); + const asset = await this.assetCore.create(authUser, dto, file, livePhotoAsset?.id, sidecarFile?.originalPath); return { id: asset.id, duplicate: false }; } catch (error: any) { @@ -142,6 +136,73 @@ export class AssetService { } } + public async importFile(authUser: AuthUserDto, dto: ImportAssetDto): Promise { + dto = { + ...dto, + assetPath: path.resolve(dto.assetPath), + sidecarPath: dto.sidecarPath ? path.resolve(dto.sidecarPath) : undefined, + }; + + const assetPathType = mime.lookup(dto.assetPath) as string; + if (!isSupportedFileType(assetPathType)) { + throw new BadRequestException(`Unsupported file type ${assetPathType}`); + } + + if (dto.sidecarPath) { + const sidecarType = mime.lookup(dto.sidecarPath) as string; + if (!isSidecarFileType(sidecarType)) { + throw new BadRequestException(`Unsupported sidecar file type ${assetPathType}`); + } + } + + for (const filepath of [dto.assetPath, dto.sidecarPath]) { + if (!filepath) { + continue; + } + + const exists = await this.storageRepository.checkFileExists(filepath, R_OK); + if (!exists) { + throw new BadRequestException('File does not exist'); + } + } + + if (!authUser.externalPath || !dto.assetPath.match(new RegExp(`^${authUser.externalPath}`))) { + throw new BadRequestException("File does not exist within user's external path"); + } + + const assetFile: UploadFile = { + checksum: await this.cryptoRepository.hashFile(dto.assetPath), + mimeType: assetPathType, + originalPath: dto.assetPath, + originalName: path.parse(dto.assetPath).name, + }; + + try { + const asset = await this.assetCore.create(authUser, dto, assetFile, undefined, dto.sidecarPath); + return { id: asset.id, duplicate: false }; + } catch (error: QueryFailedError | Error | any) { + // handle duplicates with a success response + if (error instanceof QueryFailedError && (error as any).constraint === 'UQ_userid_checksum') { + const [duplicate] = await this._assetRepository.getAssetsByChecksums(authUser.id, [assetFile.checksum]); + return { id: duplicate.id, duplicate: true }; + } + + if (error instanceof QueryFailedError && (error as any).constraint === 'UQ_4ed4f8052685ff5b1e7ca1058ba') { + const duplicate = await this._assetRepository.getByOriginalPath(dto.assetPath); + if (duplicate) { + if (duplicate.ownerId === authUser.id) { + return { id: duplicate.id, duplicate: true }; + } + + throw new BadRequestException('Path in use by another user'); + } + } + + this.logger.error(`Error importing file ${error}`, error?.stack); + throw new BadRequestException(`Error importing file`, `${error}`); + } + } + public async getUserAssetsByDeviceId(authUser: AuthUserDto, deviceId: string) { return this._assetRepository.getAllByDeviceId(authUser.id, deviceId); } @@ -304,7 +365,7 @@ export class AssetService { let videoPath = asset.originalPath; let mimeType = asset.mimeType; - await fs.access(videoPath, constants.R_OK | constants.W_OK); + await fs.access(videoPath, R_OK | W_OK); if (asset.encodedVideoPath) { videoPath = asset.encodedVideoPath == '' ? String(asset.originalPath) : String(asset.encodedVideoPath); @@ -386,13 +447,16 @@ export class AssetService { await this.jobRepository.queue({ name: JobName.SEARCH_REMOVE_ASSET, data: { ids: [id] } }); result.push({ id, status: DeleteAssetStatusEnum.SUCCESS }); - deleteQueue.push( - asset.originalPath, - asset.webpPath, - asset.resizePath, - asset.encodedVideoPath, - asset.sidecarPath, - ); + + if (!asset.isReadOnly) { + deleteQueue.push( + asset.originalPath, + asset.webpPath, + asset.resizePath, + asset.encodedVideoPath, + asset.sidecarPath, + ); + } // TODO refactor this to use cascades if (asset.livePhotoVideoId && !ids.includes(asset.livePhotoVideoId)) { @@ -608,61 +672,9 @@ export class AssetService { } private checkDownloadAccess(authUser: AuthUserDto) { - this.shareCore.checkDownloadAccess(authUser); - } - - async createAssetsSharedLink(authUser: AuthUserDto, dto: CreateAssetsShareLinkDto): Promise { - const assets = []; - - await this.checkAssetsAccess(authUser, dto.assetIds); - for (const assetId of dto.assetIds) { - const asset = await this._assetRepository.getById(assetId); - assets.push(asset); - } - - const sharedLink = await this.shareCore.create(authUser.id, { - type: SharedLinkType.INDIVIDUAL, - expiresAt: dto.expiresAt, - allowUpload: dto.allowUpload, - assets, - description: dto.description, - allowDownload: dto.allowDownload, - showExif: dto.showExif, - }); - - return mapSharedLink(sharedLink); - } - - async addAssetsToSharedLink(authUser: AuthUserDto, dto: AddAssetsDto): Promise { - if (!authUser.sharedLinkId) { + if (authUser.isPublicUser && !authUser.isAllowDownload) { throw new ForbiddenException(); } - - const assets = []; - - for (const assetId of dto.assetIds) { - const asset = await this._assetRepository.getById(assetId); - assets.push(asset); - } - - const updatedLink = await this.shareCore.addAssets(authUser.id, authUser.sharedLinkId, assets); - return mapSharedLink(updatedLink); - } - - async removeAssetsFromSharedLink(authUser: AuthUserDto, dto: RemoveAssetsDto): Promise { - if (!authUser.sharedLinkId) { - throw new ForbiddenException(); - } - - const assets = []; - - for (const assetId of dto.assetIds) { - const asset = await this._assetRepository.getById(assetId); - assets.push(asset); - } - - const updatedLink = await this.shareCore.removeAssets(authUser.id, authUser.sharedLinkId, assets); - return mapSharedLink(updatedLink); } getExifPermission(authUser: AuthUserDto) { @@ -730,7 +742,7 @@ export class AssetService { return; } - await fs.access(filepath, constants.R_OK); + await fs.access(filepath, R_OK); return new StreamableFile(createReadStream(filepath)); } diff --git a/server/src/immich/api-v1/asset/dto/create-asset-shared-link.dto.ts b/server/src/immich/api-v1/asset/dto/create-asset-shared-link.dto.ts deleted file mode 100644 index 258e7009a..000000000 --- a/server/src/immich/api-v1/asset/dto/create-asset-shared-link.dto.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsArray, IsBoolean, IsDate, IsNotEmpty, IsOptional, IsString } from 'class-validator'; - -export class CreateAssetsShareLinkDto { - @IsArray() - @IsString({ each: true }) - @IsNotEmpty({ each: true }) - @ApiProperty({ - isArray: true, - type: String, - title: 'Array asset IDs to be shared', - example: [ - 'bf973405-3f2a-48d2-a687-2ed4167164be', - 'dd41870b-5d00-46d2-924e-1d8489a0aa0f', - 'fad77c3f-deef-4e7e-9608-14c1aa4e559a', - ], - }) - assetIds!: string[]; - - @IsDate() - @Type(() => Date) - @IsOptional() - expiresAt?: Date; - - @IsBoolean() - @IsOptional() - allowUpload?: boolean; - - @IsBoolean() - @IsOptional() - allowDownload?: boolean; - - @IsBoolean() - @IsOptional() - showExif?: boolean; - - @IsString() - @IsOptional() - description?: string; -} diff --git a/server/src/immich/api-v1/asset/dto/create-asset.dto.ts b/server/src/immich/api-v1/asset/dto/create-asset.dto.ts index 1c4880fc8..bf54d4ce3 100644 --- a/server/src/immich/api-v1/asset/dto/create-asset.dto.ts +++ b/server/src/immich/api-v1/asset/dto/create-asset.dto.ts @@ -1,9 +1,11 @@ import { AssetType } from '@app/infra/entities'; import { ApiProperty } from '@nestjs/swagger'; -import { IsBoolean, IsEnum, IsNotEmpty, IsOptional } from 'class-validator'; +import { Transform } from 'class-transformer'; +import { IsBoolean, IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { ImmichFile } from '../../../config/asset-upload.config'; +import { toBoolean, toSanitized } from '../../../utils/transform.util'; -export class CreateAssetDto { +export class CreateAssetBase { @IsNotEmpty() deviceAssetId!: string; @@ -32,11 +34,18 @@ export class CreateAssetDto { @IsBoolean() isVisible?: boolean; - @IsNotEmpty() - fileExtension!: string; - @IsOptional() duration?: string; +} + +export class CreateAssetDto extends CreateAssetBase { + @IsOptional() + @IsBoolean() + @Transform(toBoolean) + isReadOnly?: boolean = false; + + @IsNotEmpty() + fileExtension!: string; // The properties below are added to correctly generate the API docs // and client SDKs. Validation should be handled in the controller. @@ -50,6 +59,23 @@ export class CreateAssetDto { sidecarData?: any; } +export class ImportAssetDto extends CreateAssetBase { + @IsOptional() + @Transform(toBoolean) + isReadOnly?: boolean = true; + + @IsString() + @IsNotEmpty() + @Transform(toSanitized) + assetPath!: string; + + @IsString() + @IsOptional() + @IsNotEmpty() + @Transform(toSanitized) + sidecarPath?: string; +} + export interface UploadFile { mimeType: string; checksum: Buffer; diff --git a/server/src/immich/config/asset-upload.config.spec.ts b/server/src/immich/config/asset-upload.config.spec.ts index 9161d9071..6978547f1 100644 --- a/server/src/immich/config/asset-upload.config.spec.ts +++ b/server/src/immich/config/asset-upload.config.spec.ts @@ -50,47 +50,49 @@ describe('assetUploadOption', () => { }); for (const { mimetype, extension } of [ - { mimetype: 'image/dng', extension: 'dng' }, + { mimetype: 'image/avif', extension: 'avif' }, { mimetype: 'image/gif', extension: 'gif' }, { mimetype: 'image/heic', extension: 'heic' }, { mimetype: 'image/heif', extension: 'heif' }, { mimetype: 'image/jpeg', extension: 'jpeg' }, { mimetype: 'image/jpeg', extension: 'jpg' }, + { mimetype: 'image/jxl', extension: 'jxl' }, { mimetype: 'image/png', extension: 'png' }, { mimetype: 'image/tiff', extension: 'tiff' }, { mimetype: 'image/webp', extension: 'webp' }, - { mimetype: 'image/avif', extension: 'avif' }, { mimetype: 'image/x-adobe-dng', extension: 'dng' }, - { mimetype: 'image/x-fuji-raf', extension: 'raf' }, - { mimetype: 'image/x-nikon-nef', extension: 'nef' }, - { mimetype: 'image/x-samsung-srw', extension: 'srw' }, - { mimetype: 'image/x-sony-arw', extension: 'arw' }, - { mimetype: 'image/x-canon-crw', extension: 'crw' }, + { mimetype: 'image/x-arriflex-ari', extension: 'ari' }, { mimetype: 'image/x-canon-cr2', extension: 'cr2' }, { mimetype: 'image/x-canon-cr3', extension: 'cr3' }, + { mimetype: 'image/x-canon-crw', extension: 'crw' }, { mimetype: 'image/x-epson-erf', extension: 'erf' }, + { mimetype: 'image/x-fuji-raf', extension: 'raf' }, + { mimetype: 'image/x-hasselblad-3fr', extension: '3fr' }, + { mimetype: 'image/x-hasselblad-fff', extension: 'fff' }, { mimetype: 'image/x-kodak-dcr', extension: 'dcr' }, { mimetype: 'image/x-kodak-k25', extension: 'k25' }, { mimetype: 'image/x-kodak-kdc', extension: 'kdc' }, + { mimetype: 'image/x-leica-rwl', extension: 'rwl' }, { mimetype: 'image/x-minolta-mrw', extension: 'mrw' }, + { mimetype: 'image/x-nikon-nef', extension: 'nef' }, { mimetype: 'image/x-olympus-orf', extension: 'orf' }, + { mimetype: 'image/x-olympus-ori', extension: 'ori' }, { mimetype: 'image/x-panasonic-raw', extension: 'raw' }, { mimetype: 'image/x-pentax-pef', extension: 'pef' }, - { mimetype: 'image/x-sigma-x3f', extension: 'x3f' }, - { mimetype: 'image/x-sony-srf', extension: 'srf' }, - { mimetype: 'image/x-sony-sr2', extension: 'sr2' }, - { mimetype: 'image/x-hasselblad-3fr', extension: '3fr' }, - { mimetype: 'image/x-hasselblad-fff', extension: 'fff' }, - { mimetype: 'image/x-leica-rwl', extension: 'rwl' }, - { mimetype: 'image/x-olympus-ori', extension: 'ori' }, - { mimetype: 'image/x-phaseone-iiq', extension: 'iiq' }, - { mimetype: 'image/x-arriflex-ari', extension: 'ari' }, - { mimetype: 'image/x-phaseone-cap', extension: 'cap' }, { mimetype: 'image/x-phantom-cin', extension: 'cin' }, - { mimetype: 'video/avi', extension: 'avi' }, - { mimetype: 'video/mov', extension: 'mov' }, + { mimetype: 'image/x-phaseone-cap', extension: 'cap' }, + { mimetype: 'image/x-phaseone-iiq', extension: 'iiq' }, + { mimetype: 'image/x-samsung-srw', extension: 'srw' }, + { mimetype: 'image/x-sigma-x3f', extension: 'x3f' }, + { mimetype: 'image/x-sony-arw', extension: 'arw' }, + { mimetype: 'image/x-sony-sr2', extension: 'sr2' }, + { mimetype: 'image/x-sony-srf', extension: 'srf' }, + { mimetype: 'video/3gpp', extension: '3gp' }, + { mimetype: 'video/mp2t', extension: 'm2ts' }, + { mimetype: 'video/mp2t', extension: 'mts' }, { mimetype: 'video/mp4', extension: 'mp4' }, { mimetype: 'video/mpeg', extension: 'mpg' }, + { mimetype: 'video/quicktime', extension: 'mov' }, { mimetype: 'video/webm', extension: 'webm' }, { mimetype: 'video/x-flv', extension: 'flv' }, { mimetype: 'video/x-matroska', extension: 'mkv' }, diff --git a/server/src/immich/config/asset-upload.config.ts b/server/src/immich/config/asset-upload.config.ts index bb8c95671..714aca0d3 100644 --- a/server/src/immich/config/asset-upload.config.ts +++ b/server/src/immich/config/asset-upload.config.ts @@ -1,3 +1,4 @@ +import { isSidecarFileType, isSupportedFileType } from '@app/domain'; import { StorageCore, StorageFolder } from '@app/domain/storage'; import { BadRequestException, Logger, UnauthorizedException } from '@nestjs/common'; import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface'; @@ -53,21 +54,19 @@ function fileFilter(req: AuthRequest, file: any, cb: any) { if (!req.user || (req.user.isPublicUser && !req.user.isAllowUpload)) { return cb(new UnauthorizedException()); } - if ( - file.mimetype.match( - /\/(jpg|jpeg|png|gif|avi|mov|mp4|webm|x-msvideo|quicktime|heic|heif|avif|dng|x-adobe-dng|webp|tiff|3gpp|nef|x-nikon-nef|x-fuji-raf|x-samsung-srw|mpeg|x-flv|x-ms-wmv|x-matroska|x-sony-arw|arw|x-canon-crw|x-canon-cr2|x-canon-cr3|x-epson-erf|x-kodak-dcr|x-kodak-kdc|x-kodak-k25|x-minolta-mrw|x-olympus-orf|x-panasonic-raw|x-pentax-pef|x-sigma-x3f|x-sony-srf|x-sony-sr2|x-hasselblad-3fr|x-hasselblad-fff|x-leica-rwl|x-olympus-ori|x-phaseone-iiq|x-arriflex-ari|x-phaseone-cap|x-phantom-cin)$/, - ) - ) { - cb(null, true); - } else { - // Additionally support XML but only for sidecar files - if (file.fieldname == 'sidecarData' && file.mimetype.match(/\/xml$/)) { - return cb(null, true); - } - logger.error(`Unsupported file type ${extname(file.originalname)} file MIME type ${file.mimetype}`); - cb(new BadRequestException(`Unsupported file type ${extname(file.originalname)}`), false); + if (isSupportedFileType(file.mimetype)) { + cb(null, true); + return; } + + // Additionally support XML but only for sidecar files. + if (file.fieldname === 'sidecarData' && isSidecarFileType(file.mimetype)) { + return cb(null, true); + } + + logger.error(`Unsupported file type ${extname(file.originalname)} file MIME type ${file.mimetype}`); + cb(new BadRequestException(`Unsupported file type ${extname(file.originalname)}`), false); } function destination(req: AuthRequest, file: Express.Multer.File, cb: any) { diff --git a/server/src/immich/controllers/shared-link.controller.ts b/server/src/immich/controllers/shared-link.controller.ts index 1ab8f505f..aa0e53397 100644 --- a/server/src/immich/controllers/shared-link.controller.ts +++ b/server/src/immich/controllers/shared-link.controller.ts @@ -1,13 +1,21 @@ -import { AuthUserDto, EditSharedLinkDto, SharedLinkResponseDto, SharedLinkService } from '@app/domain'; -import { Body, Controller, Delete, Get, Param, Patch } from '@nestjs/common'; +import { + AssetIdsDto, + AssetIdsResponseDto, + AuthUserDto, + SharedLinkCreateDto, + SharedLinkEditDto, + SharedLinkResponseDto, + SharedLinkService, +} from '@app/domain'; +import { Body, Controller, Delete, Get, Param, Patch, Post, Put } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { AuthUser } from '../decorators/auth-user.decorator'; import { Authenticated, SharedLinkRoute } from '../decorators/authenticated.decorator'; import { UseValidation } from '../decorators/use-validation.decorator'; import { UUIDParamDto } from './dto/uuid-param.dto'; -@ApiTags('share') -@Controller('share') +@ApiTags('Shared Link') +@Controller('shared-link') @Authenticated() @UseValidation() export class SharedLinkController { @@ -29,11 +37,16 @@ export class SharedLinkController { return this.service.get(authUser, id); } + @Post() + createSharedLink(@AuthUser() authUser: AuthUserDto, @Body() dto: SharedLinkCreateDto) { + return this.service.create(authUser, dto); + } + @Patch(':id') updateSharedLink( @AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto, - @Body() dto: EditSharedLinkDto, + @Body() dto: SharedLinkEditDto, ): Promise { return this.service.update(authUser, id, dto); } @@ -42,4 +55,24 @@ export class SharedLinkController { removeSharedLink(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise { return this.service.remove(authUser, id); } + + @SharedLinkRoute() + @Put(':id/assets') + addSharedLinkAssets( + @AuthUser() authUser: AuthUserDto, + @Param() { id }: UUIDParamDto, + @Body() dto: AssetIdsDto, + ): Promise { + return this.service.addAssets(authUser, id, dto); + } + + @SharedLinkRoute() + @Delete(':id/assets') + removeSharedLinkAssets( + @AuthUser() authUser: AuthUserDto, + @Param() { id }: UUIDParamDto, + @Body() dto: AssetIdsDto, + ): Promise { + return this.service.removeAssets(authUser, id, dto); + } } diff --git a/server/src/infra/entities/asset.entity.ts b/server/src/infra/entities/asset.entity.ts index 82172bcfa..c070b5cd1 100644 --- a/server/src/infra/entities/asset.entity.ts +++ b/server/src/infra/entities/asset.entity.ts @@ -42,7 +42,7 @@ export class AssetEntity { @Column() type!: AssetType; - @Column() + @Column({ unique: true }) originalPath!: string; @Column({ type: 'varchar', nullable: true }) @@ -75,6 +75,9 @@ export class AssetEntity { @Column({ type: 'boolean', default: false }) isArchived!: boolean; + @Column({ type: 'boolean', default: false }) + isReadOnly!: boolean; + @Column({ type: 'varchar', nullable: true }) mimeType!: string | null; diff --git a/server/src/infra/entities/shared-link.entity.ts b/server/src/infra/entities/shared-link.entity.ts index 7bca8dddf..e06635d6a 100644 --- a/server/src/infra/entities/shared-link.entity.ts +++ b/server/src/infra/entities/shared-link.entity.ts @@ -18,8 +18,8 @@ export class SharedLinkEntity { @PrimaryGeneratedColumn('uuid') id!: string; - @Column({ nullable: true }) - description?: string; + @Column({ type: 'varchar', nullable: true }) + description!: string | null; @Column() userId!: string; @@ -55,6 +55,9 @@ export class SharedLinkEntity { @Index('IDX_sharedlink_albumId') @ManyToOne(() => AlbumEntity, (album) => album.sharedLinks, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) album?: AlbumEntity; + + @Column({ type: 'varchar', nullable: true }) + albumId!: string | null; } export enum SharedLinkType { diff --git a/server/src/infra/entities/user.entity.ts b/server/src/infra/entities/user.entity.ts index f175a603c..7cdac1f82 100644 --- a/server/src/infra/entities/user.entity.ts +++ b/server/src/infra/entities/user.entity.ts @@ -30,6 +30,9 @@ export class UserEntity { @Column({ type: 'varchar', unique: true, default: null }) storageLabel!: string | null; + @Column({ type: 'varchar', default: null }) + externalPath!: string | null; + @Column({ default: '', select: false }) password?: string; diff --git a/server/src/infra/migrations/1686584273471-ImportAsset.ts b/server/src/infra/migrations/1686584273471-ImportAsset.ts new file mode 100644 index 000000000..d9f5819a8 --- /dev/null +++ b/server/src/infra/migrations/1686584273471-ImportAsset.ts @@ -0,0 +1,18 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class ImportAsset1686584273471 implements MigrationInterface { + name = 'ImportAsset1686584273471' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "assets" ADD "isReadOnly" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "assets" ADD CONSTRAINT "UQ_4ed4f8052685ff5b1e7ca1058ba" UNIQUE ("originalPath")`); + await queryRunner.query(`ALTER TABLE "users" ADD "externalPath" character varying`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "assets" DROP CONSTRAINT "UQ_4ed4f8052685ff5b1e7ca1058ba"`); + await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "isReadOnly"`); + await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "externalPath"`); + } + +} diff --git a/server/src/infra/repositories/access.repository.ts b/server/src/infra/repositories/access.repository.ts index c0d54e239..95dfdadfb 100644 --- a/server/src/infra/repositories/access.repository.ts +++ b/server/src/infra/repositories/access.repository.ts @@ -95,4 +95,13 @@ export class AccessRepository implements IAccessRepository { })) ); } + + hasAlbumOwnerAccess(userId: string, albumId: string): Promise { + return this.albumRepository.exist({ + where: { + id: albumId, + ownerId: userId, + }, + }); + } } diff --git a/server/src/infra/repositories/asset.repository.ts b/server/src/infra/repositories/asset.repository.ts index 54b4523e4..1139dbf11 100644 --- a/server/src/infra/repositories/asset.repository.ts +++ b/server/src/infra/repositories/asset.repository.ts @@ -248,6 +248,13 @@ export class AssetRepository implements IAssetRepository { }); } + getLastUpdatedAssetForAlbumId(albumId: string): Promise { + return this.repository.findOne({ + where: { albums: { id: albumId } }, + order: { updatedAt: 'DESC' }, + }); + } + async getMapMarkers(ownerId: string, options: MapMarkerSearchOptions = {}): Promise { const { isFavorite, fileCreatedAfter, fileCreatedBefore } = options; diff --git a/server/src/infra/repositories/crypto.repository.ts b/server/src/infra/repositories/crypto.repository.ts index 6ded5b020..af76d46a7 100644 --- a/server/src/infra/repositories/crypto.repository.ts +++ b/server/src/infra/repositories/crypto.repository.ts @@ -2,6 +2,7 @@ import { ICryptoRepository } from '@app/domain'; import { Injectable } from '@nestjs/common'; import { compareSync, hash } from 'bcrypt'; import { createHash, randomBytes } from 'crypto'; +import { createReadStream } from 'fs'; @Injectable() export class CryptoRepository implements ICryptoRepository { @@ -13,4 +14,14 @@ export class CryptoRepository implements ICryptoRepository { hashSha256(value: string) { return createHash('sha256').update(value).digest('base64'); } + + hashFile(filepath: string): Promise { + return new Promise((resolve, reject) => { + const hash = createHash('sha1'); + const stream = createReadStream(filepath); + stream.on('error', (err) => reject(err)); + stream.on('data', (chunk) => hash.update(chunk)); + stream.on('end', () => resolve(hash.digest())); + }); + } } diff --git a/server/test/fixtures.ts b/server/test/fixtures.ts index f60a0743a..970f15282 100644 --- a/server/test/fixtures.ts +++ b/server/test/fixtures.ts @@ -50,6 +50,7 @@ export const authStub = { isAdmin: true, isPublicUser: false, isAllowUpload: true, + externalPath: null, }), user1: Object.freeze({ id: 'user-id', @@ -60,6 +61,7 @@ export const authStub = { isAllowDownload: true, isShowExif: true, accessTokenId: 'token-id', + externalPath: null, }), user2: Object.freeze({ id: 'user-2', @@ -70,6 +72,18 @@ export const authStub = { isAllowDownload: true, isShowExif: true, accessTokenId: 'token-id', + externalPath: null, + }), + external1: Object.freeze({ + id: 'user-id', + email: 'immich@test.com', + isAdmin: false, + isPublicUser: false, + isAllowUpload: true, + isAllowDownload: true, + isShowExif: true, + accessTokenId: 'token-id', + externalPath: '/data/user1', }), adminSharedLink: Object.freeze({ id: 'admin_id', @@ -111,6 +125,7 @@ export const userEntityStub = { firstName: 'admin_first_name', lastName: 'admin_last_name', storageLabel: 'admin', + externalPath: null, oauthId: '', shouldChangePassword: false, profileImagePath: '', @@ -126,6 +141,7 @@ export const userEntityStub = { firstName: 'immich_first_name', lastName: 'immich_last_name', storageLabel: null, + externalPath: null, oauthId: '', shouldChangePassword: false, profileImagePath: '', @@ -141,6 +157,7 @@ export const userEntityStub = { firstName: 'immich_first_name', lastName: 'immich_last_name', storageLabel: null, + externalPath: null, oauthId: '', shouldChangePassword: false, profileImagePath: '', @@ -156,6 +173,7 @@ export const userEntityStub = { firstName: 'immich_first_name', lastName: 'immich_last_name', storageLabel: 'label-1', + externalPath: null, oauthId: '', shouldChangePassword: false, profileImagePath: '', @@ -212,6 +230,7 @@ export const assetEntityStub = { sharedLinks: [], faces: [], sidecarPath: null, + isReadOnly: false, }), noWebpPath: Object.freeze({ id: 'asset-id', @@ -242,6 +261,7 @@ export const assetEntityStub = { originalFileName: 'asset-id.ext', faces: [], sidecarPath: null, + isReadOnly: false, }), noThumbhash: Object.freeze({ id: 'asset-id', @@ -263,6 +283,7 @@ export const assetEntityStub = { mimeType: null, isFavorite: true, isArchived: false, + isReadOnly: false, duration: null, isVisible: true, livePhotoVideo: null, @@ -293,6 +314,7 @@ export const assetEntityStub = { mimeType: null, isFavorite: true, isArchived: false, + isReadOnly: false, duration: null, isVisible: true, livePhotoVideo: null, @@ -324,6 +346,7 @@ export const assetEntityStub = { mimeType: null, isFavorite: true, isArchived: false, + isReadOnly: false, duration: null, isVisible: true, livePhotoVideo: null, @@ -375,6 +398,7 @@ export const assetEntityStub = { mimeType: null, isFavorite: false, isArchived: false, + isReadOnly: false, duration: null, isVisible: true, livePhotoVideo: null, @@ -408,6 +432,7 @@ export const assetEntityStub = { mimeType: null, isFavorite: true, isArchived: false, + isReadOnly: false, duration: null, isVisible: true, livePhotoVideo: null, @@ -777,6 +802,21 @@ export const loginResponseStub = { }; export const sharedLinkStub = { + individual: Object.freeze({ + id: '123', + userId: authStub.admin.id, + user: userEntityStub.admin, + key: sharedLinkBytes, + type: SharedLinkType.INDIVIDUAL, + createdAt: today, + expiresAt: tomorrow, + allowUpload: true, + allowDownload: true, + showExif: true, + album: undefined, + description: null, + assets: [assetEntityStub.image], + } as SharedLinkEntity), valid: Object.freeze({ id: '123', userId: authStub.admin.id, @@ -789,6 +829,8 @@ export const sharedLinkStub = { allowDownload: true, showExif: true, album: undefined, + albumId: null, + description: null, assets: [], } as SharedLinkEntity), expired: Object.freeze({ @@ -802,6 +844,8 @@ export const sharedLinkStub = { allowUpload: true, allowDownload: true, showExif: true, + description: null, + albumId: null, assets: [], } as SharedLinkEntity), readonlyNoExif: Object.freeze({ @@ -815,7 +859,9 @@ export const sharedLinkStub = { allowUpload: false, allowDownload: false, showExif: false, + description: null, assets: [], + albumId: 'album-123', album: { id: 'album-123', ownerId: authStub.admin.id, @@ -844,6 +890,7 @@ export const sharedLinkStub = { updatedAt: today, isFavorite: false, isArchived: false, + isReadOnly: false, mimeType: 'image/jpeg', smartInfo: { assetId: 'id_1', @@ -903,7 +950,7 @@ export const sharedLinkResponseStub = { allowUpload: true, assets: [], createdAt: today, - description: undefined, + description: null, expiresAt: tomorrow, id: '123', key: sharedLinkBytes.toString('base64url'), @@ -917,7 +964,7 @@ export const sharedLinkResponseStub = { allowUpload: true, assets: [], createdAt: today, - description: undefined, + description: null, expiresAt: yesterday, id: '123', key: sharedLinkBytes.toString('base64url'), @@ -932,7 +979,7 @@ export const sharedLinkResponseStub = { type: SharedLinkType.ALBUM, createdAt: today, expiresAt: tomorrow, - description: undefined, + description: null, allowUpload: false, allowDownload: false, showExif: true, @@ -946,7 +993,7 @@ export const sharedLinkResponseStub = { type: SharedLinkType.ALBUM, createdAt: today, expiresAt: tomorrow, - description: undefined, + description: null, allowUpload: false, allowDownload: false, showExif: false, diff --git a/server/test/repositories/access.repository.mock.ts b/server/test/repositories/access.repository.mock.ts index 6bbf04717..055cc226e 100644 --- a/server/test/repositories/access.repository.mock.ts +++ b/server/test/repositories/access.repository.mock.ts @@ -3,9 +3,12 @@ import { IAccessRepository } from '@app/domain'; export const newAccessRepositoryMock = (): jest.Mocked => { return { hasPartnerAccess: jest.fn(), + hasAlbumAssetAccess: jest.fn(), hasOwnerAssetAccess: jest.fn(), hasPartnerAssetAccess: jest.fn(), hasSharedLinkAssetAccess: jest.fn(), + + hasAlbumOwnerAccess: jest.fn(), }; }; diff --git a/server/test/repositories/asset.repository.mock.ts b/server/test/repositories/asset.repository.mock.ts index 5418176f3..51dbb3a27 100644 --- a/server/test/repositories/asset.repository.mock.ts +++ b/server/test/repositories/asset.repository.mock.ts @@ -7,6 +7,7 @@ export const newAssetRepositoryMock = (): jest.Mocked => { getWithout: jest.fn(), getWith: jest.fn(), getFirstAssetForAlbumId: jest.fn(), + getLastUpdatedAssetForAlbumId: jest.fn(), getAll: jest.fn().mockResolvedValue({ items: [], hasNextPage: false, diff --git a/server/test/repositories/crypto.repository.mock.ts b/server/test/repositories/crypto.repository.mock.ts index bbf315444..b2f159c1e 100644 --- a/server/test/repositories/crypto.repository.mock.ts +++ b/server/test/repositories/crypto.repository.mock.ts @@ -6,5 +6,6 @@ export const newCryptoRepositoryMock = (): jest.Mocked => { compareBcrypt: jest.fn().mockReturnValue(true), hashBcrypt: jest.fn().mockImplementation((input) => Promise.resolve(`${input} (hashed)`)), hashSha256: jest.fn().mockImplementation((input) => `${input} (hashed)`), + hashFile: jest.fn().mockImplementation((input) => `${input} (file-hashed)`), }; }; diff --git a/server/tsconfig.build.json b/server/tsconfig.build.json index 64f86c6bd..0d7cd0873 100644 --- a/server/tsconfig.build.json +++ b/server/tsconfig.build.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", - "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] + "exclude": ["dist", "node_modules", "upload", "test", "**/*spec.ts"] } diff --git a/web/src/api/api.ts b/web/src/api/api.ts index 3361f5e18..2924d717d 100644 --- a/web/src/api/api.ts +++ b/web/src/api/api.ts @@ -7,16 +7,16 @@ import { Configuration, ConfigurationParameters, JobApi, + JobName, OAuthApi, - PersonApi, PartnerApi, + PersonApi, SearchApi, ServerInfoApi, - ShareApi, + SharedLinkApi, SystemConfigApi, UserApi, - UserApiFp, - JobName + UserApiFp } from './open-api'; import { BASE_PATH } from './open-api/base'; import { DUMMY_BASE_URL, toPathString } from './open-api/common'; @@ -32,7 +32,7 @@ export class ImmichApi { public partnerApi: PartnerApi; public searchApi: SearchApi; public serverInfoApi: ServerInfoApi; - public shareApi: ShareApi; + public sharedLinkApi: SharedLinkApi; public personApi: PersonApi; public systemConfigApi: SystemConfigApi; public userApi: UserApi; @@ -51,7 +51,7 @@ export class ImmichApi { this.partnerApi = new PartnerApi(this.config); this.searchApi = new SearchApi(this.config); this.serverInfoApi = new ServerInfoApi(this.config); - this.shareApi = new ShareApi(this.config); + this.sharedLinkApi = new SharedLinkApi(this.config); this.personApi = new PersonApi(this.config); this.systemConfigApi = new SystemConfigApi(this.config); this.userApi = new UserApi(this.config); diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index b49965514..0c7325579 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -284,6 +284,12 @@ export interface AlbumResponseDto { * @memberof AlbumResponseDto */ 'owner': UserResponseDto; + /** + * + * @type {string} + * @memberof AlbumResponseDto + */ + 'lastModifiedAssetTimestamp'?: string; } /** * @@ -897,92 +903,6 @@ export interface CreateAlbumDto { */ 'assetIds'?: Array; } -/** - * - * @export - * @interface CreateAlbumShareLinkDto - */ -export interface CreateAlbumShareLinkDto { - /** - * - * @type {string} - * @memberof CreateAlbumShareLinkDto - */ - 'albumId': string; - /** - * - * @type {string} - * @memberof CreateAlbumShareLinkDto - */ - 'expiresAt'?: string; - /** - * - * @type {boolean} - * @memberof CreateAlbumShareLinkDto - */ - 'allowUpload'?: boolean; - /** - * - * @type {boolean} - * @memberof CreateAlbumShareLinkDto - */ - 'allowDownload'?: boolean; - /** - * - * @type {boolean} - * @memberof CreateAlbumShareLinkDto - */ - 'showExif'?: boolean; - /** - * - * @type {string} - * @memberof CreateAlbumShareLinkDto - */ - 'description'?: string; -} -/** - * - * @export - * @interface CreateAssetsShareLinkDto - */ -export interface CreateAssetsShareLinkDto { - /** - * - * @type {Array} - * @memberof CreateAssetsShareLinkDto - */ - 'assetIds': Array; - /** - * - * @type {string} - * @memberof CreateAssetsShareLinkDto - */ - 'expiresAt'?: string; - /** - * - * @type {boolean} - * @memberof CreateAssetsShareLinkDto - */ - 'allowUpload'?: boolean; - /** - * - * @type {boolean} - * @memberof CreateAssetsShareLinkDto - */ - 'allowDownload'?: boolean; - /** - * - * @type {boolean} - * @memberof CreateAssetsShareLinkDto - */ - 'showExif'?: boolean; - /** - * - * @type {string} - * @memberof CreateAssetsShareLinkDto - */ - 'description'?: string; -} /** * * @export @@ -1059,6 +979,12 @@ export interface CreateUserDto { * @memberof CreateUserDto */ 'storageLabel'?: string | null; + /** + * + * @type {string} + * @memberof CreateUserDto + */ + 'externalPath'?: string | null; } /** * @@ -1195,43 +1121,6 @@ export interface DownloadFilesDto { */ 'assetIds': Array; } -/** - * - * @export - * @interface EditSharedLinkDto - */ -export interface EditSharedLinkDto { - /** - * - * @type {string} - * @memberof EditSharedLinkDto - */ - 'description'?: string; - /** - * - * @type {string} - * @memberof EditSharedLinkDto - */ - 'expiresAt'?: string | null; - /** - * - * @type {boolean} - * @memberof EditSharedLinkDto - */ - 'allowUpload'?: boolean; - /** - * - * @type {boolean} - * @memberof EditSharedLinkDto - */ - 'allowDownload'?: boolean; - /** - * - * @type {boolean} - * @memberof EditSharedLinkDto - */ - 'showExif'?: boolean; -} /** * * @export @@ -1411,6 +1300,87 @@ export interface GetAssetCountByTimeBucketDto { } +/** + * + * @export + * @interface ImportAssetDto + */ +export interface ImportAssetDto { + /** + * + * @type {AssetTypeEnum} + * @memberof ImportAssetDto + */ + 'assetType': AssetTypeEnum; + /** + * + * @type {boolean} + * @memberof ImportAssetDto + */ + 'isReadOnly'?: boolean; + /** + * + * @type {string} + * @memberof ImportAssetDto + */ + 'assetPath': string; + /** + * + * @type {string} + * @memberof ImportAssetDto + */ + 'sidecarPath'?: string; + /** + * + * @type {string} + * @memberof ImportAssetDto + */ + 'deviceAssetId': string; + /** + * + * @type {string} + * @memberof ImportAssetDto + */ + 'deviceId': string; + /** + * + * @type {string} + * @memberof ImportAssetDto + */ + 'fileCreatedAt': string; + /** + * + * @type {string} + * @memberof ImportAssetDto + */ + 'fileModifiedAt': string; + /** + * + * @type {boolean} + * @memberof ImportAssetDto + */ + 'isFavorite': boolean; + /** + * + * @type {boolean} + * @memberof ImportAssetDto + */ + 'isArchived'?: boolean; + /** + * + * @type {boolean} + * @memberof ImportAssetDto + */ + 'isVisible'?: boolean; + /** + * + * @type {string} + * @memberof ImportAssetDto + */ + 'duration'?: string; +} + + /** * * @export @@ -2116,6 +2086,100 @@ export interface ServerVersionReponseDto { */ 'patch': number; } +/** + * + * @export + * @interface SharedLinkCreateDto + */ +export interface SharedLinkCreateDto { + /** + * + * @type {SharedLinkType} + * @memberof SharedLinkCreateDto + */ + 'type': SharedLinkType; + /** + * + * @type {Array} + * @memberof SharedLinkCreateDto + */ + 'assetIds'?: Array; + /** + * + * @type {string} + * @memberof SharedLinkCreateDto + */ + 'albumId'?: string; + /** + * + * @type {string} + * @memberof SharedLinkCreateDto + */ + 'description'?: string; + /** + * + * @type {string} + * @memberof SharedLinkCreateDto + */ + 'expiresAt'?: string | null; + /** + * + * @type {boolean} + * @memberof SharedLinkCreateDto + */ + 'allowUpload'?: boolean; + /** + * + * @type {boolean} + * @memberof SharedLinkCreateDto + */ + 'allowDownload'?: boolean; + /** + * + * @type {boolean} + * @memberof SharedLinkCreateDto + */ + 'showExif'?: boolean; +} + + +/** + * + * @export + * @interface SharedLinkEditDto + */ +export interface SharedLinkEditDto { + /** + * + * @type {string} + * @memberof SharedLinkEditDto + */ + 'description'?: string; + /** + * + * @type {string} + * @memberof SharedLinkEditDto + */ + 'expiresAt'?: string | null; + /** + * + * @type {boolean} + * @memberof SharedLinkEditDto + */ + 'allowUpload'?: boolean; + /** + * + * @type {boolean} + * @memberof SharedLinkEditDto + */ + 'allowDownload'?: boolean; + /** + * + * @type {boolean} + * @memberof SharedLinkEditDto + */ + 'showExif'?: boolean; +} /** * * @export @@ -2139,7 +2203,7 @@ export interface SharedLinkResponseDto { * @type {string} * @memberof SharedLinkResponseDto */ - 'description'?: string; + 'description': string | null; /** * * @type {string} @@ -2759,6 +2823,12 @@ export interface UpdateUserDto { * @memberof UpdateUserDto */ 'storageLabel'?: string; + /** + * + * @type {string} + * @memberof UpdateUserDto + */ + 'externalPath'?: string; /** * * @type {boolean} @@ -2864,6 +2934,12 @@ export interface UserResponseDto { * @memberof UserResponseDto */ 'storageLabel': string | null; + /** + * + * @type {string} + * @memberof UserResponseDto + */ + 'externalPath': string | null; /** * * @type {string} @@ -3536,50 +3612,6 @@ export const AlbumApiAxiosParamCreator = function (configuration?: Configuration options: localVarRequestOptions, }; }, - /** - * - * @param {CreateAlbumShareLinkDto} createAlbumShareLinkDto - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - createAlbumSharedLink: async (createAlbumShareLinkDto: CreateAlbumShareLinkDto, options: AxiosRequestConfig = {}): Promise => { - // verify required parameter 'createAlbumShareLinkDto' is not null or undefined - assertParamExists('createAlbumSharedLink', 'createAlbumShareLinkDto', createAlbumShareLinkDto) - const localVarPath = `/album/create-shared-link`; - // use dummy base URL string because the URL constructor only accepts absolute URLs. - const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); - let baseOptions; - if (configuration) { - baseOptions = configuration.baseOptions; - } - - const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; - const localVarHeaderParameter = {} as any; - const localVarQueryParameter = {} as any; - - // authentication cookie required - - // authentication api_key required - await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) - - // authentication bearer required - // http bearer authentication required - await setBearerAuthToObject(localVarHeaderParameter, configuration) - - - - localVarHeaderParameter['Content-Type'] = 'application/json'; - - setSearchParams(localVarUrlObj, localVarQueryParameter); - let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; - localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - localVarRequestOptions.data = serializeDataIfNeeded(createAlbumShareLinkDto, localVarRequestOptions, configuration) - - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - }; - }, /** * * @param {string} id @@ -3997,16 +4029,6 @@ export const AlbumApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.createAlbum(createAlbumDto, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, - /** - * - * @param {CreateAlbumShareLinkDto} createAlbumShareLinkDto - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async createAlbumSharedLink(createAlbumShareLinkDto: CreateAlbumShareLinkDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.createAlbumSharedLink(createAlbumShareLinkDto, options); - return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); - }, /** * * @param {string} id @@ -4134,15 +4156,6 @@ export const AlbumApiFactory = function (configuration?: Configuration, basePath createAlbum(createAlbumDto: CreateAlbumDto, options?: any): AxiosPromise { return localVarFp.createAlbum(createAlbumDto, options).then((request) => request(axios, basePath)); }, - /** - * - * @param {CreateAlbumShareLinkDto} createAlbumShareLinkDto - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - createAlbumSharedLink(createAlbumShareLinkDto: CreateAlbumShareLinkDto, options?: any): AxiosPromise { - return localVarFp.createAlbumSharedLink(createAlbumShareLinkDto, options).then((request) => request(axios, basePath)); - }, /** * * @param {string} id @@ -4288,20 +4301,6 @@ export interface AlbumApiCreateAlbumRequest { readonly createAlbumDto: CreateAlbumDto } -/** - * Request parameters for createAlbumSharedLink operation in AlbumApi. - * @export - * @interface AlbumApiCreateAlbumSharedLinkRequest - */ -export interface AlbumApiCreateAlbumSharedLinkRequest { - /** - * - * @type {CreateAlbumShareLinkDto} - * @memberof AlbumApiCreateAlbumSharedLink - */ - readonly createAlbumShareLinkDto: CreateAlbumShareLinkDto -} - /** * Request parameters for deleteAlbum operation in AlbumApi. * @export @@ -4496,17 +4495,6 @@ export class AlbumApi extends BaseAPI { return AlbumApiFp(this.configuration).createAlbum(requestParameters.createAlbumDto, options).then((request) => request(this.axios, this.basePath)); } - /** - * - * @param {AlbumApiCreateAlbumSharedLinkRequest} requestParameters Request parameters. - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof AlbumApi - */ - public createAlbumSharedLink(requestParameters: AlbumApiCreateAlbumSharedLinkRequest, options?: AxiosRequestConfig) { - return AlbumApiFp(this.configuration).createAlbumSharedLink(requestParameters.createAlbumShareLinkDto, options).then((request) => request(this.axios, this.basePath)); - } - /** * * @param {AlbumApiDeleteAlbumRequest} requestParameters Request parameters. @@ -4602,55 +4590,6 @@ export class AlbumApi extends BaseAPI { */ export const AssetApiAxiosParamCreator = function (configuration?: Configuration) { return { - /** - * - * @param {AddAssetsDto} addAssetsDto - * @param {string} [key] - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - addAssetsToSharedLink: async (addAssetsDto: AddAssetsDto, key?: string, options: AxiosRequestConfig = {}): Promise => { - // verify required parameter 'addAssetsDto' is not null or undefined - assertParamExists('addAssetsToSharedLink', 'addAssetsDto', addAssetsDto) - const localVarPath = `/asset/shared-link/add`; - // use dummy base URL string because the URL constructor only accepts absolute URLs. - const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); - let baseOptions; - if (configuration) { - baseOptions = configuration.baseOptions; - } - - const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options}; - const localVarHeaderParameter = {} as any; - const localVarQueryParameter = {} as any; - - // authentication cookie required - - // authentication api_key required - await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) - - // authentication bearer required - // http bearer authentication required - await setBearerAuthToObject(localVarHeaderParameter, configuration) - - if (key !== undefined) { - localVarQueryParameter['key'] = key; - } - - - - localVarHeaderParameter['Content-Type'] = 'application/json'; - - setSearchParams(localVarUrlObj, localVarQueryParameter); - let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; - localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - localVarRequestOptions.data = serializeDataIfNeeded(addAssetsDto, localVarRequestOptions, configuration) - - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - }; - }, /** * Checks if assets exist by checksums * @param {AssetBulkUploadCheckDto} assetBulkUploadCheckDto @@ -4788,50 +4727,6 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration options: localVarRequestOptions, }; }, - /** - * - * @param {CreateAssetsShareLinkDto} createAssetsShareLinkDto - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - createAssetsSharedLink: async (createAssetsShareLinkDto: CreateAssetsShareLinkDto, options: AxiosRequestConfig = {}): Promise => { - // verify required parameter 'createAssetsShareLinkDto' is not null or undefined - assertParamExists('createAssetsSharedLink', 'createAssetsShareLinkDto', createAssetsShareLinkDto) - const localVarPath = `/asset/shared-link`; - // use dummy base URL string because the URL constructor only accepts absolute URLs. - const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); - let baseOptions; - if (configuration) { - baseOptions = configuration.baseOptions; - } - - const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; - const localVarHeaderParameter = {} as any; - const localVarQueryParameter = {} as any; - - // authentication cookie required - - // authentication api_key required - await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) - - // authentication bearer required - // http bearer authentication required - await setBearerAuthToObject(localVarHeaderParameter, configuration) - - - - localVarHeaderParameter['Content-Type'] = 'application/json'; - - setSearchParams(localVarUrlObj, localVarQueryParameter); - let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; - localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - localVarRequestOptions.data = serializeDataIfNeeded(createAssetsShareLinkDto, localVarRequestOptions, configuration) - - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - }; - }, /** * * @param {DeleteAssetDto} deleteAssetDto @@ -5618,15 +5513,14 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration }, /** * - * @param {RemoveAssetsDto} removeAssetsDto - * @param {string} [key] + * @param {ImportAssetDto} importAssetDto * @param {*} [options] Override http request option. * @throws {RequiredError} */ - removeAssetsFromSharedLink: async (removeAssetsDto: RemoveAssetsDto, key?: string, options: AxiosRequestConfig = {}): Promise => { - // verify required parameter 'removeAssetsDto' is not null or undefined - assertParamExists('removeAssetsFromSharedLink', 'removeAssetsDto', removeAssetsDto) - const localVarPath = `/asset/shared-link/remove`; + importFile: async (importAssetDto: ImportAssetDto, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'importAssetDto' is not null or undefined + assertParamExists('importFile', 'importAssetDto', importAssetDto) + const localVarPath = `/asset/import`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; @@ -5634,7 +5528,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration baseOptions = configuration.baseOptions; } - const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options}; + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; @@ -5647,10 +5541,6 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration // http bearer authentication required await setBearerAuthToObject(localVarHeaderParameter, configuration) - if (key !== undefined) { - localVarQueryParameter['key'] = key; - } - localVarHeaderParameter['Content-Type'] = 'application/json'; @@ -5658,7 +5548,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - localVarRequestOptions.data = serializeDataIfNeeded(removeAssetsDto, localVarRequestOptions, configuration) + localVarRequestOptions.data = serializeDataIfNeeded(importAssetDto, localVarRequestOptions, configuration) return { url: toPathString(localVarUrlObj), @@ -5818,26 +5708,29 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration * * @param {AssetTypeEnum} assetType * @param {File} assetData + * @param {string} fileExtension * @param {string} deviceAssetId * @param {string} deviceId * @param {string} fileCreatedAt * @param {string} fileModifiedAt * @param {boolean} isFavorite - * @param {string} fileExtension * @param {string} [key] * @param {File} [livePhotoData] * @param {File} [sidecarData] + * @param {boolean} [isReadOnly] * @param {boolean} [isArchived] * @param {boolean} [isVisible] * @param {string} [duration] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - uploadFile: async (assetType: AssetTypeEnum, assetData: File, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, fileExtension: string, key?: string, livePhotoData?: File, sidecarData?: File, isArchived?: boolean, isVisible?: boolean, duration?: string, options: AxiosRequestConfig = {}): Promise => { + uploadFile: async (assetType: AssetTypeEnum, assetData: File, fileExtension: string, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, key?: string, livePhotoData?: File, sidecarData?: File, isReadOnly?: boolean, isArchived?: boolean, isVisible?: boolean, duration?: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'assetType' is not null or undefined assertParamExists('uploadFile', 'assetType', assetType) // verify required parameter 'assetData' is not null or undefined assertParamExists('uploadFile', 'assetData', assetData) + // verify required parameter 'fileExtension' is not null or undefined + assertParamExists('uploadFile', 'fileExtension', fileExtension) // verify required parameter 'deviceAssetId' is not null or undefined assertParamExists('uploadFile', 'deviceAssetId', deviceAssetId) // verify required parameter 'deviceId' is not null or undefined @@ -5848,8 +5741,6 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration assertParamExists('uploadFile', 'fileModifiedAt', fileModifiedAt) // verify required parameter 'isFavorite' is not null or undefined assertParamExists('uploadFile', 'isFavorite', isFavorite) - // verify required parameter 'fileExtension' is not null or undefined - assertParamExists('uploadFile', 'fileExtension', fileExtension) const localVarPath = `/asset/upload`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -5893,6 +5784,14 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration localVarFormParams.append('sidecarData', sidecarData as any); } + if (isReadOnly !== undefined) { + localVarFormParams.append('isReadOnly', isReadOnly as any); + } + + if (fileExtension !== undefined) { + localVarFormParams.append('fileExtension', fileExtension as any); + } + if (deviceAssetId !== undefined) { localVarFormParams.append('deviceAssetId', deviceAssetId as any); } @@ -5921,10 +5820,6 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration localVarFormParams.append('isVisible', isVisible as any); } - if (fileExtension !== undefined) { - localVarFormParams.append('fileExtension', fileExtension as any); - } - if (duration !== undefined) { localVarFormParams.append('duration', duration as any); } @@ -5952,17 +5847,6 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration export const AssetApiFp = function(configuration?: Configuration) { const localVarAxiosParamCreator = AssetApiAxiosParamCreator(configuration) return { - /** - * - * @param {AddAssetsDto} addAssetsDto - * @param {string} [key] - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async addAssetsToSharedLink(addAssetsDto: AddAssetsDto, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.addAssetsToSharedLink(addAssetsDto, key, options); - return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); - }, /** * Checks if assets exist by checksums * @param {AssetBulkUploadCheckDto} assetBulkUploadCheckDto @@ -5994,16 +5878,6 @@ export const AssetApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.checkExistingAssets(checkExistingAssetsDto, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, - /** - * - * @param {CreateAssetsShareLinkDto} createAssetsShareLinkDto - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async createAssetsSharedLink(createAssetsShareLinkDto: CreateAssetsShareLinkDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.createAssetsSharedLink(createAssetsShareLinkDto, options); - return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); - }, /** * * @param {DeleteAssetDto} deleteAssetDto @@ -6185,13 +6059,12 @@ export const AssetApiFp = function(configuration?: Configuration) { }, /** * - * @param {RemoveAssetsDto} removeAssetsDto - * @param {string} [key] + * @param {ImportAssetDto} importAssetDto * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async removeAssetsFromSharedLink(removeAssetsDto: RemoveAssetsDto, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.removeAssetsFromSharedLink(removeAssetsDto, key, options); + async importFile(importAssetDto: ImportAssetDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.importFile(importAssetDto, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -6232,23 +6105,24 @@ export const AssetApiFp = function(configuration?: Configuration) { * * @param {AssetTypeEnum} assetType * @param {File} assetData + * @param {string} fileExtension * @param {string} deviceAssetId * @param {string} deviceId * @param {string} fileCreatedAt * @param {string} fileModifiedAt * @param {boolean} isFavorite - * @param {string} fileExtension * @param {string} [key] * @param {File} [livePhotoData] * @param {File} [sidecarData] + * @param {boolean} [isReadOnly] * @param {boolean} [isArchived] * @param {boolean} [isVisible] * @param {string} [duration] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async uploadFile(assetType: AssetTypeEnum, assetData: File, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, fileExtension: string, key?: string, livePhotoData?: File, sidecarData?: File, isArchived?: boolean, isVisible?: boolean, duration?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.uploadFile(assetType, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, fileExtension, key, livePhotoData, sidecarData, isArchived, isVisible, duration, options); + async uploadFile(assetType: AssetTypeEnum, assetData: File, fileExtension: string, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, key?: string, livePhotoData?: File, sidecarData?: File, isReadOnly?: boolean, isArchived?: boolean, isVisible?: boolean, duration?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.uploadFile(assetType, assetData, fileExtension, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, key, livePhotoData, sidecarData, isReadOnly, isArchived, isVisible, duration, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, } @@ -6261,16 +6135,6 @@ export const AssetApiFp = function(configuration?: Configuration) { export const AssetApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { const localVarFp = AssetApiFp(configuration) return { - /** - * - * @param {AddAssetsDto} addAssetsDto - * @param {string} [key] - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - addAssetsToSharedLink(addAssetsDto: AddAssetsDto, key?: string, options?: any): AxiosPromise { - return localVarFp.addAssetsToSharedLink(addAssetsDto, key, options).then((request) => request(axios, basePath)); - }, /** * Checks if assets exist by checksums * @param {AssetBulkUploadCheckDto} assetBulkUploadCheckDto @@ -6299,15 +6163,6 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath checkExistingAssets(checkExistingAssetsDto: CheckExistingAssetsDto, options?: any): AxiosPromise { return localVarFp.checkExistingAssets(checkExistingAssetsDto, options).then((request) => request(axios, basePath)); }, - /** - * - * @param {CreateAssetsShareLinkDto} createAssetsShareLinkDto - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - createAssetsSharedLink(createAssetsShareLinkDto: CreateAssetsShareLinkDto, options?: any): AxiosPromise { - return localVarFp.createAssetsSharedLink(createAssetsShareLinkDto, options).then((request) => request(axios, basePath)); - }, /** * * @param {DeleteAssetDto} deleteAssetDto @@ -6472,13 +6327,12 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath }, /** * - * @param {RemoveAssetsDto} removeAssetsDto - * @param {string} [key] + * @param {ImportAssetDto} importAssetDto * @param {*} [options] Override http request option. * @throws {RequiredError} */ - removeAssetsFromSharedLink(removeAssetsDto: RemoveAssetsDto, key?: string, options?: any): AxiosPromise { - return localVarFp.removeAssetsFromSharedLink(removeAssetsDto, key, options).then((request) => request(axios, basePath)); + importFile(importAssetDto: ImportAssetDto, options?: any): AxiosPromise { + return localVarFp.importFile(importAssetDto, options).then((request) => request(axios, basePath)); }, /** * @@ -6515,48 +6369,28 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath * * @param {AssetTypeEnum} assetType * @param {File} assetData + * @param {string} fileExtension * @param {string} deviceAssetId * @param {string} deviceId * @param {string} fileCreatedAt * @param {string} fileModifiedAt * @param {boolean} isFavorite - * @param {string} fileExtension * @param {string} [key] * @param {File} [livePhotoData] * @param {File} [sidecarData] + * @param {boolean} [isReadOnly] * @param {boolean} [isArchived] * @param {boolean} [isVisible] * @param {string} [duration] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - uploadFile(assetType: AssetTypeEnum, assetData: File, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, fileExtension: string, key?: string, livePhotoData?: File, sidecarData?: File, isArchived?: boolean, isVisible?: boolean, duration?: string, options?: any): AxiosPromise { - return localVarFp.uploadFile(assetType, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, fileExtension, key, livePhotoData, sidecarData, isArchived, isVisible, duration, options).then((request) => request(axios, basePath)); + uploadFile(assetType: AssetTypeEnum, assetData: File, fileExtension: string, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, key?: string, livePhotoData?: File, sidecarData?: File, isReadOnly?: boolean, isArchived?: boolean, isVisible?: boolean, duration?: string, options?: any): AxiosPromise { + return localVarFp.uploadFile(assetType, assetData, fileExtension, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, key, livePhotoData, sidecarData, isReadOnly, isArchived, isVisible, duration, options).then((request) => request(axios, basePath)); }, }; }; -/** - * Request parameters for addAssetsToSharedLink operation in AssetApi. - * @export - * @interface AssetApiAddAssetsToSharedLinkRequest - */ -export interface AssetApiAddAssetsToSharedLinkRequest { - /** - * - * @type {AddAssetsDto} - * @memberof AssetApiAddAssetsToSharedLink - */ - readonly addAssetsDto: AddAssetsDto - - /** - * - * @type {string} - * @memberof AssetApiAddAssetsToSharedLink - */ - readonly key?: string -} - /** * Request parameters for bulkUploadCheck operation in AssetApi. * @export @@ -6606,20 +6440,6 @@ export interface AssetApiCheckExistingAssetsRequest { readonly checkExistingAssetsDto: CheckExistingAssetsDto } -/** - * Request parameters for createAssetsSharedLink operation in AssetApi. - * @export - * @interface AssetApiCreateAssetsSharedLinkRequest - */ -export interface AssetApiCreateAssetsSharedLinkRequest { - /** - * - * @type {CreateAssetsShareLinkDto} - * @memberof AssetApiCreateAssetsSharedLink - */ - readonly createAssetsShareLinkDto: CreateAssetsShareLinkDto -} - /** * Request parameters for deleteAsset operation in AssetApi. * @export @@ -6887,24 +6707,17 @@ export interface AssetApiGetUserAssetsByDeviceIdRequest { } /** - * Request parameters for removeAssetsFromSharedLink operation in AssetApi. + * Request parameters for importFile operation in AssetApi. * @export - * @interface AssetApiRemoveAssetsFromSharedLinkRequest + * @interface AssetApiImportFileRequest */ -export interface AssetApiRemoveAssetsFromSharedLinkRequest { +export interface AssetApiImportFileRequest { /** * - * @type {RemoveAssetsDto} - * @memberof AssetApiRemoveAssetsFromSharedLink + * @type {ImportAssetDto} + * @memberof AssetApiImportFile */ - readonly removeAssetsDto: RemoveAssetsDto - - /** - * - * @type {string} - * @memberof AssetApiRemoveAssetsFromSharedLink - */ - readonly key?: string + readonly importAssetDto: ImportAssetDto } /** @@ -6997,6 +6810,13 @@ export interface AssetApiUploadFileRequest { */ readonly assetData: File + /** + * + * @type {string} + * @memberof AssetApiUploadFile + */ + readonly fileExtension: string + /** * * @type {string} @@ -7032,13 +6852,6 @@ export interface AssetApiUploadFileRequest { */ readonly isFavorite: boolean - /** - * - * @type {string} - * @memberof AssetApiUploadFile - */ - readonly fileExtension: string - /** * * @type {string} @@ -7060,6 +6873,13 @@ export interface AssetApiUploadFileRequest { */ readonly sidecarData?: File + /** + * + * @type {boolean} + * @memberof AssetApiUploadFile + */ + readonly isReadOnly?: boolean + /** * * @type {boolean} @@ -7089,17 +6909,6 @@ export interface AssetApiUploadFileRequest { * @extends {BaseAPI} */ export class AssetApi extends BaseAPI { - /** - * - * @param {AssetApiAddAssetsToSharedLinkRequest} requestParameters Request parameters. - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof AssetApi - */ - public addAssetsToSharedLink(requestParameters: AssetApiAddAssetsToSharedLinkRequest, options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).addAssetsToSharedLink(requestParameters.addAssetsDto, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); - } - /** * Checks if assets exist by checksums * @param {AssetApiBulkUploadCheckRequest} requestParameters Request parameters. @@ -7133,17 +6942,6 @@ export class AssetApi extends BaseAPI { return AssetApiFp(this.configuration).checkExistingAssets(requestParameters.checkExistingAssetsDto, options).then((request) => request(this.axios, this.basePath)); } - /** - * - * @param {AssetApiCreateAssetsSharedLinkRequest} requestParameters Request parameters. - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof AssetApi - */ - public createAssetsSharedLink(requestParameters: AssetApiCreateAssetsSharedLinkRequest, options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).createAssetsSharedLink(requestParameters.createAssetsShareLinkDto, options).then((request) => request(this.axios, this.basePath)); - } - /** * * @param {AssetApiDeleteAssetRequest} requestParameters Request parameters. @@ -7328,13 +7126,13 @@ export class AssetApi extends BaseAPI { /** * - * @param {AssetApiRemoveAssetsFromSharedLinkRequest} requestParameters Request parameters. + * @param {AssetApiImportFileRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof AssetApi */ - public removeAssetsFromSharedLink(requestParameters: AssetApiRemoveAssetsFromSharedLinkRequest, options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).removeAssetsFromSharedLink(requestParameters.removeAssetsDto, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); + public importFile(requestParameters: AssetApiImportFileRequest, options?: AxiosRequestConfig) { + return AssetApiFp(this.configuration).importFile(requestParameters.importAssetDto, options).then((request) => request(this.axios, this.basePath)); } /** @@ -7378,7 +7176,7 @@ export class AssetApi extends BaseAPI { * @memberof AssetApi */ public uploadFile(requestParameters: AssetApiUploadFileRequest, options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).uploadFile(requestParameters.assetType, requestParameters.assetData, requestParameters.deviceAssetId, requestParameters.deviceId, requestParameters.fileCreatedAt, requestParameters.fileModifiedAt, requestParameters.isFavorite, requestParameters.fileExtension, requestParameters.key, requestParameters.livePhotoData, requestParameters.sidecarData, requestParameters.isArchived, requestParameters.isVisible, requestParameters.duration, options).then((request) => request(this.axios, this.basePath)); + return AssetApiFp(this.configuration).uploadFile(requestParameters.assetType, requestParameters.assetData, requestParameters.fileExtension, requestParameters.deviceAssetId, requestParameters.deviceId, requestParameters.fileCreatedAt, requestParameters.fileModifiedAt, requestParameters.isFavorite, requestParameters.key, requestParameters.livePhotoData, requestParameters.sidecarData, requestParameters.isReadOnly, requestParameters.isArchived, requestParameters.isVisible, requestParameters.duration, options).then((request) => request(this.axios, this.basePath)); } } @@ -10115,18 +9913,115 @@ export class ServerInfoApi extends BaseAPI { /** - * ShareApi - axios parameter creator + * SharedLinkApi - axios parameter creator * @export */ -export const ShareApiAxiosParamCreator = function (configuration?: Configuration) { +export const SharedLinkApiAxiosParamCreator = function (configuration?: Configuration) { return { + /** + * + * @param {string} id + * @param {AssetIdsDto} assetIdsDto + * @param {string} [key] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + addSharedLinkAssets: async (id: string, assetIdsDto: AssetIdsDto, key?: string, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('addSharedLinkAssets', 'id', id) + // verify required parameter 'assetIdsDto' is not null or undefined + assertParamExists('addSharedLinkAssets', 'assetIdsDto', assetIdsDto) + const localVarPath = `/shared-link/{id}/assets` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication cookie required + + // authentication api_key required + await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + if (key !== undefined) { + localVarQueryParameter['key'] = key; + } + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(assetIdsDto, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {SharedLinkCreateDto} sharedLinkCreateDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createSharedLink: async (sharedLinkCreateDto: SharedLinkCreateDto, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'sharedLinkCreateDto' is not null or undefined + assertParamExists('createSharedLink', 'sharedLinkCreateDto', sharedLinkCreateDto) + const localVarPath = `/shared-link`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication cookie required + + // authentication api_key required + await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(sharedLinkCreateDto, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @param {*} [options] Override http request option. * @throws {RequiredError} */ getAllSharedLinks: async (options: AxiosRequestConfig = {}): Promise => { - const localVarPath = `/share`; + const localVarPath = `/shared-link`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; @@ -10165,7 +10060,7 @@ export const ShareApiAxiosParamCreator = function (configuration?: Configuration * @throws {RequiredError} */ getMySharedLink: async (key?: string, options: AxiosRequestConfig = {}): Promise => { - const localVarPath = `/share/me`; + const localVarPath = `/shared-link/me`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; @@ -10210,7 +10105,7 @@ export const ShareApiAxiosParamCreator = function (configuration?: Configuration getSharedLinkById: async (id: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'id' is not null or undefined assertParamExists('getSharedLinkById', 'id', id) - const localVarPath = `/share/{id}` + const localVarPath = `/shared-link/{id}` .replace(`{${"id"}}`, encodeURIComponent(String(id))); // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -10252,7 +10147,7 @@ export const ShareApiAxiosParamCreator = function (configuration?: Configuration removeSharedLink: async (id: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'id' is not null or undefined assertParamExists('removeSharedLink', 'id', id) - const localVarPath = `/share/{id}` + const localVarPath = `/shared-link/{id}` .replace(`{${"id"}}`, encodeURIComponent(String(id))); // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -10288,16 +10183,69 @@ export const ShareApiAxiosParamCreator = function (configuration?: Configuration /** * * @param {string} id - * @param {EditSharedLinkDto} editSharedLinkDto + * @param {AssetIdsDto} assetIdsDto + * @param {string} [key] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - updateSharedLink: async (id: string, editSharedLinkDto: EditSharedLinkDto, options: AxiosRequestConfig = {}): Promise => { + removeSharedLinkAssets: async (id: string, assetIdsDto: AssetIdsDto, key?: string, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('removeSharedLinkAssets', 'id', id) + // verify required parameter 'assetIdsDto' is not null or undefined + assertParamExists('removeSharedLinkAssets', 'assetIdsDto', assetIdsDto) + const localVarPath = `/shared-link/{id}/assets` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication cookie required + + // authentication api_key required + await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + if (key !== undefined) { + localVarQueryParameter['key'] = key; + } + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(assetIdsDto, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {string} id + * @param {SharedLinkEditDto} sharedLinkEditDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateSharedLink: async (id: string, sharedLinkEditDto: SharedLinkEditDto, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'id' is not null or undefined assertParamExists('updateSharedLink', 'id', id) - // verify required parameter 'editSharedLinkDto' is not null or undefined - assertParamExists('updateSharedLink', 'editSharedLinkDto', editSharedLinkDto) - const localVarPath = `/share/{id}` + // verify required parameter 'sharedLinkEditDto' is not null or undefined + assertParamExists('updateSharedLink', 'sharedLinkEditDto', sharedLinkEditDto) + const localVarPath = `/shared-link/{id}` .replace(`{${"id"}}`, encodeURIComponent(String(id))); // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -10326,7 +10274,7 @@ export const ShareApiAxiosParamCreator = function (configuration?: Configuration setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - localVarRequestOptions.data = serializeDataIfNeeded(editSharedLinkDto, localVarRequestOptions, configuration) + localVarRequestOptions.data = serializeDataIfNeeded(sharedLinkEditDto, localVarRequestOptions, configuration) return { url: toPathString(localVarUrlObj), @@ -10337,12 +10285,34 @@ export const ShareApiAxiosParamCreator = function (configuration?: Configuration }; /** - * ShareApi - functional programming interface + * SharedLinkApi - functional programming interface * @export */ -export const ShareApiFp = function(configuration?: Configuration) { - const localVarAxiosParamCreator = ShareApiAxiosParamCreator(configuration) +export const SharedLinkApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = SharedLinkApiAxiosParamCreator(configuration) return { + /** + * + * @param {string} id + * @param {AssetIdsDto} assetIdsDto + * @param {string} [key] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async addSharedLinkAssets(id: string, assetIdsDto: AssetIdsDto, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.addSharedLinkAssets(id, assetIdsDto, key, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + /** + * + * @param {SharedLinkCreateDto} sharedLinkCreateDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async createSharedLink(sharedLinkCreateDto: SharedLinkCreateDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.createSharedLink(sharedLinkCreateDto, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @param {*} [options] Override http request option. @@ -10385,24 +10355,56 @@ export const ShareApiFp = function(configuration?: Configuration) { /** * * @param {string} id - * @param {EditSharedLinkDto} editSharedLinkDto + * @param {AssetIdsDto} assetIdsDto + * @param {string} [key] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async updateSharedLink(id: string, editSharedLinkDto: EditSharedLinkDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.updateSharedLink(id, editSharedLinkDto, options); + async removeSharedLinkAssets(id: string, assetIdsDto: AssetIdsDto, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.removeSharedLinkAssets(id, assetIdsDto, key, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + /** + * + * @param {string} id + * @param {SharedLinkEditDto} sharedLinkEditDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updateSharedLink(id: string, sharedLinkEditDto: SharedLinkEditDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updateSharedLink(id, sharedLinkEditDto, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, } }; /** - * ShareApi - factory interface + * SharedLinkApi - factory interface * @export */ -export const ShareApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { - const localVarFp = ShareApiFp(configuration) +export const SharedLinkApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = SharedLinkApiFp(configuration) return { + /** + * + * @param {string} id + * @param {AssetIdsDto} assetIdsDto + * @param {string} [key] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + addSharedLinkAssets(id: string, assetIdsDto: AssetIdsDto, key?: string, options?: any): AxiosPromise> { + return localVarFp.addSharedLinkAssets(id, assetIdsDto, key, options).then((request) => request(axios, basePath)); + }, + /** + * + * @param {SharedLinkCreateDto} sharedLinkCreateDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createSharedLink(sharedLinkCreateDto: SharedLinkCreateDto, options?: any): AxiosPromise { + return localVarFp.createSharedLink(sharedLinkCreateDto, options).then((request) => request(axios, basePath)); + }, /** * * @param {*} [options] Override http request option. @@ -10441,138 +10443,252 @@ export const ShareApiFactory = function (configuration?: Configuration, basePath /** * * @param {string} id - * @param {EditSharedLinkDto} editSharedLinkDto + * @param {AssetIdsDto} assetIdsDto + * @param {string} [key] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - updateSharedLink(id: string, editSharedLinkDto: EditSharedLinkDto, options?: any): AxiosPromise { - return localVarFp.updateSharedLink(id, editSharedLinkDto, options).then((request) => request(axios, basePath)); + removeSharedLinkAssets(id: string, assetIdsDto: AssetIdsDto, key?: string, options?: any): AxiosPromise> { + return localVarFp.removeSharedLinkAssets(id, assetIdsDto, key, options).then((request) => request(axios, basePath)); + }, + /** + * + * @param {string} id + * @param {SharedLinkEditDto} sharedLinkEditDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateSharedLink(id: string, sharedLinkEditDto: SharedLinkEditDto, options?: any): AxiosPromise { + return localVarFp.updateSharedLink(id, sharedLinkEditDto, options).then((request) => request(axios, basePath)); }, }; }; /** - * Request parameters for getMySharedLink operation in ShareApi. + * Request parameters for addSharedLinkAssets operation in SharedLinkApi. * @export - * @interface ShareApiGetMySharedLinkRequest + * @interface SharedLinkApiAddSharedLinkAssetsRequest */ -export interface ShareApiGetMySharedLinkRequest { +export interface SharedLinkApiAddSharedLinkAssetsRequest { /** * * @type {string} - * @memberof ShareApiGetMySharedLink + * @memberof SharedLinkApiAddSharedLinkAssets + */ + readonly id: string + + /** + * + * @type {AssetIdsDto} + * @memberof SharedLinkApiAddSharedLinkAssets + */ + readonly assetIdsDto: AssetIdsDto + + /** + * + * @type {string} + * @memberof SharedLinkApiAddSharedLinkAssets */ readonly key?: string } /** - * Request parameters for getSharedLinkById operation in ShareApi. + * Request parameters for createSharedLink operation in SharedLinkApi. * @export - * @interface ShareApiGetSharedLinkByIdRequest + * @interface SharedLinkApiCreateSharedLinkRequest */ -export interface ShareApiGetSharedLinkByIdRequest { +export interface SharedLinkApiCreateSharedLinkRequest { + /** + * + * @type {SharedLinkCreateDto} + * @memberof SharedLinkApiCreateSharedLink + */ + readonly sharedLinkCreateDto: SharedLinkCreateDto +} + +/** + * Request parameters for getMySharedLink operation in SharedLinkApi. + * @export + * @interface SharedLinkApiGetMySharedLinkRequest + */ +export interface SharedLinkApiGetMySharedLinkRequest { /** * * @type {string} - * @memberof ShareApiGetSharedLinkById + * @memberof SharedLinkApiGetMySharedLink + */ + readonly key?: string +} + +/** + * Request parameters for getSharedLinkById operation in SharedLinkApi. + * @export + * @interface SharedLinkApiGetSharedLinkByIdRequest + */ +export interface SharedLinkApiGetSharedLinkByIdRequest { + /** + * + * @type {string} + * @memberof SharedLinkApiGetSharedLinkById */ readonly id: string } /** - * Request parameters for removeSharedLink operation in ShareApi. + * Request parameters for removeSharedLink operation in SharedLinkApi. * @export - * @interface ShareApiRemoveSharedLinkRequest + * @interface SharedLinkApiRemoveSharedLinkRequest */ -export interface ShareApiRemoveSharedLinkRequest { +export interface SharedLinkApiRemoveSharedLinkRequest { /** * * @type {string} - * @memberof ShareApiRemoveSharedLink + * @memberof SharedLinkApiRemoveSharedLink */ readonly id: string } /** - * Request parameters for updateSharedLink operation in ShareApi. + * Request parameters for removeSharedLinkAssets operation in SharedLinkApi. * @export - * @interface ShareApiUpdateSharedLinkRequest + * @interface SharedLinkApiRemoveSharedLinkAssetsRequest */ -export interface ShareApiUpdateSharedLinkRequest { +export interface SharedLinkApiRemoveSharedLinkAssetsRequest { /** * * @type {string} - * @memberof ShareApiUpdateSharedLink + * @memberof SharedLinkApiRemoveSharedLinkAssets */ readonly id: string /** * - * @type {EditSharedLinkDto} - * @memberof ShareApiUpdateSharedLink + * @type {AssetIdsDto} + * @memberof SharedLinkApiRemoveSharedLinkAssets */ - readonly editSharedLinkDto: EditSharedLinkDto + readonly assetIdsDto: AssetIdsDto + + /** + * + * @type {string} + * @memberof SharedLinkApiRemoveSharedLinkAssets + */ + readonly key?: string } /** - * ShareApi - object-oriented interface + * Request parameters for updateSharedLink operation in SharedLinkApi. * @export - * @class ShareApi + * @interface SharedLinkApiUpdateSharedLinkRequest + */ +export interface SharedLinkApiUpdateSharedLinkRequest { + /** + * + * @type {string} + * @memberof SharedLinkApiUpdateSharedLink + */ + readonly id: string + + /** + * + * @type {SharedLinkEditDto} + * @memberof SharedLinkApiUpdateSharedLink + */ + readonly sharedLinkEditDto: SharedLinkEditDto +} + +/** + * SharedLinkApi - object-oriented interface + * @export + * @class SharedLinkApi * @extends {BaseAPI} */ -export class ShareApi extends BaseAPI { +export class SharedLinkApi extends BaseAPI { + /** + * + * @param {SharedLinkApiAddSharedLinkAssetsRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof SharedLinkApi + */ + public addSharedLinkAssets(requestParameters: SharedLinkApiAddSharedLinkAssetsRequest, options?: AxiosRequestConfig) { + return SharedLinkApiFp(this.configuration).addSharedLinkAssets(requestParameters.id, requestParameters.assetIdsDto, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @param {SharedLinkApiCreateSharedLinkRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof SharedLinkApi + */ + public createSharedLink(requestParameters: SharedLinkApiCreateSharedLinkRequest, options?: AxiosRequestConfig) { + return SharedLinkApiFp(this.configuration).createSharedLink(requestParameters.sharedLinkCreateDto, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @param {*} [options] Override http request option. * @throws {RequiredError} - * @memberof ShareApi + * @memberof SharedLinkApi */ public getAllSharedLinks(options?: AxiosRequestConfig) { - return ShareApiFp(this.configuration).getAllSharedLinks(options).then((request) => request(this.axios, this.basePath)); + return SharedLinkApiFp(this.configuration).getAllSharedLinks(options).then((request) => request(this.axios, this.basePath)); } /** * - * @param {ShareApiGetMySharedLinkRequest} requestParameters Request parameters. + * @param {SharedLinkApiGetMySharedLinkRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} - * @memberof ShareApi + * @memberof SharedLinkApi */ - public getMySharedLink(requestParameters: ShareApiGetMySharedLinkRequest = {}, options?: AxiosRequestConfig) { - return ShareApiFp(this.configuration).getMySharedLink(requestParameters.key, options).then((request) => request(this.axios, this.basePath)); + public getMySharedLink(requestParameters: SharedLinkApiGetMySharedLinkRequest = {}, options?: AxiosRequestConfig) { + return SharedLinkApiFp(this.configuration).getMySharedLink(requestParameters.key, options).then((request) => request(this.axios, this.basePath)); } /** * - * @param {ShareApiGetSharedLinkByIdRequest} requestParameters Request parameters. + * @param {SharedLinkApiGetSharedLinkByIdRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} - * @memberof ShareApi + * @memberof SharedLinkApi */ - public getSharedLinkById(requestParameters: ShareApiGetSharedLinkByIdRequest, options?: AxiosRequestConfig) { - return ShareApiFp(this.configuration).getSharedLinkById(requestParameters.id, options).then((request) => request(this.axios, this.basePath)); + public getSharedLinkById(requestParameters: SharedLinkApiGetSharedLinkByIdRequest, options?: AxiosRequestConfig) { + return SharedLinkApiFp(this.configuration).getSharedLinkById(requestParameters.id, options).then((request) => request(this.axios, this.basePath)); } /** * - * @param {ShareApiRemoveSharedLinkRequest} requestParameters Request parameters. + * @param {SharedLinkApiRemoveSharedLinkRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} - * @memberof ShareApi + * @memberof SharedLinkApi */ - public removeSharedLink(requestParameters: ShareApiRemoveSharedLinkRequest, options?: AxiosRequestConfig) { - return ShareApiFp(this.configuration).removeSharedLink(requestParameters.id, options).then((request) => request(this.axios, this.basePath)); + public removeSharedLink(requestParameters: SharedLinkApiRemoveSharedLinkRequest, options?: AxiosRequestConfig) { + return SharedLinkApiFp(this.configuration).removeSharedLink(requestParameters.id, options).then((request) => request(this.axios, this.basePath)); } /** * - * @param {ShareApiUpdateSharedLinkRequest} requestParameters Request parameters. + * @param {SharedLinkApiRemoveSharedLinkAssetsRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} - * @memberof ShareApi + * @memberof SharedLinkApi */ - public updateSharedLink(requestParameters: ShareApiUpdateSharedLinkRequest, options?: AxiosRequestConfig) { - return ShareApiFp(this.configuration).updateSharedLink(requestParameters.id, requestParameters.editSharedLinkDto, options).then((request) => request(this.axios, this.basePath)); + public removeSharedLinkAssets(requestParameters: SharedLinkApiRemoveSharedLinkAssetsRequest, options?: AxiosRequestConfig) { + return SharedLinkApiFp(this.configuration).removeSharedLinkAssets(requestParameters.id, requestParameters.assetIdsDto, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @param {SharedLinkApiUpdateSharedLinkRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof SharedLinkApi + */ + public updateSharedLink(requestParameters: SharedLinkApiUpdateSharedLinkRequest, options?: AxiosRequestConfig) { + return SharedLinkApiFp(this.configuration).updateSharedLink(requestParameters.id, requestParameters.sharedLinkEditDto, options).then((request) => request(this.axios, this.basePath)); } } diff --git a/web/src/lib/components/album-page/album-card.svelte b/web/src/lib/components/album-page/album-card.svelte index 5900abb03..17ec88676 100644 --- a/web/src/lib/components/album-page/album-card.svelte +++ b/web/src/lib/components/album-page/album-card.svelte @@ -10,6 +10,8 @@ export let album: AlbumResponseDto; export let isSharingView = false; export let user: UserResponseDto; + export let showItemCount = true; + export let showContextMenu = true; $: imageData = album.albumThumbnailAssetId ? api.getAssetThumbnailUrl(album.albumThumbnailAssetId, ThumbnailFormat.Webp) @@ -63,7 +65,7 @@ data-testid="album-card" > - {#if !isSharingView} + {#if showContextMenu}
-

- {album.assetCount.toLocaleString($locale)} - {album.assetCount == 1 ? `item` : `items`} -

+ {#if showItemCount} +

+ {album.assetCount.toLocaleString($locale)} + {album.assetCount == 1 ? `item` : `items`} +

+ {/if} {#if isSharingView || album.shared}

ยท

diff --git a/web/src/lib/components/album-page/album-viewer.svelte b/web/src/lib/components/album-page/album-viewer.svelte index aa4091e03..2223fcbd2 100644 --- a/web/src/lib/components/album-page/album-viewer.svelte +++ b/web/src/lib/components/album-page/album-viewer.svelte @@ -89,6 +89,10 @@ if (from?.url.pathname === '/sharing' && album.sharedUsers.length === 0) { isCreatingSharedAlbum = true; } + + if (from?.route.id === '/(user)/search') { + backUrl = from.url.href; + } }); const albumDateFormat: Intl.DateTimeFormatOptions = { diff --git a/web/src/lib/components/album-page/user-selection-modal.svelte b/web/src/lib/components/album-page/user-selection-modal.svelte index 452443c1b..207b9e440 100644 --- a/web/src/lib/components/album-page/user-selection-modal.svelte +++ b/web/src/lib/components/album-page/user-selection-modal.svelte @@ -31,7 +31,7 @@ }); const getSharedLinks = async () => { - const { data } = await api.shareApi.getAllSharedLinks(); + const { data } = await api.sharedLinkApi.getAllSharedLinks(); sharedLinks = data.filter((link) => link.album?.id === album.id); }; diff --git a/web/src/lib/components/asset-viewer/video-viewer.svelte b/web/src/lib/components/asset-viewer/video-viewer.svelte index c931100c2..801768dd8 100644 --- a/web/src/lib/components/asset-viewer/video-viewer.svelte +++ b/web/src/lib/components/asset-viewer/video-viewer.svelte @@ -2,6 +2,7 @@ import { api } from '@api'; import { fade } from 'svelte/transition'; import { createEventDispatcher } from 'svelte'; + import { videoViewerVolume } from '$lib/stores/preferences.store'; import LoadingSpinner from '../shared-components/loading-spinner.svelte'; export let assetId: string; @@ -30,6 +31,7 @@ class="h-full object-contain" on:canplay={handleCanPlay} on:ended={() => dispatch('onVideoEnded')} + bind:volume={$videoViewerVolume} > diff --git a/web/src/lib/components/elements/dropdown.svelte b/web/src/lib/components/elements/dropdown.svelte new file mode 100644 index 000000000..4afc4acbb --- /dev/null +++ b/web/src/lib/components/elements/dropdown.svelte @@ -0,0 +1,62 @@ + + + diff --git a/web/src/lib/components/forms/edit-user-form.svelte b/web/src/lib/components/forms/edit-user-form.svelte index 7ec5f0570..3aedf0f28 100644 --- a/web/src/lib/components/forms/edit-user-form.svelte +++ b/web/src/lib/components/forms/edit-user-form.svelte @@ -19,14 +19,15 @@ const editUser = async () => { try { - const { id, email, firstName, lastName, storageLabel } = user; + const { id, email, firstName, lastName, storageLabel, externalPath } = user; const { status } = await api.userApi.updateUser({ updateUserDto: { id, email, firstName, lastName, - storageLabel: storageLabel || '' + storageLabel: storageLabel || '', + externalPath: externalPath || '' } }); @@ -131,6 +132,22 @@

+
+ + + +

+ Note: Absolute path of parent import directory. A user can only import files if they exist + at or under this path. +

+
+ {#if error}

{error}

{/if} diff --git a/web/src/lib/components/memory-page/memory-viewer.svelte b/web/src/lib/components/memory-page/memory-viewer.svelte index 23876578e..5ddb09c10 100644 --- a/web/src/lib/components/memory-page/memory-viewer.svelte +++ b/web/src/lib/components/memory-page/memory-viewer.svelte @@ -34,7 +34,8 @@ $: currentAsset = currentMemory?.assets[assetIndex]; $: nextAsset = currentMemory?.assets[assetIndex + 1]; - $: canAdvance = !!(nextMemory || nextAsset); + $: canGoForward = !!(nextMemory || nextAsset); + $: canGoBack = !!(previousMemory || previousAsset); const toNextMemory = () => goto(`?memory=${memoryIndex + 1}`); const toPreviousMemory = () => goto(`?memory=${memoryIndex - 1}`); @@ -61,7 +62,7 @@ $: paused ? pause() : play(); // Progress should be paused when it's no longer possible to advance. - $: paused ||= !canAdvance; + $: paused ||= !canGoForward; // Advance to the next asset or memory when progress is complete. $: $progress === 1 && toNext(); @@ -72,6 +73,19 @@ // Progress should be reset when the current memory or asset changes. $: memoryIndex, assetIndex, reset(); + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'ArrowRight' && canGoForward) { + e.preventDefault(); + toNext(); + } else if (e.key === 'ArrowLeft' && canGoBack) { + e.preventDefault(); + toPrevious(); + } else if (e.key === 'Escape') { + e.preventDefault(); + goto(AppRoute.PHOTOS); + } + }; + onMount(async () => { if (!$memoryStore) { const { data } = await api.assetApi.getMemoryLane({ @@ -86,6 +100,8 @@ let galleryInView = false; + +
{#if currentMemory} goto(AppRoute.PHOTOS)} forceDark> @@ -190,7 +206,7 @@
- {#if previousMemory || previousAsset} + {#if canGoBack}
- {#if canAdvance} + {#if canGoForward} import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; - import { AssetResponseDto, SharedLinkResponseDto, api } from '@api'; + import { SharedLinkResponseDto, api } from '@api'; import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte'; + import ConfirmDialogue from '../../shared-components/confirm-dialogue.svelte'; import { getAssetControlContext } from '../asset-select-control-bar.svelte'; + import { + NotificationType, + notificationController + } from '../../shared-components/notification/notification'; + import { handleError } from '../../../utils/handle-error'; export let sharedLink: SharedLinkResponseDto; - export let allAssets: AssetResponseDto[]; + + let removing = false; const { getAssets, clearSelect } = getAssetControlContext(); - const handleRemoveAssetsFromSharedLink = async () => { - if (window.confirm('Do you want to remove selected assets from the shared link?')) { - // TODO: Rename API method or change functionality. The assetIds passed - // in are kept instead of removed. - const assetsToKeep = allAssets.filter((a) => !getAssets().has(a)); - await api.assetApi.removeAssetsFromSharedLink({ - removeAssetsDto: { - assetIds: assetsToKeep.map((a) => a.id) + const handleRemove = async () => { + try { + const { data: results } = await api.sharedLinkApi.removeSharedLinkAssets({ + id: sharedLink.id, + assetIdsDto: { + assetIds: Array.from(getAssets()).map((asset) => asset.id) }, - key: sharedLink?.key + key: sharedLink.key + }); + + for (const result of results) { + if (!result.success) { + continue; + } + + sharedLink.assets = sharedLink.assets.filter((asset) => asset.id !== result.assetId); + } + + const count = results.filter((item) => item.success).length; + + notificationController.show({ + type: NotificationType.Info, + message: `Removed ${count} assets` }); - sharedLink.assets = assetsToKeep; clearSelect(); + } catch (error) { + handleError(error, 'Unable to remove assets from shared link'); } }; (removing = true)} logo={DeleteOutline} /> + +{#if removing} + handleRemove()} + on:cancel={() => (removing = false)} + /> +{/if} diff --git a/web/src/lib/components/share-page/individual-shared-viewer.svelte b/web/src/lib/components/share-page/individual-shared-viewer.svelte index 5ba129705..865747584 100644 --- a/web/src/lib/components/share-page/individual-shared-viewer.svelte +++ b/web/src/lib/components/share-page/individual-shared-viewer.svelte @@ -17,6 +17,7 @@ notificationController, NotificationType } from '../shared-components/notification/notification'; + import { handleError } from '../../utils/handle-error'; export let sharedLink: SharedLinkResponseDto; export let isOwned: boolean; @@ -26,43 +27,40 @@ $: assets = sharedLink.assets; $: isMultiSelectionMode = selectedAssets.size > 0; - const clearMultiSelectAssetAssetHandler = () => { - selectedAssets = new Set(); - }; - const downloadAssets = async () => { - await bulkDownload('immich-shared', assets, undefined, sharedLink?.key); + await bulkDownload('immich-shared', assets, undefined, sharedLink.key); }; const handleUploadAssets = async () => { try { - const results = await openFileUploadDialog(undefined, sharedLink?.key); + const results = await openFileUploadDialog(undefined, sharedLink.key); - const assetIds = results.filter((id) => !!id) as string[]; - - await api.assetApi.addAssetsToSharedLink({ - addAssetsDto: { - assetIds + const { data } = await api.sharedLinkApi.addSharedLinkAssets({ + id: sharedLink.id, + assetIdsDto: { + assetIds: results.filter((id) => !!id) as string[] }, - key: sharedLink?.key + key: sharedLink.key }); + const added = data.filter((item) => item.success).length; + notificationController.show({ - message: `Successfully add ${assetIds.length} to the shared link`, + message: `Added ${added} assets`, type: NotificationType.Info }); } catch (e) { - console.error('handleUploadAssets', e); + handleError(e, 'Unable to add assets to shared link'); } };
{#if isMultiSelectionMode} - + (selectedAssets = new Set())}> {#if isOwned} - + {/if} {:else} diff --git a/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte b/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte index 32fb22d08..236e7e40f 100644 --- a/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte +++ b/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte @@ -7,31 +7,31 @@ import { handleError } from '$lib/utils/handle-error'; import { AlbumResponseDto, + api, AssetResponseDto, SharedLinkResponseDto, - SharedLinkType, - api + SharedLinkType } from '@api'; import { createEventDispatcher, onMount } from 'svelte'; import Link from 'svelte-material-icons/Link.svelte'; import BaseModal from '../base-modal.svelte'; import type { ImmichDropDownOption } from '../dropdown-button.svelte'; import DropdownButton from '../dropdown-button.svelte'; - import { NotificationType, notificationController } from '../notification/notification'; + import { notificationController, NotificationType } from '../notification/notification'; export let shareType: SharedLinkType; export let sharedAssets: AssetResponseDto[] = []; export let album: AlbumResponseDto | undefined = undefined; export let editingLink: SharedLinkResponseDto | undefined = undefined; - let isShowSharedLink = false; - let expirationTime = ''; - let isAllowUpload = false; - let sharedLink = ''; + let sharedLink: string | null = null; let description = ''; + let allowDownload = true; + let allowUpload = false; + let showExif = true; + let expirationTime = ''; let shouldChangeExpirationTime = false; - let isAllowDownload = true; - let shouldShowExif = true; + const dispatch = createEventDispatcher(); const expiredDateOption: ImmichDropDownOption = { @@ -44,9 +44,9 @@ if (editingLink.description) { description = editingLink.description; } - isAllowUpload = editingLink.allowUpload; - isAllowDownload = editingLink.allowDownload; - shouldShowExif = editingLink.showExif; + allowUpload = editingLink.allowUpload; + allowDownload = editingLink.allowDownload; + showExif = editingLink.showExif; } }); @@ -58,49 +58,32 @@ : undefined; try { - if (shareType === SharedLinkType.Album && album) { - const { data } = await api.albumApi.createAlbumSharedLink({ - createAlbumShareLinkDto: { - albumId: album.id, - expiresAt: expirationDate, - allowUpload: isAllowUpload, - description: description, - allowDownload: isAllowDownload, - showExif: shouldShowExif - } - }); - buildSharedLink(data); - } else { - const { data } = await api.assetApi.createAssetsSharedLink({ - createAssetsShareLinkDto: { - assetIds: sharedAssets.map((a) => a.id), - expiresAt: expirationDate, - allowUpload: isAllowUpload, - description: description, - allowDownload: isAllowDownload, - showExif: shouldShowExif - } - }); - buildSharedLink(data); - } + const { data } = await api.sharedLinkApi.createSharedLink({ + sharedLinkCreateDto: { + type: shareType, + albumId: album ? album.id : undefined, + assetIds: sharedAssets.map((a) => a.id), + expiresAt: expirationDate, + allowUpload, + description, + allowDownload, + showExif + } + }); + sharedLink = `${window.location.origin}/share/${data.key}`; } catch (e) { handleError(e, 'Failed to create shared link'); } - - isShowSharedLink = true; - }; - - const buildSharedLink = (createdLink: SharedLinkResponseDto) => { - sharedLink = `${window.location.origin}/share/${createdLink.key}`; }; const handleCopy = async () => { + if (!sharedLink) { + return; + } + try { await navigator.clipboard.writeText(sharedLink); - notificationController.show({ - message: 'Copied to clipboard!', - type: NotificationType.Info - }); + notificationController.show({ message: 'Copied to clipboard!', type: NotificationType.Info }); } catch (e) { handleError( e, @@ -129,34 +112,36 @@ }; const handleEditLink = async () => { - if (editingLink) { - try { - const expirationTime = getExpirationTimeInMillisecond(); - const currentTime = new Date().getTime(); - const expirationDate: string | null = expirationTime - ? new Date(currentTime + expirationTime).toISOString() - : null; + if (!editingLink) { + return; + } - await api.shareApi.updateSharedLink({ - id: editingLink.id, - editSharedLinkDto: { - description, - expiresAt: shouldChangeExpirationTime ? expirationDate : undefined, - allowUpload: isAllowUpload, - allowDownload: isAllowDownload, - showExif: shouldShowExif - } - }); + try { + const expirationTime = getExpirationTimeInMillisecond(); + const currentTime = new Date().getTime(); + const expirationDate: string | null = expirationTime + ? new Date(currentTime + expirationTime).toISOString() + : null; - notificationController.show({ - type: NotificationType.Info, - message: 'Edited' - }); + await api.sharedLinkApi.updateSharedLink({ + id: editingLink.id, + sharedLinkEditDto: { + description, + expiresAt: shouldChangeExpirationTime ? expirationDate : undefined, + allowUpload: allowUpload, + allowDownload: allowDownload, + showExif: showExif + } + }); - dispatch('close'); - } catch (e) { - handleError(e, 'Failed to edit shared link'); - } + notificationController.show({ + type: NotificationType.Info, + message: 'Edited' + }); + + dispatch('close'); + } catch (e) { + handleError(e, 'Failed to edit shared link'); } }; @@ -212,15 +197,15 @@
- +
- +
- +
@@ -248,7 +233,7 @@
- {#if !isShowSharedLink} + {#if !sharedLink} {#if editingLink}
@@ -258,9 +243,7 @@
{/if} - {/if} - - {#if isShowSharedLink} + {:else}
diff --git a/web/src/lib/components/shared-components/search-bar/search-bar.svelte b/web/src/lib/components/shared-components/search-bar/search-bar.svelte index 61e6c3e62..8726da558 100644 --- a/web/src/lib/components/shared-components/search-bar/search-bar.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-bar.svelte @@ -94,7 +94,7 @@

Smart search is enabled by default, to search for metadata use the syntax m:your-search-term

diff --git a/web/src/lib/components/user-settings-page/user-profile-settings.svelte b/web/src/lib/components/user-settings-page/user-profile-settings.svelte index 3fc770de3..847e517db 100644 --- a/web/src/lib/components/user-settings-page/user-profile-settings.svelte +++ b/web/src/lib/components/user-settings-page/user-profile-settings.svelte @@ -75,6 +75,14 @@ required={false} /> + +
diff --git a/web/src/lib/stores/preferences.store.ts b/web/src/lib/stores/preferences.store.ts index b0f491a3a..e65c9c68b 100644 --- a/web/src/lib/stores/preferences.store.ts +++ b/web/src/lib/stores/preferences.store.ts @@ -35,3 +35,5 @@ export const mapSettings = persisted('map-settings', { dateAfter: '', dateBefore: '' }); + +export const videoViewerVolume = persisted('video-viewer-volume', 1, {}); diff --git a/web/src/lib/utils/asset-utils.spec.ts b/web/src/lib/utils/asset-utils.spec.ts index ebd82eb8b..e458bf03d 100644 --- a/web/src/lib/utils/asset-utils.spec.ts +++ b/web/src/lib/utils/asset-utils.spec.ts @@ -60,20 +60,58 @@ describe('get asset filename', () => { }); describe('get file mime type', () => { - for (const { extension, mimeType } of [ - { extension: '3gp', mimeType: 'video/3gpp' }, - { extension: 'arw', mimeType: 'image/x-sony-arw' }, - { extension: 'dng', mimeType: 'image/dng' }, - { extension: 'heic', mimeType: 'image/heic' }, - { extension: 'heif', mimeType: 'image/heif' }, - { extension: 'insp', mimeType: 'image/jpeg' }, - { extension: 'insv', mimeType: 'video/mp4' }, - { extension: 'nef', mimeType: 'image/x-nikon-nef' }, - { extension: 'raf', mimeType: 'image/x-fuji-raf' }, - { extension: 'srw', mimeType: 'image/x-samsung-srw' } + for (const { mimetype, extension } of [ + { mimetype: 'image/avif', extension: 'avif' }, + { mimetype: 'image/gif', extension: 'gif' }, + { mimetype: 'image/heic', extension: 'heic' }, + { mimetype: 'image/heif', extension: 'heif' }, + { mimetype: 'image/jpeg', extension: 'jpeg' }, + { mimetype: 'image/jpeg', extension: 'jpg' }, + { mimetype: 'image/jxl', extension: 'jxl' }, + { mimetype: 'image/png', extension: 'png' }, + { mimetype: 'image/tiff', extension: 'tiff' }, + { mimetype: 'image/webp', extension: 'webp' }, + { mimetype: 'image/x-adobe-dng', extension: 'dng' }, + { mimetype: 'image/x-arriflex-ari', extension: 'ari' }, + { mimetype: 'image/x-canon-cr2', extension: 'cr2' }, + { mimetype: 'image/x-canon-cr3', extension: 'cr3' }, + { mimetype: 'image/x-canon-crw', extension: 'crw' }, + { mimetype: 'image/x-epson-erf', extension: 'erf' }, + { mimetype: 'image/x-fuji-raf', extension: 'raf' }, + { mimetype: 'image/x-hasselblad-3fr', extension: '3fr' }, + { mimetype: 'image/x-hasselblad-fff', extension: 'fff' }, + { mimetype: 'image/x-kodak-dcr', extension: 'dcr' }, + { mimetype: 'image/x-kodak-k25', extension: 'k25' }, + { mimetype: 'image/x-kodak-kdc', extension: 'kdc' }, + { mimetype: 'image/x-leica-rwl', extension: 'rwl' }, + { mimetype: 'image/x-minolta-mrw', extension: 'mrw' }, + { mimetype: 'image/x-nikon-nef', extension: 'nef' }, + { mimetype: 'image/x-olympus-orf', extension: 'orf' }, + { mimetype: 'image/x-olympus-ori', extension: 'ori' }, + { mimetype: 'image/x-panasonic-raw', extension: 'raw' }, + { mimetype: 'image/x-pentax-pef', extension: 'pef' }, + { mimetype: 'image/x-phantom-cin', extension: 'cin' }, + { mimetype: 'image/x-phaseone-cap', extension: 'cap' }, + { mimetype: 'image/x-phaseone-iiq', extension: 'iiq' }, + { mimetype: 'image/x-samsung-srw', extension: 'srw' }, + { mimetype: 'image/x-sigma-x3f', extension: 'x3f' }, + { mimetype: 'image/x-sony-arw', extension: 'arw' }, + { mimetype: 'image/x-sony-sr2', extension: 'sr2' }, + { mimetype: 'image/x-sony-srf', extension: 'srf' }, + { mimetype: 'video/3gpp', extension: '3gp' }, + { mimetype: 'video/mp2t', extension: 'm2ts' }, + { mimetype: 'video/mp2t', extension: 'mts' }, + { mimetype: 'video/mp4', extension: 'mp4' }, + { mimetype: 'video/mpeg', extension: 'mpg' }, + { mimetype: 'video/quicktime', extension: 'mov' }, + { mimetype: 'video/webm', extension: 'webm' }, + { mimetype: 'video/x-flv', extension: 'flv' }, + { mimetype: 'video/x-matroska', extension: 'mkv' }, + { mimetype: 'video/x-ms-wmv', extension: 'wmv' }, + { mimetype: 'video/x-msvideo', extension: 'avi' } ]) { it(`returns the mime type for ${extension}`, () => { - expect(getFileMimeType({ name: `filename.${extension}` } as File)).toEqual(mimeType); + expect(getFileMimeType({ name: `filename.${extension}` } as File)).toEqual(mimetype); }); } diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts index bba879bb3..7d0b9a7a3 100644 --- a/web/src/lib/utils/asset-utils.ts +++ b/web/src/lib/utils/asset-utils.ts @@ -126,39 +126,56 @@ export function getAssetFilename(asset: AssetResponseDto): string { */ export function getFileMimeType(file: File): string { const mimeTypes: Record = { + '3fr': 'image/x-hasselblad-3fr', '3gp': 'video/3gpp', + ari: 'image/x-arriflex-ari', arw: 'image/x-sony-arw', - dng: 'image/dng', - heic: 'image/heic', - heif: 'image/heif', + avi: 'video/x-msvideo', avif: 'image/avif', - insp: 'image/jpeg', - insv: 'video/mp4', - nef: 'image/x-nikon-nef', - raf: 'image/x-fuji-raf', - srw: 'image/x-samsung-srw', - crw: 'image/x-canon-crw', + cap: 'image/x-phaseone-cap', + cin: 'image/x-phantom-cin', cr2: 'image/x-canon-cr2', cr3: 'image/x-canon-cr3', - erf: 'image/x-epson-erf', + crw: 'image/x-canon-crw', dcr: 'image/x-kodak-dcr', + dng: 'image/x-adobe-dng', + erf: 'image/x-epson-erf', + fff: 'image/x-hasselblad-fff', + flv: 'video/x-flv', + gif: 'image/gif', + heic: 'image/heic', + heif: 'image/heif', + iiq: 'image/x-phaseone-iiq', + insp: 'image/jpeg', + insv: 'video/mp4', + jpeg: 'image/jpeg', + jpg: 'image/jpeg', + jxl: 'image/jxl', k25: 'image/x-kodak-k25', kdc: 'image/x-kodak-kdc', + m2ts: 'video/mp2t', + mkv: 'video/x-matroska', + mov: 'video/quicktime', + mp4: 'video/mp4', + mpg: 'video/mpeg', mrw: 'image/x-minolta-mrw', + mts: 'video/mp2t', + nef: 'image/x-nikon-nef', orf: 'image/x-olympus-orf', - raw: 'image/x-panasonic-raw', - pef: 'image/x-pentax-pef', - x3f: 'image/x-sigma-x3f', - srf: 'image/x-sony-srf', - sr2: 'image/x-sony-sr2', - '3fr': 'image/x-hasselblad-3fr', - fff: 'image/x-hasselblad-fff', - rwl: 'image/x-leica-rwl', ori: 'image/x-olympus-ori', - iiq: 'image/x-phaseone-iiq', - ari: 'image/x-arriflex-ari', - cap: 'image/x-phaseone-cap', - cin: 'image/x-phantom-cin' + pef: 'image/x-pentax-pef', + png: 'image/png', + raf: 'image/x-fuji-raf', + raw: 'image/x-panasonic-raw', + rwl: 'image/x-leica-rwl', + sr2: 'image/x-sony-sr2', + srf: 'image/x-sony-srf', + srw: 'image/x-samsung-srw', + tiff: 'image/tiff', + webm: 'video/webm', + webp: 'image/webp', + wmv: 'video/x-ms-wmv', + x3f: 'image/x-sigma-x3f' }; // Return the MIME type determined by the browser or the MIME type based on the file extension. return file.type || (mimeTypes[getFilenameExtension(file.name)] ?? ''); diff --git a/web/src/lib/utils/file-uploader.ts b/web/src/lib/utils/file-uploader.ts index 0e8e289b0..908aec970 100644 --- a/web/src/lib/utils/file-uploader.ts +++ b/web/src/lib/utils/file-uploader.ts @@ -22,8 +22,48 @@ export const openFileUploadDialog = async ( // When adding a content type that is unsupported by browsers, make sure // to also add it to getFileMimeType() otherwise the upload will fail. - fileSelector.accept = - 'image/*,video/*,.heic,.heif,.avif,.dng,.3gp,.nef,.srw,.crw,.cr2,.cr3,.raf,.insp,.insv,.arw,.erf,.raf,.dcr,.k25,.kdc,.mrw,.orf,.raw,.pef,.x3f,.srf,.sr2,.3fr,.fff,.rwl,.ori,.iiq,.ari,.cap,.cin'; + fileSelector.accept = [ + 'image/*', + 'video/*', + '.3fr', + '.3gp', + '.ari', + '.arw', + '.avif', + '.cap', + '.cin', + '.cr2', + '.cr3', + '.crw', + '.dcr', + '.dng', + '.erf', + '.fff', + '.heic', + '.heif', + '.iiq', + '.insp', + '.insv', + '.jxl', + '.k25', + '.kdc', + '.m2ts', + '.mov', + '.mrw', + '.mts', + '.nef', + '.orf', + '.ori', + '.pef', + '.raf', + '.raf', + '.raw', + '.rwl', + '.sr2', + '.srf', + '.srw', + '.x3f' + ].join(','); fileSelector.onchange = async (e: Event) => { const target = e.target as HTMLInputElement; diff --git a/web/src/routes/(user)/albums/+page.svelte b/web/src/routes/(user)/albums/+page.svelte index da7acc74b..bbee57801 100644 --- a/web/src/routes/(user)/albums/+page.svelte +++ b/web/src/routes/(user)/albums/+page.svelte @@ -12,11 +12,15 @@ import LinkButton from '$lib/components/elements/buttons/link-button.svelte'; import { onMount } from 'svelte'; import { flip } from 'svelte/animate'; + import Dropdown from '$lib/components/elements/dropdown.svelte'; export let data: PageData; + const sortByOptions = ['Most recent photo', 'Last modified', 'Album title']; + let selectedSortBy = sortByOptions[0]; + const { - albums, + albums: unsortedAlbums, isShowContextMenu, contextMenuPosition, createAlbum, @@ -26,6 +30,28 @@ closeAlbumContextMenu } = useAlbums({ albums: data.albums }); + let albums = unsortedAlbums; + + const sortByDate = (a: string, b: string) => { + const aDate = new Date(a); + const bDate = new Date(b); + return bDate.getTime() - aDate.getTime(); + }; + + $: { + if (selectedSortBy === 'Most recent photo') { + $albums = $unsortedAlbums.sort((a, b) => + a.lastModifiedAssetTimestamp && b.lastModifiedAssetTimestamp + ? sortByDate(a.lastModifiedAssetTimestamp, b.lastModifiedAssetTimestamp) + : sortByDate(a.updatedAt, b.updatedAt) + ); + } else if (selectedSortBy === 'Last modified') { + $albums = $unsortedAlbums.sort((a, b) => sortByDate(a.updatedAt, b.updatedAt)); + } else if (selectedSortBy === 'Album title') { + $albums = $unsortedAlbums.sort((a, b) => a.albumName.localeCompare(b.albumName)); + } + } + const handleCreateAlbum = async () => { const newAlbum = await createAlbum(); if (newAlbum) { @@ -52,13 +78,15 @@ -
+
Create album
+ +
diff --git a/web/src/routes/(user)/search/+page.svelte b/web/src/routes/(user)/search/+page.svelte index 0a7b699bf..ce7585d71 100644 --- a/web/src/routes/(user)/search/+page.svelte +++ b/web/src/routes/(user)/search/+page.svelte @@ -20,19 +20,27 @@ import type { PageData } from './$types'; import SelectAll from 'svelte-material-icons/SelectAll.svelte'; import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; + import { AppRoute } from '$lib/constants'; + import AlbumCard from '$lib/components/album-page/album-card.svelte'; + import { flip } from 'svelte/animate'; export let data: PageData; // The GalleryViewer pushes it's own history state, which causes weird // behavior for history.back(). To prevent that we store the previous page // manually and navigate back to that. - let previousRoute = '/explore'; + let previousRoute = AppRoute.EXPLORE as string; + $: albums = data.results.albums.items; afterNavigate(({ from }) => { // Prevent setting previousRoute to the current page. if (from && from.route.id !== $page.route.id) { previousRoute = from.url.href; } + + if (from?.route.id === '/(user)/albums/[albumId]') { + previousRoute = AppRoute.EXPLORE; + } }); $: term = $page.url.searchParams.get('q') || data.term || ''; @@ -79,6 +87,30 @@
+ {#if albums.length} +
+
ALBUMS
+
+ {#each albums as album (album.id)} + + + + {/each} +
+ +
PHOTOS & VIDEOS
+
+ {/if}
{#if data.results?.assets?.items.length > 0}
diff --git a/web/src/routes/(user)/share/[key]/+page.server.ts b/web/src/routes/(user)/share/[key]/+page.server.ts index 080a6e391..c2863f093 100644 --- a/web/src/routes/(user)/share/[key]/+page.server.ts +++ b/web/src/routes/(user)/share/[key]/+page.server.ts @@ -7,7 +7,7 @@ export const load = (async ({ params, locals: { api } }) => { const { key } = params; try { - const { data: sharedLink } = await api.shareApi.getMySharedLink({ key }); + const { data: sharedLink } = await api.sharedLinkApi.getMySharedLink({ key }); const assetCount = sharedLink.assets.length; const assetId = sharedLink.album?.albumThumbnailAssetId || sharedLink.assets[0]?.id; diff --git a/web/src/routes/(user)/sharing/+page.svelte b/web/src/routes/(user)/sharing/+page.svelte index b783d6594..a2327d271 100644 --- a/web/src/routes/(user)/sharing/+page.svelte +++ b/web/src/routes/(user)/sharing/+page.svelte @@ -100,7 +100,7 @@ href={`albums/${album.id}`} animate:flip={{ duration: 200 }} > - + {/each}
diff --git a/web/src/routes/(user)/sharing/sharedlinks/+page.svelte b/web/src/routes/(user)/sharing/sharedlinks/+page.svelte index 7d2aab057..aee6a9863 100644 --- a/web/src/routes/(user)/sharing/sharedlinks/+page.svelte +++ b/web/src/routes/(user)/sharing/sharedlinks/+page.svelte @@ -1,7 +1,6 @@ - goto('/sharing')}> + goto(AppRoute.SHARING)}> Shared links @@ -86,16 +77,16 @@ {#each sharedLinks as link (link.id)} handleDeleteLink(link.id)} - on:edit={() => handleEditLink(link.id)} - on:copy={() => handleCopy(link.key)} + on:delete={() => (deleteLinkId = link.id)} + on:edit={() => (editSharedLink = link)} + on:copy={() => handleCopyLink(link.key)} /> {/each}
{/if}
-{#if showEditForm} +{#if editSharedLink} {/if} + +{#if deleteLinkId} + handleDeleteLink()} + on:cancel={() => (deleteLinkId = null)} + /> +{/if} diff --git a/web/src/routes/admin/user-management/+page.svelte b/web/src/routes/admin/user-management/+page.svelte index b5a3624bb..5dc4b8260 100644 --- a/web/src/routes/admin/user-management/+page.svelte +++ b/web/src/routes/admin/user-management/+page.svelte @@ -5,6 +5,8 @@ import PencilOutline from 'svelte-material-icons/PencilOutline.svelte'; import TrashCanOutline from 'svelte-material-icons/TrashCanOutline.svelte'; import DeleteRestore from 'svelte-material-icons/DeleteRestore.svelte'; + import Check from 'svelte-material-icons/Check.svelte'; + import Close from 'svelte-material-icons/Close.svelte'; import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; import CreateUserForm from '$lib/components/forms/create-user-form.svelte'; import EditUserForm from '$lib/components/forms/edit-user-form.svelte'; @@ -171,6 +173,7 @@ Email First name Last name + Can import Action @@ -191,6 +194,15 @@ {user.email} {user.firstName} {user.lastName} + +
+ {#if user.externalPath} + + {:else} + + {/if} +
+ {#if !isDeleted(user)}