Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6bac9c7e8f | ||
|
|
ff3cde4dfb | ||
|
|
7aab84f2d9 | ||
|
|
3a940711eb | ||
|
|
2b0b2bb1ae | ||
|
|
e39507552f | ||
|
|
b019ab79f9 | ||
|
|
43da8c2a72 | ||
|
|
0b65cea6fd | ||
|
|
a1806390b0 | ||
|
|
5d6559e839 | ||
|
|
29c79ad1d8 |
@@ -1,4 +1,3 @@
|
||||
|
||||
FROM node:16-bullseye-slim as builder
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
@@ -6,7 +5,7 @@ ARG DEBIAN_FRONTEND=noninteractive
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get install gcc g++ make cmake python3 python3-pip ffmpeg -y
|
||||
RUN apt-get install gcc g++ make cmake python3 python3-pip -y
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
|
||||
@@ -15,24 +14,17 @@ RUN npm rebuild @tensorflow/tfjs-node --build-from-source
|
||||
|
||||
COPY . .
|
||||
|
||||
|
||||
FROM builder as prod
|
||||
|
||||
RUN npm run build
|
||||
|
||||
RUN npm prune --omit=dev
|
||||
|
||||
|
||||
FROM node:16-bullseye-slim
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y ffmpeg \
|
||||
&& rm -rf /var/cache/apt/lists
|
||||
|
||||
COPY --from=prod /usr/src/app/node_modules ./node_modules
|
||||
COPY --from=prod /usr/src/app/dist ./dist
|
||||
|
||||
|
||||
565
machine-learning/package-lock.json
generated
565
machine-learning/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -23,19 +23,9 @@
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^8.0.0",
|
||||
"@nestjs/core": "^8.0.0",
|
||||
"@nestjs/mapped-types": "^1.0.1",
|
||||
"@nestjs/platform-express": "^8.0.0",
|
||||
"@tensorflow-models/coco-ssd": "^2.2.2",
|
||||
"@tensorflow-models/mobilenet": "^2.1.0",
|
||||
"@tensorflow/tfjs": "^3.19.0",
|
||||
"@tensorflow/tfjs-converter": "^3.19.0",
|
||||
"@tensorflow/tfjs-core": "^3.19.0",
|
||||
"@tensorflow/tfjs-node": "^3.19.0",
|
||||
"@tensorflow/tfjs-node-gpu": "^3.19.0",
|
||||
"@trpc/server": "^9.20.3",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rimraf": "^3.0.2",
|
||||
"rxjs": "^7.2.0"
|
||||
"@tensorflow/tfjs-node": "^3.19.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^8.2.4",
|
||||
@@ -52,6 +42,7 @@
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"jest": "^27.2.5",
|
||||
"prettier": "^2.3.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"source-map-support": "^0.5.20",
|
||||
"supertest": "^6.1.3",
|
||||
"ts-jest": "^27.0.3",
|
||||
|
||||
@@ -65,6 +65,7 @@ if [ "$CURRENT_SERVER" != "$NEXT_SERVER" ]; then
|
||||
|
||||
sed -i "s/^ \"version\": \"$CURRENT_SERVER\",$/ \"version\": \"$NEXT_SERVER\",/" server/package.json
|
||||
sed -i "s/^ \"version\": \"$CURRENT_SERVER\",$/ \"version\": \"$NEXT_SERVER\",/" server/package-lock.json
|
||||
sed -i "s/\"version\": \"$CURRENT_SERVER\",$/\"version\": \"$NEXT_SERVER\",/" server/immich-openapi-specs.json
|
||||
sed -i "s/\"android\.injected\.version\.name\" => \"$CURRENT_SERVER\",/\"android\.injected\.version\.name\" => \"$NEXT_SERVER\",/" mobile/android/fastlane/Fastfile
|
||||
sed -i "s/version_number: \"$CURRENT_SERVER\"$/version_number: \"$NEXT_SERVER\"/" mobile/ios/fastlane/Fastfile
|
||||
fi
|
||||
|
||||
@@ -36,7 +36,7 @@ platform :android do
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 67,
|
||||
"android.injected.version.name" => "1.44.0",
|
||||
"android.injected.version.name" => "1.45.0",
|
||||
}
|
||||
)
|
||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
* reused HTTP client for uploading assets
|
||||
* top padding missing from drag handle in add to album bottom sheet
|
||||
* Improve the gallery to improve scale, double tap, and swipe gesture detection
|
||||
* Uses clamping scroll physics on android
|
||||
* Fix back button closing the app from multi selection in Android
|
||||
* add and delete operation asset in album viewer doesn't update count in album list
|
||||
* Update share_file to latest version and migrate to using cross platform shareXFile
|
||||
@@ -5,17 +5,17 @@
|
||||
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000213">
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000299">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="61.218233">
|
||||
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="25.91737">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="41.974053">
|
||||
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="38.549017">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
@@ -360,7 +360,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 82;
|
||||
CURRENT_PROJECT_VERSION = 83;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -495,7 +495,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 82;
|
||||
CURRENT_PROJECT_VERSION = 83;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -522,7 +522,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 82;
|
||||
CURRENT_PROJECT_VERSION = 83;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.43.0</string>
|
||||
<string>1.44.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>82</string>
|
||||
<string>83</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true />
|
||||
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
||||
|
||||
@@ -19,7 +19,7 @@ platform :ios do
|
||||
desc "iOS Beta"
|
||||
lane :beta do
|
||||
increment_version_number(
|
||||
version_number: "1.44.0"
|
||||
version_number: "1.45.0"
|
||||
)
|
||||
increment_build_number(
|
||||
build_number: latest_testflight_build_number + 1,
|
||||
|
||||
@@ -5,32 +5,32 @@
|
||||
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000396">
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000424">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="2.478301">
|
||||
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.833772">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="3.846552">
|
||||
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="24.229726">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="2.367554">
|
||||
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.663451">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="4: build_app" time="75.618447">
|
||||
<testcase classname="fastlane.lanes" name="4: build_app" time="90.5773">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="47.502114">
|
||||
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="73.273138">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:io';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
@@ -155,6 +156,8 @@ class ImmichAppState extends ConsumerState<ImmichApp>
|
||||
var router = ref.watch(appRouterProvider);
|
||||
ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo();
|
||||
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||
|
||||
return MaterialApp(
|
||||
localizationsDelegates: context.localizationDelegates,
|
||||
supportedLocales: context.supportedLocales,
|
||||
|
||||
@@ -118,7 +118,6 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
'share_invite',
|
||||
style: TextStyle(color: Colors.black),
|
||||
).tr(),
|
||||
elevation: 0,
|
||||
centerTitle: false,
|
||||
|
||||
@@ -26,14 +26,10 @@ class ImageViewerService {
|
||||
if (asset.type == AssetTypeEnum.IMAGE && asset.livePhotoVideoId != null) {
|
||||
var imageResponse = await _apiService.assetApi.downloadFileWithHttpInfo(
|
||||
asset.id,
|
||||
isThumb: false,
|
||||
isWeb: false,
|
||||
);
|
||||
|
||||
var motionReponse = await _apiService.assetApi.downloadFileWithHttpInfo(
|
||||
asset.livePhotoVideoId!,
|
||||
isThumb: false,
|
||||
isWeb: false,
|
||||
);
|
||||
|
||||
final AssetEntity? entity;
|
||||
@@ -54,8 +50,6 @@ class ImageViewerService {
|
||||
} else {
|
||||
var res = await _apiService.assetApi.downloadFileWithHttpInfo(
|
||||
asset.id,
|
||||
isThumb: false,
|
||||
isWeb: false,
|
||||
);
|
||||
|
||||
final AssetEntity? entity;
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
|
||||
class TopControlAppBar extends HookConsumerWidget with PreferredSizeWidget {
|
||||
class TopControlAppBar extends HookConsumerWidget {
|
||||
const TopControlAppBar({
|
||||
Key? key,
|
||||
required this.asset,
|
||||
@@ -31,7 +31,6 @@ class TopControlAppBar extends HookConsumerWidget with PreferredSizeWidget {
|
||||
|
||||
return AppBar(
|
||||
foregroundColor: Colors.grey[100],
|
||||
toolbarHeight: 60,
|
||||
backgroundColor: Colors.transparent,
|
||||
leading: IconButton(
|
||||
onPressed: () {
|
||||
@@ -120,7 +119,4 @@ class TopControlAppBar extends HookConsumerWidget with PreferredSizeWidget {
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
final isLoadPreview = useState(AppSettingsEnum.loadPreview.defaultValue);
|
||||
final isLoadOriginal = useState(AppSettingsEnum.loadOriginal.defaultValue);
|
||||
final isZoomed = useState<bool>(false);
|
||||
final showAppBar = useState<bool>(true);
|
||||
final indexOfAsset = useState(assetList.indexOf(asset));
|
||||
final isPlayingMotionVideo = useState(false);
|
||||
late Offset localPosition;
|
||||
@@ -108,7 +109,10 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
return AssetEntityImageProvider(
|
||||
asset.local!,
|
||||
isOriginal: false,
|
||||
thumbnailSize: const ThumbnailSize.square(250),
|
||||
thumbnailSize: ThumbnailSize(
|
||||
MediaQuery.of(context).size.width.floor(),
|
||||
MediaQuery.of(context).size.height.floor(),
|
||||
),
|
||||
);
|
||||
|
||||
}
|
||||
@@ -224,36 +228,50 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
buildAppBar() {
|
||||
return AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 100),
|
||||
opacity: (showAppBar.value || !isZoomed.value) ? 1.0 : 0.0,
|
||||
child: Container(
|
||||
color: Colors.black.withOpacity(0.4),
|
||||
child: TopControlAppBar(
|
||||
isPlayingMotionVideo: isPlayingMotionVideo.value,
|
||||
asset: assetList[indexOfAsset.value],
|
||||
onMoreInfoPressed: () {
|
||||
showInfo();
|
||||
},
|
||||
onDownloadPressed: assetList[indexOfAsset.value].isLocal
|
||||
? null
|
||||
: () {
|
||||
ref.watch(imageViewerStateProvider.notifier).downloadAsset(
|
||||
assetList[indexOfAsset.value].remote!,
|
||||
context,
|
||||
);
|
||||
},
|
||||
onSharePressed: () {
|
||||
ref
|
||||
.watch(imageViewerStateProvider.notifier)
|
||||
.shareAsset(assetList[indexOfAsset.value], context);
|
||||
},
|
||||
onToggleMotionVideo: (() {
|
||||
isPlayingMotionVideo.value = !isPlayingMotionVideo.value;
|
||||
}),
|
||||
onDeletePressed: () => handleDelete((assetList[indexOfAsset.value])),
|
||||
onAddToAlbumPressed: () => addToAlbum(assetList[indexOfAsset.value]),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
appBar: TopControlAppBar(
|
||||
isPlayingMotionVideo: isPlayingMotionVideo.value,
|
||||
asset: assetList[indexOfAsset.value],
|
||||
onMoreInfoPressed: () {
|
||||
showInfo();
|
||||
},
|
||||
onDownloadPressed: assetList[indexOfAsset.value].isLocal
|
||||
? null
|
||||
: () {
|
||||
ref.watch(imageViewerStateProvider.notifier).downloadAsset(
|
||||
assetList[indexOfAsset.value].remote!,
|
||||
context,
|
||||
);
|
||||
},
|
||||
onSharePressed: () {
|
||||
ref
|
||||
.watch(imageViewerStateProvider.notifier)
|
||||
.shareAsset(assetList[indexOfAsset.value], context);
|
||||
},
|
||||
onToggleMotionVideo: (() {
|
||||
isPlayingMotionVideo.value = !isPlayingMotionVideo.value;
|
||||
}),
|
||||
onDeletePressed: () => handleDelete((assetList[indexOfAsset.value])),
|
||||
onAddToAlbumPressed: () => addToAlbum(assetList[indexOfAsset.value]),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: PhotoViewGallery.builder(
|
||||
scaleStateChangedCallback: (state) => isZoomed.value = state != PhotoViewScaleState.initial,
|
||||
body: Stack(
|
||||
children: [
|
||||
PhotoViewGallery.builder(
|
||||
scaleStateChangedCallback: (state) {
|
||||
isZoomed.value = state != PhotoViewScaleState.initial;
|
||||
showAppBar.value = !isZoomed.value;
|
||||
},
|
||||
pageController: controller,
|
||||
scrollPhysics: isZoomed.value
|
||||
? const NeverScrollableScrollPhysics() // Don't allow paging while scrolled in
|
||||
@@ -285,6 +303,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
cacheKey: getThumbnailCacheKey(asset.remote!, type: api.ThumbnailFormat.WEBP),
|
||||
httpHeaders: { 'Authorization': authToken },
|
||||
progressIndicatorBuilder: (_, __, ___) => const Center(child: ImmichLoadingIndicator(),),
|
||||
fadeInDuration: const Duration(milliseconds: 0),
|
||||
fit: BoxFit.contain,
|
||||
);
|
||||
|
||||
@@ -293,6 +312,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
cacheKey: getThumbnailCacheKey(asset.remote!, type: api.ThumbnailFormat.JPEG),
|
||||
httpHeaders: { 'Authorization': authToken },
|
||||
fit: BoxFit.contain,
|
||||
fadeInDuration: const Duration(milliseconds: 0),
|
||||
placeholder: (_, __) => webPThumbnail,
|
||||
);
|
||||
} else {
|
||||
@@ -322,6 +342,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
return PhotoViewGalleryPageOptions(
|
||||
onDragStart: (_, details, __) => localPosition = details.localPosition,
|
||||
onDragUpdate: (_, details, __) => handleSwipeUpDown(details),
|
||||
onTapDown: (_, __, ___) => showAppBar.value = !showAppBar.value,
|
||||
imageProvider: provider,
|
||||
heroAttributes: PhotoViewHeroAttributes(tag: assetList[index].id),
|
||||
minScale: PhotoViewComputedScale.contained,
|
||||
@@ -330,20 +351,32 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
return PhotoViewGalleryPageOptions.customChild(
|
||||
onDragStart: (_, details, __) => localPosition = details.localPosition,
|
||||
onDragUpdate: (_, details, __) => handleSwipeUpDown(details),
|
||||
onTapDown: (_, __, ___) => showAppBar.value = !showAppBar.value,
|
||||
heroAttributes: PhotoViewHeroAttributes(tag: assetList[index].id),
|
||||
child: VideoViewerPage(
|
||||
asset: assetList[index],
|
||||
isMotionVideo: isPlayingMotionVideo.value,
|
||||
onVideoEnded: () {
|
||||
if (isPlayingMotionVideo.value) {
|
||||
isPlayingMotionVideo.value = false;
|
||||
}
|
||||
},
|
||||
maxScale: 1.0,
|
||||
minScale: 1.0,
|
||||
child: SafeArea(
|
||||
child: VideoViewerPage(
|
||||
asset: assetList[index],
|
||||
isMotionVideo: isPlayingMotionVideo.value,
|
||||
onVideoEnded: () {
|
||||
if (isPlayingMotionVideo.value) {
|
||||
isPlayingMotionVideo.value = false;
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: buildAppBar(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -224,13 +224,28 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<bool> onWillPop() async {
|
||||
if (widget.selectionActive && _selectedAssets.isNotEmpty) {
|
||||
_deselectAll();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
_buildAssetGrid(),
|
||||
if (widget.selectionActive) _buildMultiSelectIndicator(),
|
||||
],
|
||||
return WillPopScope(
|
||||
onWillPop: onWillPop,
|
||||
child: Stack(
|
||||
children: [
|
||||
_buildAssetGrid(),
|
||||
if (widget.selectionActive) _buildMultiSelectIndicator(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,46 +200,34 @@ class HomePage extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> onWillPop() async {
|
||||
if (multiselectEnabled.state) {
|
||||
selectionEnabledHook.value = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return WillPopScope(
|
||||
onWillPop: onWillPop,
|
||||
child: SafeArea(
|
||||
bottom: !multiselectEnabled.state,
|
||||
top: true,
|
||||
child: Stack(
|
||||
children: [
|
||||
ref.watch(assetProvider).renderList == null ||
|
||||
ref.watch(assetProvider).allAssets.isEmpty
|
||||
? buildLoadingIndicator()
|
||||
: ImmichAssetGrid(
|
||||
renderList: ref.watch(assetProvider).renderList!,
|
||||
allAssets: ref.watch(assetProvider).allAssets,
|
||||
assetsPerRow: appSettingService
|
||||
.getSetting(AppSettingsEnum.tilesPerRow),
|
||||
showStorageIndicator: appSettingService
|
||||
.getSetting(AppSettingsEnum.storageIndicator),
|
||||
listener: selectionListener,
|
||||
selectionActive: selectionEnabledHook.value,
|
||||
),
|
||||
if (selectionEnabledHook.value)
|
||||
ControlBottomAppBar(
|
||||
onShare: onShareAssets,
|
||||
onDelete: onDelete,
|
||||
onAddToAlbum: onAddToAlbum,
|
||||
albums: albums,
|
||||
sharedAlbums: sharedAlbums,
|
||||
onCreateNewAlbum: onCreateNewAlbum,
|
||||
),
|
||||
],
|
||||
),
|
||||
return SafeArea(
|
||||
bottom: !multiselectEnabled.state,
|
||||
top: true,
|
||||
child: Stack(
|
||||
children: [
|
||||
ref.watch(assetProvider).renderList == null ||
|
||||
ref.watch(assetProvider).allAssets.isEmpty
|
||||
? buildLoadingIndicator()
|
||||
: ImmichAssetGrid(
|
||||
renderList: ref.watch(assetProvider).renderList!,
|
||||
allAssets: ref.watch(assetProvider).allAssets,
|
||||
assetsPerRow: appSettingService
|
||||
.getSetting(AppSettingsEnum.tilesPerRow),
|
||||
showStorageIndicator: appSettingService
|
||||
.getSetting(AppSettingsEnum.storageIndicator),
|
||||
listener: selectionListener,
|
||||
selectionActive: selectionEnabledHook.value,
|
||||
),
|
||||
if (selectionEnabledHook.value)
|
||||
ControlBottomAppBar(
|
||||
onShare: onShareAssets,
|
||||
onDelete: onDelete,
|
||||
onAddToAlbum: onAddToAlbum,
|
||||
albums: albums,
|
||||
sharedAlbums: sharedAlbums,
|
||||
onCreateNewAlbum: onCreateNewAlbum,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,8 +29,6 @@ class ShareService {
|
||||
final tempFile = await File('${tempDir.path}/$fileName').create();
|
||||
final res = await _apiService.assetApi.downloadFileWithHttpInfo(
|
||||
asset.remote!.id,
|
||||
isThumb: false,
|
||||
isWeb: false,
|
||||
);
|
||||
tempFile.writeAsBytesSync(res.bodyBytes);
|
||||
return XFile(tempFile.path);
|
||||
|
||||
2
mobile/openapi/README.md
generated
2
mobile/openapi/README.md
generated
@@ -3,7 +3,7 @@ Immich API
|
||||
|
||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||
|
||||
- API version: 1.43.0
|
||||
- API version: 1.43.1
|
||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||
|
||||
## Requirements
|
||||
|
||||
8
mobile/openapi/doc/AssetApi.md
generated
8
mobile/openapi/doc/AssetApi.md
generated
@@ -230,7 +230,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)
|
||||
|
||||
# **downloadFile**
|
||||
> Object downloadFile(assetId, isThumb, isWeb)
|
||||
> Object downloadFile(assetId)
|
||||
|
||||
|
||||
|
||||
@@ -248,11 +248,9 @@ import 'package:openapi/api.dart';
|
||||
|
||||
final api_instance = AssetApi();
|
||||
final assetId = assetId_example; // String |
|
||||
final isThumb = true; // bool |
|
||||
final isWeb = true; // bool |
|
||||
|
||||
try {
|
||||
final result = api_instance.downloadFile(assetId, isThumb, isWeb);
|
||||
final result = api_instance.downloadFile(assetId);
|
||||
print(result);
|
||||
} catch (e) {
|
||||
print('Exception when calling AssetApi->downloadFile: $e\n');
|
||||
@@ -264,8 +262,6 @@ try {
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**assetId** | **String**| |
|
||||
**isThumb** | **bool**| | [optional]
|
||||
**isWeb** | **bool**| | [optional]
|
||||
|
||||
### Return type
|
||||
|
||||
|
||||
21
mobile/openapi/lib/api/asset_api.dart
generated
21
mobile/openapi/lib/api/asset_api.dart
generated
@@ -234,11 +234,7 @@ class AssetApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] assetId (required):
|
||||
///
|
||||
/// * [bool] isThumb:
|
||||
///
|
||||
/// * [bool] isWeb:
|
||||
Future<Response> downloadFileWithHttpInfo(String assetId, { bool? isThumb, bool? isWeb, }) async {
|
||||
Future<Response> downloadFileWithHttpInfo(String assetId,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/asset/download/{assetId}'
|
||||
.replaceAll('{assetId}', assetId);
|
||||
@@ -250,13 +246,6 @@ class AssetApi {
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
if (isThumb != null) {
|
||||
queryParams.addAll(_queryParams('', 'isThumb', isThumb));
|
||||
}
|
||||
if (isWeb != null) {
|
||||
queryParams.addAll(_queryParams('', 'isWeb', isWeb));
|
||||
}
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
@@ -276,12 +265,8 @@ class AssetApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] assetId (required):
|
||||
///
|
||||
/// * [bool] isThumb:
|
||||
///
|
||||
/// * [bool] isWeb:
|
||||
Future<Object?> downloadFile(String assetId, { bool? isThumb, bool? isWeb, }) async {
|
||||
final response = await downloadFileWithHttpInfo(assetId, isThumb: isThumb, isWeb: isWeb, );
|
||||
Future<Object?> downloadFile(String assetId,) async {
|
||||
final response = await downloadFileWithHttpInfo(assetId,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
|
||||
94
mobile/openapi/lib/model/album_response_dto.dart
generated
94
mobile/openapi/lib/model/album_response_dto.dart
generated
@@ -43,51 +43,48 @@ class AlbumResponseDto {
|
||||
List<AssetResponseDto> assets;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is AlbumResponseDto &&
|
||||
other.assetCount == assetCount &&
|
||||
other.id == id &&
|
||||
other.ownerId == ownerId &&
|
||||
other.albumName == albumName &&
|
||||
other.createdAt == createdAt &&
|
||||
other.albumThumbnailAssetId == albumThumbnailAssetId &&
|
||||
other.shared == shared &&
|
||||
other.sharedUsers == sharedUsers &&
|
||||
other.assets == assets;
|
||||
bool operator ==(Object other) => identical(this, other) || other is AlbumResponseDto &&
|
||||
other.assetCount == assetCount &&
|
||||
other.id == id &&
|
||||
other.ownerId == ownerId &&
|
||||
other.albumName == albumName &&
|
||||
other.createdAt == createdAt &&
|
||||
other.albumThumbnailAssetId == albumThumbnailAssetId &&
|
||||
other.shared == shared &&
|
||||
other.sharedUsers == sharedUsers &&
|
||||
other.assets == assets;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(assetCount.hashCode) +
|
||||
(id.hashCode) +
|
||||
(ownerId.hashCode) +
|
||||
(albumName.hashCode) +
|
||||
(createdAt.hashCode) +
|
||||
(albumThumbnailAssetId == null ? 0 : albumThumbnailAssetId!.hashCode) +
|
||||
(shared.hashCode) +
|
||||
(sharedUsers.hashCode) +
|
||||
(assets.hashCode);
|
||||
// ignore: unnecessary_parenthesis
|
||||
(assetCount.hashCode) +
|
||||
(id.hashCode) +
|
||||
(ownerId.hashCode) +
|
||||
(albumName.hashCode) +
|
||||
(createdAt.hashCode) +
|
||||
(albumThumbnailAssetId == null ? 0 : albumThumbnailAssetId!.hashCode) +
|
||||
(shared.hashCode) +
|
||||
(sharedUsers.hashCode) +
|
||||
(assets.hashCode);
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'AlbumResponseDto[assetCount=$assetCount, id=$id, ownerId=$ownerId, albumName=$albumName, createdAt=$createdAt, albumThumbnailAssetId=$albumThumbnailAssetId, shared=$shared, sharedUsers=$sharedUsers, assets=$assets]';
|
||||
String toString() => 'AlbumResponseDto[assetCount=$assetCount, id=$id, ownerId=$ownerId, albumName=$albumName, createdAt=$createdAt, albumThumbnailAssetId=$albumThumbnailAssetId, shared=$shared, sharedUsers=$sharedUsers, assets=$assets]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'assetCount'] = this.assetCount;
|
||||
json[r'id'] = this.id;
|
||||
json[r'ownerId'] = this.ownerId;
|
||||
json[r'albumName'] = this.albumName;
|
||||
json[r'createdAt'] = this.createdAt;
|
||||
json[r'assetCount'] = this.assetCount;
|
||||
json[r'id'] = this.id;
|
||||
json[r'ownerId'] = this.ownerId;
|
||||
json[r'albumName'] = this.albumName;
|
||||
json[r'createdAt'] = this.createdAt;
|
||||
if (this.albumThumbnailAssetId != null) {
|
||||
json[r'albumThumbnailAssetId'] = this.albumThumbnailAssetId;
|
||||
} else {
|
||||
// json[r'albumThumbnailAssetId'] = null;
|
||||
}
|
||||
json[r'shared'] = this.shared;
|
||||
json[r'sharedUsers'] = this.sharedUsers;
|
||||
json[r'assets'] = this.assets;
|
||||
json[r'shared'] = this.shared;
|
||||
json[r'sharedUsers'] = this.sharedUsers;
|
||||
json[r'assets'] = this.assets;
|
||||
return json;
|
||||
}
|
||||
|
||||
@@ -101,13 +98,13 @@ class AlbumResponseDto {
|
||||
// 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 "AlbumResponseDto[$key]" is missing from JSON.');
|
||||
// assert(json[key] != null, 'Required key "AlbumResponseDto[$key]" has a null value in JSON.');
|
||||
// });
|
||||
// return true;
|
||||
// }());
|
||||
assert(() {
|
||||
requiredKeys.forEach((key) {
|
||||
assert(json.containsKey(key), 'Required key "AlbumResponseDto[$key]" is missing from JSON.');
|
||||
assert(json[key] != null, 'Required key "AlbumResponseDto[$key]" has a null value in JSON.');
|
||||
});
|
||||
return true;
|
||||
}());
|
||||
|
||||
return AlbumResponseDto(
|
||||
assetCount: mapValueOfType<int>(json, r'assetCount')!,
|
||||
@@ -115,8 +112,7 @@ class AlbumResponseDto {
|
||||
ownerId: mapValueOfType<String>(json, r'ownerId')!,
|
||||
albumName: mapValueOfType<String>(json, r'albumName')!,
|
||||
createdAt: mapValueOfType<String>(json, r'createdAt')!,
|
||||
albumThumbnailAssetId:
|
||||
mapValueOfType<String>(json, r'albumThumbnailAssetId'),
|
||||
albumThumbnailAssetId: mapValueOfType<String>(json, r'albumThumbnailAssetId'),
|
||||
shared: mapValueOfType<bool>(json, r'shared')!,
|
||||
sharedUsers: UserResponseDto.listFromJson(json[r'sharedUsers'])!,
|
||||
assets: AssetResponseDto.listFromJson(json[r'assets'])!,
|
||||
@@ -125,10 +121,7 @@ class AlbumResponseDto {
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<AlbumResponseDto>? listFromJson(
|
||||
dynamic json, {
|
||||
bool growable = false,
|
||||
}) {
|
||||
static List<AlbumResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <AlbumResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
@@ -156,18 +149,12 @@ class AlbumResponseDto {
|
||||
}
|
||||
|
||||
// maps a json object with a list of AlbumResponseDto-objects as value to a dart map
|
||||
static Map<String, List<AlbumResponseDto>> mapListFromJson(
|
||||
dynamic json, {
|
||||
bool growable = false,
|
||||
}) {
|
||||
static Map<String, List<AlbumResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<AlbumResponseDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = AlbumResponseDto.listFromJson(
|
||||
entry.value,
|
||||
growable: growable,
|
||||
);
|
||||
final value = AlbumResponseDto.listFromJson(entry.value, growable: growable,);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
@@ -189,3 +176,4 @@ class AlbumResponseDto {
|
||||
'assets',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
133
mobile/openapi/lib/model/asset_response_dto.dart
generated
133
mobile/openapi/lib/model/asset_response_dto.dart
generated
@@ -82,76 +82,73 @@ class AssetResponseDto {
|
||||
List<TagResponseDto> tags;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is AssetResponseDto &&
|
||||
other.type == type &&
|
||||
other.id == id &&
|
||||
other.deviceAssetId == deviceAssetId &&
|
||||
other.ownerId == ownerId &&
|
||||
other.deviceId == deviceId &&
|
||||
other.originalPath == originalPath &&
|
||||
other.resizePath == resizePath &&
|
||||
other.createdAt == createdAt &&
|
||||
other.modifiedAt == modifiedAt &&
|
||||
other.isFavorite == isFavorite &&
|
||||
other.mimeType == mimeType &&
|
||||
other.duration == duration &&
|
||||
other.webpPath == webpPath &&
|
||||
other.encodedVideoPath == encodedVideoPath &&
|
||||
other.exifInfo == exifInfo &&
|
||||
other.smartInfo == smartInfo &&
|
||||
other.livePhotoVideoId == livePhotoVideoId &&
|
||||
other.tags == tags;
|
||||
bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto &&
|
||||
other.type == type &&
|
||||
other.id == id &&
|
||||
other.deviceAssetId == deviceAssetId &&
|
||||
other.ownerId == ownerId &&
|
||||
other.deviceId == deviceId &&
|
||||
other.originalPath == originalPath &&
|
||||
other.resizePath == resizePath &&
|
||||
other.createdAt == createdAt &&
|
||||
other.modifiedAt == modifiedAt &&
|
||||
other.isFavorite == isFavorite &&
|
||||
other.mimeType == mimeType &&
|
||||
other.duration == duration &&
|
||||
other.webpPath == webpPath &&
|
||||
other.encodedVideoPath == encodedVideoPath &&
|
||||
other.exifInfo == exifInfo &&
|
||||
other.smartInfo == smartInfo &&
|
||||
other.livePhotoVideoId == livePhotoVideoId &&
|
||||
other.tags == tags;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(type.hashCode) +
|
||||
(id.hashCode) +
|
||||
(deviceAssetId.hashCode) +
|
||||
(ownerId.hashCode) +
|
||||
(deviceId.hashCode) +
|
||||
(originalPath.hashCode) +
|
||||
(resizePath == null ? 0 : resizePath!.hashCode) +
|
||||
(createdAt.hashCode) +
|
||||
(modifiedAt.hashCode) +
|
||||
(isFavorite.hashCode) +
|
||||
(mimeType == null ? 0 : mimeType!.hashCode) +
|
||||
(duration.hashCode) +
|
||||
(webpPath == null ? 0 : webpPath!.hashCode) +
|
||||
(encodedVideoPath == null ? 0 : encodedVideoPath!.hashCode) +
|
||||
(exifInfo == null ? 0 : exifInfo!.hashCode) +
|
||||
(smartInfo == null ? 0 : smartInfo!.hashCode) +
|
||||
(livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) +
|
||||
(tags.hashCode);
|
||||
// ignore: unnecessary_parenthesis
|
||||
(type.hashCode) +
|
||||
(id.hashCode) +
|
||||
(deviceAssetId.hashCode) +
|
||||
(ownerId.hashCode) +
|
||||
(deviceId.hashCode) +
|
||||
(originalPath.hashCode) +
|
||||
(resizePath == null ? 0 : resizePath!.hashCode) +
|
||||
(createdAt.hashCode) +
|
||||
(modifiedAt.hashCode) +
|
||||
(isFavorite.hashCode) +
|
||||
(mimeType == null ? 0 : mimeType!.hashCode) +
|
||||
(duration.hashCode) +
|
||||
(webpPath == null ? 0 : webpPath!.hashCode) +
|
||||
(encodedVideoPath == null ? 0 : encodedVideoPath!.hashCode) +
|
||||
(exifInfo == null ? 0 : exifInfo!.hashCode) +
|
||||
(smartInfo == null ? 0 : smartInfo!.hashCode) +
|
||||
(livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) +
|
||||
(tags.hashCode);
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'AssetResponseDto[type=$type, id=$id, deviceAssetId=$deviceAssetId, ownerId=$ownerId, deviceId=$deviceId, originalPath=$originalPath, resizePath=$resizePath, createdAt=$createdAt, modifiedAt=$modifiedAt, isFavorite=$isFavorite, mimeType=$mimeType, duration=$duration, webpPath=$webpPath, encodedVideoPath=$encodedVideoPath, exifInfo=$exifInfo, smartInfo=$smartInfo, livePhotoVideoId=$livePhotoVideoId, tags=$tags]';
|
||||
String toString() => 'AssetResponseDto[type=$type, id=$id, deviceAssetId=$deviceAssetId, ownerId=$ownerId, deviceId=$deviceId, originalPath=$originalPath, resizePath=$resizePath, createdAt=$createdAt, modifiedAt=$modifiedAt, isFavorite=$isFavorite, mimeType=$mimeType, duration=$duration, webpPath=$webpPath, encodedVideoPath=$encodedVideoPath, exifInfo=$exifInfo, smartInfo=$smartInfo, livePhotoVideoId=$livePhotoVideoId, tags=$tags]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'type'] = this.type;
|
||||
json[r'id'] = this.id;
|
||||
json[r'deviceAssetId'] = this.deviceAssetId;
|
||||
json[r'ownerId'] = this.ownerId;
|
||||
json[r'deviceId'] = this.deviceId;
|
||||
json[r'originalPath'] = this.originalPath;
|
||||
json[r'type'] = this.type;
|
||||
json[r'id'] = this.id;
|
||||
json[r'deviceAssetId'] = this.deviceAssetId;
|
||||
json[r'ownerId'] = this.ownerId;
|
||||
json[r'deviceId'] = this.deviceId;
|
||||
json[r'originalPath'] = this.originalPath;
|
||||
if (this.resizePath != null) {
|
||||
json[r'resizePath'] = this.resizePath;
|
||||
} else {
|
||||
// json[r'resizePath'] = null;
|
||||
}
|
||||
json[r'createdAt'] = this.createdAt;
|
||||
json[r'modifiedAt'] = this.modifiedAt;
|
||||
json[r'isFavorite'] = this.isFavorite;
|
||||
json[r'createdAt'] = this.createdAt;
|
||||
json[r'modifiedAt'] = this.modifiedAt;
|
||||
json[r'isFavorite'] = this.isFavorite;
|
||||
if (this.mimeType != null) {
|
||||
json[r'mimeType'] = this.mimeType;
|
||||
} else {
|
||||
// json[r'mimeType'] = null;
|
||||
}
|
||||
json[r'duration'] = this.duration;
|
||||
json[r'duration'] = this.duration;
|
||||
if (this.webpPath != null) {
|
||||
json[r'webpPath'] = this.webpPath;
|
||||
} else {
|
||||
@@ -177,7 +174,7 @@ class AssetResponseDto {
|
||||
} else {
|
||||
// json[r'livePhotoVideoId'] = null;
|
||||
}
|
||||
json[r'tags'] = this.tags;
|
||||
json[r'tags'] = this.tags;
|
||||
return json;
|
||||
}
|
||||
|
||||
@@ -191,13 +188,13 @@ class AssetResponseDto {
|
||||
// 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 "AssetResponseDto[$key]" is missing from JSON.');
|
||||
// assert(json[key] != null, 'Required key "AssetResponseDto[$key]" has a null value in JSON.');
|
||||
// });
|
||||
// return true;
|
||||
// }());
|
||||
assert(() {
|
||||
requiredKeys.forEach((key) {
|
||||
assert(json.containsKey(key), 'Required key "AssetResponseDto[$key]" is missing from JSON.');
|
||||
assert(json[key] != null, 'Required key "AssetResponseDto[$key]" has a null value in JSON.');
|
||||
});
|
||||
return true;
|
||||
}());
|
||||
|
||||
return AssetResponseDto(
|
||||
type: AssetTypeEnum.fromJson(json[r'type'])!,
|
||||
@@ -223,10 +220,7 @@ class AssetResponseDto {
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<AssetResponseDto>? listFromJson(
|
||||
dynamic json, {
|
||||
bool growable = false,
|
||||
}) {
|
||||
static List<AssetResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <AssetResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
@@ -254,18 +248,12 @@ class AssetResponseDto {
|
||||
}
|
||||
|
||||
// maps a json object with a list of AssetResponseDto-objects as value to a dart map
|
||||
static Map<String, List<AssetResponseDto>> mapListFromJson(
|
||||
dynamic json, {
|
||||
bool growable = false,
|
||||
}) {
|
||||
static Map<String, List<AssetResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<AssetResponseDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = AssetResponseDto.listFromJson(
|
||||
entry.value,
|
||||
growable: growable,
|
||||
);
|
||||
final value = AssetResponseDto.listFromJson(entry.value, growable: growable,);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
@@ -292,3 +280,4 @@ class AssetResponseDto {
|
||||
'tags',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
2
mobile/openapi/test/asset_api_test.dart
generated
2
mobile/openapi/test/asset_api_test.dart
generated
@@ -47,7 +47,7 @@ void main() {
|
||||
|
||||
//
|
||||
//
|
||||
//Future<Object> downloadFile(String assetId, { bool isThumb, bool isWeb }) async
|
||||
//Future<Object> downloadFile(String assetId) async
|
||||
test('test downloadFile', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ FROM node:16-alpine3.14 as builder
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN apk add --update-cache build-base python3 libheif vips-dev ffmpeg exiftool perl
|
||||
RUN apk add --update-cache build-base python3 libheif vips-dev ffmpeg perl
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
|
||||
@@ -14,14 +14,14 @@ COPY . .
|
||||
FROM builder as prod
|
||||
|
||||
RUN npm run build
|
||||
RUN npm prune --omit=dev
|
||||
RUN npm prune --omit=dev --omit=optional
|
||||
|
||||
|
||||
FROM node:16-alpine3.14
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN apk add --no-cache libheif vips ffmpeg exiftool perl
|
||||
RUN apk add --no-cache libheif vips ffmpeg perl
|
||||
|
||||
COPY --from=prod /usr/src/app/node_modules ./node_modules
|
||||
COPY --from=prod /usr/src/app/dist ./dist
|
||||
@@ -32,7 +32,7 @@ COPY LICENSE /LICENSE
|
||||
COPY package.json package-lock.json ./
|
||||
COPY start-server.sh start-microservices.sh ./
|
||||
|
||||
RUN npm link
|
||||
RUN npm link && npm cache clean --force
|
||||
|
||||
VOLUME /usr/src/app/upload
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
Put,
|
||||
UploadedFiles,
|
||||
Patch,
|
||||
StreamableFile,
|
||||
} from '@nestjs/common';
|
||||
import { Authenticated } from '../../decorators/authenticated.decorator';
|
||||
import { AssetService } from './asset.service';
|
||||
@@ -28,7 +29,7 @@ import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
|
||||
import { ApiBearerAuth, ApiBody, ApiConsumes, ApiHeader, ApiTags } from '@nestjs/swagger';
|
||||
import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
|
||||
import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
|
||||
import { AssetResponseDto } from '@app/domain';
|
||||
import { AssetResponseDto, ImmichReadStream } from '@app/domain';
|
||||
import { CheckDuplicateAssetResponseDto } from './response-dto/check-duplicate-asset-response.dto';
|
||||
import { AssetFileUploadDto } from './dto/asset-file-upload.dto';
|
||||
import { CreateAssetDto, mapToUploadFile } from './dto/create-asset.dto';
|
||||
@@ -55,6 +56,10 @@ import { UpdateAssetsToSharedLinkDto } from './dto/add-assets-to-shared-link.dto
|
||||
import { AssetSearchDto } from './dto/asset-search.dto';
|
||||
import { assetUploadOption, ImmichFile } from '../../config/asset-upload.config';
|
||||
|
||||
function asStreamableFile({ stream, type, length }: ImmichReadStream) {
|
||||
return new StreamableFile(stream, { type, length });
|
||||
}
|
||||
|
||||
@ApiBearerAuth()
|
||||
@ApiTags('Asset')
|
||||
@Controller('asset')
|
||||
@@ -92,7 +97,7 @@ export class AssetController {
|
||||
|
||||
const responseDto = await this.assetService.uploadFile(authUser, dto, file, livePhotoFile);
|
||||
if (responseDto.duplicate) {
|
||||
res.send(200);
|
||||
res.status(200);
|
||||
}
|
||||
|
||||
return responseDto;
|
||||
@@ -103,12 +108,9 @@ export class AssetController {
|
||||
async downloadFile(
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
@Response({ passthrough: true }) res: Res,
|
||||
@Query(new ValidationPipe({ transform: true })) query: ServeFileDto,
|
||||
@Param('assetId') assetId: string,
|
||||
): Promise<any> {
|
||||
this.assetService.checkDownloadAccess(authUser);
|
||||
await this.assetService.checkAssetsAccess(authUser, [assetId]);
|
||||
return this.assetService.downloadFile(query, assetId, res);
|
||||
return this.assetService.downloadFile(authUser, assetId).then(asStreamableFile);
|
||||
}
|
||||
|
||||
@Authenticated({ isShared: true })
|
||||
|
||||
@@ -9,12 +9,13 @@ import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-use
|
||||
import { DownloadService } from '../../modules/download/download.service';
|
||||
import { AlbumRepository, IAlbumRepository } from '../album/album-repository';
|
||||
import { StorageService } from '@app/storage';
|
||||
import { ICryptoRepository, IJobRepository, ISharedLinkRepository, JobName } from '@app/domain';
|
||||
import { ICryptoRepository, IJobRepository, ISharedLinkRepository, IStorageRepository, JobName } from '@app/domain';
|
||||
import {
|
||||
authStub,
|
||||
newCryptoRepositoryMock,
|
||||
newJobRepositoryMock,
|
||||
newSharedLinkRepositoryMock,
|
||||
newStorageRepositoryMock,
|
||||
sharedLinkResponseStub,
|
||||
sharedLinkStub,
|
||||
} from '@app/domain/../test';
|
||||
@@ -110,6 +111,7 @@ describe('AssetService', () => {
|
||||
let sharedLinkRepositoryMock: jest.Mocked<ISharedLinkRepository>;
|
||||
let cryptoMock: jest.Mocked<ICryptoRepository>;
|
||||
let jobMock: jest.Mocked<IJobRepository>;
|
||||
let storageMock: jest.Mocked<IStorageRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
assetRepositoryMock = {
|
||||
@@ -154,6 +156,7 @@ describe('AssetService', () => {
|
||||
sharedLinkRepositoryMock = newSharedLinkRepositoryMock();
|
||||
jobMock = newJobRepositoryMock();
|
||||
cryptoMock = newCryptoRepositoryMock();
|
||||
storageMock = newStorageRepositoryMock();
|
||||
|
||||
sut = new AssetService(
|
||||
assetRepositoryMock,
|
||||
@@ -164,6 +167,7 @@ describe('AssetService', () => {
|
||||
sharedLinkRepositoryMock,
|
||||
jobMock,
|
||||
cryptoMock,
|
||||
storageMock,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -413,4 +417,15 @@ describe('AssetService', () => {
|
||||
expect(() => sut.checkDownloadAccess(authStub.readonlySharedLink)).toThrow(ForbiddenException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('downloadFile', () => {
|
||||
it('should download a single file', async () => {
|
||||
assetRepositoryMock.countByIdAndUser.mockResolvedValue(1);
|
||||
assetRepositoryMock.get.mockResolvedValue(_getAsset_1());
|
||||
|
||||
await sut.downloadFile(authStub.admin, 'id_1');
|
||||
|
||||
expect(storageMock.createReadStream).toHaveBeenCalledWith('fake_path/asset_1.jpeg', 'image/jpeg');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
StreamableFile,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { QueryFailedError, Repository } from 'typeorm';
|
||||
import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
||||
import { AssetEntity, AssetType, SharedLinkType } from '@app/infra';
|
||||
@@ -23,7 +22,14 @@ import { SearchAssetDto } from './dto/search-asset.dto';
|
||||
import fs from 'fs/promises';
|
||||
import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
|
||||
import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
|
||||
import { AssetResponseDto, JobName, mapAsset, mapAssetWithoutExif } from '@app/domain';
|
||||
import {
|
||||
AssetResponseDto,
|
||||
ImmichReadStream,
|
||||
IStorageRepository,
|
||||
JobName,
|
||||
mapAsset,
|
||||
mapAssetWithoutExif,
|
||||
} from '@app/domain';
|
||||
import { CreateAssetDto, UploadFile } from './dto/create-asset.dto';
|
||||
import { DeleteAssetResponseDto, DeleteAssetStatusEnum } from './response-dto/delete-asset-response.dto';
|
||||
import { GetAssetThumbnailDto, GetAssetThumbnailFormatEnum } from './dto/get-asset-thumbnail.dto';
|
||||
@@ -73,6 +79,7 @@ export class AssetService {
|
||||
@Inject(ISharedLinkRepository) sharedLinkRepository: ISharedLinkRepository,
|
||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||
@Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
|
||||
@Inject(IStorageRepository) private storage: IStorageRepository,
|
||||
) {
|
||||
this.assetCore = new AssetCore(_assetRepository, jobRepository, storageService);
|
||||
this.shareCore = new ShareCore(sharedLinkRepository, cryptoRepository);
|
||||
@@ -189,62 +196,21 @@ export class AssetService {
|
||||
return this.downloadService.downloadArchive(`immich-${now}`, assetToDownload);
|
||||
}
|
||||
|
||||
public async downloadFile(query: ServeFileDto, assetId: string, res: Res) {
|
||||
public async downloadFile(authUser: AuthUserDto, assetId: string): Promise<ImmichReadStream> {
|
||||
this.checkDownloadAccess(authUser);
|
||||
await this.checkAssetsAccess(authUser, [assetId]);
|
||||
|
||||
try {
|
||||
let fileReadStream = null;
|
||||
const asset = await this._assetRepository.getById(assetId);
|
||||
|
||||
// Download Video
|
||||
if (asset.type === AssetType.VIDEO) {
|
||||
const { size } = await fileInfo(asset.originalPath);
|
||||
|
||||
res.set({
|
||||
'Content-Type': asset.mimeType,
|
||||
'Content-Length': size,
|
||||
});
|
||||
|
||||
await fs.access(asset.originalPath, constants.R_OK | constants.W_OK);
|
||||
fileReadStream = createReadStream(asset.originalPath);
|
||||
} else {
|
||||
// Download Image
|
||||
if (!query.isThumb) {
|
||||
/**
|
||||
* Download Image Original File
|
||||
*/
|
||||
const { size } = await fileInfo(asset.originalPath);
|
||||
|
||||
res.set({
|
||||
'Content-Type': asset.mimeType,
|
||||
'Content-Length': size,
|
||||
});
|
||||
|
||||
await fs.access(asset.originalPath, constants.R_OK | constants.W_OK);
|
||||
fileReadStream = createReadStream(asset.originalPath);
|
||||
} else {
|
||||
/**
|
||||
* Download Image Resize File
|
||||
*/
|
||||
if (!asset.resizePath) {
|
||||
throw new NotFoundException('resizePath not set');
|
||||
}
|
||||
|
||||
const { size } = await fileInfo(asset.resizePath);
|
||||
|
||||
res.set({
|
||||
'Content-Type': 'image/jpeg',
|
||||
'Content-Length': size,
|
||||
});
|
||||
|
||||
await fs.access(asset.resizePath, constants.R_OK | constants.W_OK);
|
||||
fileReadStream = createReadStream(asset.resizePath);
|
||||
}
|
||||
const asset = await this._assetRepository.get(assetId);
|
||||
if (asset && asset.originalPath && asset.mimeType) {
|
||||
return this.storage.createReadStream(asset.originalPath, asset.mimeType);
|
||||
}
|
||||
|
||||
return new StreamableFile(fileReadStream);
|
||||
} catch (e) {
|
||||
Logger.error(`Error download asset ${e}`, 'downloadFile');
|
||||
throw new InternalServerErrorException(`Failed to download asset ${e}`, 'DownloadFile');
|
||||
}
|
||||
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
public async getAssetThumbnail(
|
||||
@@ -255,8 +221,7 @@ export class AssetService {
|
||||
) {
|
||||
let fileReadStream: ReadStream;
|
||||
|
||||
const asset = await this.assetRepository.findOne({ where: { id: assetId } });
|
||||
|
||||
const asset = await this._assetRepository.get(assetId);
|
||||
if (!asset) {
|
||||
throw new NotFoundException('Asset not found');
|
||||
}
|
||||
@@ -460,7 +425,7 @@ export class AssetService {
|
||||
try {
|
||||
await this._assetRepository.remove(asset);
|
||||
|
||||
result.push({ id: asset.id, status: DeleteAssetStatusEnum.SUCCESS });
|
||||
result.push({ id, status: DeleteAssetStatusEnum.SUCCESS });
|
||||
deleteQueue.push(asset as any);
|
||||
|
||||
// TODO refactor this to use cascades
|
||||
@@ -584,18 +549,6 @@ export class AssetService {
|
||||
return this._assetRepository.getAssetByChecksum(userId, checksum);
|
||||
}
|
||||
|
||||
calculateChecksum(filePath: string): Promise<Buffer> {
|
||||
const fileReadStream = createReadStream(filePath);
|
||||
const sha1Hash = createHash('sha1');
|
||||
const deferred = new Promise<Buffer>((resolve, reject) => {
|
||||
sha1Hash.once('error', (err) => reject(err));
|
||||
sha1Hash.once('finish', () => resolve(sha1Hash.read()));
|
||||
});
|
||||
|
||||
fileReadStream.pipe(sha1Hash);
|
||||
return deferred;
|
||||
}
|
||||
|
||||
getAssetCountByUserId(authUser: AuthUserDto): Promise<AssetCountByUserIdResponseDto> {
|
||||
return this._assetRepository.getAssetCountByUserId(authUser.id);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { JobService } from './job.service';
|
||||
import { JobController } from './job.controller';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ExifEntity } from '@app/infra';
|
||||
import { TagModule } from '../tag/tag.module';
|
||||
import { AssetModule } from '../asset/asset.module';
|
||||
import { StorageModule } from '@app/storage';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([ExifEntity]), TagModule, AssetModule, StorageModule],
|
||||
imports: [AssetModule],
|
||||
controllers: [JobController],
|
||||
providers: [JobService],
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { immichAppConfig } from '@app/common/config';
|
||||
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AssetModule } from './api-v1/asset/asset.module';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { ServerInfoModule } from './api-v1/server-info/server-info.module';
|
||||
@@ -61,12 +61,4 @@ import { AuthGuard } from './middlewares/auth.guard';
|
||||
],
|
||||
providers: [{ provide: APP_GUARD, useExisting: AuthGuard }, AuthGuard],
|
||||
})
|
||||
export class AppModule implements NestModule {
|
||||
// TODO: check if consumer is needed or remove
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
configure(consumer: MiddlewareConsumer): void {
|
||||
if (process.env.NODE_ENV == 'development') {
|
||||
// consumer.apply(AppLoggerMiddleware).forRoutes('*');
|
||||
}
|
||||
}
|
||||
}
|
||||
export class AppModule {}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Injectable, NestMiddleware, Logger } from '@nestjs/common';
|
||||
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
|
||||
@Injectable()
|
||||
export class AppLoggerMiddleware implements NestMiddleware {
|
||||
private logger = new Logger('HTTP');
|
||||
|
||||
use(request: Request, response: Response, next: NextFunction): void {
|
||||
const { ip, method, baseUrl } = request;
|
||||
const userAgent = request.get('user-agent') || '';
|
||||
|
||||
response.on('close', () => {
|
||||
const { statusCode } = response;
|
||||
const contentLength = response.get('content-length');
|
||||
|
||||
this.logger.log(`${method} ${baseUrl} ${statusCode} ${contentLength} - ${userAgent} ${ip}`);
|
||||
});
|
||||
|
||||
next();
|
||||
}
|
||||
}
|
||||
@@ -1109,24 +1109,6 @@
|
||||
"operationId": "downloadFile",
|
||||
"description": "",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "isThumb",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"title": "Is serve thumbnail (resize) file",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "isWeb",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"title": "Is request made from web",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "assetId",
|
||||
"required": true,
|
||||
@@ -2707,7 +2689,7 @@
|
||||
"info": {
|
||||
"title": "Immich",
|
||||
"description": "Immich API",
|
||||
"version": "1.43.1",
|
||||
"version": "1.45.0",
|
||||
"contact": {}
|
||||
},
|
||||
"tags": [],
|
||||
|
||||
@@ -6,11 +6,6 @@ import { ICryptoRepository } from '../crypto/crypto.repository';
|
||||
import { LoginResponseDto, mapLoginResponse } from './response-dto';
|
||||
import { IUserTokenRepository, UserTokenCore } from '../user-token';
|
||||
|
||||
export type JwtValidationResult = {
|
||||
status: boolean;
|
||||
userId: string | null;
|
||||
};
|
||||
|
||||
export class AuthCore {
|
||||
private userTokenCore: UserTokenCore;
|
||||
constructor(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export * from './auth-user.dto';
|
||||
export * from './change-password.dto';
|
||||
export * from './jwt-payload.dto';
|
||||
export * from './login-credential.dto';
|
||||
export * from './sign-up.dto';
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export class JwtPayloadDto {
|
||||
userId!: string;
|
||||
email!: string;
|
||||
}
|
||||
@@ -8,6 +8,7 @@ export * from './domain.module';
|
||||
export * from './job';
|
||||
export * from './oauth';
|
||||
export * from './share';
|
||||
export * from './storage';
|
||||
export * from './system-config';
|
||||
export * from './tag';
|
||||
export * from './user';
|
||||
|
||||
1
server/libs/domain/src/storage/index.ts
Normal file
1
server/libs/domain/src/storage/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './storage.repository';
|
||||
13
server/libs/domain/src/storage/storage.repository.ts
Normal file
13
server/libs/domain/src/storage/storage.repository.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { ReadStream } from 'fs';
|
||||
|
||||
export interface ImmichReadStream {
|
||||
stream: ReadStream;
|
||||
type: string;
|
||||
length: number;
|
||||
}
|
||||
|
||||
export const IStorageRepository = 'IStorageRepository';
|
||||
|
||||
export interface IStorageRepository {
|
||||
createReadStream(filepath: string, mimeType: string): Promise<ImmichReadStream>;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ export * from './device-info.repository.mock';
|
||||
export * from './fixtures';
|
||||
export * from './job.repository.mock';
|
||||
export * from './shared-link.repository.mock';
|
||||
export * from './storage.repository.mock';
|
||||
export * from './system-config.repository.mock';
|
||||
export * from './user-token.repository.mock';
|
||||
export * from './user.repository.mock';
|
||||
|
||||
7
server/libs/domain/test/storage.repository.mock.ts
Normal file
7
server/libs/domain/test/storage.repository.mock.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { IStorageRepository } from '../src';
|
||||
|
||||
export const newStorageRepositoryMock = (): jest.Mocked<IStorageRepository> => {
|
||||
return {
|
||||
createReadStream: jest.fn(),
|
||||
};
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
IJobRepository,
|
||||
IKeyRepository,
|
||||
ISharedLinkRepository,
|
||||
IStorageRepository,
|
||||
ISystemConfigRepository,
|
||||
IUserRepository,
|
||||
QueueName,
|
||||
@@ -29,6 +30,7 @@ import {
|
||||
UserTokenEntity,
|
||||
} from './db';
|
||||
import { JobRepository } from './job';
|
||||
import { FilesystemProvider } from './storage';
|
||||
|
||||
const providers: Provider[] = [
|
||||
{ provide: ICryptoRepository, useClass: CryptoRepository },
|
||||
@@ -36,6 +38,7 @@ const providers: Provider[] = [
|
||||
{ provide: IKeyRepository, useClass: APIKeyRepository },
|
||||
{ provide: IJobRepository, useClass: JobRepository },
|
||||
{ provide: ISharedLinkRepository, useClass: SharedLinkRepository },
|
||||
{ provide: IStorageRepository, useClass: FilesystemProvider },
|
||||
{ provide: ISystemConfigRepository, useClass: SystemConfigRepository },
|
||||
{ provide: IUserRepository, useClass: UserRepository },
|
||||
{ provide: IUserTokenRepository, useClass: UserTokenRepository },
|
||||
|
||||
18
server/libs/infra/src/storage/filesystem.provider.ts
Normal file
18
server/libs/infra/src/storage/filesystem.provider.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { ImmichReadStream, IStorageRepository } from '@app/domain';
|
||||
import { constants, createReadStream, stat } from 'fs';
|
||||
import fs from 'fs/promises';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const fileInfo = promisify(stat);
|
||||
|
||||
export class FilesystemProvider implements IStorageRepository {
|
||||
async createReadStream(filepath: string, mimeType: string): Promise<ImmichReadStream> {
|
||||
const { size } = await fileInfo(filepath);
|
||||
await fs.access(filepath, constants.R_OK | constants.W_OK);
|
||||
return {
|
||||
stream: createReadStream(filepath),
|
||||
length: size,
|
||||
type: mimeType,
|
||||
};
|
||||
}
|
||||
}
|
||||
1
server/libs/infra/src/storage/index.ts
Normal file
1
server/libs/infra/src/storage/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './filesystem.provider';
|
||||
1247
server/package-lock.json
generated
1247
server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich",
|
||||
"version": "1.44.0",
|
||||
"version": "1.45.0",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
@@ -42,7 +42,6 @@
|
||||
"@nestjs/common": "^9.2.1",
|
||||
"@nestjs/config": "^2.2.0",
|
||||
"@nestjs/core": "^9.2.1",
|
||||
"@nestjs/mapped-types": "1.2.0",
|
||||
"@nestjs/platform-express": "^9.2.1",
|
||||
"@nestjs/platform-socket.io": "^9.2.1",
|
||||
"@nestjs/schedule": "^2.1.0",
|
||||
@@ -58,15 +57,11 @@
|
||||
"class-validator": "^0.13.2",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"diskusage": "^1.1.3",
|
||||
"dotenv": "^14.2.0",
|
||||
"exiftool-vendored": "^19.0.0",
|
||||
"fdir": "^5.3.0",
|
||||
"exiftool-vendored.pl": "^12.54.0",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
"geo-tz": "^7.0.2",
|
||||
"handlebars": "^4.7.7",
|
||||
"i18n-iso-countries": "^7.5.0",
|
||||
"ioredis": "^5.2.4",
|
||||
"jest-when": "^3.5.2",
|
||||
"joi": "^17.5.0",
|
||||
"local-reverse-geocoder": "0.12.5",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -77,11 +72,9 @@
|
||||
"pg": "^8.8.0",
|
||||
"redis": "^4.5.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rimraf": "^3.0.2",
|
||||
"rxjs": "^7.2.0",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"sharp": "^0.28.0",
|
||||
"systeminformation": "^5.11.0",
|
||||
"typeorm": "^0.3.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -107,11 +100,14 @@
|
||||
"@types/supertest": "^2.0.11",
|
||||
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
||||
"@typescript-eslint/parser": "^5.48.1",
|
||||
"dotenv": "^14.2.0",
|
||||
"eslint": "^8.31.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"jest": "^27.2.5",
|
||||
"jest-when": "^3.5.2",
|
||||
"prettier": "^2.3.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"source-map-support": "^0.5.20",
|
||||
"supertest": "^6.1.3",
|
||||
"ts-jest": "^27.0.3",
|
||||
|
||||
@@ -1,29 +1,40 @@
|
||||
# Our Node base image
|
||||
FROM node:16-alpine3.14 as base
|
||||
|
||||
COPY LICENSE /licenses/LICENSE.txt
|
||||
COPY LICENSE /LICENSE
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
EXPOSE 3000
|
||||
RUN apk add --no-cache setpriv
|
||||
|
||||
FROM base as builder
|
||||
|
||||
RUN chown node:node /usr/src/app
|
||||
|
||||
RUN apk add --no-cache setpriv
|
||||
|
||||
COPY --chown=node:node package*.json ./
|
||||
|
||||
RUN npm ci
|
||||
|
||||
COPY --chown=node:node . .
|
||||
|
||||
RUN npm run build
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
FROM base AS dev
|
||||
FROM builder AS dev
|
||||
ENV CHOKIDAR_USEPOLLING=true
|
||||
EXPOSE 24678
|
||||
CMD ["npm", "run", "dev"]
|
||||
|
||||
FROM base as prod
|
||||
FROM builder AS prod
|
||||
|
||||
RUN npm run build
|
||||
RUN npm prune --omit=dev
|
||||
|
||||
FROM base
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY --from=prod /usr/src/app/node_modules ./node_modules
|
||||
COPY --from=prod /usr/src/app/build ./build
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
COPY entrypoint.sh ./
|
||||
|
||||
7
web/package-lock.json
generated
7
web/package-lock.json
generated
@@ -14,7 +14,6 @@
|
||||
"exifr": "^7.1.3",
|
||||
"handlebars": "^4.7.7",
|
||||
"leaflet": "^1.8.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.21",
|
||||
"luxon": "^3.1.1",
|
||||
"socket.io-client": "^4.5.1",
|
||||
@@ -8999,7 +8998,8 @@
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
"version": "4.17.21",
|
||||
@@ -17880,7 +17880,8 @@
|
||||
"lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
},
|
||||
"lodash-es": {
|
||||
"version": "4.17.21",
|
||||
|
||||
@@ -66,7 +66,6 @@
|
||||
"exifr": "^7.1.3",
|
||||
"handlebars": "^4.7.7",
|
||||
"leaflet": "^1.8.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.21",
|
||||
"luxon": "^3.1.1",
|
||||
"socket.io-client": "^4.5.1",
|
||||
|
||||
32
web/src/api/open-api/api.ts
generated
32
web/src/api/open-api/api.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.43.0
|
||||
* The version of the OpenAPI document: 1.43.1
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
@@ -3729,12 +3729,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
||||
/**
|
||||
*
|
||||
* @param {string} assetId
|
||||
* @param {boolean} [isThumb]
|
||||
* @param {boolean} [isWeb]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
downloadFile: async (assetId: string, isThumb?: boolean, isWeb?: boolean, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
downloadFile: async (assetId: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'assetId' is not null or undefined
|
||||
assertParamExists('downloadFile', 'assetId', assetId)
|
||||
const localVarPath = `/asset/download/{assetId}`
|
||||
@@ -3754,14 +3752,6 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
if (isThumb !== undefined) {
|
||||
localVarQueryParameter['isThumb'] = isThumb;
|
||||
}
|
||||
|
||||
if (isWeb !== undefined) {
|
||||
localVarQueryParameter['isWeb'] = isWeb;
|
||||
}
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
@@ -4489,13 +4479,11 @@ export const AssetApiFp = function(configuration?: Configuration) {
|
||||
/**
|
||||
*
|
||||
* @param {string} assetId
|
||||
* @param {boolean} [isThumb]
|
||||
* @param {boolean} [isWeb]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async downloadFile(assetId: string, isThumb?: boolean, isWeb?: boolean, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.downloadFile(assetId, isThumb, isWeb, options);
|
||||
async downloadFile(assetId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.downloadFile(assetId, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
@@ -4719,13 +4707,11 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
|
||||
/**
|
||||
*
|
||||
* @param {string} assetId
|
||||
* @param {boolean} [isThumb]
|
||||
* @param {boolean} [isWeb]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
downloadFile(assetId: string, isThumb?: boolean, isWeb?: boolean, options?: any): AxiosPromise<object> {
|
||||
return localVarFp.downloadFile(assetId, isThumb, isWeb, options).then((request) => request(axios, basePath));
|
||||
downloadFile(assetId: string, options?: any): AxiosPromise<object> {
|
||||
return localVarFp.downloadFile(assetId, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
@@ -4939,14 +4925,12 @@ export class AssetApi extends BaseAPI {
|
||||
/**
|
||||
*
|
||||
* @param {string} assetId
|
||||
* @param {boolean} [isThumb]
|
||||
* @param {boolean} [isWeb]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof AssetApi
|
||||
*/
|
||||
public downloadFile(assetId: string, isThumb?: boolean, isWeb?: boolean, options?: AxiosRequestConfig) {
|
||||
return AssetApiFp(this.configuration).downloadFile(assetId, isThumb, isWeb, options).then((request) => request(this.axios, this.basePath));
|
||||
public downloadFile(assetId: string, options?: AxiosRequestConfig) {
|
||||
return AssetApiFp(this.configuration).downloadFile(assetId, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
2
web/src/api/open-api/base.ts
generated
2
web/src/api/open-api/base.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.43.0
|
||||
* The version of the OpenAPI document: 1.43.1
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
web/src/api/open-api/common.ts
generated
2
web/src/api/open-api/common.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.43.0
|
||||
* The version of the OpenAPI document: 1.43.1
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
web/src/api/open-api/configuration.ts
generated
2
web/src/api/open-api/configuration.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.43.0
|
||||
* The version of the OpenAPI document: 1.43.1
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
web/src/api/open-api/index.ts
generated
2
web/src/api/open-api/index.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.43.0
|
||||
* The version of the OpenAPI document: 1.43.1
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
@@ -136,10 +136,8 @@
|
||||
|
||||
$downloadAssets[imageFileName] = 0;
|
||||
|
||||
const { data, status } = await api.assetApi.downloadFile(assetId, false, false, {
|
||||
params: {
|
||||
key
|
||||
},
|
||||
const { data, status } = await api.assetApi.downloadFile(assetId, {
|
||||
params: { key },
|
||||
responseType: 'blob',
|
||||
onDownloadProgress: (progressEvent) => {
|
||||
if (progressEvent.lengthComputable) {
|
||||
|
||||
Reference in New Issue
Block a user