Merge branch 'main' of https://github.com/immich-app/immich into thumbhash_mobile

This commit is contained in:
Luke McCarthy 2023-06-22 10:51:54 -04:00
commit 4fab2bcf63
130 changed files with 3823 additions and 2714 deletions

View file

@ -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

View file

@ -52,7 +52,7 @@ class GalleryViewerPage extends HookConsumerWidget {
final showAppBar = useState<bool>(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;
}
},
),
);
}

View file

@ -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<VideoPlayer> {
),
);
} 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(),
),
],
),
),
);

View file

@ -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);
}
}

View file

@ -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"};
}

View file

@ -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

View file

@ -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)

View file

@ -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<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = 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)

View file

@ -19,6 +19,7 @@ Name | Type | Description | Notes
**sharedUsers** | [**List<UserResponseDto>**](UserResponseDto.md) | | [default to const []]
**assets** | [**List<AssetResponseDto>**](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)

View file

@ -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<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = AssetApi();
final 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<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = AssetApi();
final 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<DeleteAssetResponseDto> 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<HttpBearerAuth>('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]

View file

@ -1,20 +0,0 @@
# openapi.model.CreateAssetsShareLinkDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**assetIds** | **List<String>** | | [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)

View file

@ -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)

26
mobile/openapi/doc/ImportAssetDto.md generated Normal file
View file

@ -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)

View file

