mirror of
https://github.com/immich-app/immich.git
synced 2025-07-09 03:06:56 -04:00
Merge branch 'main' into main
This commit is contained in:
commit
086bbfcf56
@ -112,7 +112,8 @@ The `immich-server` container will need access to the gallery. Modify your docke
|
|||||||
```
|
```
|
||||||
|
|
||||||
:::tip
|
:::tip
|
||||||
The `ro` flag at the end only gives read-only access to the volumes. This will disallow the images from being deleted in the web UI.
|
The `ro` flag at the end only gives read-only access to the volumes.
|
||||||
|
This will disallow the images from being deleted in the web UI, or adding metadata to the library ([XMP sidecars](/docs/features/xmp-sidecars)).
|
||||||
:::
|
:::
|
||||||
|
|
||||||
:::info
|
:::info
|
||||||
|
@ -7,7 +7,7 @@ in a directory on the same machine.
|
|||||||
# Mount the directory into the containers.
|
# Mount the directory into the containers.
|
||||||
|
|
||||||
Edit `docker-compose.yml` to add one or more new mount points in the section `immich-server:` under `volumes:`.
|
Edit `docker-compose.yml` to add one or more new mount points in the section `immich-server:` under `volumes:`.
|
||||||
If you want Immich to be able to delete the images in the external library, remove `:ro` from the end of the mount point.
|
If you want Immich to be able to delete the images in the external library or add metadata ([XMP sidecars](/docs/features/xmp-sidecars)), remove `:ro` from the end of the mount point.
|
||||||
|
|
||||||
```diff
|
```diff
|
||||||
immich-server:
|
immich-server:
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
name: immich-e2e
|
name: immich-e2e
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
@ -236,6 +236,32 @@ describe('/users', () => {
|
|||||||
const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
|
const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
|
||||||
expect(after).toMatchObject({ download: { archiveSize: 1_234_567 } });
|
expect(after).toMatchObject({ download: { archiveSize: 1_234_567 } });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should require a boolean for download include embedded videos', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.put(`/users/me/preferences`)
|
||||||
|
.send({ download: { includeEmbeddedVideos: 1_234_567.89 } })
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest(['download.includeEmbeddedVideos must be a boolean value']));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update download include embedded videos', async () => {
|
||||||
|
const before = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
|
||||||
|
expect(before).toMatchObject({ download: { includeEmbeddedVideos: false } });
|
||||||
|
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.put(`/users/me/preferences`)
|
||||||
|
.send({ download: { includeEmbeddedVideos: true } })
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toMatchObject({ download: { includeEmbeddedVideos: true } });
|
||||||
|
|
||||||
|
const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
|
||||||
|
expect(after).toMatchObject({ download: { includeEmbeddedVideos: true } });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /users/:id', () => {
|
describe('GET /users/:id', () => {
|
||||||
|
@ -38,11 +38,17 @@ FROM python:3.11-slim-bookworm@sha256:1c0c54195c7c7b46e61a2f3b906e9b55a8165f2038
|
|||||||
|
|
||||||
FROM prod-cpu AS prod-openvino
|
FROM prod-cpu AS prod-openvino
|
||||||
|
|
||||||
COPY scripts/configure-apt.sh ./
|
RUN apt-get update && \
|
||||||
RUN ./configure-apt.sh && \
|
apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \
|
||||||
apt-get update && \
|
wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17193.4/intel-igc-core_1.0.17193.4_amd64.deb && \
|
||||||
apt-get install -t unstable --no-install-recommends -yqq intel-opencl-icd && \
|
wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17193.4/intel-igc-opencl_1.0.17193.4_amd64.deb && \
|
||||||
rm configure-apt.sh
|
wget https://github.com/intel/compute-runtime/releases/download/24.26.30049.6/intel-opencl-icd-dbgsym_24.26.30049.6_amd64.ddeb && \
|
||||||
|
wget https://github.com/intel/compute-runtime/releases/download/24.26.30049.6/intel-opencl-icd_24.26.30049.6_amd64.deb && \
|
||||||
|
wget https://github.com/intel/compute-runtime/releases/download/24.26.30049.6/libigdgmm12_22.3.20_amd64.deb && \
|
||||||
|
dpkg -i *.deb && \
|
||||||
|
rm *.deb && \
|
||||||
|
apt-get remove wget -yqq && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
FROM nvidia/cuda:12.3.2-cudnn9-runtime-ubuntu22.04@sha256:fa44193567d1908f7ca1f3abf8623ce9c63bc8cba7bcfdb32702eb04d326f7a8 AS prod-cuda
|
FROM nvidia/cuda:12.3.2-cudnn9-runtime-ubuntu22.04@sha256:fa44193567d1908f7ca1f3abf8623ce9c63bc8cba7bcfdb32702eb04d326f7a8 AS prod-cuda
|
||||||
|
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"flutter": "3.22.3"
|
"flutter": "3.24.0"
|
||||||
}
|
}
|
2
mobile/.vscode/settings.json
vendored
2
mobile/.vscode/settings.json
vendored
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"dart.flutterSdkPath": ".fvm/versions/3.22.3",
|
"dart.flutterSdkPath": ".fvm/versions/3.24.0",
|
||||||
"search.exclude": {
|
"search.exclude": {
|
||||||
"**/.fvm": true
|
"**/.fvm": true
|
||||||
},
|
},
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
<application android:label="Immich" android:name=".ImmichApp" android:usesCleartextTraffic="true"
|
<application android:label="Immich" android:name=".ImmichApp" android:usesCleartextTraffic="true"
|
||||||
android:icon="@mipmap/ic_launcher" android:requestLegacyExternalStorage="true"
|
android:icon="@mipmap/ic_launcher" android:requestLegacyExternalStorage="true"
|
||||||
android:largeHeap="true">
|
android:largeHeap="true" android:enableOnBackInvokedCallback="true">
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||||
@ -35,7 +35,7 @@
|
|||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="io.flutter.embedding.android.EnableImpeller"
|
android:name="io.flutter.embedding.android.EnableImpeller"
|
||||||
android:value="false" />
|
android:value="true" />
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.firebase.messaging.default_notification_icon"
|
android:name="com.google.firebase.messaging.default_notification_icon"
|
||||||
@ -84,10 +84,6 @@
|
|||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<queries>
|
<queries>
|
||||||
<intent>
|
<intent>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
@ -10,6 +10,18 @@ allprojects {
|
|||||||
rootProject.buildDir = '../build'
|
rootProject.buildDir = '../build'
|
||||||
|
|
||||||
subprojects {
|
subprojects {
|
||||||
|
// fix for verifyReleaseResources
|
||||||
|
// ============
|
||||||
|
afterEvaluate { project ->
|
||||||
|
if (project.plugins.hasPlugin("com.android.application") ||
|
||||||
|
project.plugins.hasPlugin("com.android.library")) {
|
||||||
|
project.android {
|
||||||
|
compileSdkVersion 34
|
||||||
|
buildToolsVersion "34.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ============
|
||||||
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,3 +36,4 @@ tasks.register("clean", Delete) {
|
|||||||
tasks.named('wrapper') {
|
tasks.named('wrapper') {
|
||||||
distributionType = Wrapper.DistributionType.ALL
|
distributionType = Wrapper.DistributionType.ALL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,7 +202,7 @@ SPEC CHECKSUMS:
|
|||||||
fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c
|
fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c
|
||||||
geolocator_apple: 6cbaf322953988e009e5ecb481f07efece75c450
|
geolocator_apple: 6cbaf322953988e009e5ecb481f07efece75c450
|
||||||
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||||
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
|
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
|
||||||
isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073
|
isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073
|
||||||
MapLibre: 620fc933c1d6029b33738c905c1490d024e5d4ef
|
MapLibre: 620fc933c1d6029b33738c905c1490d024e5d4ef
|
||||||
maplibre_gl: a2efec727dd340e4c65e26d2b03b584f14881fd9
|
maplibre_gl: a2efec727dd340e4c65e26d2b03b584f14881fd9
|
||||||
|
@ -6,7 +6,7 @@ import path_provider_ios
|
|||||||
import photo_manager
|
import photo_manager
|
||||||
import permission_handler_apple
|
import permission_handler_apple
|
||||||
|
|
||||||
@UIApplicationMain
|
@main
|
||||||
@objc class AppDelegate: FlutterAppDelegate {
|
@objc class AppDelegate: FlutterAppDelegate {
|
||||||
|
|
||||||
override func application(
|
override func application(
|
||||||
|
@ -39,7 +39,6 @@ import 'package:path_provider/path_provider.dart';
|
|||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
ImmichWidgetsBinding();
|
ImmichWidgetsBinding();
|
||||||
|
|
||||||
final db = await loadDb();
|
final db = await loadDb();
|
||||||
await initApp();
|
await initApp();
|
||||||
await migrateDatabaseIfNeeded(db);
|
await migrateDatabaseIfNeeded(db);
|
||||||
@ -73,6 +72,7 @@ Future<void> initApp() async {
|
|||||||
var log = Logger("ImmichErrorLogger");
|
var log = Logger("ImmichErrorLogger");
|
||||||
|
|
||||||
FlutterError.onError = (details) {
|
FlutterError.onError = (details) {
|
||||||
|
debugPrint("FlutterError - Catch all: $details");
|
||||||
FlutterError.presentError(details);
|
FlutterError.presentError(details);
|
||||||
log.severe(
|
log.severe(
|
||||||
'FlutterError - Catch all',
|
'FlutterError - Catch all',
|
||||||
|
@ -264,7 +264,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
return PopScope(
|
return PopScope(
|
||||||
// Change immersive mode back to normal "edgeToEdge" mode
|
// Change immersive mode back to normal "edgeToEdge" mode
|
||||||
onPopInvoked: (_) =>
|
onPopInvokedWithResult: (didPop, _) =>
|
||||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge),
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge),
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.black,
|
||||||
|
@ -74,7 +74,7 @@ class HeaderSettingsPage extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: PopScope(
|
body: PopScope(
|
||||||
onPopInvoked: (_) => saveHeaders(headers.value),
|
onPopInvokedWithResult: (didPop, _) => saveHeaders(headers.value),
|
||||||
child: ListView.separated(
|
child: ListView.separated(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 16.0),
|
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 16.0),
|
||||||
itemCount: list.length,
|
itemCount: list.length,
|
||||||
|
@ -177,7 +177,7 @@ class TabControllerPage extends HookConsumerWidget {
|
|||||||
final tabsRouter = AutoTabsRouter.of(context);
|
final tabsRouter = AutoTabsRouter.of(context);
|
||||||
return PopScope(
|
return PopScope(
|
||||||
canPop: tabsRouter.activeIndex == 0,
|
canPop: tabsRouter.activeIndex == 0,
|
||||||
onPopInvoked: (didPop) =>
|
onPopInvokedWithResult: (didPop, _) =>
|
||||||
!didPop ? tabsRouter.setActiveIndex(0) : null,
|
!didPop ? tabsRouter.setActiveIndex(0) : null,
|
||||||
child: LayoutBuilder(
|
child: LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
|
@ -123,7 +123,7 @@ class VideoViewerPage extends HookConsumerWidget {
|
|||||||
final size = MediaQuery.sizeOf(context);
|
final size = MediaQuery.sizeOf(context);
|
||||||
|
|
||||||
return PopScope(
|
return PopScope(
|
||||||
onPopInvoked: (pop) {
|
onPopInvokedWithResult: (didPop, _) {
|
||||||
ref.read(videoPlaybackValueProvider.notifier).value =
|
ref.read(videoPlaybackValueProvider.notifier).value =
|
||||||
VideoPlaybackValue.uninitialized();
|
VideoPlaybackValue.uninitialized();
|
||||||
},
|
},
|
||||||
|
@ -59,6 +59,8 @@ class DraggableScrollbar extends StatefulWidget {
|
|||||||
|
|
||||||
final Function(bool scrolling) scrollStateListener;
|
final Function(bool scrolling) scrollStateListener;
|
||||||
|
|
||||||
|
final double viewPortHeight;
|
||||||
|
|
||||||
DraggableScrollbar.semicircle({
|
DraggableScrollbar.semicircle({
|
||||||
super.key,
|
super.key,
|
||||||
Key? scrollThumbKey,
|
Key? scrollThumbKey,
|
||||||
@ -67,6 +69,7 @@ class DraggableScrollbar extends StatefulWidget {
|
|||||||
required this.controller,
|
required this.controller,
|
||||||
required this.itemPositionsListener,
|
required this.itemPositionsListener,
|
||||||
required this.scrollStateListener,
|
required this.scrollStateListener,
|
||||||
|
required this.viewPortHeight,
|
||||||
this.heightScrollThumb = 48.0,
|
this.heightScrollThumb = 48.0,
|
||||||
this.backgroundColor = Colors.white,
|
this.backgroundColor = Colors.white,
|
||||||
this.padding,
|
this.padding,
|
||||||
@ -251,7 +254,7 @@ class DraggableScrollbarState extends State<DraggableScrollbar>
|
|||||||
}
|
}
|
||||||
|
|
||||||
double get barMaxScrollExtent =>
|
double get barMaxScrollExtent =>
|
||||||
(context.size?.height ?? 0) -
|
widget.viewPortHeight -
|
||||||
widget.heightScrollThumb -
|
widget.heightScrollThumb -
|
||||||
(widget.heightOffset ?? 0);
|
(widget.heightOffset ?? 0);
|
||||||
|
|
||||||
@ -316,6 +319,7 @@ class DraggableScrollbarState extends State<DraggableScrollbar>
|
|||||||
}
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
|
try {
|
||||||
int firstItemIndex =
|
int firstItemIndex =
|
||||||
widget.itemPositionsListener.itemPositions.value.first.index;
|
widget.itemPositionsListener.itemPositions.value.first.index;
|
||||||
|
|
||||||
@ -347,6 +351,7 @@ class DraggableScrollbarState extends State<DraggableScrollbar>
|
|||||||
_fadeoutTimer = null;
|
_fadeoutTimer = null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} catch (_) {}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -262,8 +262,9 @@ class ImmichAssetGridViewState extends ConsumerState<ImmichAssetGridView> {
|
|||||||
shrinkWrap: widget.shrinkWrap,
|
shrinkWrap: widget.shrinkWrap,
|
||||||
);
|
);
|
||||||
|
|
||||||
final child = useDragScrolling
|
final child = (useDragScrolling && ModalRoute.of(context) != null)
|
||||||
? DraggableScrollbar.semicircle(
|
? DraggableScrollbar.semicircle(
|
||||||
|
viewPortHeight: context.height,
|
||||||
scrollStateListener: dragScrolling,
|
scrollStateListener: dragScrolling,
|
||||||
itemPositionsListener: _itemPositionsListener,
|
itemPositionsListener: _itemPositionsListener,
|
||||||
controller: _itemScrollController,
|
controller: _itemScrollController,
|
||||||
@ -281,6 +282,7 @@ class ImmichAssetGridViewState extends ConsumerState<ImmichAssetGridView> {
|
|||||||
child: listWidget,
|
child: listWidget,
|
||||||
)
|
)
|
||||||
: listWidget;
|
: listWidget;
|
||||||
|
|
||||||
return widget.onRefresh == null
|
return widget.onRefresh == null
|
||||||
? child
|
? child
|
||||||
: appBarOffset()
|
: appBarOffset()
|
||||||
@ -528,7 +530,7 @@ class ImmichAssetGridViewState extends ConsumerState<ImmichAssetGridView> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return PopScope(
|
return PopScope(
|
||||||
canPop: !(widget.selectionActive && _selectedAssets.isNotEmpty),
|
canPop: !(widget.selectionActive && _selectedAssets.isNotEmpty),
|
||||||
onPopInvoked: (didPop) => !didPop ? _deselectAll() : null,
|
onPopInvokedWithResult: (didPop, _) => !didPop ? _deselectAll() : null,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
AssetDragRegion(
|
AssetDragRegion(
|
||||||
|
@ -58,7 +58,7 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
|||||||
isLabelVisible: serverInfoState.isVersionMismatch ||
|
isLabelVisible: serverInfoState.isVersionMismatch ||
|
||||||
((user?.isAdmin ?? false) &&
|
((user?.isAdmin ?? false) &&
|
||||||
serverInfoState.isNewReleaseAvailable),
|
serverInfoState.isNewReleaseAvailable),
|
||||||
offset: const Offset(2, 2),
|
offset: const Offset(-2, -12),
|
||||||
child: user == null
|
child: user == null
|
||||||
? const Icon(
|
? const Icon(
|
||||||
Icons.face_outlined,
|
Icons.face_outlined,
|
||||||
@ -132,7 +132,7 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
|||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
alignment: Alignment.bottomRight,
|
alignment: Alignment.bottomRight,
|
||||||
isLabelVisible: indicatorIcon != null,
|
isLabelVisible: indicatorIcon != null,
|
||||||
offset: const Offset(2, 2),
|
offset: const Offset(-2, -12),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.backup_rounded,
|
Icons.backup_rounded,
|
||||||
size: widgetSize,
|
size: widgetSize,
|
||||||
|
14
mobile/openapi/lib/model/download_response.dart
generated
14
mobile/openapi/lib/model/download_response.dart
generated
@ -14,25 +14,31 @@ class DownloadResponse {
|
|||||||
/// Returns a new [DownloadResponse] instance.
|
/// Returns a new [DownloadResponse] instance.
|
||||||
DownloadResponse({
|
DownloadResponse({
|
||||||
required this.archiveSize,
|
required this.archiveSize,
|
||||||
|
this.includeEmbeddedVideos = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
int archiveSize;
|
int archiveSize;
|
||||||
|
|
||||||
|
bool includeEmbeddedVideos;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is DownloadResponse &&
|
bool operator ==(Object other) => identical(this, other) || other is DownloadResponse &&
|
||||||
other.archiveSize == archiveSize;
|
other.archiveSize == archiveSize &&
|
||||||
|
other.includeEmbeddedVideos == includeEmbeddedVideos;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
(archiveSize.hashCode);
|
(archiveSize.hashCode) +
|
||||||
|
(includeEmbeddedVideos.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'DownloadResponse[archiveSize=$archiveSize]';
|
String toString() => 'DownloadResponse[archiveSize=$archiveSize, includeEmbeddedVideos=$includeEmbeddedVideos]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
json[r'archiveSize'] = this.archiveSize;
|
json[r'archiveSize'] = this.archiveSize;
|
||||||
|
json[r'includeEmbeddedVideos'] = this.includeEmbeddedVideos;
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,6 +51,7 @@ class DownloadResponse {
|
|||||||
|
|
||||||
return DownloadResponse(
|
return DownloadResponse(
|
||||||
archiveSize: mapValueOfType<int>(json, r'archiveSize')!,
|
archiveSize: mapValueOfType<int>(json, r'archiveSize')!,
|
||||||
|
includeEmbeddedVideos: mapValueOfType<bool>(json, r'includeEmbeddedVideos')!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@ -93,6 +100,7 @@ class DownloadResponse {
|
|||||||
/// The list of required keys that must be present in a JSON.
|
/// The list of required keys that must be present in a JSON.
|
||||||
static const requiredKeys = <String>{
|
static const requiredKeys = <String>{
|
||||||
'archiveSize',
|
'archiveSize',
|
||||||
|
'includeEmbeddedVideos',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
23
mobile/openapi/lib/model/download_update.dart
generated
23
mobile/openapi/lib/model/download_update.dart
generated
@ -14,6 +14,7 @@ class DownloadUpdate {
|
|||||||
/// Returns a new [DownloadUpdate] instance.
|
/// Returns a new [DownloadUpdate] instance.
|
||||||
DownloadUpdate({
|
DownloadUpdate({
|
||||||
this.archiveSize,
|
this.archiveSize,
|
||||||
|
this.includeEmbeddedVideos,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Minimum value: 1
|
/// Minimum value: 1
|
||||||
@ -25,17 +26,27 @@ class DownloadUpdate {
|
|||||||
///
|
///
|
||||||
int? archiveSize;
|
int? archiveSize;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// 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? includeEmbeddedVideos;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is DownloadUpdate &&
|
bool operator ==(Object other) => identical(this, other) || other is DownloadUpdate &&
|
||||||
other.archiveSize == archiveSize;
|
other.archiveSize == archiveSize &&
|
||||||
|
other.includeEmbeddedVideos == includeEmbeddedVideos;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
(archiveSize == null ? 0 : archiveSize!.hashCode);
|
(archiveSize == null ? 0 : archiveSize!.hashCode) +
|
||||||
|
(includeEmbeddedVideos == null ? 0 : includeEmbeddedVideos!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'DownloadUpdate[archiveSize=$archiveSize]';
|
String toString() => 'DownloadUpdate[archiveSize=$archiveSize, includeEmbeddedVideos=$includeEmbeddedVideos]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@ -44,6 +55,11 @@ class DownloadUpdate {
|
|||||||
} else {
|
} else {
|
||||||
// json[r'archiveSize'] = null;
|
// json[r'archiveSize'] = null;
|
||||||
}
|
}
|
||||||
|
if (this.includeEmbeddedVideos != null) {
|
||||||
|
json[r'includeEmbeddedVideos'] = this.includeEmbeddedVideos;
|
||||||
|
} else {
|
||||||
|
// json[r'includeEmbeddedVideos'] = null;
|
||||||
|
}
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,6 +72,7 @@ class DownloadUpdate {
|
|||||||
|
|
||||||
return DownloadUpdate(
|
return DownloadUpdate(
|
||||||
archiveSize: mapValueOfType<int>(json, r'archiveSize'),
|
archiveSize: mapValueOfType<int>(json, r'archiveSize'),
|
||||||
|
includeEmbeddedVideos: mapValueOfType<bool>(json, r'includeEmbeddedVideos'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -5,23 +5,23 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: _fe_analyzer_shared
|
name: _fe_analyzer_shared
|
||||||
sha256: "5aaf60d96c4cd00fe7f21594b5ad6a1b699c80a27420f8a837f4d68473ef09e3"
|
sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "68.0.0"
|
version: "72.0.0"
|
||||||
_macros:
|
_macros:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: dart
|
description: dart
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.1.0"
|
version: "0.3.2"
|
||||||
analyzer:
|
analyzer:
|
||||||
dependency: "direct overridden"
|
dependency: "direct overridden"
|
||||||
description:
|
description:
|
||||||
name: analyzer
|
name: analyzer
|
||||||
sha256: "21f1d3720fd1c70316399d5e2bccaebb415c434592d778cce8acb967b8578808"
|
sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.5.0"
|
version: "6.7.0"
|
||||||
analyzer_plugin:
|
analyzer_plugin:
|
||||||
dependency: "direct overridden"
|
dependency: "direct overridden"
|
||||||
description:
|
description:
|
||||||
@ -540,10 +540,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_local_notifications
|
name: flutter_local_notifications
|
||||||
sha256: "55b9b229307a10974b26296ff29f2e132256ba4bd74266939118eaefa941cb00"
|
sha256: dd6676d8c2926537eccdf9f72128bbb2a9d0814689527b17f92c248ff192eaf3
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "16.3.3"
|
version: "17.2.1+2"
|
||||||
flutter_local_notifications_linux:
|
flutter_local_notifications_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -901,18 +901,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker
|
name: leak_tracker
|
||||||
sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a"
|
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.0.4"
|
version: "10.0.5"
|
||||||
leak_tracker_flutter_testing:
|
leak_tracker_flutter_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker_flutter_testing
|
name: leak_tracker_flutter_testing
|
||||||
sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8"
|
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.3"
|
version: "3.0.5"
|
||||||
leak_tracker_testing:
|
leak_tracker_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -941,10 +941,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: macros
|
name: macros
|
||||||
sha256: "12e8a9842b5a7390de7a781ec63d793527582398d16ea26c60fed58833c9ae79"
|
sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.0-main.0"
|
version: "0.1.2-main.4"
|
||||||
maplibre_gl:
|
maplibre_gl:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -981,10 +981,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
|
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.0"
|
version: "0.11.1"
|
||||||
meta:
|
meta:
|
||||||
dependency: "direct overridden"
|
dependency: "direct overridden"
|
||||||
description:
|
description:
|
||||||
@ -1212,10 +1212,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: platform
|
name: platform
|
||||||
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
|
sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.4"
|
version: "3.1.5"
|
||||||
plugin_platform_interface:
|
plugin_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1537,10 +1537,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f"
|
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.0"
|
version: "0.7.2"
|
||||||
thumbhash:
|
thumbhash:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -1737,10 +1737,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vm_service
|
name: vm_service
|
||||||
sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
|
sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "14.2.1"
|
version: "14.2.4"
|
||||||
wakelock_plus:
|
wakelock_plus:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -1847,4 +1847,4 @@ packages:
|
|||||||
version: "3.1.2"
|
version: "3.1.2"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.4.0 <4.0.0"
|
dart: ">=3.4.0 <4.0.0"
|
||||||
flutter: ">=3.22.3"
|
flutter: ">=3.24.0"
|
||||||
|
@ -6,7 +6,7 @@ version: 1.112.1+154
|
|||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.3.0 <4.0.0'
|
sdk: '>=3.3.0 <4.0.0'
|
||||||
flutter: 3.22.3
|
flutter: 3.24.0
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
@ -50,7 +50,7 @@ dependencies:
|
|||||||
device_info_plus: ^9.1.1
|
device_info_plus: ^9.1.1
|
||||||
connectivity_plus: ^5.0.2
|
connectivity_plus: ^5.0.2
|
||||||
wakelock_plus: ^1.1.4
|
wakelock_plus: ^1.1.4
|
||||||
flutter_local_notifications: ^16.3.2
|
flutter_local_notifications: ^17.2.1+2
|
||||||
timezone: ^0.9.2
|
timezone: ^0.9.2
|
||||||
octo_image: ^2.0.0
|
octo_image: ^2.0.0
|
||||||
thumbhash: 0.1.0+1
|
thumbhash: 0.1.0+1
|
||||||
|
@ -8497,10 +8497,15 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"archiveSize": {
|
"archiveSize": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"includeEmbeddedVideos": {
|
||||||
|
"default": false,
|
||||||
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"archiveSize"
|
"archiveSize",
|
||||||
|
"includeEmbeddedVideos"
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
@ -8527,6 +8532,9 @@
|
|||||||
"archiveSize": {
|
"archiveSize": {
|
||||||
"minimum": 1,
|
"minimum": 1,
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"includeEmbeddedVideos": {
|
||||||
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
@ -86,6 +86,7 @@ export type AvatarResponse = {
|
|||||||
};
|
};
|
||||||
export type DownloadResponse = {
|
export type DownloadResponse = {
|
||||||
archiveSize: number;
|
archiveSize: number;
|
||||||
|
includeEmbeddedVideos: boolean;
|
||||||
};
|
};
|
||||||
export type EmailNotificationsResponse = {
|
export type EmailNotificationsResponse = {
|
||||||
albumInvite: boolean;
|
albumInvite: boolean;
|
||||||
@ -115,6 +116,7 @@ export type AvatarUpdate = {
|
|||||||
};
|
};
|
||||||
export type DownloadUpdate = {
|
export type DownloadUpdate = {
|
||||||
archiveSize?: number;
|
archiveSize?: number;
|
||||||
|
includeEmbeddedVideos?: boolean;
|
||||||
};
|
};
|
||||||
export type EmailNotificationsUpdate = {
|
export type EmailNotificationsUpdate = {
|
||||||
albumInvite?: boolean;
|
albumInvite?: boolean;
|
||||||
|
@ -5,6 +5,7 @@ import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE, ModuleRef } from '@ne
|
|||||||
import { EventEmitterModule } from '@nestjs/event-emitter';
|
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||||
import { ScheduleModule, SchedulerRegistry } from '@nestjs/schedule';
|
import { ScheduleModule, SchedulerRegistry } from '@nestjs/schedule';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import _ from 'lodash';
|
||||||
import { ClsModule } from 'nestjs-cls';
|
import { ClsModule } from 'nestjs-cls';
|
||||||
import { OpenTelemetryModule } from 'nestjs-otel';
|
import { OpenTelemetryModule } from 'nestjs-otel';
|
||||||
import { commands } from 'src/commands';
|
import { commands } from 'src/commands';
|
||||||
@ -13,6 +14,7 @@ import { controllers } from 'src/controllers';
|
|||||||
import { databaseConfig } from 'src/database.config';
|
import { databaseConfig } from 'src/database.config';
|
||||||
import { entities } from 'src/entities';
|
import { entities } from 'src/entities';
|
||||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||||
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { AuthGuard } from 'src/middleware/auth.guard';
|
import { AuthGuard } from 'src/middleware/auth.guard';
|
||||||
import { ErrorInterceptor } from 'src/middleware/error.interceptor';
|
import { ErrorInterceptor } from 'src/middleware/error.interceptor';
|
||||||
import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor';
|
import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor';
|
||||||
@ -54,15 +56,25 @@ export class ApiModule implements OnModuleInit, OnModuleDestroy {
|
|||||||
constructor(
|
constructor(
|
||||||
private moduleRef: ModuleRef,
|
private moduleRef: ModuleRef,
|
||||||
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
||||||
|
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async onModuleInit() {
|
async onModuleInit() {
|
||||||
setupEventHandlers(this.moduleRef);
|
const items = setupEventHandlers(this.moduleRef);
|
||||||
await this.eventRepository.emit('onBootstrapEvent', 'api');
|
|
||||||
|
await this.eventRepository.emit('onBootstrap', 'api');
|
||||||
|
|
||||||
|
this.logger.setContext('EventLoader');
|
||||||
|
const eventMap = _.groupBy(items, 'event');
|
||||||
|
for (const [event, handlers] of Object.entries(eventMap)) {
|
||||||
|
for (const { priority, label } of handlers) {
|
||||||
|
this.logger.verbose(`Added ${event} {${label}${priority ? '' : ', ' + priority}} event`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async onModuleDestroy() {
|
async onModuleDestroy() {
|
||||||
await this.eventRepository.emit('onShutdownEvent');
|
await this.eventRepository.emit('onShutdown');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,11 +90,11 @@ export class MicroservicesModule implements OnModuleInit, OnModuleDestroy {
|
|||||||
|
|
||||||
async onModuleInit() {
|
async onModuleInit() {
|
||||||
setupEventHandlers(this.moduleRef);
|
setupEventHandlers(this.moduleRef);
|
||||||
await this.eventRepository.emit('onBootstrapEvent', 'microservices');
|
await this.eventRepository.emit('onBootstrap', 'microservices');
|
||||||
}
|
}
|
||||||
|
|
||||||
async onModuleDestroy() {
|
async onModuleDestroy() {
|
||||||
await this.eventRepository.emit('onShutdownEvent');
|
await this.eventRepository.emit('onShutdown');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import { OnEventOptions } from '@nestjs/event-emitter/dist/interfaces';
|
|||||||
import { ApiExtension, ApiOperation, ApiProperty, ApiTags } from '@nestjs/swagger';
|
import { ApiExtension, ApiOperation, ApiProperty, ApiTags } from '@nestjs/swagger';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { ADDED_IN_PREFIX, DEPRECATED_IN_PREFIX, LIFECYCLE_EXTENSION } from 'src/constants';
|
import { ADDED_IN_PREFIX, DEPRECATED_IN_PREFIX, LIFECYCLE_EXTENSION } from 'src/constants';
|
||||||
import { ServerEvent } from 'src/interfaces/event.interface';
|
import { EmitEvent, ServerEvent } from 'src/interfaces/event.interface';
|
||||||
import { Metadata } from 'src/middleware/auth.guard';
|
import { Metadata } from 'src/middleware/auth.guard';
|
||||||
import { setUnion } from 'src/utils/set';
|
import { setUnion } from 'src/utils/set';
|
||||||
|
|
||||||
@ -136,11 +136,12 @@ export const GenerateSql = (...options: GenerateSqlQueries[]) => SetMetadata(GEN
|
|||||||
export const OnServerEvent = (event: ServerEvent, options?: OnEventOptions) =>
|
export const OnServerEvent = (event: ServerEvent, options?: OnEventOptions) =>
|
||||||
OnEvent(event, { suppressErrors: false, ...options });
|
OnEvent(event, { suppressErrors: false, ...options });
|
||||||
|
|
||||||
export type HandlerOptions = {
|
export type EmitConfig = {
|
||||||
|
event: EmitEvent;
|
||||||
/** lower value has higher priority, defaults to 0 */
|
/** lower value has higher priority, defaults to 0 */
|
||||||
priority: number;
|
priority?: number;
|
||||||
};
|
};
|
||||||
export const EventHandlerOptions = (options: HandlerOptions) => SetMetadata(Metadata.EVENT_HANDLER_OPTIONS, options);
|
export const OnEmit = (config: EmitConfig) => SetMetadata(Metadata.ON_EMIT_CONFIG, config);
|
||||||
|
|
||||||
type LifecycleRelease = 'NEXT_RELEASE' | string;
|
type LifecycleRelease = 'NEXT_RELEASE' | string;
|
||||||
type LifecycleMetadata = {
|
type LifecycleMetadata = {
|
||||||
|
@ -33,12 +33,15 @@ class EmailNotificationsUpdate {
|
|||||||
albumUpdate?: boolean;
|
albumUpdate?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class DownloadUpdate {
|
class DownloadUpdate implements Partial<DownloadResponse> {
|
||||||
@Optional()
|
@Optional()
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@IsPositive()
|
@IsPositive()
|
||||||
@ApiProperty({ type: 'integer' })
|
@ApiProperty({ type: 'integer' })
|
||||||
archiveSize?: number;
|
archiveSize?: number;
|
||||||
|
|
||||||
|
@ValidateBoolean({ optional: true })
|
||||||
|
includeEmbeddedVideos?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class PurchaseUpdate {
|
class PurchaseUpdate {
|
||||||
@ -104,6 +107,8 @@ class EmailNotificationsResponse {
|
|||||||
class DownloadResponse {
|
class DownloadResponse {
|
||||||
@ApiProperty({ type: 'integer' })
|
@ApiProperty({ type: 'integer' })
|
||||||
archiveSize!: number;
|
archiveSize!: number;
|
||||||
|
|
||||||
|
includeEmbeddedVideos: boolean = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
class PurchaseResponse {
|
class PurchaseResponse {
|
||||||
|
@ -35,6 +35,7 @@ export interface UserPreferences {
|
|||||||
};
|
};
|
||||||
download: {
|
download: {
|
||||||
archiveSize: number;
|
archiveSize: number;
|
||||||
|
includeEmbeddedVideos: boolean;
|
||||||
};
|
};
|
||||||
purchase: {
|
purchase: {
|
||||||
showSupportBadge: boolean;
|
showSupportBadge: boolean;
|
||||||
@ -65,6 +66,7 @@ export const getDefaultPreferences = (user: { email: string }): UserPreferences
|
|||||||
},
|
},
|
||||||
download: {
|
download: {
|
||||||
archiveSize: HumanReadableSize.GiB * 4,
|
archiveSize: HumanReadableSize.GiB * 4,
|
||||||
|
includeEmbeddedVideos: false,
|
||||||
},
|
},
|
||||||
purchase: {
|
purchase: {
|
||||||
showSupportBadge: true,
|
showSupportBadge: true,
|
||||||
|
@ -4,41 +4,27 @@ import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.d
|
|||||||
|
|
||||||
export const IEventRepository = 'IEventRepository';
|
export const IEventRepository = 'IEventRepository';
|
||||||
|
|
||||||
export type SystemConfigUpdateEvent = { newConfig: SystemConfig; oldConfig: SystemConfig };
|
type EmitEventMap = {
|
||||||
export type AlbumUpdateEvent = {
|
|
||||||
id: string;
|
|
||||||
/** user id */
|
|
||||||
updatedBy: string;
|
|
||||||
};
|
|
||||||
export type AlbumInviteEvent = { id: string; userId: string };
|
|
||||||
export type UserSignupEvent = { notify: boolean; id: string; tempPassword?: string };
|
|
||||||
|
|
||||||
type MaybePromise<T> = Promise<T> | T;
|
|
||||||
type Handler<T = undefined> = (data: T) => MaybePromise<void>;
|
|
||||||
|
|
||||||
const noop = () => {};
|
|
||||||
const dummyHandlers = {
|
|
||||||
// app events
|
// app events
|
||||||
onBootstrapEvent: noop as Handler<'api' | 'microservices'>,
|
onBootstrap: ['api' | 'microservices'];
|
||||||
onShutdownEvent: noop as () => MaybePromise<void>,
|
onShutdown: [];
|
||||||
|
|
||||||
// config events
|
// config events
|
||||||
onConfigUpdateEvent: noop as Handler<SystemConfigUpdateEvent>,
|
onConfigUpdate: [{ newConfig: SystemConfig; oldConfig: SystemConfig }];
|
||||||
onConfigValidateEvent: noop as Handler<SystemConfigUpdateEvent>,
|
onConfigValidate: [{ newConfig: SystemConfig; oldConfig: SystemConfig }];
|
||||||
|
|
||||||
// album events
|
// album events
|
||||||
onAlbumUpdateEvent: noop as Handler<AlbumUpdateEvent>,
|
onAlbumUpdate: [{ id: string; updatedBy: string }];
|
||||||
onAlbumInviteEvent: noop as Handler<AlbumInviteEvent>,
|
onAlbumInvite: [{ id: string; userId: string }];
|
||||||
|
|
||||||
// user events
|
// user events
|
||||||
onUserSignupEvent: noop as Handler<UserSignupEvent>,
|
onUserSignup: [{ notify: boolean; id: string; tempPassword?: string }];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EventHandlers = typeof dummyHandlers;
|
export type EmitEvent = keyof EmitEventMap;
|
||||||
export type EmitEvent = keyof EventHandlers;
|
export type EmitHandler<T extends EmitEvent> = (...args: ArgsOf<T>) => Promise<void> | void;
|
||||||
export type EmitEventHandler<T extends EmitEvent> = (...args: Parameters<EventHandlers[T]>) => MaybePromise<void>;
|
export type ArgOf<T extends EmitEvent> = EmitEventMap[T][0];
|
||||||
export const events = Object.keys(dummyHandlers) as EmitEvent[];
|
export type ArgsOf<T extends EmitEvent> = EmitEventMap[T];
|
||||||
export type OnEvents = Partial<EventHandlers>;
|
|
||||||
|
|
||||||
export enum ClientEvent {
|
export enum ClientEvent {
|
||||||
UPLOAD_SUCCESS = 'on_upload_success',
|
UPLOAD_SUCCESS = 'on_upload_success',
|
||||||
@ -81,8 +67,8 @@ export interface ServerEventMap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IEventRepository {
|
export interface IEventRepository {
|
||||||
on<T extends EmitEvent>(event: T, handler: EmitEventHandler<T>): void;
|
on<T extends keyof EmitEventMap>(event: T, handler: EmitHandler<T>): void;
|
||||||
emit<T extends EmitEvent>(event: T, ...args: Parameters<EmitEventHandler<T>>): Promise<void>;
|
emit<T extends keyof EmitEventMap>(event: T, ...args: ArgsOf<T>): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send to connected clients for a specific user
|
* Send to connected clients for a specific user
|
||||||
|
@ -20,7 +20,7 @@ export enum Metadata {
|
|||||||
ADMIN_ROUTE = 'admin_route',
|
ADMIN_ROUTE = 'admin_route',
|
||||||
SHARED_ROUTE = 'shared_route',
|
SHARED_ROUTE = 'shared_route',
|
||||||
API_KEY_SECURITY = 'api_key',
|
API_KEY_SECURITY = 'api_key',
|
||||||
EVENT_HANDLER_OPTIONS = 'event_handler_options',
|
ON_EMIT_CONFIG = 'on_emit_config',
|
||||||
}
|
}
|
||||||
|
|
||||||
type AdminRoute = { admin?: true };
|
type AdminRoute = { admin?: true };
|
||||||
|
@ -9,9 +9,10 @@ import {
|
|||||||
} from '@nestjs/websockets';
|
} from '@nestjs/websockets';
|
||||||
import { Server, Socket } from 'socket.io';
|
import { Server, Socket } from 'socket.io';
|
||||||
import {
|
import {
|
||||||
|
ArgsOf,
|
||||||
ClientEventMap,
|
ClientEventMap,
|
||||||
EmitEvent,
|
EmitEvent,
|
||||||
EmitEventHandler,
|
EmitHandler,
|
||||||
IEventRepository,
|
IEventRepository,
|
||||||
ServerEvent,
|
ServerEvent,
|
||||||
ServerEventMap,
|
ServerEventMap,
|
||||||
@ -20,6 +21,8 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
|||||||
import { AuthService } from 'src/services/auth.service';
|
import { AuthService } from 'src/services/auth.service';
|
||||||
import { Instrumentation } from 'src/utils/instrumentation';
|
import { Instrumentation } from 'src/utils/instrumentation';
|
||||||
|
|
||||||
|
type EmitHandlers = Partial<{ [T in EmitEvent]: EmitHandler<T>[] }>;
|
||||||
|
|
||||||
@Instrumentation()
|
@Instrumentation()
|
||||||
@WebSocketGateway({
|
@WebSocketGateway({
|
||||||
cors: true,
|
cors: true,
|
||||||
@ -28,7 +31,7 @@ import { Instrumentation } from 'src/utils/instrumentation';
|
|||||||
})
|
})
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit, IEventRepository {
|
export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit, IEventRepository {
|
||||||
private emitHandlers: Partial<Record<EmitEvent, EmitEventHandler<EmitEvent>[]>> = {};
|
private emitHandlers: EmitHandlers = {};
|
||||||
|
|
||||||
@WebSocketServer()
|
@WebSocketServer()
|
||||||
private server?: Server;
|
private server?: Server;
|
||||||
@ -78,12 +81,15 @@ export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect
|
|||||||
await client.leave(client.nsp.name);
|
await client.leave(client.nsp.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
on<T extends EmitEvent>(event: T, handler: EmitEventHandler<T>): void {
|
on<T extends EmitEvent>(event: T, handler: EmitHandler<T>): void {
|
||||||
const handlers: EmitEventHandler<EmitEvent>[] = this.emitHandlers[event] || [];
|
if (!this.emitHandlers[event]) {
|
||||||
this.emitHandlers[event] = [...handlers, handler];
|
this.emitHandlers[event] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async emit<T extends EmitEvent>(event: T, ...args: Parameters<EmitEventHandler<T>>): Promise<void> {
|
this.emitHandlers[event].push(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
async emit<T extends EmitEvent>(event: T, ...args: ArgsOf<T>): Promise<void> {
|
||||||
const handlers = this.emitHandlers[event] || [];
|
const handlers = this.emitHandlers[event] || [];
|
||||||
for (const handler of handlers) {
|
for (const handler of handlers) {
|
||||||
await handler(...args);
|
await handler(...args);
|
||||||
|
@ -380,7 +380,7 @@ describe(AlbumService.name, () => {
|
|||||||
userId: authStub.user2.user.id,
|
userId: authStub.user2.user.id,
|
||||||
albumId: albumStub.sharedWithAdmin.id,
|
albumId: albumStub.sharedWithAdmin.id,
|
||||||
});
|
});
|
||||||
expect(eventMock.emit).toHaveBeenCalledWith('onAlbumInviteEvent', {
|
expect(eventMock.emit).toHaveBeenCalledWith('onAlbumInvite', {
|
||||||
id: albumStub.sharedWithAdmin.id,
|
id: albumStub.sharedWithAdmin.id,
|
||||||
userId: userStub.user2.id,
|
userId: userStub.user2.id,
|
||||||
});
|
});
|
||||||
@ -568,7 +568,7 @@ describe(AlbumService.name, () => {
|
|||||||
albumThumbnailAssetId: 'asset-1',
|
albumThumbnailAssetId: 'asset-1',
|
||||||
});
|
});
|
||||||
expect(albumMock.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']);
|
expect(albumMock.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']);
|
||||||
expect(eventMock.emit).toHaveBeenCalledWith('onAlbumUpdateEvent', {
|
expect(eventMock.emit).toHaveBeenCalledWith('onAlbumUpdate', {
|
||||||
id: 'album-123',
|
id: 'album-123',
|
||||||
updatedBy: authStub.admin.user.id,
|
updatedBy: authStub.admin.user.id,
|
||||||
});
|
});
|
||||||
@ -612,7 +612,7 @@ describe(AlbumService.name, () => {
|
|||||||
albumThumbnailAssetId: 'asset-1',
|
albumThumbnailAssetId: 'asset-1',
|
||||||
});
|
});
|
||||||
expect(albumMock.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']);
|
expect(albumMock.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']);
|
||||||
expect(eventMock.emit).toHaveBeenCalledWith('onAlbumUpdateEvent', {
|
expect(eventMock.emit).toHaveBeenCalledWith('onAlbumUpdate', {
|
||||||
id: 'album-123',
|
id: 'album-123',
|
||||||
updatedBy: authStub.user1.user.id,
|
updatedBy: authStub.user1.user.id,
|
||||||
});
|
});
|
||||||
|
@ -187,7 +187,7 @@ export class AlbumService {
|
|||||||
albumThumbnailAssetId: album.albumThumbnailAssetId ?? firstNewAssetId,
|
albumThumbnailAssetId: album.albumThumbnailAssetId ?? firstNewAssetId,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.eventRepository.emit('onAlbumUpdateEvent', { id, updatedBy: auth.user.id });
|
await this.eventRepository.emit('onAlbumUpdate', { id, updatedBy: auth.user.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
@ -235,7 +235,7 @@ export class AlbumService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.albumUserRepository.create({ userId: userId, albumId: id, role });
|
await this.albumUserRepository.create({ userId: userId, albumId: id, role });
|
||||||
await this.eventRepository.emit('onAlbumInviteEvent', { id, userId });
|
await this.eventRepository.emit('onAlbumInvite', { id, userId });
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.findOrFail(id, { withAssets: true }).then(mapAlbumWithoutAssets);
|
return this.findOrFail(id, { withAssets: true }).then(mapAlbumWithoutAssets);
|
||||||
|
@ -45,7 +45,7 @@ describe(DatabaseService.name, () => {
|
|||||||
it('should throw an error if PostgreSQL version is below minimum supported version', async () => {
|
it('should throw an error if PostgreSQL version is below minimum supported version', async () => {
|
||||||
databaseMock.getPostgresVersion.mockResolvedValueOnce('13.10.0');
|
databaseMock.getPostgresVersion.mockResolvedValueOnce('13.10.0');
|
||||||
|
|
||||||
await expect(sut.onBootstrapEvent()).rejects.toThrow('Invalid PostgreSQL version. Found 13.10.0');
|
await expect(sut.onBootstrap()).rejects.toThrow('Invalid PostgreSQL version. Found 13.10.0');
|
||||||
|
|
||||||
expect(databaseMock.getPostgresVersion).toHaveBeenCalledTimes(1);
|
expect(databaseMock.getPostgresVersion).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
@ -65,7 +65,7 @@ describe(DatabaseService.name, () => {
|
|||||||
availableVersion: minVersionInRange,
|
availableVersion: minVersionInRange,
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(sut.onBootstrapEvent()).resolves.toBeUndefined();
|
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||||
|
|
||||||
expect(databaseMock.getPostgresVersion).toHaveBeenCalled();
|
expect(databaseMock.getPostgresVersion).toHaveBeenCalled();
|
||||||
expect(databaseMock.createExtension).toHaveBeenCalledWith(extension);
|
expect(databaseMock.createExtension).toHaveBeenCalledWith(extension);
|
||||||
@ -79,7 +79,7 @@ describe(DatabaseService.name, () => {
|
|||||||
databaseMock.getExtensionVersion.mockResolvedValue({ installedVersion: null, availableVersion: null });
|
databaseMock.getExtensionVersion.mockResolvedValue({ installedVersion: null, availableVersion: null });
|
||||||
const message = `The ${extensionName} extension is not available in this Postgres instance.
|
const message = `The ${extensionName} extension is not available in this Postgres instance.
|
||||||
If using a container image, ensure the image has the extension installed.`;
|
If using a container image, ensure the image has the extension installed.`;
|
||||||
await expect(sut.onBootstrapEvent()).rejects.toThrow(message);
|
await expect(sut.onBootstrap()).rejects.toThrow(message);
|
||||||
|
|
||||||
expect(databaseMock.createExtension).not.toHaveBeenCalled();
|
expect(databaseMock.createExtension).not.toHaveBeenCalled();
|
||||||
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||||
@ -91,7 +91,7 @@ describe(DatabaseService.name, () => {
|
|||||||
availableVersion: versionBelowRange,
|
availableVersion: versionBelowRange,
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(sut.onBootstrapEvent()).rejects.toThrow(
|
await expect(sut.onBootstrap()).rejects.toThrow(
|
||||||
`The ${extensionName} extension version is ${versionBelowRange}, but Immich only supports ${extensionRange}`,
|
`The ${extensionName} extension version is ${versionBelowRange}, but Immich only supports ${extensionRange}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -101,7 +101,7 @@ describe(DatabaseService.name, () => {
|
|||||||
it(`should throw an error if ${extension} extension version is a nightly`, async () => {
|
it(`should throw an error if ${extension} extension version is a nightly`, async () => {
|
||||||
databaseMock.getExtensionVersion.mockResolvedValue({ installedVersion: '0.0.0', availableVersion: '0.0.0' });
|
databaseMock.getExtensionVersion.mockResolvedValue({ installedVersion: '0.0.0', availableVersion: '0.0.0' });
|
||||||
|
|
||||||
await expect(sut.onBootstrapEvent()).rejects.toThrow(
|
await expect(sut.onBootstrap()).rejects.toThrow(
|
||||||
`The ${extensionName} extension version is 0.0.0, which means it is a nightly release.`,
|
`The ${extensionName} extension version is 0.0.0, which means it is a nightly release.`,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -117,7 +117,7 @@ describe(DatabaseService.name, () => {
|
|||||||
});
|
});
|
||||||
databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: false });
|
databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: false });
|
||||||
|
|
||||||
await expect(sut.onBootstrapEvent()).resolves.toBeUndefined();
|
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||||
|
|
||||||
expect(databaseMock.updateVectorExtension).toHaveBeenCalledWith(extension, updateInRange);
|
expect(databaseMock.updateVectorExtension).toHaveBeenCalledWith(extension, updateInRange);
|
||||||
expect(databaseMock.updateVectorExtension).toHaveBeenCalledTimes(1);
|
expect(databaseMock.updateVectorExtension).toHaveBeenCalledTimes(1);
|
||||||
@ -132,7 +132,7 @@ describe(DatabaseService.name, () => {
|
|||||||
installedVersion: minVersionInRange,
|
installedVersion: minVersionInRange,
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(sut.onBootstrapEvent()).resolves.toBeUndefined();
|
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||||
|
|
||||||
expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled();
|
expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled();
|
||||||
expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1);
|
expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1);
|
||||||
@ -145,7 +145,7 @@ describe(DatabaseService.name, () => {
|
|||||||
installedVersion: null,
|
installedVersion: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(sut.onBootstrapEvent()).rejects.toThrow();
|
await expect(sut.onBootstrap()).rejects.toThrow();
|
||||||
|
|
||||||
expect(databaseMock.createExtension).not.toHaveBeenCalled();
|
expect(databaseMock.createExtension).not.toHaveBeenCalled();
|
||||||
expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled();
|
expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled();
|
||||||
@ -159,7 +159,7 @@ describe(DatabaseService.name, () => {
|
|||||||
installedVersion: minVersionInRange,
|
installedVersion: minVersionInRange,
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(sut.onBootstrapEvent()).rejects.toThrow();
|
await expect(sut.onBootstrap()).rejects.toThrow();
|
||||||
|
|
||||||
expect(databaseMock.createExtension).not.toHaveBeenCalled();
|
expect(databaseMock.createExtension).not.toHaveBeenCalled();
|
||||||
expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled();
|
expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled();
|
||||||
@ -173,7 +173,7 @@ describe(DatabaseService.name, () => {
|
|||||||
installedVersion: updateInRange,
|
installedVersion: updateInRange,
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(sut.onBootstrapEvent()).rejects.toThrow(
|
await expect(sut.onBootstrap()).rejects.toThrow(
|
||||||
`The database currently has ${extensionName} ${updateInRange} activated, but the Postgres instance only has ${minVersionInRange} available.`,
|
`The database currently has ${extensionName} ${updateInRange} activated, but the Postgres instance only has ${minVersionInRange} available.`,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -189,7 +189,7 @@ describe(DatabaseService.name, () => {
|
|||||||
});
|
});
|
||||||
databaseMock.updateVectorExtension.mockRejectedValue(new Error('Failed to update extension'));
|
databaseMock.updateVectorExtension.mockRejectedValue(new Error('Failed to update extension'));
|
||||||
|
|
||||||
await expect(sut.onBootstrapEvent()).rejects.toThrow('Failed to update extension');
|
await expect(sut.onBootstrap()).rejects.toThrow('Failed to update extension');
|
||||||
|
|
||||||
expect(loggerMock.warn.mock.calls[0][0]).toContain(
|
expect(loggerMock.warn.mock.calls[0][0]).toContain(
|
||||||
`The ${extensionName} extension can be updated to ${updateInRange}.`,
|
`The ${extensionName} extension can be updated to ${updateInRange}.`,
|
||||||
@ -206,7 +206,7 @@ describe(DatabaseService.name, () => {
|
|||||||
});
|
});
|
||||||
databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: true });
|
databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: true });
|
||||||
|
|
||||||
await expect(sut.onBootstrapEvent()).resolves.toBeUndefined();
|
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||||
|
|
||||||
expect(loggerMock.warn).toHaveBeenCalledTimes(1);
|
expect(loggerMock.warn).toHaveBeenCalledTimes(1);
|
||||||
expect(loggerMock.warn.mock.calls[0][0]).toContain(extensionName);
|
expect(loggerMock.warn.mock.calls[0][0]).toContain(extensionName);
|
||||||
@ -218,7 +218,7 @@ describe(DatabaseService.name, () => {
|
|||||||
it(`should reindex ${extension} indices if needed`, async () => {
|
it(`should reindex ${extension} indices if needed`, async () => {
|
||||||
databaseMock.shouldReindex.mockResolvedValue(true);
|
databaseMock.shouldReindex.mockResolvedValue(true);
|
||||||
|
|
||||||
await expect(sut.onBootstrapEvent()).resolves.toBeUndefined();
|
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||||
|
|
||||||
expect(databaseMock.shouldReindex).toHaveBeenCalledTimes(2);
|
expect(databaseMock.shouldReindex).toHaveBeenCalledTimes(2);
|
||||||
expect(databaseMock.reindex).toHaveBeenCalledTimes(2);
|
expect(databaseMock.reindex).toHaveBeenCalledTimes(2);
|
||||||
@ -229,7 +229,7 @@ describe(DatabaseService.name, () => {
|
|||||||
it(`should not reindex ${extension} indices if not needed`, async () => {
|
it(`should not reindex ${extension} indices if not needed`, async () => {
|
||||||
databaseMock.shouldReindex.mockResolvedValue(false);
|
databaseMock.shouldReindex.mockResolvedValue(false);
|
||||||
|
|
||||||
await expect(sut.onBootstrapEvent()).resolves.toBeUndefined();
|
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||||
|
|
||||||
expect(databaseMock.shouldReindex).toHaveBeenCalledTimes(2);
|
expect(databaseMock.shouldReindex).toHaveBeenCalledTimes(2);
|
||||||
expect(databaseMock.reindex).toHaveBeenCalledTimes(0);
|
expect(databaseMock.reindex).toHaveBeenCalledTimes(0);
|
||||||
@ -240,7 +240,7 @@ describe(DatabaseService.name, () => {
|
|||||||
it('should skip migrations if DB_SKIP_MIGRATIONS=true', async () => {
|
it('should skip migrations if DB_SKIP_MIGRATIONS=true', async () => {
|
||||||
process.env.DB_SKIP_MIGRATIONS = 'true';
|
process.env.DB_SKIP_MIGRATIONS = 'true';
|
||||||
|
|
||||||
await expect(sut.onBootstrapEvent()).resolves.toBeUndefined();
|
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||||
|
|
||||||
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@ -255,7 +255,7 @@ describe(DatabaseService.name, () => {
|
|||||||
databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: false });
|
databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: false });
|
||||||
databaseMock.createExtension.mockRejectedValue(new Error('Failed to create extension'));
|
databaseMock.createExtension.mockRejectedValue(new Error('Failed to create extension'));
|
||||||
|
|
||||||
await expect(sut.onBootstrapEvent()).rejects.toThrow('Failed to create extension');
|
await expect(sut.onBootstrap()).rejects.toThrow('Failed to create extension');
|
||||||
|
|
||||||
expect(loggerMock.fatal).toHaveBeenCalledTimes(1);
|
expect(loggerMock.fatal).toHaveBeenCalledTimes(1);
|
||||||
expect(loggerMock.fatal.mock.calls[0][0]).toContain(
|
expect(loggerMock.fatal.mock.calls[0][0]).toContain(
|
||||||
@ -274,7 +274,7 @@ describe(DatabaseService.name, () => {
|
|||||||
databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: false });
|
databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: false });
|
||||||
databaseMock.createExtension.mockRejectedValue(new Error('Failed to create extension'));
|
databaseMock.createExtension.mockRejectedValue(new Error('Failed to create extension'));
|
||||||
|
|
||||||
await expect(sut.onBootstrapEvent()).rejects.toThrow('Failed to create extension');
|
await expect(sut.onBootstrap()).rejects.toThrow('Failed to create extension');
|
||||||
|
|
||||||
expect(loggerMock.fatal).toHaveBeenCalledTimes(1);
|
expect(loggerMock.fatal).toHaveBeenCalledTimes(1);
|
||||||
expect(loggerMock.fatal.mock.calls[0][0]).toContain(
|
expect(loggerMock.fatal.mock.calls[0][0]).toContain(
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import semver from 'semver';
|
import semver from 'semver';
|
||||||
import { getVectorExtension } from 'src/database.config';
|
import { getVectorExtension } from 'src/database.config';
|
||||||
import { EventHandlerOptions } from 'src/decorators';
|
import { OnEmit } from 'src/decorators';
|
||||||
import {
|
import {
|
||||||
DatabaseExtension,
|
DatabaseExtension,
|
||||||
DatabaseLock,
|
DatabaseLock,
|
||||||
@ -10,7 +10,6 @@ import {
|
|||||||
VectorExtension,
|
VectorExtension,
|
||||||
VectorIndex,
|
VectorIndex,
|
||||||
} from 'src/interfaces/database.interface';
|
} from 'src/interfaces/database.interface';
|
||||||
import { OnEvents } from 'src/interfaces/event.interface';
|
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
|
|
||||||
type CreateFailedArgs = { name: string; extension: string; otherName: string };
|
type CreateFailedArgs = { name: string; extension: string; otherName: string };
|
||||||
@ -61,7 +60,7 @@ const messages = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DatabaseService implements OnEvents {
|
export class DatabaseService {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
|
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
|
||||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||||
@ -69,8 +68,8 @@ export class DatabaseService implements OnEvents {
|
|||||||
this.logger.setContext(DatabaseService.name);
|
this.logger.setContext(DatabaseService.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventHandlerOptions({ priority: -200 })
|
@OnEmit({ event: 'onBootstrap', priority: -200 })
|
||||||
async onBootstrapEvent() {
|
async onBootstrap() {
|
||||||
const version = await this.databaseRepository.getPostgresVersion();
|
const version = await this.databaseRepository.getPostgresVersion();
|
||||||
const current = semver.coerce(version);
|
const current = semver.coerce(version);
|
||||||
const postgresRange = this.databaseRepository.getPostgresVersionRange();
|
const postgresRange = this.databaseRepository.getPostgresVersionRange();
|
||||||
|
@ -226,5 +226,31 @@ describe(DownloadService.name, () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should skip the video portion of an android live photo by default', async () => {
|
||||||
|
const assetIds = [assetStub.livePhotoStillAsset.id];
|
||||||
|
const assets = [
|
||||||
|
assetStub.livePhotoStillAsset,
|
||||||
|
{ ...assetStub.livePhotoMotionAsset, originalPath: 'upload/encoded-video/uuid-MP.mp4' },
|
||||||
|
];
|
||||||
|
|
||||||
|
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(assetIds));
|
||||||
|
assetMock.getByIds.mockImplementation(
|
||||||
|
(ids) =>
|
||||||
|
Promise.resolve(
|
||||||
|
ids.map((id) => assets.find((asset) => asset.id === id)).filter((asset) => !!asset),
|
||||||
|
) as Promise<AssetEntity[]>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(sut.getDownloadInfo(authStub.admin, { assetIds })).resolves.toEqual({
|
||||||
|
totalSize: 25_000,
|
||||||
|
archives: [
|
||||||
|
{
|
||||||
|
assetIds: [assetStub.livePhotoStillAsset.id],
|
||||||
|
size: 25_000,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||||
import { parse } from 'node:path';
|
import { parse } from 'node:path';
|
||||||
import { AccessCore } from 'src/cores/access.core';
|
import { AccessCore } from 'src/cores/access.core';
|
||||||
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
import { AssetIdsDto } from 'src/dtos/asset.dto';
|
import { AssetIdsDto } from 'src/dtos/asset.dto';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { DownloadArchiveInfo, DownloadInfoDto, DownloadResponseDto } from 'src/dtos/download.dto';
|
import { DownloadArchiveInfo, DownloadInfoDto, DownloadResponseDto } from 'src/dtos/download.dto';
|
||||||
@ -12,6 +13,7 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
|||||||
import { ImmichReadStream, IStorageRepository } from 'src/interfaces/storage.interface';
|
import { ImmichReadStream, IStorageRepository } from 'src/interfaces/storage.interface';
|
||||||
import { HumanReadableSize } from 'src/utils/bytes';
|
import { HumanReadableSize } from 'src/utils/bytes';
|
||||||
import { usePagination } from 'src/utils/pagination';
|
import { usePagination } from 'src/utils/pagination';
|
||||||
|
import { getPreferences } from 'src/utils/preferences';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DownloadService {
|
export class DownloadService {
|
||||||
@ -32,12 +34,22 @@ export class DownloadService {
|
|||||||
const archives: DownloadArchiveInfo[] = [];
|
const archives: DownloadArchiveInfo[] = [];
|
||||||
let archive: DownloadArchiveInfo = { size: 0, assetIds: [] };
|
let archive: DownloadArchiveInfo = { size: 0, assetIds: [] };
|
||||||
|
|
||||||
|
const preferences = getPreferences(auth.user);
|
||||||
|
|
||||||
const assetPagination = await this.getDownloadAssets(auth, dto);
|
const assetPagination = await this.getDownloadAssets(auth, dto);
|
||||||
for await (const assets of assetPagination) {
|
for await (const assets of assetPagination) {
|
||||||
// motion part of live photos
|
// motion part of live photos
|
||||||
const motionIds = assets.map((asset) => asset.livePhotoVideoId).filter<string>((id): id is string => !!id);
|
const motionIds = assets.map((asset) => asset.livePhotoVideoId).filter((id): id is string => !!id);
|
||||||
if (motionIds.length > 0) {
|
if (motionIds.length > 0) {
|
||||||
assets.push(...(await this.assetRepository.getByIds(motionIds, { exifInfo: true })));
|
const motionAssets = await this.assetRepository.getByIds(motionIds, { exifInfo: true });
|
||||||
|
for (const motionAsset of motionAssets) {
|
||||||
|
if (
|
||||||
|
!StorageCore.isAndroidMotionPath(motionAsset.originalPath) ||
|
||||||
|
preferences.download.includeEmbeddedVideos
|
||||||
|
) {
|
||||||
|
assets.push(motionAsset);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const asset of assets) {
|
for (const asset of assets) {
|
||||||
|
@ -73,7 +73,7 @@ describe(LibraryService.name, () => {
|
|||||||
it('should init cron job and subscribe to config changes', async () => {
|
it('should init cron job and subscribe to config changes', async () => {
|
||||||
systemMock.get.mockResolvedValue(systemConfigStub.libraryScan);
|
systemMock.get.mockResolvedValue(systemConfigStub.libraryScan);
|
||||||
|
|
||||||
await sut.onBootstrapEvent();
|
await sut.onBootstrap();
|
||||||
expect(systemMock.get).toHaveBeenCalled();
|
expect(systemMock.get).toHaveBeenCalled();
|
||||||
expect(jobMock.addCronJob).toHaveBeenCalled();
|
expect(jobMock.addCronJob).toHaveBeenCalled();
|
||||||
|
|
||||||
@ -105,7 +105,7 @@ describe(LibraryService.name, () => {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
await sut.onBootstrapEvent();
|
await sut.onBootstrap();
|
||||||
|
|
||||||
expect(storageMock.watch.mock.calls).toEqual(
|
expect(storageMock.watch.mock.calls).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
@ -118,7 +118,7 @@ describe(LibraryService.name, () => {
|
|||||||
it('should not initialize watcher when watching is disabled', async () => {
|
it('should not initialize watcher when watching is disabled', async () => {
|
||||||
systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchDisabled);
|
systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchDisabled);
|
||||||
|
|
||||||
await sut.onBootstrapEvent();
|
await sut.onBootstrap();
|
||||||
|
|
||||||
expect(storageMock.watch).not.toHaveBeenCalled();
|
expect(storageMock.watch).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@ -127,7 +127,7 @@ describe(LibraryService.name, () => {
|
|||||||
systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
|
systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
|
||||||
databaseMock.tryLock.mockResolvedValue(false);
|
databaseMock.tryLock.mockResolvedValue(false);
|
||||||
|
|
||||||
await sut.onBootstrapEvent();
|
await sut.onBootstrap();
|
||||||
|
|
||||||
expect(storageMock.watch).not.toHaveBeenCalled();
|
expect(storageMock.watch).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@ -136,7 +136,7 @@ describe(LibraryService.name, () => {
|
|||||||
describe('onConfigValidateEvent', () => {
|
describe('onConfigValidateEvent', () => {
|
||||||
it('should allow a valid cron expression', () => {
|
it('should allow a valid cron expression', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
sut.onConfigValidateEvent({
|
sut.onConfigValidate({
|
||||||
newConfig: { library: { scan: { cronExpression: '0 0 * * *' } } } as SystemConfig,
|
newConfig: { library: { scan: { cronExpression: '0 0 * * *' } } } as SystemConfig,
|
||||||
oldConfig: {} as SystemConfig,
|
oldConfig: {} as SystemConfig,
|
||||||
}),
|
}),
|
||||||
@ -145,7 +145,7 @@ describe(LibraryService.name, () => {
|
|||||||
|
|
||||||
it('should fail for an invalid cron expression', () => {
|
it('should fail for an invalid cron expression', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
sut.onConfigValidateEvent({
|
sut.onConfigValidate({
|
||||||
newConfig: { library: { scan: { cronExpression: 'foo' } } } as SystemConfig,
|
newConfig: { library: { scan: { cronExpression: 'foo' } } } as SystemConfig,
|
||||||
oldConfig: {} as SystemConfig,
|
oldConfig: {} as SystemConfig,
|
||||||
}),
|
}),
|
||||||
@ -730,7 +730,7 @@ describe(LibraryService.name, () => {
|
|||||||
const mockClose = vitest.fn();
|
const mockClose = vitest.fn();
|
||||||
storageMock.watch.mockImplementation(makeMockWatcher({ close: mockClose }));
|
storageMock.watch.mockImplementation(makeMockWatcher({ close: mockClose }));
|
||||||
|
|
||||||
await sut.onBootstrapEvent();
|
await sut.onBootstrap();
|
||||||
await sut.delete(libraryStub.externalLibraryWithImportPaths1.id);
|
await sut.delete(libraryStub.externalLibraryWithImportPaths1.id);
|
||||||
|
|
||||||
expect(mockClose).toHaveBeenCalled();
|
expect(mockClose).toHaveBeenCalled();
|
||||||
@ -861,7 +861,7 @@ describe(LibraryService.name, () => {
|
|||||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
||||||
libraryMock.getAll.mockResolvedValue([]);
|
libraryMock.getAll.mockResolvedValue([]);
|
||||||
|
|
||||||
await sut.onBootstrapEvent();
|
await sut.onBootstrap();
|
||||||
await sut.create({
|
await sut.create({
|
||||||
ownerId: authStub.admin.user.id,
|
ownerId: authStub.admin.user.id,
|
||||||
importPaths: libraryStub.externalLibraryWithImportPaths1.importPaths,
|
importPaths: libraryStub.externalLibraryWithImportPaths1.importPaths,
|
||||||
@ -917,7 +917,7 @@ describe(LibraryService.name, () => {
|
|||||||
systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
|
systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
|
||||||
libraryMock.getAll.mockResolvedValue([]);
|
libraryMock.getAll.mockResolvedValue([]);
|
||||||
|
|
||||||
await sut.onBootstrapEvent();
|
await sut.onBootstrap();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update library', async () => {
|
it('should update library', async () => {
|
||||||
@ -933,7 +933,7 @@ describe(LibraryService.name, () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchDisabled);
|
systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchDisabled);
|
||||||
|
|
||||||
await sut.onBootstrapEvent();
|
await sut.onBootstrap();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not watch library', async () => {
|
it('should not watch library', async () => {
|
||||||
@ -949,7 +949,7 @@ describe(LibraryService.name, () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
|
systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
|
||||||
libraryMock.getAll.mockResolvedValue([]);
|
libraryMock.getAll.mockResolvedValue([]);
|
||||||
await sut.onBootstrapEvent();
|
await sut.onBootstrap();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should watch library', async () => {
|
it('should watch library', async () => {
|
||||||
@ -1107,8 +1107,8 @@ describe(LibraryService.name, () => {
|
|||||||
const mockClose = vitest.fn();
|
const mockClose = vitest.fn();
|
||||||
storageMock.watch.mockImplementation(makeMockWatcher({ close: mockClose }));
|
storageMock.watch.mockImplementation(makeMockWatcher({ close: mockClose }));
|
||||||
|
|
||||||
await sut.onBootstrapEvent();
|
await sut.onBootstrap();
|
||||||
await sut.onShutdownEvent();
|
await sut.onShutdown();
|
||||||
|
|
||||||
expect(mockClose).toHaveBeenCalledTimes(2);
|
expect(mockClose).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
@ -6,6 +6,7 @@ import path, { basename, parse } from 'node:path';
|
|||||||
import picomatch from 'picomatch';
|
import picomatch from 'picomatch';
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||||
|
import { OnEmit } from 'src/decorators';
|
||||||
import {
|
import {
|
||||||
CreateLibraryDto,
|
CreateLibraryDto,
|
||||||
LibraryResponseDto,
|
LibraryResponseDto,
|
||||||
@ -22,7 +23,7 @@ import { AssetType } from 'src/enum';
|
|||||||
import { IAssetRepository, WithProperty } from 'src/interfaces/asset.interface';
|
import { IAssetRepository, WithProperty } from 'src/interfaces/asset.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
|
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||||
import { OnEvents, SystemConfigUpdateEvent } from 'src/interfaces/event.interface';
|
import { ArgOf } from 'src/interfaces/event.interface';
|
||||||
import {
|
import {
|
||||||
IBaseJob,
|
IBaseJob,
|
||||||
IEntityJob,
|
IEntityJob,
|
||||||
@ -45,7 +46,7 @@ import { validateCronExpression } from 'src/validation';
|
|||||||
const LIBRARY_SCAN_BATCH_SIZE = 5000;
|
const LIBRARY_SCAN_BATCH_SIZE = 5000;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LibraryService implements OnEvents {
|
export class LibraryService {
|
||||||
private configCore: SystemConfigCore;
|
private configCore: SystemConfigCore;
|
||||||
private watchLibraries = false;
|
private watchLibraries = false;
|
||||||
private watchLock = false;
|
private watchLock = false;
|
||||||
@ -65,7 +66,8 @@ export class LibraryService implements OnEvents {
|
|||||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
|
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
async onBootstrapEvent() {
|
@OnEmit({ event: 'onBootstrap' })
|
||||||
|
async onBootstrap() {
|
||||||
const config = await this.configCore.getConfig({ withCache: false });
|
const config = await this.configCore.getConfig({ withCache: false });
|
||||||
|
|
||||||
const { watch, scan } = config.library;
|
const { watch, scan } = config.library;
|
||||||
@ -102,7 +104,7 @@ export class LibraryService implements OnEvents {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onConfigValidateEvent({ newConfig }: SystemConfigUpdateEvent) {
|
onConfigValidate({ newConfig }: ArgOf<'onConfigValidate'>) {
|
||||||
const { scan } = newConfig.library;
|
const { scan } = newConfig.library;
|
||||||
if (!validateCronExpression(scan.cronExpression)) {
|
if (!validateCronExpression(scan.cronExpression)) {
|
||||||
throw new Error(`Invalid cron expression ${scan.cronExpression}`);
|
throw new Error(`Invalid cron expression ${scan.cronExpression}`);
|
||||||
@ -187,7 +189,8 @@ export class LibraryService implements OnEvents {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async onShutdownEvent() {
|
@OnEmit({ event: 'onShutdown' })
|
||||||
|
async onShutdown() {
|
||||||
await this.unwatchAll();
|
await this.unwatchAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,7 +95,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await sut.onShutdownEvent();
|
await sut.onShutdown();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be defined', () => {
|
it('should be defined', () => {
|
||||||
@ -104,7 +104,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
describe('onBootstrapEvent', () => {
|
describe('onBootstrapEvent', () => {
|
||||||
it('should pause and resume queue during init', async () => {
|
it('should pause and resume queue during init', async () => {
|
||||||
await sut.onBootstrapEvent('microservices');
|
await sut.onBootstrap('microservices');
|
||||||
|
|
||||||
expect(jobMock.pause).toHaveBeenCalledTimes(1);
|
expect(jobMock.pause).toHaveBeenCalledTimes(1);
|
||||||
expect(mapMock.init).toHaveBeenCalledTimes(1);
|
expect(mapMock.init).toHaveBeenCalledTimes(1);
|
||||||
@ -114,7 +114,7 @@ describe(MetadataService.name, () => {
|
|||||||
it('should return if reverse geocoding is disabled', async () => {
|
it('should return if reverse geocoding is disabled', async () => {
|
||||||
systemMock.get.mockResolvedValue({ reverseGeocoding: { enabled: false } });
|
systemMock.get.mockResolvedValue({ reverseGeocoding: { enabled: false } });
|
||||||
|
|
||||||
await sut.onBootstrapEvent('microservices');
|
await sut.onBootstrap('microservices');
|
||||||
|
|
||||||
expect(jobMock.pause).not.toHaveBeenCalled();
|
expect(jobMock.pause).not.toHaveBeenCalled();
|
||||||
expect(mapMock.init).not.toHaveBeenCalled();
|
expect(mapMock.init).not.toHaveBeenCalled();
|
||||||
|
@ -8,6 +8,7 @@ import path from 'node:path';
|
|||||||
import { SystemConfig } from 'src/config';
|
import { SystemConfig } from 'src/config';
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||||
|
import { OnEmit } from 'src/decorators';
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { ExifEntity } from 'src/entities/exif.entity';
|
import { ExifEntity } from 'src/entities/exif.entity';
|
||||||
import { AssetType } from 'src/enum';
|
import { AssetType } from 'src/enum';
|
||||||
@ -15,7 +16,7 @@ import { IAlbumRepository } from 'src/interfaces/album.interface';
|
|||||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
|
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||||
import { ClientEvent, IEventRepository, OnEvents } from 'src/interfaces/event.interface';
|
import { ArgOf, ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
|
||||||
import {
|
import {
|
||||||
IBaseJob,
|
IBaseJob,
|
||||||
IEntityJob,
|
IEntityJob,
|
||||||
@ -86,7 +87,7 @@ const validate = <T>(value: T): NonNullable<T> | null => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MetadataService implements OnEvents {
|
export class MetadataService {
|
||||||
private storageCore: StorageCore;
|
private storageCore: StorageCore;
|
||||||
private configCore: SystemConfigCore;
|
private configCore: SystemConfigCore;
|
||||||
|
|
||||||
@ -120,7 +121,8 @@ export class MetadataService implements OnEvents {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async onBootstrapEvent(app: 'api' | 'microservices') {
|
@OnEmit({ event: 'onBootstrap' })
|
||||||
|
async onBootstrap(app: ArgOf<'onBootstrap'>) {
|
||||||
if (app !== 'microservices') {
|
if (app !== 'microservices') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -128,7 +130,8 @@ export class MetadataService implements OnEvents {
|
|||||||
await this.init(config);
|
await this.init(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
async onConfigUpdateEvent({ newConfig }: { newConfig: SystemConfig }) {
|
@OnEmit({ event: 'onConfigUpdate' })
|
||||||
|
async onConfigUpdate({ newConfig }: ArgOf<'onConfigUpdate'>) {
|
||||||
await this.init(newConfig);
|
await this.init(newConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,7 +153,8 @@ export class MetadataService implements OnEvents {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async onShutdownEvent() {
|
@OnEmit({ event: 'onShutdown' })
|
||||||
|
async onShutdown() {
|
||||||
await this.repository.teardown();
|
await this.repository.teardown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { OnEvents } from 'src/interfaces/event.interface';
|
import { OnEmit } from 'src/decorators';
|
||||||
|
import { ArgOf } from 'src/interfaces/event.interface';
|
||||||
import { IDeleteFilesJob, JobName } from 'src/interfaces/job.interface';
|
import { IDeleteFilesJob, JobName } from 'src/interfaces/job.interface';
|
||||||
import { AssetService } from 'src/services/asset.service';
|
import { AssetService } from 'src/services/asset.service';
|
||||||
import { AuditService } from 'src/services/audit.service';
|
import { AuditService } from 'src/services/audit.service';
|
||||||
@ -19,7 +20,7 @@ import { VersionService } from 'src/services/version.service';
|
|||||||
import { otelShutdown } from 'src/utils/instrumentation';
|
import { otelShutdown } from 'src/utils/instrumentation';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MicroservicesService implements OnEvents {
|
export class MicroservicesService {
|
||||||
constructor(
|
constructor(
|
||||||
private auditService: AuditService,
|
private auditService: AuditService,
|
||||||
private assetService: AssetService,
|
private assetService: AssetService,
|
||||||
@ -38,7 +39,8 @@ export class MicroservicesService implements OnEvents {
|
|||||||
private versionService: VersionService,
|
private versionService: VersionService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async onBootstrapEvent(app: 'api' | 'microservices') {
|
@OnEmit({ event: 'onBootstrap' })
|
||||||
|
async onBootstrap(app: ArgOf<'onBootstrap'>) {
|
||||||
if (app !== 'microservices') {
|
if (app !== 'microservices') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -90,7 +90,7 @@ describe(NotificationService.name, () => {
|
|||||||
const newConfig = configs.smtpEnabled;
|
const newConfig = configs.smtpEnabled;
|
||||||
|
|
||||||
notificationMock.verifySmtp.mockResolvedValue(true);
|
notificationMock.verifySmtp.mockResolvedValue(true);
|
||||||
await expect(sut.onConfigValidateEvent({ oldConfig, newConfig })).resolves.not.toThrow();
|
await expect(sut.onConfigValidate({ oldConfig, newConfig })).resolves.not.toThrow();
|
||||||
expect(notificationMock.verifySmtp).toHaveBeenCalledWith(newConfig.notifications.smtp.transport);
|
expect(notificationMock.verifySmtp).toHaveBeenCalledWith(newConfig.notifications.smtp.transport);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -99,7 +99,7 @@ describe(NotificationService.name, () => {
|
|||||||
const newConfig = configs.smtpTransport;
|
const newConfig = configs.smtpTransport;
|
||||||
|
|
||||||
notificationMock.verifySmtp.mockResolvedValue(true);
|
notificationMock.verifySmtp.mockResolvedValue(true);
|
||||||
await expect(sut.onConfigValidateEvent({ oldConfig, newConfig })).resolves.not.toThrow();
|
await expect(sut.onConfigValidate({ oldConfig, newConfig })).resolves.not.toThrow();
|
||||||
expect(notificationMock.verifySmtp).toHaveBeenCalledWith(newConfig.notifications.smtp.transport);
|
expect(notificationMock.verifySmtp).toHaveBeenCalledWith(newConfig.notifications.smtp.transport);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -107,7 +107,7 @@ describe(NotificationService.name, () => {
|
|||||||
const oldConfig = { ...configs.smtpEnabled };
|
const oldConfig = { ...configs.smtpEnabled };
|
||||||
const newConfig = { ...configs.smtpEnabled };
|
const newConfig = { ...configs.smtpEnabled };
|
||||||
|
|
||||||
await expect(sut.onConfigValidateEvent({ oldConfig, newConfig })).resolves.not.toThrow();
|
await expect(sut.onConfigValidate({ oldConfig, newConfig })).resolves.not.toThrow();
|
||||||
expect(notificationMock.verifySmtp).not.toHaveBeenCalled();
|
expect(notificationMock.verifySmtp).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -115,19 +115,19 @@ describe(NotificationService.name, () => {
|
|||||||
const oldConfig = { ...configs.smtpEnabled };
|
const oldConfig = { ...configs.smtpEnabled };
|
||||||
const newConfig = { ...configs.smtpDisabled };
|
const newConfig = { ...configs.smtpDisabled };
|
||||||
|
|
||||||
await expect(sut.onConfigValidateEvent({ oldConfig, newConfig })).resolves.not.toThrow();
|
await expect(sut.onConfigValidate({ oldConfig, newConfig })).resolves.not.toThrow();
|
||||||
expect(notificationMock.verifySmtp).not.toHaveBeenCalled();
|
expect(notificationMock.verifySmtp).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('onUserSignupEvent', () => {
|
describe('onUserSignupEvent', () => {
|
||||||
it('skips when notify is false', async () => {
|
it('skips when notify is false', async () => {
|
||||||
await sut.onUserSignupEvent({ id: '', notify: false });
|
await sut.onUserSignup({ id: '', notify: false });
|
||||||
expect(jobMock.queue).not.toHaveBeenCalled();
|
expect(jobMock.queue).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should queue notify signup event if notify is true', async () => {
|
it('should queue notify signup event if notify is true', async () => {
|
||||||
await sut.onUserSignupEvent({ id: '', notify: true });
|
await sut.onUserSignup({ id: '', notify: true });
|
||||||
expect(jobMock.queue).toHaveBeenCalledWith({
|
expect(jobMock.queue).toHaveBeenCalledWith({
|
||||||
name: JobName.NOTIFY_SIGNUP,
|
name: JobName.NOTIFY_SIGNUP,
|
||||||
data: { id: '', tempPassword: undefined },
|
data: { id: '', tempPassword: undefined },
|
||||||
@ -137,7 +137,7 @@ describe(NotificationService.name, () => {
|
|||||||
|
|
||||||
describe('onAlbumUpdateEvent', () => {
|
describe('onAlbumUpdateEvent', () => {
|
||||||
it('should queue notify album update event', async () => {
|
it('should queue notify album update event', async () => {
|
||||||
await sut.onAlbumUpdateEvent({ id: '', updatedBy: '42' });
|
await sut.onAlbumUpdate({ id: '', updatedBy: '42' });
|
||||||
expect(jobMock.queue).toHaveBeenCalledWith({
|
expect(jobMock.queue).toHaveBeenCalledWith({
|
||||||
name: JobName.NOTIFY_ALBUM_UPDATE,
|
name: JobName.NOTIFY_ALBUM_UPDATE,
|
||||||
data: { id: '', senderId: '42' },
|
data: { id: '', senderId: '42' },
|
||||||
@ -147,7 +147,7 @@ describe(NotificationService.name, () => {
|
|||||||
|
|
||||||
describe('onAlbumInviteEvent', () => {
|
describe('onAlbumInviteEvent', () => {
|
||||||
it('should queue notify album invite event', async () => {
|
it('should queue notify album invite event', async () => {
|
||||||
await sut.onAlbumInviteEvent({ id: '', userId: '42' });
|
await sut.onAlbumInvite({ id: '', userId: '42' });
|
||||||
expect(jobMock.queue).toHaveBeenCalledWith({
|
expect(jobMock.queue).toHaveBeenCalledWith({
|
||||||
name: JobName.NOTIFY_ALBUM_INVITE,
|
name: JobName.NOTIFY_ALBUM_INVITE,
|
||||||
data: { id: '', recipientId: '42' },
|
data: { id: '', recipientId: '42' },
|
||||||
|
@ -2,17 +2,12 @@ import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
|
|||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
import { DEFAULT_EXTERNAL_DOMAIN } from 'src/constants';
|
import { DEFAULT_EXTERNAL_DOMAIN } from 'src/constants';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||||
|
import { OnEmit } from 'src/decorators';
|
||||||
import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto';
|
import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto';
|
||||||
import { AlbumEntity } from 'src/entities/album.entity';
|
import { AlbumEntity } from 'src/entities/album.entity';
|
||||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||||
import {
|
import { ArgOf } from 'src/interfaces/event.interface';
|
||||||
AlbumInviteEvent,
|
|
||||||
AlbumUpdateEvent,
|
|
||||||
OnEvents,
|
|
||||||
SystemConfigUpdateEvent,
|
|
||||||
UserSignupEvent,
|
|
||||||
} from 'src/interfaces/event.interface';
|
|
||||||
import {
|
import {
|
||||||
IEmailJob,
|
IEmailJob,
|
||||||
IJobRepository,
|
IJobRepository,
|
||||||
@ -30,7 +25,7 @@ import { getFilenameExtension } from 'src/utils/file';
|
|||||||
import { getPreferences } from 'src/utils/preferences';
|
import { getPreferences } from 'src/utils/preferences';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NotificationService implements OnEvents {
|
export class NotificationService {
|
||||||
private configCore: SystemConfigCore;
|
private configCore: SystemConfigCore;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -46,7 +41,8 @@ export class NotificationService implements OnEvents {
|
|||||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, logger);
|
this.configCore = SystemConfigCore.create(systemMetadataRepository, logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
async onConfigValidateEvent({ oldConfig, newConfig }: SystemConfigUpdateEvent) {
|
@OnEmit({ event: 'onConfigValidate', priority: -100 })
|
||||||
|
async onConfigValidate({ oldConfig, newConfig }: ArgOf<'onConfigValidate'>) {
|
||||||
try {
|
try {
|
||||||
if (
|
if (
|
||||||
newConfig.notifications.smtp.enabled &&
|
newConfig.notifications.smtp.enabled &&
|
||||||
@ -60,17 +56,20 @@ export class NotificationService implements OnEvents {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async onUserSignupEvent({ notify, id, tempPassword }: UserSignupEvent) {
|
@OnEmit({ event: 'onUserSignup' })
|
||||||
|
async onUserSignup({ notify, id, tempPassword }: ArgOf<'onUserSignup'>) {
|
||||||
if (notify) {
|
if (notify) {
|
||||||
await this.jobRepository.queue({ name: JobName.NOTIFY_SIGNUP, data: { id, tempPassword } });
|
await this.jobRepository.queue({ name: JobName.NOTIFY_SIGNUP, data: { id, tempPassword } });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async onAlbumUpdateEvent({ id, updatedBy }: AlbumUpdateEvent) {
|
@OnEmit({ event: 'onAlbumUpdate' })
|
||||||
|
async onAlbumUpdate({ id, updatedBy }: ArgOf<'onAlbumUpdate'>) {
|
||||||
await this.jobRepository.queue({ name: JobName.NOTIFY_ALBUM_UPDATE, data: { id, senderId: updatedBy } });
|
await this.jobRepository.queue({ name: JobName.NOTIFY_ALBUM_UPDATE, data: { id, senderId: updatedBy } });
|
||||||
}
|
}
|
||||||
|
|
||||||
async onAlbumInviteEvent({ id, userId }: AlbumInviteEvent) {
|
@OnEmit({ event: 'onAlbumInvite' })
|
||||||
|
async onAlbumInvite({ id, userId }: ArgOf<'onAlbumInvite'>) {
|
||||||
await this.jobRepository.queue({ name: JobName.NOTIFY_ALBUM_INVITE, data: { id, recipientId: userId } });
|
await this.jobRepository.queue({ name: JobName.NOTIFY_ALBUM_INVITE, data: { id, recipientId: userId } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ import { getBuildMetadata, getServerLicensePublicKey } from 'src/config';
|
|||||||
import { serverVersion } from 'src/constants';
|
import { serverVersion } from 'src/constants';
|
||||||
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
|
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||||
|
import { OnEmit } from 'src/decorators';
|
||||||
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
|
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
|
||||||
import {
|
import {
|
||||||
ServerAboutResponseDto,
|
ServerAboutResponseDto,
|
||||||
@ -16,7 +17,6 @@ import {
|
|||||||
} from 'src/dtos/server.dto';
|
} from 'src/dtos/server.dto';
|
||||||
import { SystemMetadataKey } from 'src/enum';
|
import { SystemMetadataKey } from 'src/enum';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import { OnEvents } from 'src/interfaces/event.interface';
|
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
|
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
|
||||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||||
@ -27,7 +27,7 @@ import { mimeTypes } from 'src/utils/mime-types';
|
|||||||
import { isDuplicateDetectionEnabled, isFacialRecognitionEnabled, isSmartSearchEnabled } from 'src/utils/misc';
|
import { isDuplicateDetectionEnabled, isFacialRecognitionEnabled, isSmartSearchEnabled } from 'src/utils/misc';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ServerService implements OnEvents {
|
export class ServerService {
|
||||||
private configCore: SystemConfigCore;
|
private configCore: SystemConfigCore;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -42,7 +42,8 @@ export class ServerService implements OnEvents {
|
|||||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
|
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
async onBootstrapEvent(): Promise<void> {
|
@OnEmit({ event: 'onBootstrap' })
|
||||||
|
async onBootstrap(): Promise<void> {
|
||||||
const featureFlags = await this.getFeatures();
|
const featureFlags = await this.getFeatures();
|
||||||
if (featureFlags.configFile) {
|
if (featureFlags.configFile) {
|
||||||
await this.systemMetadataRepository.set(SystemMetadataKey.ADMIN_ONBOARDING, {
|
await this.systemMetadataRepository.set(SystemMetadataKey.ADMIN_ONBOARDING, {
|
||||||
|
@ -49,7 +49,7 @@ describe(SmartInfoService.name, () => {
|
|||||||
describe('onConfigValidateEvent', () => {
|
describe('onConfigValidateEvent', () => {
|
||||||
it('should allow a valid model', () => {
|
it('should allow a valid model', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
sut.onConfigValidateEvent({
|
sut.onConfigValidate({
|
||||||
newConfig: { machineLearning: { clip: { modelName: 'ViT-B-16__openai' } } } as SystemConfig,
|
newConfig: { machineLearning: { clip: { modelName: 'ViT-B-16__openai' } } } as SystemConfig,
|
||||||
oldConfig: {} as SystemConfig,
|
oldConfig: {} as SystemConfig,
|
||||||
}),
|
}),
|
||||||
@ -58,7 +58,7 @@ describe(SmartInfoService.name, () => {
|
|||||||
|
|
||||||
it('should allow including organization', () => {
|
it('should allow including organization', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
sut.onConfigValidateEvent({
|
sut.onConfigValidate({
|
||||||
newConfig: { machineLearning: { clip: { modelName: 'immich-app/ViT-B-16__openai' } } } as SystemConfig,
|
newConfig: { machineLearning: { clip: { modelName: 'immich-app/ViT-B-16__openai' } } } as SystemConfig,
|
||||||
oldConfig: {} as SystemConfig,
|
oldConfig: {} as SystemConfig,
|
||||||
}),
|
}),
|
||||||
@ -67,7 +67,7 @@ describe(SmartInfoService.name, () => {
|
|||||||
|
|
||||||
it('should fail for an unsupported model', () => {
|
it('should fail for an unsupported model', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
sut.onConfigValidateEvent({
|
sut.onConfigValidate({
|
||||||
newConfig: { machineLearning: { clip: { modelName: 'test-model' } } } as SystemConfig,
|
newConfig: { machineLearning: { clip: { modelName: 'test-model' } } } as SystemConfig,
|
||||||
oldConfig: {} as SystemConfig,
|
oldConfig: {} as SystemConfig,
|
||||||
}),
|
}),
|
||||||
@ -77,7 +77,7 @@ describe(SmartInfoService.name, () => {
|
|||||||
|
|
||||||
describe('onBootstrapEvent', () => {
|
describe('onBootstrapEvent', () => {
|
||||||
it('should return if not microservices', async () => {
|
it('should return if not microservices', async () => {
|
||||||
await sut.onBootstrapEvent('api');
|
await sut.onBootstrap('api');
|
||||||
|
|
||||||
expect(systemMock.get).not.toHaveBeenCalled();
|
expect(systemMock.get).not.toHaveBeenCalled();
|
||||||
expect(searchMock.getDimensionSize).not.toHaveBeenCalled();
|
expect(searchMock.getDimensionSize).not.toHaveBeenCalled();
|
||||||
@ -92,7 +92,7 @@ describe(SmartInfoService.name, () => {
|
|||||||
it('should return if machine learning is disabled', async () => {
|
it('should return if machine learning is disabled', async () => {
|
||||||
systemMock.get.mockResolvedValue(systemConfigStub.machineLearningDisabled);
|
systemMock.get.mockResolvedValue(systemConfigStub.machineLearningDisabled);
|
||||||
|
|
||||||
await sut.onBootstrapEvent('microservices');
|
await sut.onBootstrap('microservices');
|
||||||
|
|
||||||
expect(systemMock.get).toHaveBeenCalledTimes(1);
|
expect(systemMock.get).toHaveBeenCalledTimes(1);
|
||||||
expect(searchMock.getDimensionSize).not.toHaveBeenCalled();
|
expect(searchMock.getDimensionSize).not.toHaveBeenCalled();
|
||||||
@ -107,7 +107,7 @@ describe(SmartInfoService.name, () => {
|
|||||||
it('should return if model and DB dimension size are equal', async () => {
|
it('should return if model and DB dimension size are equal', async () => {
|
||||||
searchMock.getDimensionSize.mockResolvedValue(512);
|
searchMock.getDimensionSize.mockResolvedValue(512);
|
||||||
|
|
||||||
await sut.onBootstrapEvent('microservices');
|
await sut.onBootstrap('microservices');
|
||||||
|
|
||||||
expect(systemMock.get).toHaveBeenCalledTimes(1);
|
expect(systemMock.get).toHaveBeenCalledTimes(1);
|
||||||
expect(searchMock.getDimensionSize).toHaveBeenCalledTimes(1);
|
expect(searchMock.getDimensionSize).toHaveBeenCalledTimes(1);
|
||||||
@ -123,7 +123,7 @@ describe(SmartInfoService.name, () => {
|
|||||||
searchMock.getDimensionSize.mockResolvedValue(768);
|
searchMock.getDimensionSize.mockResolvedValue(768);
|
||||||
jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||||
|
|
||||||
await sut.onBootstrapEvent('microservices');
|
await sut.onBootstrap('microservices');
|
||||||
|
|
||||||
expect(systemMock.get).toHaveBeenCalledTimes(1);
|
expect(systemMock.get).toHaveBeenCalledTimes(1);
|
||||||
expect(searchMock.getDimensionSize).toHaveBeenCalledTimes(1);
|
expect(searchMock.getDimensionSize).toHaveBeenCalledTimes(1);
|
||||||
@ -138,7 +138,7 @@ describe(SmartInfoService.name, () => {
|
|||||||
searchMock.getDimensionSize.mockResolvedValue(768);
|
searchMock.getDimensionSize.mockResolvedValue(768);
|
||||||
jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: true });
|
jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: true });
|
||||||
|
|
||||||
await sut.onBootstrapEvent('microservices');
|
await sut.onBootstrap('microservices');
|
||||||
|
|
||||||
expect(systemMock.get).toHaveBeenCalledTimes(1);
|
expect(systemMock.get).toHaveBeenCalledTimes(1);
|
||||||
expect(searchMock.getDimensionSize).toHaveBeenCalledTimes(1);
|
expect(searchMock.getDimensionSize).toHaveBeenCalledTimes(1);
|
||||||
@ -154,7 +154,7 @@ describe(SmartInfoService.name, () => {
|
|||||||
it('should return if machine learning is disabled', async () => {
|
it('should return if machine learning is disabled', async () => {
|
||||||
systemMock.get.mockResolvedValue(systemConfigStub.machineLearningDisabled);
|
systemMock.get.mockResolvedValue(systemConfigStub.machineLearningDisabled);
|
||||||
|
|
||||||
await sut.onConfigUpdateEvent({
|
await sut.onConfigUpdate({
|
||||||
newConfig: systemConfigStub.machineLearningDisabled as SystemConfig,
|
newConfig: systemConfigStub.machineLearningDisabled as SystemConfig,
|
||||||
oldConfig: systemConfigStub.machineLearningDisabled as SystemConfig,
|
oldConfig: systemConfigStub.machineLearningDisabled as SystemConfig,
|
||||||
});
|
});
|
||||||
@ -172,7 +172,7 @@ describe(SmartInfoService.name, () => {
|
|||||||
it('should return if model and DB dimension size are equal', async () => {
|
it('should return if model and DB dimension size are equal', async () => {
|
||||||
searchMock.getDimensionSize.mockResolvedValue(512);
|
searchMock.getDimensionSize.mockResolvedValue(512);
|
||||||
|
|
||||||
await sut.onConfigUpdateEvent({
|
await sut.onConfigUpdate({
|
||||||
newConfig: {
|
newConfig: {
|
||||||
machineLearning: { clip: { modelName: 'ViT-B-16__openai', enabled: true }, enabled: true },
|
machineLearning: { clip: { modelName: 'ViT-B-16__openai', enabled: true }, enabled: true },
|
||||||
} as SystemConfig,
|
} as SystemConfig,
|
||||||
@ -194,7 +194,7 @@ describe(SmartInfoService.name, () => {
|
|||||||
searchMock.getDimensionSize.mockResolvedValue(512);
|
searchMock.getDimensionSize.mockResolvedValue(512);
|
||||||
jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||||
|
|
||||||
await sut.onConfigUpdateEvent({
|
await sut.onConfigUpdate({
|
||||||
newConfig: {
|
newConfig: {
|
||||||
machineLearning: { clip: { modelName: 'ViT-L-14-quickgelu__dfn2b', enabled: true }, enabled: true },
|
machineLearning: { clip: { modelName: 'ViT-L-14-quickgelu__dfn2b', enabled: true }, enabled: true },
|
||||||
} as SystemConfig,
|
} as SystemConfig,
|
||||||
@ -215,7 +215,7 @@ describe(SmartInfoService.name, () => {
|
|||||||
searchMock.getDimensionSize.mockResolvedValue(512);
|
searchMock.getDimensionSize.mockResolvedValue(512);
|
||||||
jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||||
|
|
||||||
await sut.onConfigUpdateEvent({
|
await sut.onConfigUpdate({
|
||||||
newConfig: {
|
newConfig: {
|
||||||
machineLearning: { clip: { modelName: 'ViT-B-32__openai', enabled: true }, enabled: true },
|
machineLearning: { clip: { modelName: 'ViT-B-32__openai', enabled: true }, enabled: true },
|
||||||
} as SystemConfig,
|
} as SystemConfig,
|
||||||
@ -237,7 +237,7 @@ describe(SmartInfoService.name, () => {
|
|||||||
searchMock.getDimensionSize.mockResolvedValue(512);
|
searchMock.getDimensionSize.mockResolvedValue(512);
|
||||||
jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: true });
|
jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: true });
|
||||||
|
|
||||||
await sut.onConfigUpdateEvent({
|
await sut.onConfigUpdate({
|
||||||
newConfig: {
|
newConfig: {
|
||||||
machineLearning: { clip: { modelName: 'ViT-B-32__openai', enabled: true }, enabled: true },
|
machineLearning: { clip: { modelName: 'ViT-B-32__openai', enabled: true }, enabled: true },
|
||||||
} as SystemConfig,
|
} as SystemConfig,
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { SystemConfig } from 'src/config';
|
import { SystemConfig } from 'src/config';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||||
|
import { OnEmit } from 'src/decorators';
|
||||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||||
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
|
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||||
import { OnEvents, SystemConfigUpdateEvent } from 'src/interfaces/event.interface';
|
import { ArgOf } from 'src/interfaces/event.interface';
|
||||||
import {
|
import {
|
||||||
IBaseJob,
|
IBaseJob,
|
||||||
IEntityJob,
|
IEntityJob,
|
||||||
@ -21,7 +22,7 @@ import { getCLIPModelInfo, isSmartSearchEnabled } from 'src/utils/misc';
|
|||||||
import { usePagination } from 'src/utils/pagination';
|
import { usePagination } from 'src/utils/pagination';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SmartInfoService implements OnEvents {
|
export class SmartInfoService {
|
||||||
private configCore: SystemConfigCore;
|
private configCore: SystemConfigCore;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -37,7 +38,8 @@ export class SmartInfoService implements OnEvents {
|
|||||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
|
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
async onBootstrapEvent(app: 'api' | 'microservices') {
|
@OnEmit({ event: 'onBootstrap' })
|
||||||
|
async onBootstrap(app: ArgOf<'onBootstrap'>) {
|
||||||
if (app !== 'microservices') {
|
if (app !== 'microservices') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -46,7 +48,8 @@ export class SmartInfoService implements OnEvents {
|
|||||||
await this.init(config);
|
await this.init(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
onConfigValidateEvent({ newConfig }: SystemConfigUpdateEvent) {
|
@OnEmit({ event: 'onConfigValidate' })
|
||||||
|
onConfigValidate({ newConfig }: ArgOf<'onConfigValidate'>) {
|
||||||
try {
|
try {
|
||||||
getCLIPModelInfo(newConfig.machineLearning.clip.modelName);
|
getCLIPModelInfo(newConfig.machineLearning.clip.modelName);
|
||||||
} catch {
|
} catch {
|
||||||
@ -56,7 +59,8 @@ export class SmartInfoService implements OnEvents {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async onConfigUpdateEvent({ oldConfig, newConfig }: SystemConfigUpdateEvent) {
|
@OnEmit({ event: 'onConfigUpdate' })
|
||||||
|
async onConfigUpdate({ oldConfig, newConfig }: ArgOf<'onConfigUpdate'>) {
|
||||||
await this.init(newConfig, oldConfig);
|
await this.init(newConfig, oldConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,10 +76,10 @@ describe(StorageTemplateService.name, () => {
|
|||||||
SystemConfigCore.create(systemMock, loggerMock).config$.next(defaults);
|
SystemConfigCore.create(systemMock, loggerMock).config$.next(defaults);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('onConfigValidateEvent', () => {
|
describe('onConfigValidate', () => {
|
||||||
it('should allow valid templates', () => {
|
it('should allow valid templates', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
sut.onConfigValidateEvent({
|
sut.onConfigValidate({
|
||||||
newConfig: {
|
newConfig: {
|
||||||
storageTemplate: {
|
storageTemplate: {
|
||||||
template:
|
template:
|
||||||
@ -93,7 +93,7 @@ describe(StorageTemplateService.name, () => {
|
|||||||
|
|
||||||
it('should fail for an invalid template', () => {
|
it('should fail for an invalid template', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
sut.onConfigValidateEvent({
|
sut.onConfigValidate({
|
||||||
newConfig: {
|
newConfig: {
|
||||||
storageTemplate: {
|
storageTemplate: {
|
||||||
template: '{{foo}}',
|
template: '{{foo}}',
|
||||||
|
@ -15,6 +15,7 @@ import {
|
|||||||
} from 'src/constants';
|
} from 'src/constants';
|
||||||
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
|
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||||
|
import { OnEmit } from 'src/decorators';
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { AssetPathType } from 'src/entities/move.entity';
|
import { AssetPathType } from 'src/entities/move.entity';
|
||||||
import { AssetType } from 'src/enum';
|
import { AssetType } from 'src/enum';
|
||||||
@ -22,7 +23,7 @@ import { IAlbumRepository } from 'src/interfaces/album.interface';
|
|||||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
|
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||||
import { OnEvents, SystemConfigUpdateEvent } from 'src/interfaces/event.interface';
|
import { ArgOf } from 'src/interfaces/event.interface';
|
||||||
import { IEntityJob, JOBS_ASSET_PAGINATION_SIZE, JobStatus } from 'src/interfaces/job.interface';
|
import { IEntityJob, JOBS_ASSET_PAGINATION_SIZE, JobStatus } from 'src/interfaces/job.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { IMoveRepository } from 'src/interfaces/move.interface';
|
import { IMoveRepository } from 'src/interfaces/move.interface';
|
||||||
@ -46,7 +47,7 @@ interface RenderMetadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class StorageTemplateService implements OnEvents {
|
export class StorageTemplateService {
|
||||||
private configCore: SystemConfigCore;
|
private configCore: SystemConfigCore;
|
||||||
private storageCore: StorageCore;
|
private storageCore: StorageCore;
|
||||||
private _template: {
|
private _template: {
|
||||||
@ -88,7 +89,8 @@ export class StorageTemplateService implements OnEvents {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onConfigValidateEvent({ newConfig }: SystemConfigUpdateEvent) {
|
@OnEmit({ event: 'onConfigValidate' })
|
||||||
|
onConfigValidate({ newConfig }: ArgOf<'onConfigValidate'>) {
|
||||||
try {
|
try {
|
||||||
const { compiled } = this.compile(newConfig.storageTemplate.template);
|
const { compiled } = this.compile(newConfig.storageTemplate.template);
|
||||||
this.render(compiled, {
|
this.render(compiled, {
|
||||||
|
@ -20,9 +20,9 @@ describe(StorageService.name, () => {
|
|||||||
expect(sut).toBeDefined();
|
expect(sut).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('onBootstrapEvent', () => {
|
describe('onBootstrap', () => {
|
||||||
it('should create the library folder on initialization', () => {
|
it('should create the library folder on initialization', () => {
|
||||||
sut.onBootstrapEvent();
|
sut.onBootstrap();
|
||||||
expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/library');
|
expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/library');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
|
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
|
||||||
import { OnEvents } from 'src/interfaces/event.interface';
|
import { OnEmit } from 'src/decorators';
|
||||||
import { IDeleteFilesJob, JobStatus } from 'src/interfaces/job.interface';
|
import { IDeleteFilesJob, JobStatus } from 'src/interfaces/job.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class StorageService implements OnEvents {
|
export class StorageService {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||||
@ -14,7 +14,8 @@ export class StorageService implements OnEvents {
|
|||||||
this.logger.setContext(StorageService.name);
|
this.logger.setContext(StorageService.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
onBootstrapEvent() {
|
@OnEmit({ event: 'onBootstrap' })
|
||||||
|
onBootstrap() {
|
||||||
const libraryBase = StorageCore.getBaseFolder(StorageFolder.LIBRARY);
|
const libraryBase = StorageCore.getBaseFolder(StorageFolder.LIBRARY);
|
||||||
this.storageRepository.mkdirSync(libraryBase);
|
this.storageRepository.mkdirSync(libraryBase);
|
||||||
}
|
}
|
||||||
|
@ -13,20 +13,14 @@ import {
|
|||||||
supportedYearTokens,
|
supportedYearTokens,
|
||||||
} from 'src/constants';
|
} from 'src/constants';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||||
import { EventHandlerOptions, OnServerEvent } from 'src/decorators';
|
import { OnEmit, OnServerEvent } from 'src/decorators';
|
||||||
import { SystemConfigDto, SystemConfigTemplateStorageOptionDto, mapConfig } from 'src/dtos/system-config.dto';
|
import { SystemConfigDto, SystemConfigTemplateStorageOptionDto, mapConfig } from 'src/dtos/system-config.dto';
|
||||||
import {
|
import { ArgOf, ClientEvent, IEventRepository, ServerEvent } from 'src/interfaces/event.interface';
|
||||||
ClientEvent,
|
|
||||||
IEventRepository,
|
|
||||||
OnEvents,
|
|
||||||
ServerEvent,
|
|
||||||
SystemConfigUpdateEvent,
|
|
||||||
} from 'src/interfaces/event.interface';
|
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SystemConfigService implements OnEvents {
|
export class SystemConfigService {
|
||||||
private core: SystemConfigCore;
|
private core: SystemConfigCore;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -39,8 +33,8 @@ export class SystemConfigService implements OnEvents {
|
|||||||
this.core.config$.subscribe((config) => this.setLogLevel(config));
|
this.core.config$.subscribe((config) => this.setLogLevel(config));
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventHandlerOptions({ priority: -100 })
|
@OnEmit({ event: 'onBootstrap', priority: -100 })
|
||||||
async onBootstrapEvent() {
|
async onBootstrap() {
|
||||||
const config = await this.core.getConfig({ withCache: false });
|
const config = await this.core.getConfig({ withCache: false });
|
||||||
this.core.config$.next(config);
|
this.core.config$.next(config);
|
||||||
}
|
}
|
||||||
@ -54,7 +48,8 @@ export class SystemConfigService implements OnEvents {
|
|||||||
return mapConfig(defaults);
|
return mapConfig(defaults);
|
||||||
}
|
}
|
||||||
|
|
||||||
onConfigValidateEvent({ newConfig, oldConfig }: SystemConfigUpdateEvent) {
|
@OnEmit({ event: 'onConfigValidate' })
|
||||||
|
onConfigValidate({ newConfig, oldConfig }: ArgOf<'onConfigValidate'>) {
|
||||||
if (!_.isEqual(instanceToPlain(newConfig.logging), oldConfig.logging) && this.getEnvLogLevel()) {
|
if (!_.isEqual(instanceToPlain(newConfig.logging), oldConfig.logging) && this.getEnvLogLevel()) {
|
||||||
throw new Error('Logging cannot be changed while the environment variable IMMICH_LOG_LEVEL is set.');
|
throw new Error('Logging cannot be changed while the environment variable IMMICH_LOG_LEVEL is set.');
|
||||||
}
|
}
|
||||||
@ -68,7 +63,7 @@ export class SystemConfigService implements OnEvents {
|
|||||||
const oldConfig = await this.core.getConfig({ withCache: false });
|
const oldConfig = await this.core.getConfig({ withCache: false });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.eventRepository.emit('onConfigValidateEvent', { newConfig: dto, oldConfig });
|
await this.eventRepository.emit('onConfigValidate', { newConfig: dto, oldConfig });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.warn(`Unable to save system config due to a validation error: ${error}`);
|
this.logger.warn(`Unable to save system config due to a validation error: ${error}`);
|
||||||
throw new BadRequestException(error instanceof Error ? error.message : error);
|
throw new BadRequestException(error instanceof Error ? error.message : error);
|
||||||
@ -79,7 +74,7 @@ export class SystemConfigService implements OnEvents {
|
|||||||
// TODO probably move web socket emits to a separate service
|
// TODO probably move web socket emits to a separate service
|
||||||
this.eventRepository.clientBroadcast(ClientEvent.CONFIG_UPDATE, {});
|
this.eventRepository.clientBroadcast(ClientEvent.CONFIG_UPDATE, {});
|
||||||
this.eventRepository.serverSend(ServerEvent.CONFIG_UPDATE, null);
|
this.eventRepository.serverSend(ServerEvent.CONFIG_UPDATE, null);
|
||||||
await this.eventRepository.emit('onConfigUpdateEvent', { newConfig, oldConfig });
|
await this.eventRepository.emit('onConfigUpdate', { newConfig, oldConfig });
|
||||||
|
|
||||||
return mapConfig(newConfig);
|
return mapConfig(newConfig);
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,7 @@ export class UserAdminService {
|
|||||||
const { notify, ...rest } = dto;
|
const { notify, ...rest } = dto;
|
||||||
const user = await this.userCore.createUser(rest);
|
const user = await this.userCore.createUser(rest);
|
||||||
|
|
||||||
await this.eventRepository.emit('onUserSignupEvent', {
|
await this.eventRepository.emit('onUserSignup', {
|
||||||
notify: !!notify,
|
notify: !!notify,
|
||||||
id: user.id,
|
id: user.id,
|
||||||
tempPassword: user.shouldChangePassword ? rest.password : undefined,
|
tempPassword: user.shouldChangePassword ? rest.password : undefined,
|
||||||
|
@ -3,11 +3,11 @@ import { DateTime } from 'luxon';
|
|||||||
import semver, { SemVer } from 'semver';
|
import semver, { SemVer } from 'semver';
|
||||||
import { isDev, serverVersion } from 'src/constants';
|
import { isDev, serverVersion } from 'src/constants';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||||
import { OnServerEvent } from 'src/decorators';
|
import { OnEmit, OnServerEvent } from 'src/decorators';
|
||||||
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
|
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
|
||||||
import { VersionCheckMetadata } from 'src/entities/system-metadata.entity';
|
import { VersionCheckMetadata } from 'src/entities/system-metadata.entity';
|
||||||
import { SystemMetadataKey } from 'src/enum';
|
import { SystemMetadataKey } from 'src/enum';
|
||||||
import { ClientEvent, IEventRepository, OnEvents, ServerEvent, ServerEventMap } from 'src/interfaces/event.interface';
|
import { ClientEvent, IEventRepository, ServerEvent, ServerEventMap } from 'src/interfaces/event.interface';
|
||||||
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
|
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
|
||||||
@ -23,7 +23,7 @@ const asNotification = ({ checkedAt, releaseVersion }: VersionCheckMetadata): Re
|
|||||||
};
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class VersionService implements OnEvents {
|
export class VersionService {
|
||||||
private configCore: SystemConfigCore;
|
private configCore: SystemConfigCore;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -37,7 +37,8 @@ export class VersionService implements OnEvents {
|
|||||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
|
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
async onBootstrapEvent(): Promise<void> {
|
@OnEmit({ event: 'onBootstrap' })
|
||||||
|
async onBootstrap(): Promise<void> {
|
||||||
await this.handleVersionCheck();
|
await this.handleVersionCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,33 +1,57 @@
|
|||||||
import { ModuleRef, Reflector } from '@nestjs/core';
|
import { ModuleRef, Reflector } from '@nestjs/core';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { HandlerOptions } from 'src/decorators';
|
import { EmitConfig } from 'src/decorators';
|
||||||
import { EmitEvent, EmitEventHandler, IEventRepository, OnEvents, events } from 'src/interfaces/event.interface';
|
import { EmitEvent, EmitHandler, IEventRepository } from 'src/interfaces/event.interface';
|
||||||
import { Metadata } from 'src/middleware/auth.guard';
|
import { Metadata } from 'src/middleware/auth.guard';
|
||||||
import { services } from 'src/services';
|
import { services } from 'src/services';
|
||||||
|
|
||||||
|
type Item<T extends EmitEvent> = {
|
||||||
|
event: T;
|
||||||
|
handler: EmitHandler<T>;
|
||||||
|
priority: number;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
export const setupEventHandlers = (moduleRef: ModuleRef) => {
|
export const setupEventHandlers = (moduleRef: ModuleRef) => {
|
||||||
const reflector = moduleRef.get(Reflector, { strict: false });
|
const reflector = moduleRef.get(Reflector, { strict: false });
|
||||||
const repository = moduleRef.get<IEventRepository>(IEventRepository);
|
const repository = moduleRef.get<IEventRepository>(IEventRepository);
|
||||||
const handlers: Array<{ event: EmitEvent; handler: EmitEventHandler<EmitEvent>; priority: number }> = [];
|
const items: Item<EmitEvent>[] = [];
|
||||||
|
|
||||||
// discovery
|
// discovery
|
||||||
for (const Service of services) {
|
for (const Service of services) {
|
||||||
const instance = moduleRef.get<OnEvents>(Service);
|
const instance = moduleRef.get<any>(Service);
|
||||||
for (const event of events) {
|
const ctx = Object.getPrototypeOf(instance);
|
||||||
const handler = instance[event] as EmitEventHandler<typeof event>;
|
for (const property of Object.getOwnPropertyNames(ctx)) {
|
||||||
|
const descriptor = Object.getOwnPropertyDescriptor(ctx, property);
|
||||||
|
if (!descriptor || descriptor.get || descriptor.set) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handler = instance[property];
|
||||||
if (typeof handler !== 'function') {
|
if (typeof handler !== 'function') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = reflector.get<HandlerOptions>(Metadata.EVENT_HANDLER_OPTIONS, handler);
|
const options = reflector.get<EmitConfig>(Metadata.ON_EMIT_CONFIG, handler);
|
||||||
const priority = options?.priority || 0;
|
if (!options) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
handlers.push({ event, handler: handler.bind(instance), priority });
|
items.push({
|
||||||
|
event: options.event,
|
||||||
|
priority: options.priority || 0,
|
||||||
|
handler: handler.bind(instance),
|
||||||
|
label: `${Service.name}.${handler.name}`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handlers = _.orderBy(items, ['priority'], ['asc']);
|
||||||
|
|
||||||
// register by priority
|
// register by priority
|
||||||
for (const { event, handler } of _.orderBy(handlers, ['priority'], ['asc'])) {
|
for (const { event, handler } of handlers) {
|
||||||
repository.on(event, handler);
|
repository.on(event as EmitEvent, handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return handlers;
|
||||||
};
|
};
|
||||||
|
@ -14,13 +14,21 @@
|
|||||||
SettingInputFieldType,
|
SettingInputFieldType,
|
||||||
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||||
import { ByteUnit, convertFromBytes, convertToBytes } from '$lib/utils/byte-units';
|
import { ByteUnit, convertFromBytes, convertToBytes } from '$lib/utils/byte-units';
|
||||||
|
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||||
|
|
||||||
let archiveSize = convertFromBytes($preferences?.download?.archiveSize || 4, ByteUnit.GiB);
|
let archiveSize = convertFromBytes($preferences?.download?.archiveSize || 4, ByteUnit.GiB);
|
||||||
|
let includeEmbeddedVideos = $preferences?.download?.includeEmbeddedVideos || false;
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
try {
|
try {
|
||||||
const dto = { download: { archiveSize: Math.floor(convertToBytes(archiveSize, ByteUnit.GiB)) } };
|
const newPreferences = await updateMyPreferences({
|
||||||
const newPreferences = await updateMyPreferences({ userPreferencesUpdateDto: dto });
|
userPreferencesUpdateDto: {
|
||||||
|
download: {
|
||||||
|
archiveSize: Math.floor(convertToBytes(archiveSize, ByteUnit.GiB)),
|
||||||
|
includeEmbeddedVideos,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
$preferences = newPreferences;
|
$preferences = newPreferences;
|
||||||
|
|
||||||
notificationController.show({ message: $t('saved_settings'), type: NotificationType.Info });
|
notificationController.show({ message: $t('saved_settings'), type: NotificationType.Info });
|
||||||
@ -34,14 +42,17 @@
|
|||||||
<div in:fade={{ duration: 500 }}>
|
<div in:fade={{ duration: 500 }}>
|
||||||
<form autocomplete="off" on:submit|preventDefault>
|
<form autocomplete="off" on:submit|preventDefault>
|
||||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||||
<div class="ml-4">
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.NUMBER}
|
inputType={SettingInputFieldType.NUMBER}
|
||||||
label={$t('archive_size')}
|
label={$t('archive_size')}
|
||||||
desc={$t('archive_size_description')}
|
desc={$t('archive_size_description')}
|
||||||
bind:value={archiveSize}
|
bind:value={archiveSize}
|
||||||
/>
|
/>
|
||||||
</div>
|
<SettingSwitch
|
||||||
|
title={$t('download_include_embedded_motion_videos')}
|
||||||
|
subtitle={$t('download_include_embedded_motion_videos_description')}
|
||||||
|
bind:checked={includeEmbeddedVideos}
|
||||||
|
></SettingSwitch>
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
<Button type="submit" size="sm" on:click={() => handleSave()}>{$t('save')}</Button>
|
<Button type="submit" size="sm" on:click={() => handleSave()}>{$t('save')}</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -368,7 +368,7 @@
|
|||||||
"appears_in": "Appears in",
|
"appears_in": "Appears in",
|
||||||
"archive": "Archive",
|
"archive": "Archive",
|
||||||
"archive_or_unarchive_photo": "Archive or unarchive photo",
|
"archive_or_unarchive_photo": "Archive or unarchive photo",
|
||||||
"archive_size": "Archive Size",
|
"archive_size": "Archive size",
|
||||||
"archive_size_description": "Configure the archive size for downloads (in GiB)",
|
"archive_size_description": "Configure the archive size for downloads (in GiB)",
|
||||||
"archived_count": "{count, plural, other {Archived #}}",
|
"archived_count": "{count, plural, other {Archived #}}",
|
||||||
"are_these_the_same_person": "Are these the same person?",
|
"are_these_the_same_person": "Are these the same person?",
|
||||||
@ -512,6 +512,8 @@
|
|||||||
"do_not_show_again": "Do not show this message again",
|
"do_not_show_again": "Do not show this message again",
|
||||||
"done": "Done",
|
"done": "Done",
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
|
"download_include_embedded_motion_videos": "Embedded videos",
|
||||||
|
"download_include_embedded_motion_videos_description": "Include videos embedded in motion photos as a separate file",
|
||||||
"download_settings": "Download",
|
"download_settings": "Download",
|
||||||
"download_settings_description": "Manage settings related to asset download",
|
"download_settings_description": "Manage settings related to asset download",
|
||||||
"downloading": "Downloading",
|
"downloading": "Downloading",
|
||||||
|
@ -172,14 +172,20 @@ export const downloadFile = async (asset: AssetResponseDto) => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const isAndroidMotionVideo = (asset: AssetResponseDto) => {
|
||||||
|
return asset.originalPath.includes('encoded-video');
|
||||||
|
};
|
||||||
|
|
||||||
if (asset.livePhotoVideoId) {
|
if (asset.livePhotoVideoId) {
|
||||||
const motionAsset = await getAssetInfo({ id: asset.livePhotoVideoId, key: getKey() });
|
const motionAsset = await getAssetInfo({ id: asset.livePhotoVideoId, key: getKey() });
|
||||||
|
if (!isAndroidMotionVideo(motionAsset) || get(preferences).download.includeEmbeddedVideos) {
|
||||||
assets.push({
|
assets.push({
|
||||||
filename: motionAsset.originalFileName,
|
filename: motionAsset.originalFileName,
|
||||||
id: asset.livePhotoVideoId,
|
id: asset.livePhotoVideoId,
|
||||||
size: motionAsset.exifInfo?.fileSizeInByte || 0,
|
size: motionAsset.exifInfo?.fileSizeInByte || 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const { filename, id, size } of assets) {
|
for (const { filename, id, size } of assets) {
|
||||||
const downloadKey = filename;
|
const downloadKey = filename;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user