Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c234c95880 | ||
|
|
7cc7fc0a0c | ||
|
|
897d49f734 | ||
|
|
051c958c8b | ||
|
|
56627caf5b |
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Alex
|
||||
Copyright (c) 2022 Hau Tran
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
5
Makefile
5
Makefile
@@ -2,4 +2,7 @@ dev:
|
||||
docker-compose -f ./server/docker-compose.yml up
|
||||
|
||||
dev-update:
|
||||
docker-compose -f ./server/docker-compose.yml up --build -V
|
||||
docker-compose -f ./server/docker-compose.yml up --build -V
|
||||
|
||||
dev-scale:
|
||||
docker-compose -f ./server/docker-compose.yml up --build -V --scale immich_server=3 --remove-orphans
|
||||
|
||||
25
README.md
25
README.md
@@ -28,17 +28,13 @@ This project is under heavy development, there will be continous functions, feat
|
||||
|
||||
# Features
|
||||
|
||||
[x] Upload assets(videos/images)
|
||||
|
||||
[x] View assets
|
||||
|
||||
[x] Quick navigation with drag scroll bar
|
||||
|
||||
[x] Auto Backup
|
||||
|
||||
[x] Support HEIC/HEIF Backup
|
||||
|
||||
[x] Extract and display EXIF info
|
||||
- Upload assets(videos/images).
|
||||
- View assets.
|
||||
- Quick navigation with drag scroll bar.
|
||||
- Auto Backup.
|
||||
- Support HEIC/HEIF Backup.
|
||||
- Extract and display EXIF info.
|
||||
- Real-time render from multi-device upload event.
|
||||
|
||||
# Development
|
||||
|
||||
@@ -53,17 +49,18 @@ You can use docker compose for development, there are several services that comp
|
||||
|
||||
Navigate to `server` directory and run
|
||||
|
||||
```
|
||||
````
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Then populate the value in there.
|
||||
|
||||
Pay attention to the key `UPLOAD_LOCATION`, this directory must exist and is owned by the user that run the `docker-compose` command below.
|
||||
|
||||
To start, run
|
||||
|
||||
```bash
|
||||
docker-compose -f ./server/docker-compose.yml up
|
||||
```
|
||||
````
|
||||
|
||||
To force rebuild node modules after installing new packages
|
||||
|
||||
|
||||
@@ -2,9 +2,11 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/home/providers/asset.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/providers/app_state.provider.dart';
|
||||
import 'package:immich_mobile/shared/providers/backup.provider.dart';
|
||||
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
||||
import 'constants/hive_box.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
@@ -36,20 +38,23 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
|
||||
switch (state) {
|
||||
case AppLifecycleState.resumed:
|
||||
debugPrint("[APP STATE] resumed");
|
||||
ref.read(appStateProvider.notifier).state = AppStateEnum.resumed;
|
||||
ref.read(backupProvider.notifier).resumeBackup();
|
||||
ref.watch(appStateProvider.notifier).state = AppStateEnum.resumed;
|
||||
ref.watch(backupProvider.notifier).resumeBackup();
|
||||
ref.watch(websocketProvider.notifier).connect();
|
||||
ref.watch(assetProvider.notifier).getAllAsset();
|
||||
break;
|
||||
case AppLifecycleState.inactive:
|
||||
debugPrint("[APP STATE] inactive");
|
||||
ref.read(appStateProvider.notifier).state = AppStateEnum.inactive;
|
||||
ref.watch(appStateProvider.notifier).state = AppStateEnum.inactive;
|
||||
ref.watch(websocketProvider.notifier).disconnect();
|
||||
break;
|
||||
case AppLifecycleState.paused:
|
||||
debugPrint("[APP STATE] paused");
|
||||
ref.read(appStateProvider.notifier).state = AppStateEnum.paused;
|
||||
ref.watch(appStateProvider.notifier).state = AppStateEnum.paused;
|
||||
break;
|
||||
case AppLifecycleState.detached:
|
||||
debugPrint("[APP STATE] detached");
|
||||
ref.read(appStateProvider.notifier).state = AppStateEnum.detached;
|
||||
ref.watch(appStateProvider.notifier).state = AppStateEnum.detached;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
@@ -10,7 +9,6 @@ import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
|
||||
import 'package:immich_mobile/modules/home/services/asset.service.dart';
|
||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
||||
import 'package:immich_mobile/shared/models/immich_asset_with_exif.model.dart';
|
||||
import 'package:photo_view/photo_view.dart';
|
||||
|
||||
// ignore: must_be_immutable
|
||||
class ImageViewerPage extends HookConsumerWidget {
|
||||
@@ -35,6 +33,7 @@ class ImageViewerPage extends HookConsumerWidget {
|
||||
|
||||
useEffect(() {
|
||||
getAssetExif();
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
return Scaffold(
|
||||
@@ -60,12 +59,34 @@ class ImageViewerPage extends HookConsumerWidget {
|
||||
imageUrl: imageUrl,
|
||||
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
|
||||
fadeInDuration: const Duration(milliseconds: 250),
|
||||
errorWidget: (context, url, error) => const Icon(Icons.error),
|
||||
imageBuilder: (context, imageProvider) {
|
||||
return PhotoView(imageProvider: imageProvider);
|
||||
},
|
||||
errorWidget: (context, url, error) => ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 300),
|
||||
child: Wrap(
|
||||
spacing: 32,
|
||||
runSpacing: 32,
|
||||
alignment: WrapAlignment.center,
|
||||
children: [
|
||||
const Text(
|
||||
"Failed To Render Image - Possibly Corrupted Data",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 16, color: Colors.white),
|
||||
),
|
||||
SingleChildScrollView(
|
||||
child: Text(
|
||||
error.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[400]),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// imageBuilder: (context, imageProvider) {
|
||||
// return PhotoView(imageProvider: imageProvider);
|
||||
// },
|
||||
placeholder: (context, url) {
|
||||
return CachedNetworkImage(
|
||||
cacheKey: thumbnailUrl,
|
||||
fit: BoxFit.cover,
|
||||
imageUrl: thumbnailUrl,
|
||||
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
|
||||
@@ -74,7 +95,10 @@ class ImageViewerPage extends HookConsumerWidget {
|
||||
scale: 0.2,
|
||||
child: CircularProgressIndicator(value: downloadProgress.progress),
|
||||
),
|
||||
errorWidget: (context, url, error) => const Icon(Icons.error),
|
||||
errorWidget: (context, url, error) => Icon(
|
||||
Icons.error,
|
||||
color: Colors.grey[300],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import 'dart:convert';
|
||||
|
||||
class DeleteAssetResponse {
|
||||
final String id;
|
||||
final String status;
|
||||
|
||||
DeleteAssetResponse({
|
||||
required this.id,
|
||||
required this.status,
|
||||
});
|
||||
|
||||
DeleteAssetResponse copyWith({
|
||||
String? id,
|
||||
String? status,
|
||||
}) {
|
||||
return DeleteAssetResponse(
|
||||
id: id ?? this.id,
|
||||
status: status ?? this.status,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
'status': status,
|
||||
};
|
||||
}
|
||||
|
||||
factory DeleteAssetResponse.fromMap(Map<String, dynamic> map) {
|
||||
return DeleteAssetResponse(
|
||||
id: map['id'] ?? '',
|
||||
status: map['status'] ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
String toJson() => json.encode(toMap());
|
||||
|
||||
factory DeleteAssetResponse.fromJson(String source) => DeleteAssetResponse.fromMap(json.decode(source));
|
||||
|
||||
@override
|
||||
String toString() => 'DeleteAssetResponse(id: $id, status: $status)';
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is DeleteAssetResponse && other.id == id && other.status == status;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode ^ status.hashCode;
|
||||
}
|
||||
@@ -1,99 +1,74 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/home/models/get_all_asset_respose.model.dart';
|
||||
import 'package:immich_mobile/modules/home/models/delete_asset_response.model.dart';
|
||||
import 'package:immich_mobile/modules/home/services/asset.service.dart';
|
||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:immich_mobile/shared/services/device_info.service.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
class AssetNotifier extends StateNotifier<List<ImmichAssetGroupByDate>> {
|
||||
class AssetNotifier extends StateNotifier<List<ImmichAsset>> {
|
||||
final AssetService _assetService = AssetService();
|
||||
final DeviceInfoService _deviceInfoService = DeviceInfoService();
|
||||
final Ref ref;
|
||||
|
||||
AssetNotifier() : super([]);
|
||||
AssetNotifier(this.ref) : super([]);
|
||||
|
||||
late String? nextPageKey = "";
|
||||
bool isFetching = false;
|
||||
getAllAsset() async {
|
||||
List<ImmichAsset>? allAssets = await _assetService.getAllAsset();
|
||||
|
||||
// Get All assets
|
||||
getAllAssets() async {
|
||||
GetAllAssetResponse? res = await _assetService.getAllAsset();
|
||||
nextPageKey = res?.nextPageKey;
|
||||
|
||||
if (res != null) {
|
||||
for (var assets in res.data) {
|
||||
state = [...state, assets];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get Asset From The Past
|
||||
getOlderAsset() async {
|
||||
if (nextPageKey != null && !isFetching) {
|
||||
isFetching = true;
|
||||
GetAllAssetResponse? res = await _assetService.getOlderAsset(nextPageKey);
|
||||
|
||||
if (res != null) {
|
||||
nextPageKey = res.nextPageKey;
|
||||
|
||||
List<ImmichAssetGroupByDate> previousState = state;
|
||||
List<ImmichAssetGroupByDate> currentState = [];
|
||||
|
||||
for (var assets in res.data) {
|
||||
currentState = [...currentState, assets];
|
||||
}
|
||||
|
||||
if (previousState.last.date == currentState.first.date) {
|
||||
previousState.last.assets = [...previousState.last.assets, ...currentState.first.assets];
|
||||
state = [...previousState, ...currentState.sublist(1)];
|
||||
} else {
|
||||
state = [...previousState, ...currentState];
|
||||
}
|
||||
}
|
||||
|
||||
isFetching = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Get newer asset from the current time
|
||||
getNewAsset() async {
|
||||
if (state.isNotEmpty) {
|
||||
var latestGroup = state.first;
|
||||
|
||||
// Sort the last asset group and put the lastest asset in front.
|
||||
latestGroup.assets.sortByCompare<DateTime>((e) => DateTime.parse(e.createdAt), (a, b) => b.compareTo(a));
|
||||
var latestAsset = latestGroup.assets.first;
|
||||
var formatDateTemplate = 'y-MM-dd';
|
||||
var latestAssetDateText = DateFormat(formatDateTemplate).format(DateTime.parse(latestAsset.createdAt));
|
||||
|
||||
List<ImmichAsset> newAssets = await _assetService.getNewAsset(latestAsset.createdAt);
|
||||
|
||||
if (newAssets.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Grouping by data
|
||||
var groupByDateList = groupBy<ImmichAsset, String>(
|
||||
newAssets, (asset) => DateFormat(formatDateTemplate).format(DateTime.parse(asset.createdAt)));
|
||||
|
||||
groupByDateList.forEach((groupDateInFormattedText, assets) {
|
||||
if (groupDateInFormattedText != latestAssetDateText) {
|
||||
ImmichAssetGroupByDate newGroup = ImmichAssetGroupByDate(assets: assets, date: groupDateInFormattedText);
|
||||
state = [newGroup, ...state];
|
||||
} else {
|
||||
latestGroup.assets.insertAll(0, assets);
|
||||
|
||||
state = [latestGroup, ...state.sublist(1)];
|
||||
}
|
||||
});
|
||||
if (allAssets != null) {
|
||||
state = allAssets;
|
||||
}
|
||||
}
|
||||
|
||||
clearAllAsset() {
|
||||
state = [];
|
||||
}
|
||||
|
||||
onNewAssetUploaded(ImmichAsset newAsset) {
|
||||
state = [...state, newAsset];
|
||||
}
|
||||
|
||||
deleteAssets(Set<ImmichAsset> deleteAssets) async {
|
||||
var deviceInfo = await _deviceInfoService.getDeviceInfo();
|
||||
var deviceId = deviceInfo["deviceId"];
|
||||
List<String> deleteIdList = [];
|
||||
// Delete asset from device
|
||||
for (var asset in deleteAssets) {
|
||||
// Delete asset on device if present
|
||||
if (asset.deviceId == deviceId) {
|
||||
AssetEntity? localAsset = await AssetEntity.fromId(asset.deviceAssetId);
|
||||
|
||||
if (localAsset != null) {
|
||||
deleteIdList.add(localAsset.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final List<String> result = await PhotoManager.editor.deleteWithIds(deleteIdList);
|
||||
|
||||
// Delete asset on server
|
||||
List<DeleteAssetResponse>? deleteAssetResult = await _assetService.deleteAssets(deleteAssets);
|
||||
if (deleteAssetResult == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (var asset in deleteAssetResult) {
|
||||
if (asset.status == 'success') {
|
||||
state = state.where((immichAsset) => immichAsset.id != asset.id).toList();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final currentLocalPageProvider = StateProvider<int>((ref) => 0);
|
||||
|
||||
final assetProvider = StateNotifierProvider<AssetNotifier, List<ImmichAssetGroupByDate>>((ref) {
|
||||
return AssetNotifier();
|
||||
final assetProvider = StateNotifierProvider<AssetNotifier, List<ImmichAsset>>((ref) {
|
||||
return AssetNotifier(ref);
|
||||
});
|
||||
|
||||
final assetGroupByDateTimeProvider = StateProvider((ref) {
|
||||
var assets = ref.watch(assetProvider);
|
||||
|
||||
assets.sortByCompare<DateTime>((e) => DateTime.parse(e.createdAt), (a, b) => b.compareTo(a));
|
||||
return assets.groupListsBy((element) => DateFormat('y-MM-dd').format(DateTime.parse(element.createdAt)));
|
||||
});
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/modules/home/models/get_all_asset_respose.model.dart';
|
||||
import 'package:immich_mobile/modules/home/models/delete_asset_response.model.dart';
|
||||
import 'package:immich_mobile/modules/home/models/get_all_asset_response.model.dart';
|
||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
||||
import 'package:immich_mobile/shared/models/immich_asset_with_exif.model.dart';
|
||||
import 'package:immich_mobile/shared/services/network.service.dart';
|
||||
@@ -9,7 +10,20 @@ import 'package:immich_mobile/shared/services/network.service.dart';
|
||||
class AssetService {
|
||||
final NetworkService _networkService = NetworkService();
|
||||
|
||||
Future<GetAllAssetResponse?> getAllAsset() async {
|
||||
Future<List<ImmichAsset>?> getAllAsset() async {
|
||||
var res = await _networkService.getRequest(url: "asset/");
|
||||
try {
|
||||
List<dynamic> decodedData = jsonDecode(res.toString());
|
||||
|
||||
List<ImmichAsset> result = List.from(decodedData.map((a) => ImmichAsset.fromMap(a)));
|
||||
return result;
|
||||
} catch (e) {
|
||||
debugPrint("Error getAllAsset ${e.toString()}");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<GetAllAssetResponse?> getAllAssetWithPagination() async {
|
||||
var res = await _networkService.getRequest(url: "asset/all");
|
||||
try {
|
||||
Map<String, dynamic> decodedData = jsonDecode(res.toString());
|
||||
@@ -69,7 +83,27 @@ class AssetService {
|
||||
Map<String, dynamic> decodedData = jsonDecode(res.toString());
|
||||
|
||||
ImmichAssetWithExif result = ImmichAssetWithExif.fromMap(decodedData);
|
||||
print("result $result");
|
||||
return result;
|
||||
} catch (e) {
|
||||
debugPrint("Error getAllAsset ${e.toString()}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<DeleteAssetResponse>?> deleteAssets(Set<ImmichAsset> deleteAssets) async {
|
||||
try {
|
||||
var payload = [];
|
||||
|
||||
for (var asset in deleteAssets) {
|
||||
payload.add(asset.id);
|
||||
}
|
||||
|
||||
var res = await _networkService.deleteRequest(url: "asset/", data: {"ids": payload});
|
||||
|
||||
List<dynamic> decodedData = jsonDecode(res.toString());
|
||||
|
||||
List<DeleteAssetResponse> result = List.from(decodedData.map((a) => DeleteAssetResponse.fromMap(a)));
|
||||
|
||||
return result;
|
||||
} catch (e) {
|
||||
debugPrint("Error getAllAsset ${e.toString()}");
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/home/providers/asset.provider.dart';
|
||||
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
|
||||
|
||||
class DeleteDialog extends StatelessWidget {
|
||||
class DeleteDialog extends ConsumerWidget {
|
||||
const DeleteDialog({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final homePageState = ref.watch(homePageStateProvider);
|
||||
|
||||
return AlertDialog(
|
||||
backgroundColor: Colors.grey[200],
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
@@ -21,7 +26,12 @@ class DeleteDialog extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {},
|
||||
onPressed: () {
|
||||
ref.watch(assetProvider.notifier).deleteAssets(homePageState.selectedItems);
|
||||
ref.watch(homePageStateProvider.notifier).disableMultiSelect();
|
||||
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(
|
||||
"Delete",
|
||||
style: TextStyle(color: Colors.red[400]),
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'package:badges/badges.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
||||
@@ -4,6 +4,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/home/providers/asset.provider.dart';
|
||||
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||
import 'package:immich_mobile/shared/providers/backup.provider.dart';
|
||||
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
||||
|
||||
class ProfileDrawer extends ConsumerWidget {
|
||||
const ProfileDrawer({Key? key}) : super(key: key);
|
||||
@@ -57,7 +59,9 @@ class ProfileDrawer extends ConsumerWidget {
|
||||
bool res = await ref.read(authenticationProvider.notifier).logout();
|
||||
|
||||
if (res) {
|
||||
ref.watch(backupProvider.notifier).cancelBackup();
|
||||
ref.watch(assetProvider.notifier).clearAllAsset();
|
||||
ref.watch(websocketProvider.notifier).disconnect();
|
||||
AutoRouter.of(context).popUntilRoot();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/hive_box.dart';
|
||||
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
|
||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
||||
@@ -25,6 +26,7 @@ class ThumbnailImage extends HookConsumerWidget {
|
||||
|
||||
var selectedAsset = ref.watch(homePageStateProvider).selectedItems;
|
||||
var isMultiSelectEnable = ref.watch(homePageStateProvider).isMultiSelectEnable;
|
||||
var deviceId = ref.watch(authenticationProvider).deviceId;
|
||||
|
||||
Widget _buildSelectionIcon(ImmichAsset asset) {
|
||||
if (selectedAsset.contains(asset)) {
|
||||
@@ -42,6 +44,7 @@ class ThumbnailImage extends HookConsumerWidget {
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
debugPrint("View ${asset.id}");
|
||||
if (isMultiSelectEnable && selectedAsset.contains(asset) && selectedAsset.length == 1) {
|
||||
ref.watch(homePageStateProvider.notifier).disableMultiSelect();
|
||||
} else if (isMultiSelectEnable && selectedAsset.contains(asset) && selectedAsset.length > 1) {
|
||||
@@ -99,9 +102,10 @@ class ThumbnailImage extends HookConsumerWidget {
|
||||
child: CircularProgressIndicator(value: downloadProgress.progress),
|
||||
),
|
||||
errorWidget: (context, url, error) {
|
||||
debugPrint("Error Loading Thumbnail Widget $error");
|
||||
cacheKey.value += 1;
|
||||
return const Icon(Icons.error);
|
||||
return Icon(
|
||||
Icons.image_not_supported_outlined,
|
||||
color: Theme.of(context).primaryColor,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -116,6 +120,15 @@ class ThumbnailImage extends HookConsumerWidget {
|
||||
)
|
||||
: Container(),
|
||||
),
|
||||
Positioned(
|
||||
right: 10,
|
||||
bottom: 5,
|
||||
child: Icon(
|
||||
(deviceId != asset.deviceId) ? Icons.cloud_done_outlined : Icons.photo_library_rounded,
|
||||
color: Colors.white,
|
||||
size: 18,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -10,8 +10,8 @@ import 'package:immich_mobile/modules/home/ui/image_grid.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/immich_sliver_appbar.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/monthly_title_text.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/profile_drawer.dart';
|
||||
import 'package:immich_mobile/modules/home/models/get_all_asset_respose.model.dart';
|
||||
import 'package:immich_mobile/modules/home/providers/asset.provider.dart';
|
||||
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
||||
import 'package:sliver_tools/sliver_tools.dart';
|
||||
|
||||
class HomePage extends HookConsumerWidget {
|
||||
@@ -20,76 +20,52 @@ class HomePage extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
ScrollController _scrollController = useScrollController();
|
||||
List<ImmichAssetGroupByDate> _assetGroup = ref.watch(assetProvider);
|
||||
var assetGroupByDateTime = ref.watch(assetGroupByDateTimeProvider);
|
||||
List<Widget> _imageGridGroup = [];
|
||||
var isMultiSelectEnable = ref.watch(homePageStateProvider).isMultiSelectEnable;
|
||||
var homePageState = ref.watch(homePageStateProvider);
|
||||
|
||||
_scrollControllerCallback() {
|
||||
var endOfPage = _scrollController.position.maxScrollExtent;
|
||||
|
||||
if (_scrollController.offset >= endOfPage - (endOfPage * 0.1) && !_scrollController.position.outOfRange) {
|
||||
ref.read(assetProvider.notifier).getOlderAsset();
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() {
|
||||
ref.read(assetProvider.notifier).getAllAssets();
|
||||
|
||||
_scrollController.addListener(_scrollControllerCallback);
|
||||
return () {
|
||||
_scrollController.removeListener(_scrollControllerCallback);
|
||||
};
|
||||
ref.read(websocketProvider.notifier).connect();
|
||||
ref.read(assetProvider.notifier).getAllAsset();
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
onPopBackFromBackupPage() {
|
||||
ref.read(assetProvider.notifier).getNewAsset();
|
||||
// Remove and force getting new widget again if there is not many widget on screen.
|
||||
// Otherwise do nothing.
|
||||
|
||||
if (_imageGridGroup.isNotEmpty && _imageGridGroup.length < 20) {
|
||||
ref.read(assetProvider.notifier).getOlderAsset();
|
||||
} else if (_imageGridGroup.isEmpty) {
|
||||
ref.read(assetProvider.notifier).getAllAssets();
|
||||
}
|
||||
// ref.read(assetProvider.notifier).getAllAsset();
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
if (_assetGroup.isNotEmpty) {
|
||||
String lastGroupDate = _assetGroup[0].date;
|
||||
if (assetGroupByDateTime.isNotEmpty) {
|
||||
int? lastMonth;
|
||||
|
||||
for (var group in _assetGroup) {
|
||||
var dateTitle = group.date;
|
||||
var assetGroup = group.assets;
|
||||
assetGroupByDateTime.forEach((dateGroup, immichAssetList) {
|
||||
DateTime parseDateGroup = DateTime.parse(dateGroup);
|
||||
int currentMonth = parseDateGroup.month;
|
||||
|
||||
int? currentMonth = DateTime.tryParse(dateTitle)?.month;
|
||||
int? previousMonth = DateTime.tryParse(lastGroupDate)?.month;
|
||||
|
||||
// Add Monthly Title Group if started at the beginning of the month
|
||||
|
||||
if (currentMonth != null && previousMonth != null) {
|
||||
if ((currentMonth - previousMonth) != 0) {
|
||||
if (lastMonth != null) {
|
||||
if (currentMonth - lastMonth! != 0) {
|
||||
_imageGridGroup.add(
|
||||
MonthlyTitleText(isoDate: dateTitle),
|
||||
MonthlyTitleText(
|
||||
isoDate: dateGroup,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Add Daily Title Group
|
||||
_imageGridGroup.add(
|
||||
DailyTitleText(
|
||||
isoDate: dateTitle,
|
||||
assetGroup: assetGroup,
|
||||
isoDate: dateGroup,
|
||||
assetGroup: immichAssetList,
|
||||
),
|
||||
);
|
||||
|
||||
// Add Image Group
|
||||
_imageGridGroup.add(
|
||||
ImageGrid(assetGroup: assetGroup),
|
||||
ImageGrid(assetGroup: immichAssetList),
|
||||
);
|
||||
//
|
||||
lastGroupDate = dateTitle;
|
||||
}
|
||||
|
||||
lastMonth = currentMonth;
|
||||
});
|
||||
}
|
||||
|
||||
return SafeArea(
|
||||
|
||||
@@ -15,36 +15,38 @@ class LoginForm extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final usernameController = useTextEditingController(text: 'testuser@email.com');
|
||||
final passwordController = useTextEditingController(text: 'password');
|
||||
final serverEndpointController = useTextEditingController(text: 'http://192.168.1.204:2283');
|
||||
final serverEndpointController = useTextEditingController(text: 'http://192.168.1.103:2283');
|
||||
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 300),
|
||||
child: Wrap(
|
||||
spacing: 32,
|
||||
runSpacing: 32,
|
||||
alignment: WrapAlignment.center,
|
||||
children: [
|
||||
const Image(
|
||||
image: AssetImage('assets/immich-logo-no-outline.png'),
|
||||
width: 128,
|
||||
filterQuality: FilterQuality.high,
|
||||
),
|
||||
Text(
|
||||
'IMMICH',
|
||||
style: GoogleFonts.snowburstOne(
|
||||
textStyle:
|
||||
TextStyle(fontWeight: FontWeight.bold, fontSize: 48, color: Theme.of(context).primaryColor)),
|
||||
),
|
||||
EmailInput(controller: usernameController),
|
||||
PasswordInput(controller: passwordController),
|
||||
ServerEndpointInput(controller: serverEndpointController),
|
||||
LoginButton(
|
||||
emailController: usernameController,
|
||||
passwordController: passwordController,
|
||||
serverEndpointController: serverEndpointController,
|
||||
),
|
||||
],
|
||||
child: SingleChildScrollView(
|
||||
child: Wrap(
|
||||
spacing: 32,
|
||||
runSpacing: 32,
|
||||
alignment: WrapAlignment.center,
|
||||
children: [
|
||||
const Image(
|
||||
image: AssetImage('assets/immich-logo-no-outline.png'),
|
||||
width: 128,
|
||||
filterQuality: FilterQuality.high,
|
||||
),
|
||||
Text(
|
||||
'IMMICH',
|
||||
style: GoogleFonts.snowburstOne(
|
||||
textStyle:
|
||||
TextStyle(fontWeight: FontWeight.bold, fontSize: 48, color: Theme.of(context).primaryColor)),
|
||||
),
|
||||
EmailInput(controller: usernameController),
|
||||
PasswordInput(controller: passwordController),
|
||||
ServerEndpointInput(controller: serverEndpointController),
|
||||
LoginButton(
|
||||
emailController: usernameController,
|
||||
passwordController: passwordController,
|
||||
serverEndpointController: serverEndpointController,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
@@ -11,7 +13,7 @@ import 'package:immich_mobile/shared/services/backup.service.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
BackupNotifier(this.ref)
|
||||
BackupNotifier({this.ref})
|
||||
: super(
|
||||
BackUpState(
|
||||
backupProgress: BackUpProgressEnum.idle,
|
||||
@@ -32,22 +34,25 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
),
|
||||
);
|
||||
|
||||
final Ref ref;
|
||||
Ref? ref;
|
||||
final BackupService _backupService = BackupService();
|
||||
final ServerInfoService _serverInfoService = ServerInfoService();
|
||||
final StreamController _onAssetBackupStreamCtrl = StreamController.broadcast();
|
||||
|
||||
void getBackupInfo() async {
|
||||
_updateServerInfo();
|
||||
|
||||
List<AssetPathEntity> list = await PhotoManager.getAssetPathList(onlyAll: true, type: RequestType.common);
|
||||
List<String> didBackupAsset = await _backupService.getDeviceBackupAsset();
|
||||
|
||||
if (list.isEmpty) {
|
||||
debugPrint("No Asset On Device");
|
||||
state = state.copyWith(
|
||||
backupProgress: BackUpProgressEnum.idle, totalAssetCount: 0, assetOnDatabase: didBackupAsset.length);
|
||||
return;
|
||||
}
|
||||
|
||||
int totalAsset = list[0].assetCount;
|
||||
List<String> didBackupAsset = await _backupService.getDeviceBackupAsset();
|
||||
|
||||
state = state.copyWith(totalAssetCount: totalAsset, assetOnDatabase: didBackupAsset.length);
|
||||
}
|
||||
@@ -65,19 +70,21 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
List<AssetPathEntity> list =
|
||||
await PhotoManager.getAssetPathList(hasAll: true, onlyAll: true, type: RequestType.common);
|
||||
|
||||
// Get device assets info from database
|
||||
// Compare and find different assets that has not been backing up
|
||||
// Backup those assets
|
||||
List<String> backupAsset = await _backupService.getDeviceBackupAsset();
|
||||
|
||||
if (list.isEmpty) {
|
||||
debugPrint("No Asset On Device - Abort Backup Process");
|
||||
state = state.copyWith(
|
||||
backupProgress: BackUpProgressEnum.idle, totalAssetCount: 0, assetOnDatabase: backupAsset.length);
|
||||
return;
|
||||
}
|
||||
|
||||
int totalAsset = list[0].assetCount;
|
||||
List<AssetEntity> currentAssets = await list[0].getAssetListRange(start: 0, end: totalAsset);
|
||||
|
||||
// Get device assets info from database
|
||||
// Compare and find different assets that has not been backing up
|
||||
// Backup those assets
|
||||
List<String> backupAsset = await _backupService.getDeviceBackupAsset();
|
||||
|
||||
state = state.copyWith(totalAssetCount: totalAsset, assetOnDatabase: backupAsset.length);
|
||||
// Remove item that has already been backed up
|
||||
for (var backupAssetId in backupAsset) {
|
||||
@@ -103,7 +110,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
state = state.copyWith(backupProgress: BackUpProgressEnum.idle, progressInPercentage: 0.0);
|
||||
}
|
||||
|
||||
void _onAssetUploaded() {
|
||||
void _onAssetUploaded(String deviceAssetId, String deviceId) {
|
||||
state =
|
||||
state.copyWith(backingUpAssetCount: state.backingUpAssetCount - 1, assetOnDatabase: state.assetOnDatabase + 1);
|
||||
|
||||
@@ -136,36 +143,36 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
}
|
||||
|
||||
void resumeBackup() {
|
||||
debugPrint("[resumeBackup]");
|
||||
var authState = ref.read(authenticationProvider);
|
||||
var authState = ref?.read(authenticationProvider);
|
||||
|
||||
// Check if user is login
|
||||
var accessKey = Hive.box(userInfoBox).get(accessTokenKey);
|
||||
|
||||
// User has been logged out return
|
||||
if (accessKey == null || !authState.isAuthenticated) {
|
||||
debugPrint("[resumeBackup] not authenticated - abort");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this device is enable backup by the user
|
||||
if ((authState.deviceInfo.deviceId == authState.deviceId) && authState.deviceInfo.isAutoBackup) {
|
||||
// check if backup is alreayd in process - then return
|
||||
if (state.backupProgress == BackUpProgressEnum.inProgress) {
|
||||
debugPrint("[resumeBackup] Backup is already in progress - abort");
|
||||
if (authState != null) {
|
||||
if (accessKey == null || !authState.isAuthenticated) {
|
||||
debugPrint("[resumeBackup] not authenticated - abort");
|
||||
return;
|
||||
}
|
||||
|
||||
// Run backup
|
||||
debugPrint("[resumeBackup] Start back up");
|
||||
startBackupProcess();
|
||||
}
|
||||
// Check if this device is enable backup by the user
|
||||
if ((authState.deviceInfo.deviceId == authState.deviceId) && authState.deviceInfo.isAutoBackup) {
|
||||
// check if backup is alreayd in process - then return
|
||||
if (state.backupProgress == BackUpProgressEnum.inProgress) {
|
||||
debugPrint("[resumeBackup] Backup is already in progress - abort");
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint("[resumeBackup] User disables auto backup");
|
||||
return;
|
||||
// Run backup
|
||||
debugPrint("[resumeBackup] Start back up");
|
||||
startBackupProcess();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final backupProvider = StateNotifierProvider<BackupNotifier, BackUpState>((ref) {
|
||||
return BackupNotifier(ref);
|
||||
return BackupNotifier(ref: ref);
|
||||
});
|
||||
|
||||
113
mobile/lib/shared/providers/websocket.provider.dart
Normal file
113
mobile/lib/shared/providers/websocket.provider.dart
Normal file
@@ -0,0 +1,113 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/home/providers/asset.provider.dart';
|
||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
||||
import 'package:socket_io_client/socket_io_client.dart';
|
||||
|
||||
import 'package:immich_mobile/constants/hive_box.dart';
|
||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||
|
||||
class WebscoketState {
|
||||
final Socket? socket;
|
||||
final bool isConnected;
|
||||
|
||||
WebscoketState({
|
||||
this.socket,
|
||||
required this.isConnected,
|
||||
});
|
||||
|
||||
WebscoketState copyWith({
|
||||
Socket? socket,
|
||||
bool? isConnected,
|
||||
}) {
|
||||
return WebscoketState(
|
||||
socket: socket ?? this.socket,
|
||||
isConnected: isConnected ?? this.isConnected,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => 'WebscoketState(socket: $socket, isConnected: $isConnected)';
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is WebscoketState && other.socket == socket && other.isConnected == isConnected;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => socket.hashCode ^ isConnected.hashCode;
|
||||
}
|
||||
|
||||
class WebsocketNotifier extends StateNotifier<WebscoketState> {
|
||||
WebsocketNotifier(this.ref) : super(WebscoketState(socket: null, isConnected: false)) {
|
||||
debugPrint("Init websocket instance");
|
||||
}
|
||||
|
||||
final Ref ref;
|
||||
|
||||
connect() {
|
||||
var authenticationState = ref.watch(authenticationProvider);
|
||||
|
||||
if (authenticationState.isAuthenticated) {
|
||||
var accessToken = Hive.box(userInfoBox).get(accessTokenKey);
|
||||
var endpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
||||
try {
|
||||
debugPrint("[WEBSOCKET] Attempting to connect to ws");
|
||||
// Configure socket transports must be sepecified
|
||||
Socket socket = io(
|
||||
endpoint,
|
||||
OptionBuilder()
|
||||
.setTransports(['websocket'])
|
||||
.enableReconnection()
|
||||
.enableForceNew()
|
||||
.enableForceNewConnection()
|
||||
.enableAutoConnect()
|
||||
.setExtraHeaders({"Authorization": "Bearer $accessToken"})
|
||||
.build(),
|
||||
);
|
||||
|
||||
socket.onConnect((_) {
|
||||
debugPrint("[WEBSOCKET] Established Websocket Connection");
|
||||
state = WebscoketState(isConnected: true, socket: socket);
|
||||
});
|
||||
|
||||
socket.onDisconnect((_) {
|
||||
debugPrint("[WEBSOCKET] Disconnect to Websocket Connection");
|
||||
state = WebscoketState(isConnected: false, socket: null);
|
||||
});
|
||||
|
||||
socket.on('error', (errorMessage) {
|
||||
debugPrint("Webcoket Error - $errorMessage");
|
||||
state = WebscoketState(isConnected: false, socket: null);
|
||||
});
|
||||
|
||||
socket.on('on_upload_success', (data) {
|
||||
var jsonString = jsonDecode(data.toString());
|
||||
ImmichAsset newAsset = ImmichAsset.fromMap(jsonString);
|
||||
ref.watch(assetProvider.notifier).onNewAssetUploaded(newAsset);
|
||||
});
|
||||
} catch (e) {
|
||||
debugPrint("[WEBSOCKET] Catch Websocket Error - ${e.toString()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
debugPrint("[WEBSOCKET] Attempting to disconnect");
|
||||
var socket = state.socket?.disconnect();
|
||||
if (socket != null) {
|
||||
if (socket.disconnected) {
|
||||
state = WebscoketState(isConnected: false, socket: null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final websocketProvider = StateNotifierProvider<WebsocketNotifier, WebscoketState>((ref) {
|
||||
return WebsocketNotifier(ref);
|
||||
});
|
||||
@@ -26,7 +26,7 @@ class BackupService {
|
||||
return result.cast<String>();
|
||||
}
|
||||
|
||||
backupAsset(List<AssetEntity> assetList, CancelToken cancelToken, Function singleAssetDoneCb,
|
||||
backupAsset(List<AssetEntity> assetList, CancelToken cancelToken, Function(String, String) singleAssetDoneCb,
|
||||
Function(int, int) uploadProgress) async {
|
||||
var dio = Dio();
|
||||
dio.interceptors.add(AuthenticatedRequestInterceptor());
|
||||
@@ -77,7 +77,7 @@ class BackupService {
|
||||
);
|
||||
|
||||
if (res.statusCode == 201) {
|
||||
singleAssetDoneCb();
|
||||
singleAssetDoneCb(entity.id, deviceId);
|
||||
}
|
||||
}
|
||||
} on DioError catch (e) {
|
||||
|
||||
@@ -7,6 +7,24 @@ import 'package:immich_mobile/constants/hive_box.dart';
|
||||
import 'package:immich_mobile/utils/dio_http_interceptor.dart';
|
||||
|
||||
class NetworkService {
|
||||
Future<dynamic> deleteRequest({required String url, dynamic data}) async {
|
||||
try {
|
||||
var dio = Dio();
|
||||
dio.interceptors.add(AuthenticatedRequestInterceptor());
|
||||
|
||||
var savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
||||
Response res = await dio.delete('$savedEndpoint/$url', data: data);
|
||||
|
||||
if (res.statusCode == 200) {
|
||||
return res;
|
||||
}
|
||||
} on DioError catch (e) {
|
||||
debugPrint("DioError: ${e.response}");
|
||||
} catch (e) {
|
||||
debugPrint("ERROR getRequest: ${e.toString()}");
|
||||
}
|
||||
}
|
||||
|
||||
Future<dynamic> getRequest({required String url}) async {
|
||||
try {
|
||||
var dio = Dio();
|
||||
|
||||
@@ -22,6 +22,8 @@ class BackupControllerPage extends HookConsumerWidget {
|
||||
if (_backupState.backupProgress != BackUpProgressEnum.inProgress) {
|
||||
ref.read(backupProvider.notifier).getBackupInfo();
|
||||
}
|
||||
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
Widget _buildStorageInformation() {
|
||||
|
||||
@@ -520,6 +520,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.12.11"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.3"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -735,6 +742,20 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.5"
|
||||
socket_io_client:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: socket_io_client
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0-beta.4-nullsafety.0"
|
||||
socket_io_common:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: socket_io_common
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
source_gen:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -832,7 +853,7 @@ packages:
|
||||
name: test_api
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.3"
|
||||
version: "0.4.8"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -33,6 +33,7 @@ dependencies:
|
||||
sliver_tools: ^0.2.5
|
||||
badges: ^2.0.2
|
||||
photo_view: ^0.13.0
|
||||
socket_io_client: ^2.0.0-beta.4-nullsafety.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@@ -7,7 +7,7 @@ DB_PASSWORD=postgres
|
||||
DB_DATABASE_NAME=
|
||||
|
||||
# Upload File Config
|
||||
UPLOAD_LOCATION=./upload
|
||||
UPLOAD_LOCATION=absolute_location_on_your_machine_where_you_want_to_store_the_backup
|
||||
|
||||
# JWT SECRET
|
||||
JWT_SECRET=
|
||||
@@ -2,21 +2,18 @@ version: '3.8'
|
||||
|
||||
|
||||
services:
|
||||
server:
|
||||
container_name: immich_server
|
||||
immich_server:
|
||||
image: immich-server-dev:1.0.0
|
||||
build:
|
||||
context: .
|
||||
target: development
|
||||
dockerfile: ./Dockerfile
|
||||
command: npm run start:dev
|
||||
ports:
|
||||
- "3000:3000"
|
||||
# expose:
|
||||
# - 3000
|
||||
expose:
|
||||
- "3000"
|
||||
volumes:
|
||||
- .:/usr/src/app
|
||||
- userdata:/usr/src/app/upload
|
||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||
- /usr/src/app/node_modules
|
||||
env_file:
|
||||
- .env
|
||||
@@ -62,10 +59,9 @@ services:
|
||||
networks:
|
||||
- immich_network
|
||||
depends_on:
|
||||
- server
|
||||
- immich_server
|
||||
|
||||
networks:
|
||||
immich_network:
|
||||
volumes:
|
||||
pgdata:
|
||||
userdata:
|
||||
pgdata:
|
||||
397
server/package-lock.json
generated
397
server/package-lock.json
generated
@@ -18,7 +18,10 @@
|
||||
"@nestjs/passport": "^8.1.0",
|
||||
"@nestjs/platform-express": "^8.0.0",
|
||||
"@nestjs/platform-fastify": "^8.2.6",
|
||||
"@nestjs/platform-socket.io": "^8.2.6",
|
||||
"@nestjs/typeorm": "^8.0.3",
|
||||
"@nestjs/websockets": "^8.2.6",
|
||||
"@socket.io/redis-adapter": "^7.1.0",
|
||||
"bcrypt": "^5.0.1",
|
||||
"bull": "^4.4.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
@@ -35,6 +38,7 @@
|
||||
"rimraf": "^3.0.2",
|
||||
"rxjs": "^7.2.0",
|
||||
"sharp": "0.28",
|
||||
"socket.io-redis": "^6.1.1",
|
||||
"systeminformation": "^5.11.0",
|
||||
"typeorm": "^0.2.41"
|
||||
},
|
||||
@@ -1544,6 +1548,24 @@
|
||||
"@nestjs/core": "^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/platform-socket.io": {
|
||||
"version": "8.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-8.2.6.tgz",
|
||||
"integrity": "sha512-Gkay52E6PmhuL1e1EF1GnJuivt4NxXaqY2I3VV4LF4X2jNHmL09EAyJofs1G6ySF8QUtR9HqtW/+ohxr7RipsQ==",
|
||||
"dependencies": {
|
||||
"socket.io": "4.4.1",
|
||||
"tslib": "2.3.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/nest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^8.0.0",
|
||||
"@nestjs/websockets": "^8.0.0",
|
||||
"rxjs": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/schematics": {
|
||||
"version": "8.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-8.0.5.tgz",
|
||||
@@ -1682,6 +1704,28 @@
|
||||
"typeorm": "^0.2.34"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/websockets": {
|
||||
"version": "8.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-8.2.6.tgz",
|
||||
"integrity": "sha512-GUdPd5X+ojNeaYE+/4c4105tb8skvQt3KyR7CCzrhMziHsRakDLQ/8LO932fh8ADrkRxr7jfAbt3UTq5FggQ2w==",
|
||||
"dependencies": {
|
||||
"iterare": "1.2.1",
|
||||
"object-hash": "2.2.0",
|
||||
"tslib": "2.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^8.0.0",
|
||||
"@nestjs/core": "^8.0.0",
|
||||
"@nestjs/platform-socket.io": "^8.0.0",
|
||||
"reflect-metadata": "^0.1.12",
|
||||
"rxjs": "^7.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@nestjs/platform-socket.io": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -1770,6 +1814,28 @@
|
||||
"@sinonjs/commons": "^1.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@socket.io/base64-arraybuffer": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||
"integrity": "sha512-dOlCBKnDw4iShaIsH/bxujKTM18+2TOAsYz+KSc11Am38H4q5Xw8Bbz97ZYdrVNM+um3p7w86Bvvmcn9q+5+eQ==",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@socket.io/redis-adapter": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/redis-adapter/-/redis-adapter-7.1.0.tgz",
|
||||
"integrity": "sha512-vbsNJKUQgtVHcOqNL2ac8kSemTVNKHRzYPldqQJt0eFKvlAtAviuAMzBP0WmOp5OoRLQMjhVsVvgMzzMsVsK5g==",
|
||||
"dependencies": {
|
||||
"debug": "~4.3.1",
|
||||
"notepack.io": "~2.2.0",
|
||||
"socket.io-adapter": "~2.3.0",
|
||||
"uid2": "0.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sqltools/formatter": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.3.tgz",
|
||||
@@ -1878,6 +1944,11 @@
|
||||
"@types/redis": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/component-emitter": {
|
||||
"version": "1.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.11.tgz",
|
||||
"integrity": "sha512-SRXjM+tfsSlA9VuG8hGO2nft2p8zjXCK1VcC6N4NXbBbYbSia9kzCChYQajIjzIqOOOuh5Ock6MmV2oux4jDZQ=="
|
||||
},
|
||||
"node_modules/@types/connect": {
|
||||
"version": "3.4.35",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
|
||||
@@ -1887,12 +1958,22 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cookie": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz",
|
||||
"integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
|
||||
},
|
||||
"node_modules/@types/cookiejar": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz",
|
||||
"integrity": "sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/cors": {
|
||||
"version": "2.8.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz",
|
||||
"integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw=="
|
||||
},
|
||||
"node_modules/@types/eslint": {
|
||||
"version": "8.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.1.tgz",
|
||||
@@ -3016,6 +3097,14 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/base64id": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
|
||||
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
|
||||
"engines": {
|
||||
"node": "^4.5.0 || >= 5.9"
|
||||
}
|
||||
},
|
||||
"node_modules/bcrypt": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.0.1.tgz",
|
||||
@@ -3588,8 +3677,7 @@
|
||||
"node_modules/component-emitter": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
|
||||
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
@@ -4126,6 +4214,57 @@
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io": {
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.1.2.tgz",
|
||||
"integrity": "sha512-v/7eGHxPvO2AWsksyx2PUsQvBafuvqs0jJJQ0FdmJG1b9qIvgSbqDRGwNhfk2XHaTTbTXiC4quRE8Q9nRjsrQQ==",
|
||||
"dependencies": {
|
||||
"@types/cookie": "^0.4.1",
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/node": ">=10.0.0",
|
||||
"accepts": "~1.3.4",
|
||||
"base64id": "2.0.0",
|
||||
"cookie": "~0.4.1",
|
||||
"cors": "~2.8.5",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~5.0.0",
|
||||
"ws": "~8.2.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-parser": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.3.tgz",
|
||||
"integrity": "sha512-BtQxwF27XUNnSafQLvDi0dQ8s3i6VgzSoQMJacpIcGNrlUdfHSKbgm3jmjCVvQluGzqwujQMPAoMai3oYSTurg==",
|
||||
"dependencies": {
|
||||
"@socket.io/base64-arraybuffer": "~1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io/node_modules/ws": {
|
||||
"version": "8.2.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
|
||||
"integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.3.tgz",
|
||||
@@ -4973,9 +5112,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.14.7",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz",
|
||||
"integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==",
|
||||
"version": "1.14.8",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
|
||||
"integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -7457,6 +7596,11 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/notepack.io": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/notepack.io/-/notepack.io-2.2.0.tgz",
|
||||
"integrity": "sha512-9b5w3t5VSH6ZPosoYnyDONnUTF8o0UkBw7JLA6eBlYJWyGT1Q3vQa8Hmuj1/X6RYvHjjygBDgw6fJhe0JEojfw=="
|
||||
},
|
||||
"node_modules/npm-run-path": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
|
||||
@@ -8280,6 +8424,24 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/redis": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/redis/-/redis-3.1.2.tgz",
|
||||
"integrity": "sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==",
|
||||
"dependencies": {
|
||||
"denque": "^1.5.0",
|
||||
"redis-commands": "^1.7.0",
|
||||
"redis-errors": "^1.2.0",
|
||||
"redis-parser": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/node-redis"
|
||||
}
|
||||
},
|
||||
"node_modules/redis-commands": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz",
|
||||
@@ -8801,6 +8963,61 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.4.1.tgz",
|
||||
"integrity": "sha512-s04vrBswdQBUmuWJuuNTmXUVJhP0cVky8bBDhdkf8y0Ptsu7fKU2LuLbts9g+pdmAdyMMn8F/9Mf1/wbtUN0fg==",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.4",
|
||||
"base64id": "~2.0.0",
|
||||
"debug": "~4.3.2",
|
||||
"engine.io": "~6.1.0",
|
||||
"socket.io-adapter": "~2.3.3",
|
||||
"socket.io-parser": "~4.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-adapter": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.3.3.tgz",
|
||||
"integrity": "sha512-Qd/iwn3VskrpNO60BeRyCyr8ZWw9CPZyitW4AQwmRZ8zCiyDiL+znRnWX6tDHXnWn1sJrM1+b6Mn6wEDJJ4aYQ=="
|
||||
},
|
||||
"node_modules/socket.io-parser": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz",
|
||||
"integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==",
|
||||
"dependencies": {
|
||||
"@types/component-emitter": "^1.2.10",
|
||||
"component-emitter": "~1.3.0",
|
||||
"debug": "~4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-redis": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-redis/-/socket.io-redis-6.1.1.tgz",
|
||||
"integrity": "sha512-jeaXe3TGKC20GMSlPHEdwTUIWUpay/L7m5+S9TQcOf22p9Llx44/RkpJV08+buXTZ8E+aivOotj2RdeFJJWJJQ==",
|
||||
"deprecated": "This package has been renamed to '@socket.io/redis-adapter', please see the migration guide here: https://socket.io/docs/v4/redis-adapter/#migrating-from-socketio-redis",
|
||||
"dependencies": {
|
||||
"debug": "~4.3.1",
|
||||
"notepack.io": "~2.2.0",
|
||||
"redis": "^3.0.0",
|
||||
"socket.io-adapter": "~2.2.0",
|
||||
"uid2": "0.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-redis/node_modules/socket.io-adapter": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.2.0.tgz",
|
||||
"integrity": "sha512-rG49L+FwaVEwuAdeBRq49M97YI3ElVabJPzvHT9S6a2CWhDKnjSFasvwAwSYPRhQzfn4NtDIbCaGYgOCOU/rlg=="
|
||||
},
|
||||
"node_modules/sonic-boom": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-1.4.1.tgz",
|
||||
@@ -9838,6 +10055,11 @@
|
||||
"node": ">=4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uid2": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz",
|
||||
"integrity": "sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I="
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
|
||||
@@ -11416,6 +11638,15 @@
|
||||
"tslib": "2.3.1"
|
||||
}
|
||||
},
|
||||
"@nestjs/platform-socket.io": {
|
||||
"version": "8.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-8.2.6.tgz",
|
||||
"integrity": "sha512-Gkay52E6PmhuL1e1EF1GnJuivt4NxXaqY2I3VV4LF4X2jNHmL09EAyJofs1G6ySF8QUtR9HqtW/+ohxr7RipsQ==",
|
||||
"requires": {
|
||||
"socket.io": "4.4.1",
|
||||
"tslib": "2.3.1"
|
||||
}
|
||||
},
|
||||
"@nestjs/schematics": {
|
||||
"version": "8.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-8.0.5.tgz",
|
||||
@@ -11503,6 +11734,16 @@
|
||||
"uuid": "8.3.2"
|
||||
}
|
||||
},
|
||||
"@nestjs/websockets": {
|
||||
"version": "8.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-8.2.6.tgz",
|
||||
"integrity": "sha512-GUdPd5X+ojNeaYE+/4c4105tb8skvQt3KyR7CCzrhMziHsRakDLQ/8LO932fh8ADrkRxr7jfAbt3UTq5FggQ2w==",
|
||||
"requires": {
|
||||
"iterare": "1.2.1",
|
||||
"object-hash": "2.2.0",
|
||||
"tslib": "2.3.1"
|
||||
}
|
||||
},
|
||||
"@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -11575,6 +11816,22 @@
|
||||
"@sinonjs/commons": "^1.7.0"
|
||||
}
|
||||
},
|
||||
"@socket.io/base64-arraybuffer": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||
"integrity": "sha512-dOlCBKnDw4iShaIsH/bxujKTM18+2TOAsYz+KSc11Am38H4q5Xw8Bbz97ZYdrVNM+um3p7w86Bvvmcn9q+5+eQ=="
|
||||
},
|
||||
"@socket.io/redis-adapter": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/redis-adapter/-/redis-adapter-7.1.0.tgz",
|
||||
"integrity": "sha512-vbsNJKUQgtVHcOqNL2ac8kSemTVNKHRzYPldqQJt0eFKvlAtAviuAMzBP0WmOp5OoRLQMjhVsVvgMzzMsVsK5g==",
|
||||
"requires": {
|
||||
"debug": "~4.3.1",
|
||||
"notepack.io": "~2.2.0",
|
||||
"socket.io-adapter": "~2.3.0",
|
||||
"uid2": "0.0.3"
|
||||
}
|
||||
},
|
||||
"@sqltools/formatter": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.3.tgz",
|
||||
@@ -11680,6 +11937,11 @@
|
||||
"@types/redis": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"@types/component-emitter": {
|
||||
"version": "1.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.11.tgz",
|
||||
"integrity": "sha512-SRXjM+tfsSlA9VuG8hGO2nft2p8zjXCK1VcC6N4NXbBbYbSia9kzCChYQajIjzIqOOOuh5Ock6MmV2oux4jDZQ=="
|
||||
},
|
||||
"@types/connect": {
|
||||
"version": "3.4.35",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
|
||||
@@ -11689,12 +11951,22 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/cookie": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz",
|
||||
"integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
|
||||
},
|
||||
"@types/cookiejar": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz",
|
||||
"integrity": "sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/cors": {
|
||||
"version": "2.8.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz",
|
||||
"integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw=="
|
||||
},
|
||||
"@types/eslint": {
|
||||
"version": "8.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.1.tgz",
|
||||
@@ -12626,6 +12898,11 @@
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
|
||||
},
|
||||
"base64id": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
|
||||
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="
|
||||
},
|
||||
"bcrypt": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.0.1.tgz",
|
||||
@@ -13072,8 +13349,7 @@
|
||||
"component-emitter": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
|
||||
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
@@ -13517,6 +13793,39 @@
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"engine.io": {
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.1.2.tgz",
|
||||
"integrity": "sha512-v/7eGHxPvO2AWsksyx2PUsQvBafuvqs0jJJQ0FdmJG1b9qIvgSbqDRGwNhfk2XHaTTbTXiC4quRE8Q9nRjsrQQ==",
|
||||
"requires": {
|
||||
"@types/cookie": "^0.4.1",
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/node": ">=10.0.0",
|
||||
"accepts": "~1.3.4",
|
||||
"base64id": "2.0.0",
|
||||
"cookie": "~0.4.1",
|
||||
"cors": "~2.8.5",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~5.0.0",
|
||||
"ws": "~8.2.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"ws": {
|
||||
"version": "8.2.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
|
||||
"integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
|
||||
"requires": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"engine.io-parser": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.3.tgz",
|
||||
"integrity": "sha512-BtQxwF27XUNnSafQLvDi0dQ8s3i6VgzSoQMJacpIcGNrlUdfHSKbgm3jmjCVvQluGzqwujQMPAoMai3oYSTurg==",
|
||||
"requires": {
|
||||
"@socket.io/base64-arraybuffer": "~1.0.2"
|
||||
}
|
||||
},
|
||||
"enhanced-resolve": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.3.tgz",
|
||||
@@ -14193,9 +14502,9 @@
|
||||
}
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.14.7",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz",
|
||||
"integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ=="
|
||||
"version": "1.14.8",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
|
||||
"integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA=="
|
||||
},
|
||||
"fork-ts-checker-webpack-plugin": {
|
||||
"version": "6.5.0",
|
||||
@@ -16101,6 +16410,11 @@
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||
"dev": true
|
||||
},
|
||||
"notepack.io": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/notepack.io/-/notepack.io-2.2.0.tgz",
|
||||
"integrity": "sha512-9b5w3t5VSH6ZPosoYnyDONnUTF8o0UkBw7JLA6eBlYJWyGT1Q3vQa8Hmuj1/X6RYvHjjygBDgw6fJhe0JEojfw=="
|
||||
},
|
||||
"npm-run-path": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
|
||||
@@ -16705,6 +17019,17 @@
|
||||
"resolve": "^1.1.6"
|
||||
}
|
||||
},
|
||||
"redis": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/redis/-/redis-3.1.2.tgz",
|
||||
"integrity": "sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==",
|
||||
"requires": {
|
||||
"denque": "^1.5.0",
|
||||
"redis-commands": "^1.7.0",
|
||||
"redis-errors": "^1.2.0",
|
||||
"redis-parser": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"redis-commands": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz",
|
||||
@@ -17079,6 +17404,53 @@
|
||||
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
|
||||
"dev": true
|
||||
},
|
||||
"socket.io": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.4.1.tgz",
|
||||
"integrity": "sha512-s04vrBswdQBUmuWJuuNTmXUVJhP0cVky8bBDhdkf8y0Ptsu7fKU2LuLbts9g+pdmAdyMMn8F/9Mf1/wbtUN0fg==",
|
||||
"requires": {
|
||||
"accepts": "~1.3.4",
|
||||
"base64id": "~2.0.0",
|
||||
"debug": "~4.3.2",
|
||||
"engine.io": "~6.1.0",
|
||||
"socket.io-adapter": "~2.3.3",
|
||||
"socket.io-parser": "~4.0.4"
|
||||
}
|
||||
},
|
||||
"socket.io-adapter": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.3.3.tgz",
|
||||
"integrity": "sha512-Qd/iwn3VskrpNO60BeRyCyr8ZWw9CPZyitW4AQwmRZ8zCiyDiL+znRnWX6tDHXnWn1sJrM1+b6Mn6wEDJJ4aYQ=="
|
||||
},
|
||||
"socket.io-parser": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz",
|
||||
"integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==",
|
||||
"requires": {
|
||||
"@types/component-emitter": "^1.2.10",
|
||||
"component-emitter": "~1.3.0",
|
||||
"debug": "~4.3.1"
|
||||
}
|
||||
},
|
||||
"socket.io-redis": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-redis/-/socket.io-redis-6.1.1.tgz",
|
||||
"integrity": "sha512-jeaXe3TGKC20GMSlPHEdwTUIWUpay/L7m5+S9TQcOf22p9Llx44/RkpJV08+buXTZ8E+aivOotj2RdeFJJWJJQ==",
|
||||
"requires": {
|
||||
"debug": "~4.3.1",
|
||||
"notepack.io": "~2.2.0",
|
||||
"redis": "^3.0.0",
|
||||
"socket.io-adapter": "~2.2.0",
|
||||
"uid2": "0.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"socket.io-adapter": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.2.0.tgz",
|
||||
"integrity": "sha512-rG49L+FwaVEwuAdeBRq49M97YI3ElVabJPzvHT9S6a2CWhDKnjSFasvwAwSYPRhQzfn4NtDIbCaGYgOCOU/rlg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"sonic-boom": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-1.4.1.tgz",
|
||||
@@ -17761,6 +18133,11 @@
|
||||
"integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==",
|
||||
"dev": true
|
||||
},
|
||||
"uid2": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz",
|
||||
"integrity": "sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I="
|
||||
},
|
||||
"universalify": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
|
||||
|
||||
@@ -30,7 +30,10 @@
|
||||
"@nestjs/passport": "^8.1.0",
|
||||
"@nestjs/platform-express": "^8.0.0",
|
||||
"@nestjs/platform-fastify": "^8.2.6",
|
||||
"@nestjs/platform-socket.io": "^8.2.6",
|
||||
"@nestjs/typeorm": "^8.0.3",
|
||||
"@nestjs/websockets": "^8.2.6",
|
||||
"@socket.io/redis-adapter": "^7.1.0",
|
||||
"bcrypt": "^5.0.1",
|
||||
"bull": "^4.4.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
# events {
|
||||
# worker_connections 1000;
|
||||
# }
|
||||
|
||||
server {
|
||||
|
||||
client_max_body_size 50000M;
|
||||
|
||||
listen 80;
|
||||
@@ -10,11 +21,15 @@ server {
|
||||
proxy_buffers 64 4k;
|
||||
proxy_force_ranges on;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
|
||||
proxy_pass http://immich_server:3000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,27 +12,22 @@ import {
|
||||
Query,
|
||||
Response,
|
||||
Headers,
|
||||
BadRequestException,
|
||||
Delete,
|
||||
} from '@nestjs/common';
|
||||
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
|
||||
import { AssetService } from './asset.service';
|
||||
import { FileInterceptor, FilesInterceptor } from '@nestjs/platform-express';
|
||||
import { FilesInterceptor } from '@nestjs/platform-express';
|
||||
import { multerOption } from '../../config/multer-option.config';
|
||||
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
|
||||
import { CreateAssetDto } from './dto/create-asset.dto';
|
||||
import { createReadStream } from 'fs';
|
||||
import { ServeFileDto } from './dto/serve-file.dto';
|
||||
import { AssetOptimizeService } from '../../modules/image-optimize/image-optimize.service';
|
||||
import { AssetType } from './entities/asset.entity';
|
||||
import { AssetEntity, AssetType } from './entities/asset.entity';
|
||||
import { GetAllAssetQueryDto } from './dto/get-all-asset-query.dto';
|
||||
import { Response as Res } from 'express';
|
||||
import { promisify } from 'util';
|
||||
import { stat } from 'fs';
|
||||
import { pipeline } from 'stream';
|
||||
import { GetNewAssetQueryDto } from './dto/get-new-asset-query.dto';
|
||||
import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
|
||||
|
||||
const fileInfo = promisify(stat);
|
||||
import { DeleteAssetDto } from './dto/delete-asset.dto';
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('asset')
|
||||
@@ -73,75 +68,7 @@ export class AssetController {
|
||||
@Response({ passthrough: true }) res: Res,
|
||||
@Query(ValidationPipe) query: ServeFileDto,
|
||||
): Promise<StreamableFile> {
|
||||
let file = null;
|
||||
const asset = await this.assetService.findOne(authUser, query.did, query.aid);
|
||||
|
||||
// Handle Sending Images
|
||||
if (asset.type == AssetType.IMAGE || query.isThumb == 'true') {
|
||||
res.set({
|
||||
'Content-Type': asset.mimeType,
|
||||
});
|
||||
|
||||
if (query.isThumb === 'false' || !query.isThumb) {
|
||||
file = createReadStream(asset.originalPath);
|
||||
} else {
|
||||
file = createReadStream(asset.resizePath);
|
||||
}
|
||||
|
||||
return new StreamableFile(file);
|
||||
} else if (asset.type == AssetType.VIDEO) {
|
||||
// Handle Handling Video
|
||||
const { size } = await fileInfo(asset.originalPath);
|
||||
const range = headers.range;
|
||||
|
||||
if (range) {
|
||||
/** Extracting Start and End value from Range Header */
|
||||
let [start, end] = range.replace(/bytes=/, '').split('-');
|
||||
start = parseInt(start, 10);
|
||||
end = end ? parseInt(end, 10) : size - 1;
|
||||
|
||||
if (!isNaN(start) && isNaN(end)) {
|
||||
start = start;
|
||||
end = size - 1;
|
||||
}
|
||||
if (isNaN(start) && !isNaN(end)) {
|
||||
start = size - end;
|
||||
end = size - 1;
|
||||
}
|
||||
|
||||
// Handle unavailable range request
|
||||
if (start >= size || end >= size) {
|
||||
console.error('Bad Request');
|
||||
// Return the 416 Range Not Satisfiable.
|
||||
res.status(416).set({
|
||||
'Content-Range': `bytes */${size}`,
|
||||
});
|
||||
|
||||
throw new BadRequestException('Bad Request Range');
|
||||
}
|
||||
|
||||
/** Sending Partial Content With HTTP Code 206 */
|
||||
|
||||
res.status(206).set({
|
||||
'Content-Range': `bytes ${start}-${end}/${size}`,
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Content-Length': end - start + 1,
|
||||
'Content-Type': asset.mimeType,
|
||||
});
|
||||
|
||||
const videoStream = createReadStream(asset.originalPath, { start: start, end: end });
|
||||
|
||||
return new StreamableFile(videoStream);
|
||||
} else {
|
||||
res.set({
|
||||
'Content-Type': asset.mimeType,
|
||||
});
|
||||
|
||||
return new StreamableFile(createReadStream(asset.originalPath));
|
||||
}
|
||||
}
|
||||
|
||||
console.log('SHOULD NOT BE HERE');
|
||||
return this.assetService.serveFile(authUser, query, res, headers);
|
||||
}
|
||||
|
||||
@Get('/new')
|
||||
@@ -154,6 +81,11 @@ export class AssetController {
|
||||
return await this.assetService.getAllAssets(authUser, query);
|
||||
}
|
||||
|
||||
@Get('/')
|
||||
async getAllAssetsNoPagination(@GetAuthUser() authUser: AuthUserDto) {
|
||||
return await this.assetService.getAllAssetsNoPagination(authUser);
|
||||
}
|
||||
|
||||
@Get('/:deviceId')
|
||||
async getUserAssetsByDeviceId(@GetAuthUser() authUser: AuthUserDto, @Param('deviceId') deviceId: string) {
|
||||
return await this.assetService.getUserAssetsByDeviceId(authUser, deviceId);
|
||||
@@ -163,4 +95,24 @@ export class AssetController {
|
||||
async getAssetById(@GetAuthUser() authUser: AuthUserDto, @Param('assetId') assetId) {
|
||||
return this.assetService.getAssetById(authUser, assetId);
|
||||
}
|
||||
|
||||
@Delete('/')
|
||||
async deleteAssetById(@GetAuthUser() authUser: AuthUserDto, @Body(ValidationPipe) assetIds: DeleteAssetDto) {
|
||||
const deleteAssetList: AssetEntity[] = [];
|
||||
|
||||
assetIds.ids.forEach(async (id) => {
|
||||
const assets = await this.assetService.getAssetById(authUser, id);
|
||||
deleteAssetList.push(assets);
|
||||
});
|
||||
|
||||
const result = await this.assetService.deleteAssetById(authUser, assetIds);
|
||||
|
||||
result.forEach((res) => {
|
||||
deleteAssetList.filter((a) => a.id == res.id && res.status == 'success');
|
||||
});
|
||||
|
||||
await this.backgroundTaskService.deleteFileOnDisk(deleteAssetList);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
|
||||
import { BadRequestException, Injectable, Logger, StreamableFile } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { MoreThan, Repository } from 'typeorm';
|
||||
import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
||||
import { CreateAssetDto } from './dto/create-asset.dto';
|
||||
import { UpdateAssetDto } from './dto/update-asset.dto';
|
||||
import { AssetEntity, AssetType } from './entities/asset.entity';
|
||||
import _ from 'lodash';
|
||||
import _, { result } from 'lodash';
|
||||
import { GetAllAssetQueryDto } from './dto/get-all-asset-query.dto';
|
||||
import { GetAllAssetReponseDto } from './dto/get-all-asset-response.dto';
|
||||
import { createReadStream, stat } from 'fs';
|
||||
import { ServeFileDto } from './dto/serve-file.dto';
|
||||
import { Response as Res } from 'express';
|
||||
import { promisify } from 'util';
|
||||
import { DeleteAssetDto } from './dto/delete-asset.dto';
|
||||
|
||||
const fileInfo = promisify(stat);
|
||||
|
||||
@Injectable()
|
||||
export class AssetService {
|
||||
@@ -52,6 +59,20 @@ export class AssetService {
|
||||
return res;
|
||||
}
|
||||
|
||||
public async getAllAssetsNoPagination(authUser: AuthUserDto) {
|
||||
try {
|
||||
const assets = await this.assetRepository
|
||||
.createQueryBuilder('a')
|
||||
.where('a."userId" = :userId', { userId: authUser.id })
|
||||
.orderBy('a."createdAt"::date', 'DESC')
|
||||
.getMany();
|
||||
|
||||
return assets;
|
||||
} catch (e) {
|
||||
Logger.error(e, 'getAllAssets');
|
||||
}
|
||||
}
|
||||
|
||||
public async getAllAssets(authUser: AuthUserDto, query: GetAllAssetQueryDto): Promise<GetAllAssetReponseDto> {
|
||||
try {
|
||||
const assets = await this.assetRepository
|
||||
@@ -122,4 +143,104 @@ export class AssetService {
|
||||
relations: ['exifInfo'],
|
||||
});
|
||||
}
|
||||
|
||||
public async serveFile(authUser: AuthUserDto, query: ServeFileDto, res: Res, headers: any) {
|
||||
let file = null;
|
||||
const asset = await this.findOne(authUser, query.did, query.aid);
|
||||
|
||||
// Handle Sending Images
|
||||
if (asset.type == AssetType.IMAGE || query.isThumb == 'true') {
|
||||
res.set({
|
||||
'Content-Type': asset.mimeType,
|
||||
});
|
||||
|
||||
if (query.isThumb === 'false' || !query.isThumb) {
|
||||
file = createReadStream(asset.originalPath);
|
||||
} else {
|
||||
file = createReadStream(asset.resizePath);
|
||||
}
|
||||
|
||||
file.on('error', (error) => {
|
||||
Logger.log(`Cannot create read stream ${error}`);
|
||||
return new BadRequestException('Cannot Create Read Stream');
|
||||
});
|
||||
return new StreamableFile(file);
|
||||
} else if (asset.type == AssetType.VIDEO) {
|
||||
// Handle Handling Video
|
||||
const { size } = await fileInfo(asset.originalPath);
|
||||
const range = headers.range;
|
||||
|
||||
if (range) {
|
||||
/** Extracting Start and End value from Range Header */
|
||||
let [start, end] = range.replace(/bytes=/, '').split('-');
|
||||
start = parseInt(start, 10);
|
||||
end = end ? parseInt(end, 10) : size - 1;
|
||||
|
||||
if (!isNaN(start) && isNaN(end)) {
|
||||
start = start;
|
||||
end = size - 1;
|
||||
}
|
||||
if (isNaN(start) && !isNaN(end)) {
|
||||
start = size - end;
|
||||
end = size - 1;
|
||||
}
|
||||
|
||||
// Handle unavailable range request
|
||||
if (start >= size || end >= size) {
|
||||
console.error('Bad Request');
|
||||
// Return the 416 Range Not Satisfiable.
|
||||
res.status(416).set({
|
||||
'Content-Range': `bytes */${size}`,
|
||||
});
|
||||
|
||||
throw new BadRequestException('Bad Request Range');
|
||||
}
|
||||
|
||||
/** Sending Partial Content With HTTP Code 206 */
|
||||
|
||||
res.status(206).set({
|
||||
'Content-Range': `bytes ${start}-${end}/${size}`,
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Content-Length': end - start + 1,
|
||||
'Content-Type': asset.mimeType,
|
||||
});
|
||||
|
||||
const videoStream = createReadStream(asset.originalPath, { start: start, end: end });
|
||||
|
||||
return new StreamableFile(videoStream);
|
||||
} else {
|
||||
res.set({
|
||||
'Content-Type': asset.mimeType,
|
||||
});
|
||||
|
||||
return new StreamableFile(createReadStream(asset.originalPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteAssetById(authUser: AuthUserDto, assetIds: DeleteAssetDto) {
|
||||
let result = [];
|
||||
|
||||
const target = assetIds.ids;
|
||||
for (let assetId of target) {
|
||||
const res = await this.assetRepository.delete({
|
||||
id: assetId,
|
||||
userId: authUser.id,
|
||||
});
|
||||
|
||||
if (res.affected) {
|
||||
result.push({
|
||||
id: assetId,
|
||||
status: 'success',
|
||||
});
|
||||
} else {
|
||||
result.push({
|
||||
id: assetId,
|
||||
status: 'failed',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
6
server/src/api-v1/asset/dto/delete-asset.dto.ts
Normal file
6
server/src/api-v1/asset/dto/delete-asset.dto.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
|
||||
export class DeleteAssetDto {
|
||||
@IsNotEmpty()
|
||||
ids: string[];
|
||||
}
|
||||
47
server/src/api-v1/communication/communication.gateway.ts
Normal file
47
server/src/api-v1/communication/communication.gateway.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { OnGatewayConnection, OnGatewayDisconnect, WebSocketGateway, WebSocketServer } from '@nestjs/websockets';
|
||||
import { CommunicationService } from './communication.service';
|
||||
import { Socket, Server } from 'socket.io';
|
||||
import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { UserEntity } from '../user/entities/user.entity';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
@WebSocketGateway()
|
||||
export class CommunicationGateway implements OnGatewayConnection, OnGatewayDisconnect {
|
||||
constructor(
|
||||
private immichJwtService: ImmichJwtService,
|
||||
|
||||
@InjectRepository(UserEntity)
|
||||
private userRepository: Repository<UserEntity>,
|
||||
) {}
|
||||
|
||||
@WebSocketServer() server: Server;
|
||||
|
||||
handleDisconnect(client: Socket) {
|
||||
client.leave(client.nsp.name);
|
||||
|
||||
Logger.log(`Client ${client.id} disconnected`);
|
||||
}
|
||||
|
||||
async handleConnection(client: Socket, ...args: any[]) {
|
||||
Logger.log(`New websocket connection: ${client.id}`, 'NewWebSocketConnection');
|
||||
const accessToken = client.handshake.headers.authorization.split(' ')[1];
|
||||
const res = await this.immichJwtService.validateToken(accessToken);
|
||||
|
||||
if (!res.status) {
|
||||
client.emit('error', 'unauthorized');
|
||||
client.disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await this.userRepository.findOne({ where: { id: res.userId } });
|
||||
if (!user) {
|
||||
client.emit('error', 'unauthorized');
|
||||
client.disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
client.join(user.id);
|
||||
}
|
||||
}
|
||||
16
server/src/api-v1/communication/communication.module.ts
Normal file
16
server/src/api-v1/communication/communication.module.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { CommunicationService } from './communication.service';
|
||||
import { CommunicationGateway } from './communication.gateway';
|
||||
import { ImmichJwtModule } from '../../modules/immich-jwt/immich-jwt.module';
|
||||
import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { jwtConfig } from '../../config/jwt.config';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { UserEntity } from '../user/entities/user.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([UserEntity]), ImmichJwtModule, JwtModule.register(jwtConfig)],
|
||||
providers: [CommunicationGateway, CommunicationService, ImmichJwtService],
|
||||
exports: [CommunicationGateway],
|
||||
})
|
||||
export class CommunicationModule {}
|
||||
4
server/src/api-v1/communication/communication.service.ts
Normal file
4
server/src/api-v1/communication/communication.service.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class CommunicationService {}
|
||||
@@ -16,16 +16,7 @@ export class UserService {
|
||||
return 'This action adds a new user';
|
||||
}
|
||||
|
||||
async findAll() {
|
||||
try {
|
||||
return 'welcome';
|
||||
// return await this.userRepository.find();
|
||||
// return await this.userRepository.query('select * from users');
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
// return 'helloworld';
|
||||
}
|
||||
async findAll() {}
|
||||
|
||||
findOne(id: number) {
|
||||
return `This action returns a #${id} user`;
|
||||
|
||||
@@ -14,6 +14,7 @@ import { BullModule } from '@nestjs/bull';
|
||||
import { ImageOptimizeModule } from './modules/image-optimize/image-optimize.module';
|
||||
import { ServerInfoModule } from './api-v1/server-info/server-info.module';
|
||||
import { BackgroundTaskModule } from './modules/background-task/background-task.module';
|
||||
import { CommunicationModule } from './api-v1/communication/communication.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -40,6 +41,8 @@ import { BackgroundTaskModule } from './modules/background-task/background-task.
|
||||
ServerInfoModule,
|
||||
|
||||
BackgroundTaskModule,
|
||||
|
||||
CommunicationModule,
|
||||
],
|
||||
controllers: [],
|
||||
providers: [],
|
||||
|
||||
@@ -4,9 +4,10 @@ import { existsSync, mkdirSync } from 'fs';
|
||||
import { diskStorage } from 'multer';
|
||||
import { extname } from 'path';
|
||||
import { Request } from 'express';
|
||||
import { APP_UPLOAD_LOCATION } from '../constants/upload_location.constant';
|
||||
|
||||
export const multerConfig = {
|
||||
dest: process.env.UPLOAD_LOCATION,
|
||||
dest: APP_UPLOAD_LOCATION,
|
||||
};
|
||||
|
||||
export const multerOption: MulterOptions = {
|
||||
|
||||
1
server/src/constants/upload_location.constant.ts
Normal file
1
server/src/constants/upload_location.constant.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const APP_UPLOAD_LOCATION = './upload';
|
||||
@@ -1,11 +1,15 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { NestExpressApplication } from '@nestjs/platform-express';
|
||||
import { AppModule } from './app.module';
|
||||
import { RedisIoAdapter } from './middlewares/redis-io.adapter.middleware';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create<NestExpressApplication>(AppModule);
|
||||
|
||||
app.set('trust proxy');
|
||||
|
||||
app.useWebSocketAdapter(new RedisIoAdapter(app));
|
||||
|
||||
await app.listen(3000);
|
||||
}
|
||||
bootstrap();
|
||||
|
||||
15
server/src/middlewares/redis-io.adapter.middleware.ts
Normal file
15
server/src/middlewares/redis-io.adapter.middleware.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { IoAdapter } from '@nestjs/platform-socket.io';
|
||||
import { RedisClient, createClient } from 'redis';
|
||||
import { ServerOptions } from 'socket.io';
|
||||
import { createAdapter } from '@socket.io/redis-adapter';
|
||||
|
||||
const pubClient = createClient({ url: 'redis://immich_redis:6379' });
|
||||
const subClient = pubClient.duplicate();
|
||||
|
||||
export class RedisIoAdapter extends IoAdapter {
|
||||
createIOServer(port: number, options?: ServerOptions): any {
|
||||
const server = super.createIOServer(port, options);
|
||||
server.adapter(createAdapter(pubClient, subClient));
|
||||
return server;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { AssetEntity } from '../../api-v1/asset/entities/asset.entity';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import exifr from 'exifr';
|
||||
import { readFile } from 'fs/promises';
|
||||
import fs from 'fs';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { ExifEntity } from '../../api-v1/asset/entities/exif.entity';
|
||||
|
||||
@@ -56,4 +57,23 @@ export class BackgroundTaskProcessor {
|
||||
Logger.error(`Error extracting EXIF ${e.toString()}`, 'extractExif');
|
||||
}
|
||||
}
|
||||
|
||||
@Process('delete-file-on-disk')
|
||||
async deleteFileOnDisk(job) {
|
||||
const { assets }: { assets: AssetEntity[] } = job.data;
|
||||
|
||||
assets.forEach(async (asset) => {
|
||||
fs.unlink(asset.originalPath, (err) => {
|
||||
if (err) {
|
||||
console.log('error deleting ', asset.originalPath);
|
||||
}
|
||||
});
|
||||
|
||||
fs.unlink(asset.resizePath, (err) => {
|
||||
if (err) {
|
||||
console.log('error deleting ', asset.originalPath);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export class BackgroundTaskService {
|
||||
) {}
|
||||
|
||||
async extractExif(savedAsset: AssetEntity, fileName: string, fileSize: number) {
|
||||
const job = await this.backgroundTaskQueue.add(
|
||||
await this.backgroundTaskQueue.add(
|
||||
'extract-exif',
|
||||
{
|
||||
savedAsset,
|
||||
@@ -22,4 +22,14 @@ export class BackgroundTaskService {
|
||||
{ jobId: randomUUID() },
|
||||
);
|
||||
}
|
||||
|
||||
async deleteFileOnDisk(assets: AssetEntity[]) {
|
||||
await this.backgroundTaskQueue.add(
|
||||
'delete-file-on-disk',
|
||||
{
|
||||
assets,
|
||||
},
|
||||
{ jobId: randomUUID() },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,16 @@ import { join } from 'path';
|
||||
import { AssetModule } from '../../api-v1/asset/asset.module';
|
||||
import { AssetService } from '../../api-v1/asset/asset.service';
|
||||
import { AssetEntity } from '../../api-v1/asset/entities/asset.entity';
|
||||
import { CommunicationGateway } from '../../api-v1/communication/communication.gateway';
|
||||
import { CommunicationModule } from '../../api-v1/communication/communication.module';
|
||||
import { UserEntity } from '../../api-v1/user/entities/user.entity';
|
||||
import { ImmichJwtModule } from '../immich-jwt/immich-jwt.module';
|
||||
import { ImageOptimizeProcessor } from './image-optimize.processor';
|
||||
import { AssetOptimizeService } from './image-optimize.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
CommunicationModule,
|
||||
BullModule.registerQueue({
|
||||
name: 'optimize',
|
||||
defaultJobOptions: {
|
||||
|
||||
@@ -7,21 +7,24 @@ import sharp from 'sharp';
|
||||
import { existsSync, mkdirSync, readFile } from 'fs';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import ffmpeg from 'fluent-ffmpeg';
|
||||
import { APP_UPLOAD_LOCATION } from '../../constants/upload_location.constant';
|
||||
import { WebSocketServer } from '@nestjs/websockets';
|
||||
import { Socket, Server as SocketIoServer } from 'socket.io';
|
||||
import { CommunicationGateway } from '../../api-v1/communication/communication.gateway';
|
||||
|
||||
@Processor('optimize')
|
||||
export class ImageOptimizeProcessor {
|
||||
constructor(
|
||||
private wsCommunicateionGateway: CommunicationGateway,
|
||||
@InjectRepository(AssetEntity)
|
||||
private assetRepository: Repository<AssetEntity>,
|
||||
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
@Process('resize-image')
|
||||
async resizeUploadedImage(job: Job) {
|
||||
const { savedAsset }: { savedAsset: AssetEntity } = job.data;
|
||||
|
||||
const basePath = this.configService.get('UPLOAD_LOCATION');
|
||||
const basePath = APP_UPLOAD_LOCATION;
|
||||
const resizePath = savedAsset.originalPath.replace('/original/', '/thumb/');
|
||||
|
||||
// Create folder for thumb image if not exist
|
||||
@@ -54,7 +57,12 @@ export class ImageOptimizeProcessor {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.assetRepository.update(savedAsset, { resizePath: desitnation });
|
||||
const res = await this.assetRepository.update(savedAsset, { resizePath: desitnation });
|
||||
if (res.affected) {
|
||||
this.wsCommunicateionGateway.server
|
||||
.to(savedAsset.userId)
|
||||
.emit('on_upload_success', JSON.stringify(savedAsset));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
sharp(data)
|
||||
@@ -65,7 +73,12 @@ export class ImageOptimizeProcessor {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.assetRepository.update(savedAsset, { resizePath: resizePath });
|
||||
const res = await this.assetRepository.update(savedAsset, { resizePath: resizePath });
|
||||
if (res.affected) {
|
||||
this.wsCommunicateionGateway.server
|
||||
.to(savedAsset.userId)
|
||||
.emit('on_upload_success', JSON.stringify(savedAsset));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -77,7 +90,7 @@ export class ImageOptimizeProcessor {
|
||||
async resizeUploadedVideo(job: Job) {
|
||||
const { savedAsset, filename }: { savedAsset: AssetEntity; filename: String } = job.data;
|
||||
|
||||
const basePath = this.configService.get('UPLOAD_LOCATION');
|
||||
const basePath = APP_UPLOAD_LOCATION;
|
||||
// const resizePath = savedAsset.originalPath.replace('/original/', '/thumb/');
|
||||
// Create folder for thumb image if not exist
|
||||
const resizeDir = `${basePath}/${savedAsset.userId}/thumb/${savedAsset.deviceId}`;
|
||||
@@ -94,7 +107,12 @@ export class ImageOptimizeProcessor {
|
||||
filename: `${filename}.png`,
|
||||
})
|
||||
.on('end', async (a) => {
|
||||
await this.assetRepository.update(savedAsset, { resizePath: `${resizeDir}/${filename}.png` });
|
||||
const res = await this.assetRepository.update(savedAsset, { resizePath: `${resizeDir}/${filename}.png` });
|
||||
if (res.affected) {
|
||||
this.wsCommunicateionGateway.server
|
||||
.to(savedAsset.userId)
|
||||
.emit('on_upload_success', JSON.stringify(savedAsset));
|
||||
}
|
||||
});
|
||||
|
||||
return 'ok';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { JwtPayloadDto } from '../../api-v1/auth/dto/jwt-payload.dto';
|
||||
import { jwtSecret } from '../../constants/jwt.constant';
|
||||
|
||||
@Injectable()
|
||||
export class ImmichJwtService {
|
||||
@@ -11,4 +12,20 @@ export class ImmichJwtService {
|
||||
...payload,
|
||||
});
|
||||
}
|
||||
|
||||
public async validateToken(accessToken: string) {
|
||||
try {
|
||||
const payload = await this.jwtService.verify(accessToken, { secret: jwtSecret });
|
||||
return {
|
||||
userId: payload['userId'],
|
||||
status: true,
|
||||
};
|
||||
} catch (e) {
|
||||
Logger.error('Error validating token from websocket request', 'ValidateWebsocketToken');
|
||||
return {
|
||||
userId: null,
|
||||
status: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user