@ -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<AssetIdsResponseDto> addSharedLinkAssets(id, assetIdsDto, key)
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure API key authorization: cookie
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = 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>**](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<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = 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<SharedLinkResponseDto> getAllSharedLinks()
@ -39,13 +156,13 @@ import 'package:openapi/api.dart';
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('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<HttpBearerAuth>('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<HttpBearerAuth>('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<HttpBearerAuth>('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<AssetIdsResponseDto> removeSharedLinkAssets(id, assetIdsDto, key)
@ -254,15 +371,16 @@ import 'package:openapi/api.dart';
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('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>**](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<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = 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

View file

@ -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<String>** | | [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)

View file

@ -1,4 +1,4 @@
# openapi.model.EditSharedLinkDto
# openapi.model.SharedLinkEditDto
## Load the model package
```dart

View file

@ -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) | |

View file

@ -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]

View file

@ -13,6 +13,7 @@ Name | Type | Description | Notes
**firstName** | **String** | |
**lastName** | **String** | |
**storageLabel** | **String** | |
**externalPath** | **String** | |
**profileImagePath** | **String** | |
**shouldChangePassword** | **bool** | |
**isAdmin** | **bool** | |

View file

@ -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';

View file

@ -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<Response> createAlbumSharedLinkWithHttpInfo(CreateAlbumShareLinkDto createAlbumShareLinkDto,) async {
// ignore: prefer_const_declarations
final path = r'/album/create-shared-link';
// ignore: prefer_final_locals
Object? postBody = createAlbumShareLinkDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'POST',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [CreateAlbumShareLinkDto] createAlbumShareLinkDto (required):
Future<SharedLinkResponseDto?> 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:
///

View file

@ -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<Response> 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 = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
if (key != null) {
queryParams.addAll(_queryParams('', 'key', key));
}
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'PATCH',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [AddAssetsDto] addAssetsDto (required):
///
/// * [String] key:
Future<SharedLinkResponseDto?> 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<Response> createAssetsSharedLinkWithHttpInfo(CreateAssetsShareLinkDto createAssetsShareLinkDto,) async {
// ignore: prefer_const_declarations
final path = r'/asset/shared-link';
// ignore: prefer_final_locals
Object? postBody = createAssetsShareLinkDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'POST',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [CreateAssetsShareLinkDto] createAssetsShareLinkDto (required):
Future<SharedLinkResponseDto?> 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<Response> removeAssetsFromSharedLinkWithHttpInfo(RemoveAssetsDto removeAssetsDto, { String? key, }) async {
/// * [ImportAssetDto] importAssetDto (required):
Future<Response> 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 = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
if (key != null) {
queryParams.addAll(_queryParams('', 'key', key));
}
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'PATCH',
'POST',
queryParams,
postBody,
headerParams,
@ -1262,11 +1154,9 @@ class AssetApi {
/// Parameters:
///
/// * [RemoveAssetsDto] removeAssetsDto (required):
///
/// * [String] key:
Future<SharedLinkResponseDto?> removeAssetsFromSharedLink(RemoveAssetsDto removeAssetsDto, { String? key, }) async {
final response = await removeAssetsFromSharedLinkWithHttpInfo(removeAssetsDto, key: key, );
/// * [ImportAssetDto] importAssetDto (required):
Future<AssetFileUploadResponseDto?> 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<Response> 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<Response> 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<AssetFileUploadResponseDto?> 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<AssetFileUploadResponseDto?> 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));
}

View file

@ -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<Response> getAllSharedLinksWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/share';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
Future<List<SharedLinkResponseDto>?> 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<SharedLinkResponseDto>') as List)
.cast<SharedLinkResponseDto>()
.toList();
}
return null;
}
/// Performs an HTTP 'GET /share/me' operation and returns the [Response].
/// Parameters:
///
/// * [String] key:
Future<Response> getMySharedLinkWithHttpInfo({ String? key, }) async {
// ignore: prefer_const_declarations
final path = r'/share/me';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
if (key != null) {
queryParams.addAll(_queryParams('', 'key', key));
}
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] key:
Future<SharedLinkResponseDto?> 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<Response> getSharedLinkByIdWithHttpInfo(String id,) async {
// ignore: prefer_const_declarations
final path = r'/share/{id}'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] id (required):
Future<SharedLinkResponseDto?> 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<Response> removeSharedLinkWithHttpInfo(String id,) async {
// ignore: prefer_const_declarations
final path = r'/share/{id}'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'DELETE',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] id (required):
Future<void> 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<Response> 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 = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'PATCH',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] id (required):
///
/// * [EditSharedLinkDto] editSharedLinkDto (required):
Future<SharedLinkResponseDto?> 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;
}
}

View file

@ -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<Response> 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 = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
if (key != null) {
queryParams.addAll(_queryParams('', 'key', key));
}
const contentTypes = <String>['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<List<AssetIdsResponseDto>?> 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<AssetIdsResponseDto>') as List)
.cast<AssetIdsResponseDto>()
.toList();
}
return null;
}
/// Performs an HTTP 'POST /shared-link' operation and returns the [Response].
/// Parameters:
///
/// * [SharedLinkCreateDto] sharedLinkCreateDto (required):
Future<Response> createSharedLinkWithHttpInfo(SharedLinkCreateDto sharedLinkCreateDto,) async {
// ignore: prefer_const_declarations
final path = r'/shared-link';
// ignore: prefer_final_locals
Object? postBody = sharedLinkCreateDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'POST',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [SharedLinkCreateDto] sharedLinkCreateDto (required):
Future<SharedLinkResponseDto?> 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<Response> getAllSharedLinksWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/shared-link';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
Future<List<SharedLinkResponseDto>?> 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<SharedLinkResponseDto>') as List)
.cast<SharedLinkResponseDto>()
.toList();
}
return null;
}
/// Performs an HTTP 'GET /shared-link/me' operation and returns the [Response].
/// Parameters:
///
/// * [String] key:
Future<Response> getMySharedLinkWithHttpInfo({ String? key, }) async {
// ignore: prefer_const_declarations
final path = r'/shared-link/me';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
if (key != null) {
queryParams.addAll(_queryParams('', 'key', key));
}
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] key:
Future<SharedLinkResponseDto?> 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<Response> 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 = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] id (required):
Future<SharedLinkResponseDto?> 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<Response> 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 = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'DELETE',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] id (required):
Future<void> 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<Response> 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 = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
if (key != null) {
queryParams.addAll(_queryParams('', 'key', key));
}
const contentTypes = <String>['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<List<AssetIdsResponseDto>?> 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<AssetIdsResponseDto>') as List)
.cast<AssetIdsResponseDto>()
.toList();
}
return null;
}
/// Performs an HTTP 'PATCH /shared-link/{id}' operation and returns the [Response].
/// Parameters:
///
/// * [String] id (required):
///
/// * [SharedLinkEditDto] sharedLinkEditDto (required):
Future<Response> 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 = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'PATCH',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] id (required):
///
/// * [SharedLinkEditDto] sharedLinkEditDto (required):
Future<SharedLinkResponseDto?> 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;
}
}

View file

@ -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':

View file

@ -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<String, dynamic> toJson() {
final json = <String, dynamic>{};
@ -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;

View file

@ -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<String, dynamic> toJson() {
final json = <String, dynamic>{};
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<String, dynamic>();
// 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<String>(json, r'albumId')!,
expiresAt: mapDateTime(json, r'expiresAt', ''),
allowUpload: mapValueOfType<bool>(json, r'allowUpload'),
allowDownload: mapValueOfType<bool>(json, r'allowDownload'),
showExif: mapValueOfType<bool>(json, r'showExif'),
description: mapValueOfType<String>(json, r'description'),
);
}
return null;
}
static List<CreateAlbumShareLinkDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <CreateAlbumShareLinkDto>[];
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<String, CreateAlbumShareLinkDto> mapFromJson(dynamic json) {
final map = <String, CreateAlbumShareLinkDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // 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<String, List<CreateAlbumShareLinkDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<CreateAlbumShareLinkDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
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 = <String>{
'albumId',
};
}

View file

@ -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<String, dynamic> toJson() {
final json = <String, dynamic>{};
@ -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<String>(json, r'firstName')!,
lastName: mapValueOfType<String>(json, r'lastName')!,
storageLabel: mapValueOfType<String>(json, r'storageLabel'),
externalPath: mapValueOfType<String>(json, r'externalPath'),
);
}
return null;

View file

@ -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<String, dynamic> toJson() {
final json = <String, dynamic>{};
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<String, dynamic>();
// 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<bool>(json, r'isReadOnly') ?? true,
assetPath: mapValueOfType<String>(json, r'assetPath')!,
sidecarPath: mapValueOfType<String>(json, r'sidecarPath'),
deviceAssetId: mapValueOfType<String>(json, r'deviceAssetId')!,
deviceId: mapValueOfType<String>(json, r'deviceId')!,
fileCreatedAt: mapDateTime(json, r'fileCreatedAt', '')!,
fileModifiedAt: mapDateTime(json, r'fileModifiedAt', '')!,
isFavorite: mapValueOfType<bool>(json, r'isFavorite')!,
isArchived: mapValueOfType<bool>(json, r'isArchived'),
isVisible: mapValueOfType<bool>(json, r'isVisible'),
duration: mapValueOfType<String>(json, r'duration'),
);
}
return null;
}
static List<ImportAssetDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <ImportAssetDto>[];
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<String, ImportAssetDto> mapFromJson(dynamic json) {
final map = <String, ImportAssetDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // 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<String, List<ImportAssetDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<ImportAssetDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
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 = <String>{
'assetType',
'assetPath',
'deviceAssetId',
'deviceId',
'fileCreatedAt',
'fileModifiedAt',
'isFavorite',
};
}

View file

@ -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<String> 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<String, dynamic> toJson() {
final json = <String, dynamic>{};
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<String, dynamic>();
@ -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<String>().toList(growable: false)
: const [],
expiresAt: mapDateTime(json, r'expiresAt', ''),
allowUpload: mapValueOfType<bool>(json, r'allowUpload'),
allowDownload: mapValueOfType<bool>(json, r'allowDownload'),
showExif: mapValueOfType<bool>(json, r'showExif'),
albumId: mapValueOfType<String>(json, r'albumId'),
description: mapValueOfType<String>(json, r'description'),
expiresAt: mapDateTime(json, r'expiresAt', ''),
allowUpload: mapValueOfType<bool>(json, r'allowUpload') ?? false,
allowDownload: mapValueOfType<bool>(json, r'allowDownload') ?? true,
showExif: mapValueOfType<bool>(json, r'showExif') ?? true,
);
}
return null;
}
static List<CreateAssetsShareLinkDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <CreateAssetsShareLinkDto>[];
static List<SharedLinkCreateDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <SharedLinkCreateDto>[];
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<String, CreateAssetsShareLinkDto> mapFromJson(dynamic json) {
final map = <String, CreateAssetsShareLinkDto>{};
static Map<String, SharedLinkCreateDto> mapFromJson(dynamic json) {
final map = <String, SharedLinkCreateDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // 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<String, List<CreateAssetsShareLinkDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<CreateAssetsShareLinkDto>>{};
// maps a json object with a list of SharedLinkCreateDto-objects as value to a dart map
static Map<String, List<SharedLinkCreateDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<SharedLinkCreateDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
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 = <String>{
'assetIds',
'type',
};
}

View file

@ -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<String, dynamic> toJson() {
final json = <String, dynamic>{};
@ -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<String, dynamic>();
@ -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<String>(json, r'description'),
expiresAt: mapDateTime(json, r'expiresAt', ''),
allowUpload: mapValueOfType<bool>(json, r'allowUpload'),
@ -133,11 +133,11 @@ class EditSharedLinkDto {
return null;
}
static List<EditSharedLinkDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <EditSharedLinkDto>[];
static List<SharedLinkEditDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <SharedLinkEditDto>[];
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<String, EditSharedLinkDto> mapFromJson(dynamic json) {
final map = <String, EditSharedLinkDto>{};
static Map<String, SharedLinkEditDto> mapFromJson(dynamic json) {
final map = <String, SharedLinkEditDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // 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<String, List<EditSharedLinkDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<EditSharedLinkDto>>{};
// maps a json object with a list of SharedLinkEditDto-objects as value to a dart map
static Map<String, List<SharedLinkEditDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<SharedLinkEditDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
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;

View file

@ -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 = <String>{
'type',
'id',
'description',
'userId',
'key',
'createdAt',

View file

@ -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<String, dynamic> toJson() {
final json = <String, dynamic>{};
@ -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<String>(json, r'firstName'),
lastName: mapValueOfType<String>(json, r'lastName'),
storageLabel: mapValueOfType<String>(json, r'storageLabel'),
externalPath: mapValueOfType<String>(json, r'externalPath'),
isAdmin: mapValueOfType<bool>(json, r'isAdmin'),
shouldChangePassword: mapValueOfType<bool>(json, r'shouldChangePassword'),
);

View file

@ -32,11 +32,6 @@ void main() {
// TODO
});
//Future<SharedLinkResponseDto> createAlbumSharedLink(CreateAlbumShareLinkDto createAlbumShareLinkDto) async
test('test createAlbumSharedLink', () async {
// TODO
});
//Future deleteAlbum(String id) async
test('test deleteAlbum', () async {
// TODO

View file

@ -71,6 +71,11 @@ void main() {
// TODO
});
// DateTime lastModifiedAssetTimestamp
test('to test the property `lastModifiedAssetTimestamp`', () async {
// TODO
});
});

View file

@ -17,11 +17,6 @@ void main() {
// final instance = AssetApi();
group('tests for AssetApi', () {
//Future<SharedLinkResponseDto> addAssetsToSharedLink(AddAssetsDto addAssetsDto, { String key }) async
test('test addAssetsToSharedLink', () async {
// TODO
});
// Checks if assets exist by checksums
//
//Future<AssetBulkUploadCheckResponseDto> bulkUploadCheck(AssetBulkUploadCheckDto assetBulkUploadCheckDto) async
@ -43,11 +38,6 @@ void main() {
// TODO
});
//Future<SharedLinkResponseDto> createAssetsSharedLink(CreateAssetsShareLinkDto createAssetsShareLinkDto) async
test('test createAssetsSharedLink', () async {
// TODO
});
//Future<List<DeleteAssetResponseDto>> deleteAsset(DeleteAssetDto deleteAssetDto) async
test('test deleteAsset', () async {
// TODO
@ -141,8 +131,8 @@ void main() {
// TODO
});
//Future<SharedLinkResponseDto> removeAssetsFromSharedLink(RemoveAssetsDto removeAssetsDto, { String key }) async
test('test removeAssetsFromSharedLink', () async {
//Future<AssetFileUploadResponseDto> importFile(ImportAssetDto importAssetDto) async
test('test importFile', () async {
// TODO
});
@ -163,7 +153,7 @@ void main() {
// TODO
});
//Future<AssetFileUploadResponseDto> 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<AssetFileUploadResponseDto> 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
});

View file

@ -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
});
});
}

View file

@ -41,6 +41,11 @@ void main() {
// TODO
});
// String externalPath
test('to test the property `externalPath`', () async {
// TODO
});
});

View file

@ -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
});
});
}

View file

@ -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<List<AssetIdsResponseDto>> addSharedLinkAssets(String id, AssetIdsDto assetIdsDto, { String key }) async
test('test addSharedLinkAssets', () async {
// TODO
});
//Future<SharedLinkResponseDto> createSharedLink(SharedLinkCreateDto sharedLinkCreateDto) async
test('test createSharedLink', () async {
// TODO
});
group('tests for ShareApi', () {
//Future<List<SharedLinkResponseDto>> getAllSharedLinks() async
test('test getAllSharedLinks', () async {
// TODO
@ -37,7 +47,12 @@ void main() {
// TODO
});
//Future<SharedLinkResponseDto> updateSharedLink(String id, EditSharedLinkDto editSharedLinkDto) async
//Future<List<AssetIdsResponseDto>> removeSharedLinkAssets(String id, AssetIdsDto assetIdsDto, { String key }) async
test('test removeSharedLinkAssets', () async {
// TODO
});
//Future<SharedLinkResponseDto> updateSharedLink(String id, SharedLinkEditDto sharedLinkEditDto) async
test('test updateSharedLink', () async {
// TODO
});

View file

@ -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<String> 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
});
});

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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<SharedLinkCreateDto, 'type'>) {
const res = await request(app.getHttpServer())
.post('/shared-link')
.send({ ...data, type: SharedLinkType.ALBUM });
expect(res.status).toEqual(201);
return res.body as SharedLinkResponseDto;
}

View file

@ -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,
},
]),
);

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -2,8 +2,11 @@ export const IAccessRepository = 'IAccessRepository';
export interface IAccessRepository {
hasPartnerAccess(userId: string, partnerId: string): Promise<boolean>;
hasAlbumAssetAccess(userId: string, assetId: string): Promise<boolean>;
hasOwnerAssetAccess(userId: string, assetId: string): Promise<boolean>;
hasPartnerAssetAccess(userId: string, assetId: string): Promise<boolean>;
hasSharedLinkAssetAccess(userId: string, assetId: string): Promise<boolean>;
hasAlbumOwnerAccess(userId: string, albumId: string): Promise<boolean>;
}

View file

@ -16,6 +16,7 @@ export class AlbumResponseDto {
owner!: UserResponseDto;
@ApiProperty({ type: 'integer' })
assetCount!: number;
lastModifiedAssetTimestamp?: Date;
}
export function mapAlbum(entity: AlbumEntity): AlbumResponseDto {

View file

@ -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,

View file

@ -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<number> {

View file

@ -19,6 +19,7 @@ export class APIKeyCore {
isAdmin: user.isAdmin,
isPublicUser: false,
isAllowUpload: true,
externalPath: user.externalPath,
};
}

View file

@ -47,6 +47,7 @@ export interface IAssetRepository {
getWithout(pagination: PaginationOptions, property: WithoutProperty): Paginated<AssetEntity>;
getWith(pagination: PaginationOptions, property: WithProperty): Paginated<AssetEntity>;
getFirstAssetForAlbumId(albumId: string): Promise<AssetEntity | null>;
getLastUpdatedAssetForAlbumId(albumId: string): Promise<AssetEntity | null>;
deleteAll(ownerId: string): Promise<void>;
getAll(pagination: PaginationOptions, options?: AssetSearchOptions): Paginated<AssetEntity>;
save(asset: Partial<AssetEntity>): Promise<AssetEntity>;

View file

@ -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<AuthUserDto | null> {
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');
}
}

View file

@ -8,4 +8,5 @@ export class AuthUserDto {
isAllowDownload?: boolean;
isShowExif?: boolean;
accessTokenId?: string;
externalPath?: string | null;
}

View file

@ -2,6 +2,7 @@ export const ICryptoRepository = 'ICryptoRepository';
export interface ICryptoRepository {
randomBytes(size: number): Buffer;
hashFile(filePath: string): Promise<Buffer>;
hashSha256(data: string): string;
hashBcrypt(data: string | Buffer, saltOrRounds: string | number): Promise<string>;
compareBcrypt(data: string | Buffer, encrypted: string): boolean;

View file

@ -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);
}

View file

@ -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,
},
};

View file

@ -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;
}

View file

@ -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;
}

View file

@ -1,2 +0,0 @@
export * from './create-shared-link.dto';
export * from './edit-shared-link.dto';

View file

@ -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';

View file

@ -1 +0,0 @@
export * from './shared-link-response.dto';

View file

@ -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;

View file

@ -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<SharedLinkEntity> {
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<AuthUserDto | null> {
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');
}
}

View file

@ -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;
}

View file

@ -6,7 +6,7 @@ export interface ISharedLinkRepository {
getAll(userId: string): Promise<SharedLinkEntity[]>;
get(userId: string, id: string): Promise<SharedLinkEntity | null>;
getByKey(key: Buffer): Promise<SharedLinkEntity | null>;
create(entity: Omit<SharedLinkEntity, 'id' | 'user'>): Promise<SharedLinkEntity>;
create(entity: Partial<SharedLinkEntity>): Promise<SharedLinkEntity>;
update(entity: Partial<SharedLinkEntity>): Promise<SharedLinkEntity>;
remove(entity: SharedLinkEntity): Promise<void>;
}

View file

@ -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<IAccessRepository>;
let cryptoMock: jest.Mocked<ICryptoRepository>;
let shareMock: jest.Mocked<ISharedLinkRepository>;
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: [] });
});
});
});

View file

@ -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<SharedLinkResponseDto[]> {
getAll(authUser: AuthUserDto): Promise<SharedLinkResponseDto[]> {
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<SharedLinkResponseDto> {
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<AssetIdsResponseDto[]> {
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<AssetIdsResponseDto[]> {
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);
}

View file

@ -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();
});
});
});

View file

@ -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);

View file

@ -23,6 +23,10 @@ export class CreateUserDto {
@IsString()
@Transform(toSanitized)
storageLabel?: string | null;
@IsOptional()
@IsString()
externalPath?: string | null;
}
export class CreateAdminDto {

View file

@ -29,6 +29,10 @@ export class UpdateUserDto {
@Transform(toSanitized)
storageLabel?: string;
@IsOptional()
@IsString()
externalPath?: string;
@IsNotEmpty()
@IsUUID('4')
@ApiProperty({ format: 'uuid' })

View file

@ -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,

View file

@ -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<UserEntity> = { ...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) {

View file

@ -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,
},
]);
});

View file

@ -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);
}
}

View file

@ -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<IAlbumRepository>;
let sharedLinkRepositoryMock: jest.Mocked<ISharedLinkRepository>;
let downloadServiceMock: jest.Mocked<Partial<DownloadService>>;
let cryptoMock: jest.Mocked<ICryptoRepository>;
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 () => {

View file

@ -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<SharedLinkResponseDto> {
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();
}
}
}

View file

@ -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;
}

View file

@ -20,6 +20,10 @@ export interface AssetCheck {
checksum: Buffer;
}
export interface AssetOwnerCheck extends AssetCheck {
ownerId: string;
}
export interface IAssetRepository {
get(id: string): Promise<AssetEntity | null>;
create(
@ -39,6 +43,7 @@ export interface IAssetRepository {
getAssetByTimeBucket(userId: string, getAssetByTimeBucketDto: GetAssetByTimeBucketDto): Promise<AssetEntity[]>;
getAssetsByChecksums(userId: string, checksums: Buffer[]): Promise<AssetCheck[]>;
getExistingAssets(userId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise<string[]>;
getByOriginalPath(originalPath: string): Promise<AssetOwnerCheck | null>;
}
export const IAssetRepository = 'IAssetRepository';
@ -350,4 +355,17 @@ export class AssetRepository implements IAssetRepository {
return assetCountByUserId;
}
getByOriginalPath(originalPath: string): Promise<AssetOwnerCheck | null> {
return this.assetRepository.findOne({
select: {
id: true,
ownerId: true,
checksum: true,
},
where: {
originalPath,
},
});
}
}

View file

@ -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<AssetFileUploadResponseDto> {
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<AssetBulkUploadCheckResponseDto> {
return this.assetService.bulkUploadCheck(authUser, dto);
}
@Post('/shared-link')
createAssetsSharedLink(
@AuthUser() authUser: AuthUserDto,
@Body(ValidationPipe) dto: CreateAssetsShareLinkDto,
): Promise<SharedLinkResponseDto> {
return this.assetService.createAssetsSharedLink(authUser, dto);
}
@SharedLinkRoute()
@Patch('/shared-link/add')
addAssetsToSharedLink(
@AuthUser() authUser: AuthUserDto,
@Body(ValidationPipe) dto: AddAssetsDto,
): Promise<SharedLinkResponseDto> {
return this.assetService.addAssetsToSharedLink(authUser, dto);
}
@SharedLinkRoute()
@Patch('/shared-link/remove')
removeAssetsFromSharedLink(
@AuthUser() authUser: AuthUserDto,
@Body(ValidationPipe) dto: RemoveAssetsDto,
): Promise<SharedLinkResponseDto> {
return this.assetService.removeAssetsFromSharedLink(authUser, dto);
}
}

View file

@ -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<AssetEntity> {
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' } });

View file

@ -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<AssetEntity>; // TO BE DELETED AFTER FINISHED REFACTORING
let accessMock: jest.Mocked<IAccessRepository>;
let assetRepositoryMock: jest.Mocked<IAssetRepository>;
let downloadServiceMock: jest.Mocked<Partial<DownloadService>>;
let sharedLinkRepositoryMock: jest.Mocked<ISharedLinkRepository>;
let cryptoMock: jest.Mocked<ICryptoRepository>;
let downloadServiceMock: jest.Mocked<Partial<DownloadService>>;
let jobMock: jest.Mocked<IJobRepository>;
let storageMock: jest.Mocked<IStorageRepository>;
@ -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);

View file

@ -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<AssetEntity>,
@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
@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<AssetFileUploadResponseDto> {
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<SharedLinkResponseDto> {
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<SharedLinkResponseDto> {
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<SharedLinkResponseDto> {
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));
}

View file

@ -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;
}

View file

@ -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;

View file

@ -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' },

View file

@ -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) {

View file

@ -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<SharedLinkResponseDto> {
return this.service.update(authUser, id, dto);
}
@ -42,4 +55,24 @@ export class SharedLinkController {
removeSharedLink(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.remove(authUser, id);
}
@SharedLinkRoute()
@Put(':id/assets')
addSharedLinkAssets(
@AuthUser() authUser: AuthUserDto,
@Param() { id }: UUIDParamDto,
@Body() dto: AssetIdsDto,
): Promise<AssetIdsResponseDto[]> {
return this.service.addAssets(authUser, id, dto);
}
@SharedLinkRoute()
@Delete(':id/assets')
removeSharedLinkAssets(
@AuthUser() authUser: AuthUserDto,
@Param() { id }: UUIDParamDto,
@Body() dto: AssetIdsDto,
): Promise<AssetIdsResponseDto[]> {
return this.service.removeAssets(authUser, id, dto);
}
}

View file

@ -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;

View file

@ -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 {

View file

@ -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;

View file

@ -0,0 +1,18 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class ImportAsset1686584273471 implements MigrationInterface {
name = 'ImportAsset1686584273471'
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
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"`);
}
}

View file

@ -95,4 +95,13 @@ export class AccessRepository implements IAccessRepository {
}))
);
}
hasAlbumOwnerAccess(userId: string, albumId: string): Promise<boolean> {
return this.albumRepository.exist({
where: {
id: albumId,
ownerId: userId,
},
});
}
}

View file

@ -248,6 +248,13 @@ export class AssetRepository implements IAssetRepository {
});
}
getLastUpdatedAssetForAlbumId(albumId: string): Promise<AssetEntity | null> {
return this.repository.findOne({
where: { albums: { id: albumId } },
order: { updatedAt: 'DESC' },
});
}
async getMapMarkers(ownerId: string, options: MapMarkerSearchOptions = {}): Promise<MapMarker[]> {
const { isFavorite, fileCreatedAfter, fileCreatedBefore } = options;

View file

@ -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<Buffer> {
return new Promise<Buffer>((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()));
});
}
}

Some files were not shown because too many files have changed in this diff Show more