From db95de88299eca2f35395fc4cdc0af91175a0ab2 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Tue, 16 Apr 2024 09:55:01 +0530 Subject: [PATCH 001/240] [mob][photos] Add cast pkg dependency --- mobile/pubspec.lock | 25 +++++++++++++++++++++++++ mobile/pubspec.yaml | 4 ++++ 2 files changed, 29 insertions(+) diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index bab6f37caa..3c9ff792c9 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -209,6 +209,15 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + cast: + dependency: "direct main" + description: + path: "." + ref: multicast_version + resolved-ref: "1f39cd4d6efa9363e77b2439f0317bae0c92dda1" + url: "https://github.com/guyluz11/flutter_cast.git" + source: git + version: "2.0.9" characters: dependency: transitive description: @@ -1416,6 +1425,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + multicast_dns: + dependency: transitive + description: + name: multicast_dns + sha256: "316cc47a958d4bd3c67bd238fe8b44fdfb6133bad89cb191c0c3bd3edb14e296" + url: "https://pub.dev" + source: hosted + version: "0.3.2+6" nested: dependency: transitive description: @@ -1729,6 +1746,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + protobuf: + dependency: transitive + description: + name: protobuf + sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d" + url: "https://pub.dev" + source: hosted + version: "3.1.0" provider: dependency: "direct main" description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index f1575bdf8b..bc0d4ba2d7 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -27,6 +27,10 @@ dependencies: battery_info: ^1.1.1 bip39: ^1.0.6 cached_network_image: ^3.0.0 + cast: + git: + url: https://github.com/guyluz11/flutter_cast.git + ref: multicast_version chewie: git: url: https://github.com/ente-io/chewie.git From f645fff31c53c8f5dab6ad52102c589c67882e45 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Tue, 16 Apr 2024 15:38:41 +0530 Subject: [PATCH 002/240] [mob][photos] Add hook to show cast devices --- mobile/ios/Runner/Info.plist | 9 +++++ .../gallery/gallery_app_bar_widget.dart | 38 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index 037996520e..cdbc237749 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -105,5 +105,14 @@ UIApplicationSupportsIndirectInputEvents + NSBonjourServices + + _googlecast._tcp + F5BCEC64._googlecast._tcp + + + NSLocalNetworkUsageDescription + ${PRODUCT_NAME} uses the local network to discover Cast-enabled devices on your WiFi + network. diff --git a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart index 1026bd7fd4..aa09e49b29 100644 --- a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart +++ b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart @@ -2,6 +2,8 @@ import 'dart:async'; import 'dart:io'; import 'dart:math' as math; +import "package:cast/device.dart"; +import "package:cast/discovery_service.dart"; import "package:flutter/cupertino.dart"; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; @@ -576,6 +578,9 @@ class _GalleryAppBarWidgetState extends State { ), ); } + if (widget.collection != null) { + actions.add(castWidget(context)); + } if (items.isNotEmpty) { actions.add( PopupMenuButton( @@ -642,6 +647,39 @@ class _GalleryAppBarWidgetState extends State { return actions; } + Widget castWidget(BuildContext context) { + return FutureBuilder>( + future: CastDiscoveryService().search(), + builder: (context, snapshot) { + if (snapshot.hasError) { + return Center( + child: Text( + 'Error: ${snapshot.error.toString()}', + ), + ); + } else if (!snapshot.hasData) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + if (snapshot.data!.isEmpty) { + return const Text('No device'); + } + + return Column( + children: snapshot.data!.map((device) { + return Text(device.name); + + }).toList(), + ); + }, + ); + } + + Future _connectToYourApp( + BuildContext contect, CastDevice device,) async {} + Future onCleanUncategorizedClick(BuildContext buildContext) async { final actionResult = await showChoiceActionSheet( context, From da1d778eeb1562080c238b5c62a7097be0caba64 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 22 Apr 2024 10:47:49 +0530 Subject: [PATCH 003/240] [mob][photos] Add hook to connect to cast device --- .../gallery/gallery_app_bar_widget.dart | 46 +++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart index aa09e49b29..5431e15a26 100644 --- a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart +++ b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart @@ -4,6 +4,8 @@ import 'dart:math' as math; import "package:cast/device.dart"; import "package:cast/discovery_service.dart"; +import "package:cast/session.dart"; +import "package:cast/session_manager.dart"; import "package:flutter/cupertino.dart"; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; @@ -669,8 +671,16 @@ class _GalleryAppBarWidgetState extends State { return Column( children: snapshot.data!.map((device) { - return Text(device.name); - + return GestureDetector( + onTap: () async { + try { + await _connectToYourApp(context, device); + } catch (e) { + showGenericErrorDialog(context: context, error: e).ignore(); + } + }, + child: Text( device.name), + ); }).toList(), ); }, @@ -678,7 +688,37 @@ class _GalleryAppBarWidgetState extends State { } Future _connectToYourApp( - BuildContext contect, CastDevice device,) async {} + BuildContext context, + CastDevice object, + ) async { + final session = await CastSessionManager().startSession(object); + + session.stateStream.listen((state) { + if (state == CastSessionState.connected) { + const snackBar = SnackBar(content: Text('Connected')); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + + _sendPairingRequestToTV(session); + } + }); + + session.messageStream.listen((message) { + print('receive message: $message'); + }); + + session.sendMessage(CastSession.kNamespaceReceiver, { + 'type': 'LAUNCH', + 'appId': 'F5BCEC64', // set the appId of your app here + }); + + _sendPairingRequestToTV(session); + } + + void _sendPairingRequestToTV(CastSession session) { + print('_sendMessageToYourApp'); + + session.sendMessage('urn:x-cast:pair-request', {}); + } Future onCleanUncategorizedClick(BuildContext buildContext) async { final actionResult = await showChoiceActionSheet( From 89646ac4698820f86574a916840f979049a1267a Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Tue, 23 Apr 2024 15:55:22 +0530 Subject: [PATCH 004/240] [mob][cast] Refactor + add multiple cast plugin to gracefully handle fdroid --- mobile/lib/service_locator.dart | 28 +- .../gallery/gallery_app_bar_widget.dart | 42 +-- mobile/plugins/ente_cast/.metadata | 10 + .../plugins/ente_cast/analysis_options.yaml | 1 + mobile/plugins/ente_cast/lib/ente_cast.dart | 1 + mobile/plugins/ente_cast/lib/src/service.dart | 7 + mobile/plugins/ente_cast/pubspec.yaml | 19 + mobile/plugins/ente_cast_none/.metadata | 10 + .../ente_cast_none/analysis_options.yaml | 1 + .../ente_cast_none/lib/ente_cast_none.dart | 1 + .../ente_cast_none/lib/src/service.dart | 17 + mobile/plugins/ente_cast_none/pubspec.yaml | 18 + mobile/plugins/ente_cast_normal/.metadata | 10 + .../ente_cast_normal/analysis_options.yaml | 1 + .../lib/ente_cast_normal.dart | 1 + .../ente_cast_normal/lib/src/service.dart | 37 ++ mobile/plugins/ente_cast_normal/pubspec.lock | 333 ++++++++++++++++++ mobile/plugins/ente_cast_normal/pubspec.yaml | 22 ++ mobile/pubspec.lock | 16 +- mobile/pubspec.yaml | 8 +- 20 files changed, 532 insertions(+), 51 deletions(-) create mode 100644 mobile/plugins/ente_cast/.metadata create mode 100644 mobile/plugins/ente_cast/analysis_options.yaml create mode 100644 mobile/plugins/ente_cast/lib/ente_cast.dart create mode 100644 mobile/plugins/ente_cast/lib/src/service.dart create mode 100644 mobile/plugins/ente_cast/pubspec.yaml create mode 100644 mobile/plugins/ente_cast_none/.metadata create mode 100644 mobile/plugins/ente_cast_none/analysis_options.yaml create mode 100644 mobile/plugins/ente_cast_none/lib/ente_cast_none.dart create mode 100644 mobile/plugins/ente_cast_none/lib/src/service.dart create mode 100644 mobile/plugins/ente_cast_none/pubspec.yaml create mode 100644 mobile/plugins/ente_cast_normal/.metadata create mode 100644 mobile/plugins/ente_cast_normal/analysis_options.yaml create mode 100644 mobile/plugins/ente_cast_normal/lib/ente_cast_normal.dart create mode 100644 mobile/plugins/ente_cast_normal/lib/src/service.dart create mode 100644 mobile/plugins/ente_cast_normal/pubspec.lock create mode 100644 mobile/plugins/ente_cast_normal/pubspec.yaml diff --git a/mobile/lib/service_locator.dart b/mobile/lib/service_locator.dart index 0fec75b465..eb79aab0fb 100644 --- a/mobile/lib/service_locator.dart +++ b/mobile/lib/service_locator.dart @@ -1,7 +1,25 @@ import "package:dio/dio.dart"; +import "package:ente_cast/ente_cast.dart"; +import "package:ente_cast_normal/ente_cast_normal.dart"; import "package:ente_feature_flag/ente_feature_flag.dart"; import "package:shared_preferences/shared_preferences.dart"; +CastService? _castService; +CastService get castService { + _castService ??= CastServiceImpl(); + return _castService!; +} + +FlagService? _flagService; + +FlagService get flagService { + _flagService ??= FlagService( + ServiceLocator.instance.prefs, + ServiceLocator.instance.enteDio, + ); + return _flagService!; +} + class ServiceLocator { late final SharedPreferences prefs; late final Dio enteDio; @@ -16,13 +34,3 @@ class ServiceLocator { this.enteDio = enteDio; } } - -FlagService? _flagService; - -FlagService get flagService { - _flagService ??= FlagService( - ServiceLocator.instance.prefs, - ServiceLocator.instance.enteDio, - ); - return _flagService!; -} diff --git a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart index 6c6c1e2ad6..f433b336cb 100644 --- a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart +++ b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart @@ -2,10 +2,6 @@ import 'dart:async'; import 'dart:io'; import 'dart:math' as math; -import "package:cast/device.dart"; -import "package:cast/discovery_service.dart"; -import "package:cast/session.dart"; -import "package:cast/session_manager.dart"; import "package:flutter/cupertino.dart"; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; @@ -580,7 +576,7 @@ class _GalleryAppBarWidgetState extends State { ), ); } - if (widget.collection != null) { + if (widget.collection != null && castService.isSupported) { actions.add(castWidget(context)); } if (items.isNotEmpty) { @@ -650,8 +646,8 @@ class _GalleryAppBarWidgetState extends State { } Widget castWidget(BuildContext context) { - return FutureBuilder>( - future: CastDiscoveryService().search(), + return FutureBuilder>( + future: castService.searchDevices(), builder: (context, snapshot) { if (snapshot.hasError) { return Center( @@ -679,7 +675,7 @@ class _GalleryAppBarWidgetState extends State { showGenericErrorDialog(context: context, error: e).ignore(); } }, - child: Text( device.name), + child: Text(device.toString()), ); }).toList(), ); @@ -689,35 +685,9 @@ class _GalleryAppBarWidgetState extends State { Future _connectToYourApp( BuildContext context, - CastDevice object, + Object castDevice, ) async { - final session = await CastSessionManager().startSession(object); - - session.stateStream.listen((state) { - if (state == CastSessionState.connected) { - const snackBar = SnackBar(content: Text('Connected')); - ScaffoldMessenger.of(context).showSnackBar(snackBar); - - _sendPairingRequestToTV(session); - } - }); - - session.messageStream.listen((message) { - print('receive message: $message'); - }); - - session.sendMessage(CastSession.kNamespaceReceiver, { - 'type': 'LAUNCH', - 'appId': 'F5BCEC64', // set the appId of your app here - }); - - _sendPairingRequestToTV(session); - } - - void _sendPairingRequestToTV(CastSession session) { - print('_sendMessageToYourApp'); - - session.sendMessage('urn:x-cast:pair-request', {}); + await castService.connectDevice(context, castDevice); } Future onCleanUncategorizedClick(BuildContext buildContext) async { diff --git a/mobile/plugins/ente_cast/.metadata b/mobile/plugins/ente_cast/.metadata new file mode 100644 index 0000000000..9fc7ede54d --- /dev/null +++ b/mobile/plugins/ente_cast/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 0b8abb4724aa590dd0f429683339b1e045a1594d + channel: stable + +project_type: plugin diff --git a/mobile/plugins/ente_cast/analysis_options.yaml b/mobile/plugins/ente_cast/analysis_options.yaml new file mode 100644 index 0000000000..fac60e247c --- /dev/null +++ b/mobile/plugins/ente_cast/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options.yaml \ No newline at end of file diff --git a/mobile/plugins/ente_cast/lib/ente_cast.dart b/mobile/plugins/ente_cast/lib/ente_cast.dart new file mode 100644 index 0000000000..66a7132d8d --- /dev/null +++ b/mobile/plugins/ente_cast/lib/ente_cast.dart @@ -0,0 +1 @@ +export 'src/service.dart'; diff --git a/mobile/plugins/ente_cast/lib/src/service.dart b/mobile/plugins/ente_cast/lib/src/service.dart new file mode 100644 index 0000000000..74834f79d0 --- /dev/null +++ b/mobile/plugins/ente_cast/lib/src/service.dart @@ -0,0 +1,7 @@ +import "package:flutter/widgets.dart"; + +abstract class CastService { + bool get isSupported; + Future> searchDevices(); + Future connectDevice(BuildContext context, Object device); +} diff --git a/mobile/plugins/ente_cast/pubspec.yaml b/mobile/plugins/ente_cast/pubspec.yaml new file mode 100644 index 0000000000..8ed1e74121 --- /dev/null +++ b/mobile/plugins/ente_cast/pubspec.yaml @@ -0,0 +1,19 @@ +name: ente_cast +version: 0.0.1 +publish_to: none + +environment: + sdk: '>=3.3.0 <4.0.0' + +dependencies: + collection: + dio: ^4.0.6 + flutter: + sdk: flutter + shared_preferences: ^2.0.5 + stack_trace: + +dev_dependencies: + flutter_lints: + +flutter: \ No newline at end of file diff --git a/mobile/plugins/ente_cast_none/.metadata b/mobile/plugins/ente_cast_none/.metadata new file mode 100644 index 0000000000..9fc7ede54d --- /dev/null +++ b/mobile/plugins/ente_cast_none/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 0b8abb4724aa590dd0f429683339b1e045a1594d + channel: stable + +project_type: plugin diff --git a/mobile/plugins/ente_cast_none/analysis_options.yaml b/mobile/plugins/ente_cast_none/analysis_options.yaml new file mode 100644 index 0000000000..fac60e247c --- /dev/null +++ b/mobile/plugins/ente_cast_none/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options.yaml \ No newline at end of file diff --git a/mobile/plugins/ente_cast_none/lib/ente_cast_none.dart b/mobile/plugins/ente_cast_none/lib/ente_cast_none.dart new file mode 100644 index 0000000000..66a7132d8d --- /dev/null +++ b/mobile/plugins/ente_cast_none/lib/ente_cast_none.dart @@ -0,0 +1 @@ +export 'src/service.dart'; diff --git a/mobile/plugins/ente_cast_none/lib/src/service.dart b/mobile/plugins/ente_cast_none/lib/src/service.dart new file mode 100644 index 0000000000..964f4e472a --- /dev/null +++ b/mobile/plugins/ente_cast_none/lib/src/service.dart @@ -0,0 +1,17 @@ +import "package:ente_cast/ente_cast.dart"; +import "package:flutter/widgets.dart"; + +class CastServiceImpl extends CastService { + @override + Future connectDevice(BuildContext context, Object device) { + throw UnimplementedError(); + } + + @override + Future> searchDevices() { + throw UnimplementedError(); + } + + @override + bool get isSupported => false; +} diff --git a/mobile/plugins/ente_cast_none/pubspec.yaml b/mobile/plugins/ente_cast_none/pubspec.yaml new file mode 100644 index 0000000000..0484f000f0 --- /dev/null +++ b/mobile/plugins/ente_cast_none/pubspec.yaml @@ -0,0 +1,18 @@ +name: ente_cast_none +version: 0.0.1 +publish_to: none + +environment: + sdk: '>=3.3.0 <4.0.0' + +dependencies: + ente_cast: + path: ../ente_cast + flutter: + sdk: flutter + stack_trace: + +dev_dependencies: + flutter_lints: + +flutter: \ No newline at end of file diff --git a/mobile/plugins/ente_cast_normal/.metadata b/mobile/plugins/ente_cast_normal/.metadata new file mode 100644 index 0000000000..9fc7ede54d --- /dev/null +++ b/mobile/plugins/ente_cast_normal/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 0b8abb4724aa590dd0f429683339b1e045a1594d + channel: stable + +project_type: plugin diff --git a/mobile/plugins/ente_cast_normal/analysis_options.yaml b/mobile/plugins/ente_cast_normal/analysis_options.yaml new file mode 100644 index 0000000000..fac60e247c --- /dev/null +++ b/mobile/plugins/ente_cast_normal/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options.yaml \ No newline at end of file diff --git a/mobile/plugins/ente_cast_normal/lib/ente_cast_normal.dart b/mobile/plugins/ente_cast_normal/lib/ente_cast_normal.dart new file mode 100644 index 0000000000..66a7132d8d --- /dev/null +++ b/mobile/plugins/ente_cast_normal/lib/ente_cast_normal.dart @@ -0,0 +1 @@ +export 'src/service.dart'; diff --git a/mobile/plugins/ente_cast_normal/lib/src/service.dart b/mobile/plugins/ente_cast_normal/lib/src/service.dart new file mode 100644 index 0000000000..1664511b6c --- /dev/null +++ b/mobile/plugins/ente_cast_normal/lib/src/service.dart @@ -0,0 +1,37 @@ +import "package:cast/cast.dart"; +import "package:ente_cast/ente_cast.dart"; +import "package:flutter/material.dart"; + +class CastServiceImpl extends CastService { + @override + Future connectDevice(BuildContext context, Object device) async { + final CastDevice castDevice = device as CastDevice; + final session = await CastSessionManager().startSession(castDevice); + + session.stateStream.listen((state) { + if (state == CastSessionState.connected) { + const snackBar = SnackBar(content: Text('Connected')); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + session.sendMessage('urn:x-cast:pair-request', {}); + } + }); + session.messageStream.listen((message) { + print('receive message: $message'); + }); + + session.sendMessage(CastSession.kNamespaceReceiver, { + 'type': 'LAUNCH', + 'appId': 'F5BCEC64', // set the appId of your app here + }); + session.sendMessage('urn:x-cast:pair-request', {}); + } + + @override + Future> searchDevices() { + // TODO: implement searchDevices + throw UnimplementedError(); + } + + @override + bool get isSupported => true; +} diff --git a/mobile/plugins/ente_cast_normal/pubspec.lock b/mobile/plugins/ente_cast_normal/pubspec.lock new file mode 100644 index 0000000000..86051800c6 --- /dev/null +++ b/mobile/plugins/ente_cast_normal/pubspec.lock @@ -0,0 +1,333 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + cast: + dependency: "direct main" + description: + path: "." + ref: multicast_version + resolved-ref: "1f39cd4d6efa9363e77b2439f0317bae0c92dda1" + url: "https://github.com/guyluz11/flutter_cast.git" + source: git + version: "2.0.9" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + dio: + dependency: transitive + description: + name: dio + sha256: "7d328c4d898a61efc3cd93655a0955858e29a0aa647f0f9e02d59b3bb275e2e8" + url: "https://pub.dev" + source: hosted + version: "4.0.6" + ente_cast: + dependency: "direct main" + description: + path: "../ente_cast" + relative: true + source: path + version: "0.0.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + http: + dependency: transitive + description: + name: http + sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + url: "https://pub.dev" + source: hosted + version: "3.0.0" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + url: "https://pub.dev" + source: hosted + version: "0.8.0" + meta: + dependency: transitive + description: + name: meta + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + url: "https://pub.dev" + source: hosted + version: "1.11.0" + multicast_dns: + dependency: transitive + description: + name: multicast_dns + sha256: "316cc47a958d4bd3c67bd238fe8b44fdfb6133bad89cb191c0c3bd3edb14e296" + url: "https://pub.dev" + source: hosted + version: "0.3.2+6" + path: + dependency: transitive + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + platform: + dependency: transitive + description: + name: platform + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + protobuf: + dependency: transitive + description: + name: protobuf + sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + shared_preferences: + dependency: transitive + description: + name: shared_preferences + sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 + url: "https://pub.dev" + source: hosted + version: "2.2.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" + url: "https://pub.dev" + source: hosted + version: "2.3.5" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + stack_trace: + dependency: "direct main" + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + web: + dependency: transitive + description: + name: web + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + url: "https://pub.dev" + source: hosted + version: "0.5.1" + win32: + dependency: transitive + description: + name: win32 + sha256: "0a989dc7ca2bb51eac91e8fd00851297cfffd641aa7538b165c62637ca0eaa4a" + url: "https://pub.dev" + source: hosted + version: "5.4.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + url: "https://pub.dev" + source: hosted + version: "1.0.4" +sdks: + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.19.0" diff --git a/mobile/plugins/ente_cast_normal/pubspec.yaml b/mobile/plugins/ente_cast_normal/pubspec.yaml new file mode 100644 index 0000000000..17d1721218 --- /dev/null +++ b/mobile/plugins/ente_cast_normal/pubspec.yaml @@ -0,0 +1,22 @@ +name: ente_cast_normal +version: 0.0.1 +publish_to: none + +environment: + sdk: '>=3.3.0 <4.0.0' + +dependencies: + cast: + git: + url: https://github.com/guyluz11/flutter_cast.git + ref: multicast_version + ente_cast: + path: ../ente_cast + flutter: + sdk: flutter + stack_trace: + +dev_dependencies: + flutter_lints: + +flutter: \ No newline at end of file diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 882967ff05..f776c09099 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -210,7 +210,7 @@ packages: source: hosted version: "1.1.1" cast: - dependency: "direct main" + dependency: transitive description: path: "." ref: multicast_version @@ -443,6 +443,20 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.17" + ente_cast: + dependency: "direct main" + description: + path: "plugins/ente_cast" + relative: true + source: path + version: "0.0.1" + ente_cast_normal: + dependency: "direct main" + description: + path: "plugins/ente_cast_normal" + relative: true + source: path + version: "0.0.1" ente_feature_flag: dependency: "direct main" description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index a641211eaa..da2f4e089f 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -27,10 +27,6 @@ dependencies: battery_info: ^1.1.1 bip39: ^1.0.6 cached_network_image: ^3.0.0 - cast: - git: - url: https://github.com/guyluz11/flutter_cast.git - ref: multicast_version chewie: git: url: https://github.com/ente-io/chewie.git @@ -51,6 +47,10 @@ dependencies: dotted_border: ^2.1.0 dropdown_button2: ^2.0.0 email_validator: ^2.0.1 + ente_cast: + path: plugins/ente_cast + ente_cast_normal: + path: plugins/ente_cast_normal ente_feature_flag: path: plugins/ente_feature_flag equatable: ^2.0.5 From bd225ced041a9de92b8af6d951d36f08a6f0f156 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Tue, 23 Apr 2024 16:08:16 +0530 Subject: [PATCH 005/240] [mob][cast] Return name and castDevice as record --- mobile/lib/service_locator.dart | 32 +++++++++---------- .../gallery/gallery_app_bar_widget.dart | 8 +++-- .../plugins/ente_cast/analysis_options.yaml | 2 +- mobile/plugins/ente_cast/lib/src/service.dart | 2 +- mobile/plugins/ente_cast/pubspec.yaml | 2 +- .../ente_cast_none/analysis_options.yaml | 2 +- .../ente_cast_none/lib/src/service.dart | 9 +++--- mobile/plugins/ente_cast_none/pubspec.yaml | 2 +- .../ente_cast_normal/analysis_options.yaml | 2 +- .../ente_cast_normal/lib/src/service.dart | 7 ++-- mobile/plugins/ente_cast_normal/pubspec.yaml | 2 +- 11 files changed, 37 insertions(+), 33 deletions(-) diff --git a/mobile/lib/service_locator.dart b/mobile/lib/service_locator.dart index eb79aab0fb..397703761e 100644 --- a/mobile/lib/service_locator.dart +++ b/mobile/lib/service_locator.dart @@ -4,22 +4,6 @@ import "package:ente_cast_normal/ente_cast_normal.dart"; import "package:ente_feature_flag/ente_feature_flag.dart"; import "package:shared_preferences/shared_preferences.dart"; -CastService? _castService; -CastService get castService { - _castService ??= CastServiceImpl(); - return _castService!; -} - -FlagService? _flagService; - -FlagService get flagService { - _flagService ??= FlagService( - ServiceLocator.instance.prefs, - ServiceLocator.instance.enteDio, - ); - return _flagService!; -} - class ServiceLocator { late final SharedPreferences prefs; late final Dio enteDio; @@ -34,3 +18,19 @@ class ServiceLocator { this.enteDio = enteDio; } } + +FlagService? _flagService; + +FlagService get flagService { + _flagService ??= FlagService( + ServiceLocator.instance.prefs, + ServiceLocator.instance.enteDio, + ); + return _flagService!; +} + +CastService? _castService; +CastService get castService { + _castService ??= CastServiceImpl(); + return _castService!; +} diff --git a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart index f433b336cb..a33fc9628b 100644 --- a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart +++ b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart @@ -646,7 +646,7 @@ class _GalleryAppBarWidgetState extends State { } Widget castWidget(BuildContext context) { - return FutureBuilder>( + return FutureBuilder>( future: castService.searchDevices(), builder: (context, snapshot) { if (snapshot.hasError) { @@ -666,7 +666,9 @@ class _GalleryAppBarWidgetState extends State { } return Column( - children: snapshot.data!.map((device) { + children: snapshot.data!.map((result) { + final device = result.$2; + final name = result.$1; return GestureDetector( onTap: () async { try { @@ -675,7 +677,7 @@ class _GalleryAppBarWidgetState extends State { showGenericErrorDialog(context: context, error: e).ignore(); } }, - child: Text(device.toString()), + child: Text(name), ); }).toList(), ); diff --git a/mobile/plugins/ente_cast/analysis_options.yaml b/mobile/plugins/ente_cast/analysis_options.yaml index fac60e247c..f04c6cf0f3 100644 --- a/mobile/plugins/ente_cast/analysis_options.yaml +++ b/mobile/plugins/ente_cast/analysis_options.yaml @@ -1 +1 @@ -include: ../../analysis_options.yaml \ No newline at end of file +include: ../../analysis_options.yaml diff --git a/mobile/plugins/ente_cast/lib/src/service.dart b/mobile/plugins/ente_cast/lib/src/service.dart index 74834f79d0..230269bb39 100644 --- a/mobile/plugins/ente_cast/lib/src/service.dart +++ b/mobile/plugins/ente_cast/lib/src/service.dart @@ -2,6 +2,6 @@ import "package:flutter/widgets.dart"; abstract class CastService { bool get isSupported; - Future> searchDevices(); + Future> searchDevices(); Future connectDevice(BuildContext context, Object device); } diff --git a/mobile/plugins/ente_cast/pubspec.yaml b/mobile/plugins/ente_cast/pubspec.yaml index 8ed1e74121..967e147e91 100644 --- a/mobile/plugins/ente_cast/pubspec.yaml +++ b/mobile/plugins/ente_cast/pubspec.yaml @@ -16,4 +16,4 @@ dependencies: dev_dependencies: flutter_lints: -flutter: \ No newline at end of file +flutter: diff --git a/mobile/plugins/ente_cast_none/analysis_options.yaml b/mobile/plugins/ente_cast_none/analysis_options.yaml index fac60e247c..f04c6cf0f3 100644 --- a/mobile/plugins/ente_cast_none/analysis_options.yaml +++ b/mobile/plugins/ente_cast_none/analysis_options.yaml @@ -1 +1 @@ -include: ../../analysis_options.yaml \ No newline at end of file +include: ../../analysis_options.yaml diff --git a/mobile/plugins/ente_cast_none/lib/src/service.dart b/mobile/plugins/ente_cast_none/lib/src/service.dart index 964f4e472a..166108c528 100644 --- a/mobile/plugins/ente_cast_none/lib/src/service.dart +++ b/mobile/plugins/ente_cast_none/lib/src/service.dart @@ -8,10 +8,11 @@ class CastServiceImpl extends CastService { } @override - Future> searchDevices() { - throw UnimplementedError(); - } + bool get isSupported => false; @override - bool get isSupported => false; + Future> searchDevices() { + // TODO: implement searchDevices + throw UnimplementedError(); + } } diff --git a/mobile/plugins/ente_cast_none/pubspec.yaml b/mobile/plugins/ente_cast_none/pubspec.yaml index 0484f000f0..a4559fac53 100644 --- a/mobile/plugins/ente_cast_none/pubspec.yaml +++ b/mobile/plugins/ente_cast_none/pubspec.yaml @@ -15,4 +15,4 @@ dependencies: dev_dependencies: flutter_lints: -flutter: \ No newline at end of file +flutter: diff --git a/mobile/plugins/ente_cast_normal/analysis_options.yaml b/mobile/plugins/ente_cast_normal/analysis_options.yaml index fac60e247c..f04c6cf0f3 100644 --- a/mobile/plugins/ente_cast_normal/analysis_options.yaml +++ b/mobile/plugins/ente_cast_normal/analysis_options.yaml @@ -1 +1 @@ -include: ../../analysis_options.yaml \ No newline at end of file +include: ../../analysis_options.yaml diff --git a/mobile/plugins/ente_cast_normal/lib/src/service.dart b/mobile/plugins/ente_cast_normal/lib/src/service.dart index 1664511b6c..eac98ae51b 100644 --- a/mobile/plugins/ente_cast_normal/lib/src/service.dart +++ b/mobile/plugins/ente_cast_normal/lib/src/service.dart @@ -27,9 +27,10 @@ class CastServiceImpl extends CastService { } @override - Future> searchDevices() { - // TODO: implement searchDevices - throw UnimplementedError(); + Future> searchDevices() { + return CastDiscoveryService().search().then((devices) { + return devices.map((device) => (device.name, device)).toList(); + }); } @override diff --git a/mobile/plugins/ente_cast_normal/pubspec.yaml b/mobile/plugins/ente_cast_normal/pubspec.yaml index 17d1721218..c97d70a84b 100644 --- a/mobile/plugins/ente_cast_normal/pubspec.yaml +++ b/mobile/plugins/ente_cast_normal/pubspec.yaml @@ -19,4 +19,4 @@ dependencies: dev_dependencies: flutter_lints: -flutter: \ No newline at end of file +flutter: From 1251a014b052082c0a044aad5a25e4bddc130c58 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Wed, 24 Apr 2024 10:57:28 +0530 Subject: [PATCH 006/240] [mob][cast] Show choice to auto and manual pair --- mobile/lib/generated/intl/messages_en.dart | 2 + mobile/lib/generated/l10n.dart | 20 +++++++ mobile/lib/l10n/intl_en.arb | 2 + .../gallery/gallery_app_bar_widget.dart | 54 +++++++++++++++++++ 4 files changed, 78 insertions(+) diff --git a/mobile/lib/generated/intl/messages_en.dart b/mobile/lib/generated/intl/messages_en.dart index eef309aa5d..4de38ce120 100644 --- a/mobile/lib/generated/intl/messages_en.dart +++ b/mobile/lib/generated/intl/messages_en.dart @@ -357,6 +357,7 @@ class MessageLookup extends MessageLookupByLibrary { "Authentication failed, please try again"), "authenticationSuccessful": MessageLookupByLibrary.simpleMessage("Authentication successful!"), + "autoPair": MessageLookupByLibrary.simpleMessage("Auto pair"), "available": MessageLookupByLibrary.simpleMessage("Available"), "backedUpFolders": MessageLookupByLibrary.simpleMessage("Backed up folders"), @@ -982,6 +983,7 @@ class MessageLookup extends MessageLookupByLibrary { "orPickAnExistingOne": MessageLookupByLibrary.simpleMessage("Or pick an existing one"), "pair": MessageLookupByLibrary.simpleMessage("Pair"), + "pairWithPin": MessageLookupByLibrary.simpleMessage("Pair with PIN"), "passkey": MessageLookupByLibrary.simpleMessage("Passkey"), "passkeyAuthTitle": MessageLookupByLibrary.simpleMessage("Passkey verification"), diff --git a/mobile/lib/generated/l10n.dart b/mobile/lib/generated/l10n.dart index 3fa9c2209a..7f5dc96144 100644 --- a/mobile/lib/generated/l10n.dart +++ b/mobile/lib/generated/l10n.dart @@ -8378,6 +8378,26 @@ class S { ); } + /// `Auto pair` + String get autoPair { + return Intl.message( + 'Auto pair', + name: 'autoPair', + desc: '', + args: [], + ); + } + + /// `Pair with PIN` + String get pairWithPin { + return Intl.message( + 'Pair with PIN', + name: 'pairWithPin', + desc: '', + args: [], + ); + } + /// `Device not found` String get deviceNotFound { return Intl.message( diff --git a/mobile/lib/l10n/intl_en.arb b/mobile/lib/l10n/intl_en.arb index 7115c69508..ee00499357 100644 --- a/mobile/lib/l10n/intl_en.arb +++ b/mobile/lib/l10n/intl_en.arb @@ -1195,6 +1195,8 @@ "verifyPasskey": "Verify passkey", "playOnTv": "Play album on TV", "pair": "Pair", + "autoPair": "Auto pair", + "pairWithPin": "Pair with PIN", "deviceNotFound": "Device not found", "castInstruction": "Visit cast.ente.io on the device you want to pair.\n\nEnter the code below to play the album on your TV.", "deviceCodeHint": "Enter the code", diff --git a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart index a33fc9628b..35933272de 100644 --- a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart +++ b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart @@ -26,6 +26,7 @@ import 'package:photos/services/update_service.dart'; import 'package:photos/ui/actions/collection/collection_sharing_actions.dart'; import 'package:photos/ui/components/action_sheet_widget.dart'; import 'package:photos/ui/components/buttons/button_widget.dart'; +import "package:photos/ui/components/dialog_widget.dart"; import 'package:photos/ui/components/models/button_type.dart'; import "package:photos/ui/map/enable_map.dart"; import "package:photos/ui/map/map_screen.dart"; @@ -892,6 +893,59 @@ class _GalleryAppBarWidgetState extends State { final gw = CastGateway(NetworkClient.instance.enteDio); // stop any existing cast session gw.revokeAllTokens().ignore(); + final result = await showDialogWidget( + context: context, + title: S.of(context).playOnTv, + body: + "Auto Pair requires connecting to Google servers and only works with Chromecast supported devices. Google will not receive sensitive data, such as your photos.\n\nPair with PIN works for any large screen device you want to play your album on.", + buttons: [ + ButtonWidget( + labelText: S.of(context).autoPair, + icon: Icons.cast_outlined, + buttonType: ButtonType.trailingIconPrimary, + buttonSize: ButtonSize.large, + shouldStickToDarkTheme: true, + buttonAction: ButtonAction.first, + shouldSurfaceExecutionStates: true, + isInAlert: true, + onTap: () async { + showToast(context, "Coming soon"); + // await _castAlbum(gw); + }, + ), + ButtonWidget( + labelText: S.of(context).pairWithPin, + buttonType: ButtonType.trailingIconPrimary, + // icon for pairing with TV manually + icon: Icons.tv_outlined, + buttonSize: ButtonSize.large, + isInAlert: true, + shouldStickToDarkTheme: true, + buttonAction: ButtonAction.second, + shouldSurfaceExecutionStates: false, + ), + // cancel button + ], + ); + _logger.info("Cast result: $result"); + if (result == null) { + return; + } + if (result.action == ButtonAction.error) { + await showGenericErrorDialog( + context: context, + error: result.exception, + ); + } + if (result.action == ButtonAction.first) { + showToast(context, "Coming soon"); + } + if (result.action == ButtonAction.second) { + await _pairWithPin(gw); + } + } + + Future _pairWithPin(CastGateway gw) async { await showTextInputDialog( context, title: context.l10n.playOnTv, From bed14d8ee98459e5f6871dbf6036a08af0561e8b Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Wed, 24 Apr 2024 11:38:22 +0530 Subject: [PATCH 007/240] [mob][photos] Use cast Icon in appbar --- .../gallery/gallery_app_bar_widget.dart | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart index 35933272de..e29ca9d3bc 100644 --- a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart +++ b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart @@ -578,7 +578,17 @@ class _GalleryAppBarWidgetState extends State { ); } if (widget.collection != null && castService.isSupported) { - actions.add(castWidget(context)); + actions.add( + Tooltip( + message: "Cast album", + child: IconButton( + icon: const Icon(Icons.cast_outlined), + onPressed: () async { + await _castChoiceDialog(); + }, + ), + ), + ); } if (items.isNotEmpty) { actions.add( @@ -607,7 +617,7 @@ class _GalleryAppBarWidgetState extends State { } else if (value == AlbumPopupAction.leave) { await _leaveAlbum(context); } else if (value == AlbumPopupAction.playOnTv) { - await castAlbum(); + await _castChoiceDialog(); } else if (value == AlbumPopupAction.freeUpSpace) { await _deleteBackedUpFiles(context); } else if (value == AlbumPopupAction.setCover) { @@ -889,7 +899,7 @@ class _GalleryAppBarWidgetState extends State { setState(() {}); } - Future castAlbum() async { + Future _castChoiceDialog() async { final gw = CastGateway(NetworkClient.instance.enteDio); // stop any existing cast session gw.revokeAllTokens().ignore(); @@ -908,10 +918,6 @@ class _GalleryAppBarWidgetState extends State { buttonAction: ButtonAction.first, shouldSurfaceExecutionStates: true, isInAlert: true, - onTap: () async { - showToast(context, "Coming soon"); - // await _castAlbum(gw); - }, ), ButtonWidget( labelText: S.of(context).pairWithPin, From 729e2adfd11c6d28f5dc38d228e27c94a5bcb589 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Wed, 24 Apr 2024 13:21:03 +0530 Subject: [PATCH 008/240] [mob] Use separate widget for auto-cast --- mobile/lib/service_locator.dart | 2 +- mobile/lib/ui/cast/auto.dart | 92 +++++++++++++++++++ .../gallery/gallery_app_bar_widget.dart | 56 ++--------- 3 files changed, 101 insertions(+), 49 deletions(-) create mode 100644 mobile/lib/ui/cast/auto.dart diff --git a/mobile/lib/service_locator.dart b/mobile/lib/service_locator.dart index 397703761e..4d75d8e353 100644 --- a/mobile/lib/service_locator.dart +++ b/mobile/lib/service_locator.dart @@ -1,6 +1,6 @@ import "package:dio/dio.dart"; import "package:ente_cast/ente_cast.dart"; -import "package:ente_cast_normal/ente_cast_normal.dart"; +import "package:ente_cast_none/ente_cast_none.dart"; import "package:ente_feature_flag/ente_feature_flag.dart"; import "package:shared_preferences/shared_preferences.dart"; diff --git a/mobile/lib/ui/cast/auto.dart b/mobile/lib/ui/cast/auto.dart new file mode 100644 index 0000000000..5d8b64175e --- /dev/null +++ b/mobile/lib/ui/cast/auto.dart @@ -0,0 +1,92 @@ +import "dart:io"; + +import "package:flutter/material.dart"; +import "package:photos/service_locator.dart"; +import "package:photos/theme/ente_theme.dart"; +import "package:photos/ui/common/loading_widget.dart"; +import "package:photos/utils/dialog_util.dart"; + +class AutoCastDialog extends StatefulWidget { + AutoCastDialog({ + Key? key, + }) : super(key: key) {} + + @override + State createState() => _AutoCastDialogState(); +} + +class _AutoCastDialogState extends State { + final bool doesUserExist = true; + + @override + Widget build(BuildContext context) { + final textStyle = getEnteTextTheme(context); + + final AlertDialog alert = AlertDialog( + title: Text( + "Connect to device", + style: textStyle.largeBold, + ), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "You'll see available Cast devices here.", + style: textStyle.bodyMuted, + ), + if (Platform.isIOS) + Text( + "Make sure Local Network permissions are turned on for the Ente Photos app, in Settings.", + style: textStyle.bodyMuted, + ), + const SizedBox(height: 16), + FutureBuilder>( + future: castService.searchDevices(), + builder: (context, snapshot) { + if (snapshot.hasError) { + return Center( + child: Text( + 'Error: ${snapshot.error.toString()}', + ), + ); + } else if (!snapshot.hasData) { + return const EnteLoadingWidget(); + } + + if (snapshot.data!.isEmpty) { + return const Center(child: Text('No device')); + } + + return Column( + children: snapshot.data!.map((result) { + final device = result.$2; + final name = result.$1; + return GestureDetector( + onTap: () async { + try { + await _connectToYourApp(context, device); + } catch (e) { + showGenericErrorDialog(context: context, error: e) + .ignore(); + } + }, + child: Text(name), + ); + }).toList(), + ); + }, + ), + ], + ), + ); + return alert; + } + + Future _connectToYourApp( + BuildContext context, + Object castDevice, + ) async { + await castService.connectDevice(context, castDevice); + } +} diff --git a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart index e29ca9d3bc..0863435163 100644 --- a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart +++ b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart @@ -24,6 +24,7 @@ import 'package:photos/services/collections_service.dart'; import 'package:photos/services/sync_service.dart'; import 'package:photos/services/update_service.dart'; import 'package:photos/ui/actions/collection/collection_sharing_actions.dart'; +import "package:photos/ui/cast/auto.dart"; import 'package:photos/ui/components/action_sheet_widget.dart'; import 'package:photos/ui/components/buttons/button_widget.dart'; import "package:photos/ui/components/dialog_widget.dart"; @@ -656,53 +657,6 @@ class _GalleryAppBarWidgetState extends State { return actions; } - Widget castWidget(BuildContext context) { - return FutureBuilder>( - future: castService.searchDevices(), - builder: (context, snapshot) { - if (snapshot.hasError) { - return Center( - child: Text( - 'Error: ${snapshot.error.toString()}', - ), - ); - } else if (!snapshot.hasData) { - return const Center( - child: CircularProgressIndicator(), - ); - } - - if (snapshot.data!.isEmpty) { - return const Text('No device'); - } - - return Column( - children: snapshot.data!.map((result) { - final device = result.$2; - final name = result.$1; - return GestureDetector( - onTap: () async { - try { - await _connectToYourApp(context, device); - } catch (e) { - showGenericErrorDialog(context: context, error: e).ignore(); - } - }, - child: Text(name), - ); - }).toList(), - ); - }, - ); - } - - Future _connectToYourApp( - BuildContext context, - Object castDevice, - ) async { - await castService.connectDevice(context, castDevice); - } - Future onCleanUncategorizedClick(BuildContext buildContext) async { final actionResult = await showChoiceActionSheet( context, @@ -944,7 +898,13 @@ class _GalleryAppBarWidgetState extends State { ); } if (result.action == ButtonAction.first) { - showToast(context, "Coming soon"); + await showDialog( + context: context, + barrierDismissible: true, + builder: (BuildContext context) { + return AutoCastDialog(); + }, + ); } if (result.action == ButtonAction.second) { await _pairWithPin(gw); From aced4bb5cf2caea91c5ee97e0ac75fdb1b0d8aaf Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Wed, 24 Apr 2024 15:22:24 +0530 Subject: [PATCH 009/240] [mob][photos] Update cast selection dialog --- mobile/lib/service_locator.dart | 2 +- mobile/lib/ui/cast/choose.dart | 83 +++++++++++++++++++ .../gallery/gallery_app_bar_widget.dart | 47 +++-------- 3 files changed, 94 insertions(+), 38 deletions(-) create mode 100644 mobile/lib/ui/cast/choose.dart diff --git a/mobile/lib/service_locator.dart b/mobile/lib/service_locator.dart index 4d75d8e353..397703761e 100644 --- a/mobile/lib/service_locator.dart +++ b/mobile/lib/service_locator.dart @@ -1,6 +1,6 @@ import "package:dio/dio.dart"; import "package:ente_cast/ente_cast.dart"; -import "package:ente_cast_none/ente_cast_none.dart"; +import "package:ente_cast_normal/ente_cast_normal.dart"; import "package:ente_feature_flag/ente_feature_flag.dart"; import "package:shared_preferences/shared_preferences.dart"; diff --git a/mobile/lib/ui/cast/choose.dart b/mobile/lib/ui/cast/choose.dart new file mode 100644 index 0000000000..0b82ea299e --- /dev/null +++ b/mobile/lib/ui/cast/choose.dart @@ -0,0 +1,83 @@ +import "package:flutter/material.dart"; +import "package:photos/generated/l10n.dart"; +import "package:photos/service_locator.dart"; +import "package:photos/theme/ente_theme.dart"; +import "package:photos/ui/components/buttons/button_widget.dart"; +import "package:photos/ui/components/models/button_type.dart"; + +class CastChooseDialog extends StatefulWidget { + CastChooseDialog({ + Key? key, + }) : super(key: key) {} + + @override + State createState() => _AutoCastDialogState(); +} + +class _AutoCastDialogState extends State { + final bool doesUserExist = true; + + @override + Widget build(BuildContext context) { + final textStyle = getEnteTextTheme(context); + final AlertDialog alert = AlertDialog( + title: Text( + "Play album on TV", + style: textStyle.largeBold, + ), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 8), + Text( + "Auto Pair requires connecting to Google servers and only works with Chromecast supported devices. Google will not receive sensitive data, such as your photos.", + style: textStyle.bodyMuted, + ), + const SizedBox(height: 12), + ButtonWidget( + labelText: S.of(context).autoPair, + icon: Icons.cast_outlined, + buttonType: ButtonType.primary, + buttonSize: ButtonSize.large, + shouldStickToDarkTheme: true, + buttonAction: ButtonAction.first, + shouldSurfaceExecutionStates: false, + isInAlert: true, + onTap: () async { + Navigator.of(context).pop(ButtonAction.first); + }, + ), + const SizedBox(height: 36), + Text( + "Pair with PIN works for any large screen device you want to play your album on.", + style: textStyle.bodyMuted, + ), + const SizedBox(height: 12), + ButtonWidget( + labelText: S.of(context).pairWithPin, + buttonType: ButtonType.primary, + // icon for pairing with TV manually + icon: Icons.tv_outlined, + buttonSize: ButtonSize.large, + isInAlert: true, + onTap: () async { + Navigator.of(context).pop(ButtonAction.second); + }, + shouldStickToDarkTheme: true, + buttonAction: ButtonAction.second, + shouldSurfaceExecutionStates: false, + ), + ], + ), + ); + return alert; + } + + Future _connectToYourApp( + BuildContext context, + Object castDevice, + ) async { + await castService.connectDevice(context, castDevice); + } +} diff --git a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart index 0863435163..7b45ae53f7 100644 --- a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart +++ b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart @@ -25,9 +25,9 @@ import 'package:photos/services/sync_service.dart'; import 'package:photos/services/update_service.dart'; import 'package:photos/ui/actions/collection/collection_sharing_actions.dart'; import "package:photos/ui/cast/auto.dart"; +import "package:photos/ui/cast/choose.dart"; import 'package:photos/ui/components/action_sheet_widget.dart'; import 'package:photos/ui/components/buttons/button_widget.dart'; -import "package:photos/ui/components/dialog_widget.dart"; import 'package:photos/ui/components/models/button_type.dart'; import "package:photos/ui/map/enable_map.dart"; import "package:photos/ui/map/map_screen.dart"; @@ -857,47 +857,20 @@ class _GalleryAppBarWidgetState extends State { final gw = CastGateway(NetworkClient.instance.enteDio); // stop any existing cast session gw.revokeAllTokens().ignore(); - final result = await showDialogWidget( + final result = await showDialog( context: context, - title: S.of(context).playOnTv, - body: - "Auto Pair requires connecting to Google servers and only works with Chromecast supported devices. Google will not receive sensitive data, such as your photos.\n\nPair with PIN works for any large screen device you want to play your album on.", - buttons: [ - ButtonWidget( - labelText: S.of(context).autoPair, - icon: Icons.cast_outlined, - buttonType: ButtonType.trailingIconPrimary, - buttonSize: ButtonSize.large, - shouldStickToDarkTheme: true, - buttonAction: ButtonAction.first, - shouldSurfaceExecutionStates: true, - isInAlert: true, - ), - ButtonWidget( - labelText: S.of(context).pairWithPin, - buttonType: ButtonType.trailingIconPrimary, - // icon for pairing with TV manually - icon: Icons.tv_outlined, - buttonSize: ButtonSize.large, - isInAlert: true, - shouldStickToDarkTheme: true, - buttonAction: ButtonAction.second, - shouldSurfaceExecutionStates: false, - ), - // cancel button - ], + barrierDismissible: true, + builder: (BuildContext context) { + return CastChooseDialog(); + }, ); _logger.info("Cast result: $result"); if (result == null) { return; } - if (result.action == ButtonAction.error) { - await showGenericErrorDialog( - context: context, - error: result.exception, - ); - } - if (result.action == ButtonAction.first) { + // wait to allow the dialog to close + await Future.delayed(const Duration(milliseconds: 100)); + if (result == ButtonAction.first) { await showDialog( context: context, barrierDismissible: true, @@ -906,7 +879,7 @@ class _GalleryAppBarWidgetState extends State { }, ); } - if (result.action == ButtonAction.second) { + if (result == ButtonAction.second) { await _pairWithPin(gw); } } From e903fbf9bcc15b2fb10a6f27c144e37e119ddae7 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Thu, 25 Apr 2024 11:50:02 +0530 Subject: [PATCH 010/240] [mob][photos] Continue showing pair dialog in case of error --- .../ui/viewer/gallery/gallery_app_bar_widget.dart | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart index 7b45ae53f7..9e53014ed9 100644 --- a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart +++ b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart @@ -880,11 +880,11 @@ class _GalleryAppBarWidgetState extends State { ); } if (result == ButtonAction.second) { - await _pairWithPin(gw); + await _pairWithPin(gw, ''); } } - Future _pairWithPin(CastGateway gw) async { + Future _pairWithPin(CastGateway gw, String code) async { await showTextInputDialog( context, title: context.l10n.playOnTv, @@ -892,12 +892,17 @@ class _GalleryAppBarWidgetState extends State { submitButtonLabel: S.of(context).pair, textInputType: TextInputType.streetAddress, hintText: context.l10n.deviceCodeHint, + showOnlyLoadingState: true, + alwaysShowSuccessState: false, + initialValue: code, onSubmit: (String text) async { try { - final code = text.trim(); + code = text.trim(); final String? publicKey = await gw.getPublicKey(code); if (publicKey == null) { showToast(context, S.of(context).deviceNotFound); + // show _pairPin again + Future.delayed(Duration.zero, () => _pairWithPin(gw, code)); return; } final String castToken = const Uuid().v4().toString(); @@ -912,6 +917,7 @@ class _GalleryAppBarWidgetState extends State { } catch (e, s) { _logger.severe("Failed to cast album", e, s); await showGenericErrorDialog(context: context, error: e); + Future.delayed(Duration.zero, () => _pairWithPin(gw, code)); } }, ); From 36dbda895c3983114c8f527df6dc55cca86255e3 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Thu, 25 Apr 2024 11:50:49 +0530 Subject: [PATCH 011/240] [mob][photos] Send pair req after getting receiver status --- .../ente_cast_normal/lib/src/service.dart | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/mobile/plugins/ente_cast_normal/lib/src/service.dart b/mobile/plugins/ente_cast_normal/lib/src/service.dart index eac98ae51b..4c4624d8c6 100644 --- a/mobile/plugins/ente_cast_normal/lib/src/service.dart +++ b/mobile/plugins/ente_cast_normal/lib/src/service.dart @@ -1,29 +1,49 @@ +import "dart:developer" as dev; + import "package:cast/cast.dart"; import "package:ente_cast/ente_cast.dart"; import "package:flutter/material.dart"; class CastServiceImpl extends CastService { + final String _appId = 'F5BCEC64'; + final String _pairRequestNamespace = 'urn:x-cast:pair-request'; + final Map sessionIDToDeviceID = {}; + @override Future connectDevice(BuildContext context, Object device) async { final CastDevice castDevice = device as CastDevice; final session = await CastSessionManager().startSession(castDevice); + session.messageStream.listen((message) { + if (message['type'] == "RECEIVER_STATUS") { + dev.log( + "got RECEIVER_STATUS, Send request to pair", + name: "CastServiceImpl", + ); + session.sendMessage(_pairRequestNamespace, {}); + } else { + print('receive message: $message'); + } + }); session.stateStream.listen((state) { if (state == CastSessionState.connected) { const snackBar = SnackBar(content: Text('Connected')); ScaffoldMessenger.of(context).showSnackBar(snackBar); - session.sendMessage('urn:x-cast:pair-request', {}); + sessionIDToDeviceID[session.sessionId] = castDevice; + debugPrint("Send request to pair"); + session.sendMessage(_pairRequestNamespace, {}); + } else if (state == CastSessionState.closed) { + dev.log('Session closed', name: 'CastServiceImpl'); + sessionIDToDeviceID.remove(session.sessionId); } }); - session.messageStream.listen((message) { - print('receive message: $message'); - }); + debugPrint("Send request to launch"); session.sendMessage(CastSession.kNamespaceReceiver, { 'type': 'LAUNCH', - 'appId': 'F5BCEC64', // set the appId of your app here + 'appId': _appId, // set the appId of your app here }); - session.sendMessage('urn:x-cast:pair-request', {}); + // session.sendMessage('urn:x-cast:pair-request', {}); } @override From f777bdba1b0952d176e102de5c0f425d34d84c93 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Thu, 25 Apr 2024 12:44:46 +0530 Subject: [PATCH 012/240] [mob][photos] Extract strings --- mobile/lib/generated/intl/messages_en.dart | 4 ++++ mobile/lib/generated/l10n.dart | 20 ++++++++++++++++++++ mobile/lib/l10n/intl_en.arb | 4 +++- mobile/lib/ui/cast/choose.dart | 19 ++++++------------- 4 files changed, 33 insertions(+), 14 deletions(-) diff --git a/mobile/lib/generated/intl/messages_en.dart b/mobile/lib/generated/intl/messages_en.dart index 4de38ce120..5c5a9fd4c6 100644 --- a/mobile/lib/generated/intl/messages_en.dart +++ b/mobile/lib/generated/intl/messages_en.dart @@ -358,6 +358,8 @@ class MessageLookup extends MessageLookupByLibrary { "authenticationSuccessful": MessageLookupByLibrary.simpleMessage("Authentication successful!"), "autoPair": MessageLookupByLibrary.simpleMessage("Auto pair"), + "autoPairGoogle": MessageLookupByLibrary.simpleMessage( + "Auto Pair requires connecting to Google servers and only works with Chromecast supported devices. Google will not receive sensitive data, such as your photos."), "available": MessageLookupByLibrary.simpleMessage("Available"), "backedUpFolders": MessageLookupByLibrary.simpleMessage("Backed up folders"), @@ -903,6 +905,8 @@ class MessageLookup extends MessageLookupByLibrary { "manageParticipants": MessageLookupByLibrary.simpleMessage("Manage"), "manageSubscription": MessageLookupByLibrary.simpleMessage("Manage subscription"), + "manualPairDesc": MessageLookupByLibrary.simpleMessage( + "Pair with PIN works for any large screen device you want to play your album on."), "map": MessageLookupByLibrary.simpleMessage("Map"), "maps": MessageLookupByLibrary.simpleMessage("Maps"), "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), diff --git a/mobile/lib/generated/l10n.dart b/mobile/lib/generated/l10n.dart index 7f5dc96144..7af032ce1e 100644 --- a/mobile/lib/generated/l10n.dart +++ b/mobile/lib/generated/l10n.dart @@ -8583,6 +8583,26 @@ class S { args: [], ); } + + /// `Auto Pair requires connecting to Google servers and only works with Chromecast supported devices. Google will not receive sensitive data, such as your photos.` + String get autoPairGoogle { + return Intl.message( + 'Auto Pair requires connecting to Google servers and only works with Chromecast supported devices. Google will not receive sensitive data, such as your photos.', + name: 'autoPairGoogle', + desc: '', + args: [], + ); + } + + /// `Pair with PIN works for any large screen device you want to play your album on.` + String get manualPairDesc { + return Intl.message( + 'Pair with PIN works for any large screen device you want to play your album on.', + name: 'manualPairDesc', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/mobile/lib/l10n/intl_en.arb b/mobile/lib/l10n/intl_en.arb index ee00499357..0404e33f0f 100644 --- a/mobile/lib/l10n/intl_en.arb +++ b/mobile/lib/l10n/intl_en.arb @@ -1214,5 +1214,7 @@ "endpointUpdatedMessage": "Endpoint updated successfully", "customEndpoint": "Connected to {endpoint}", "createCollaborativeLink": "Create collaborative link", - "search": "Search" + "search": "Search", + "autoPairGoogle": "Auto Pair requires connecting to Google servers and only works with Chromecast supported devices. Google will not receive sensitive data, such as your photos.", + "manualPairDesc": "Pair with PIN works for any large screen device you want to play your album on." } \ No newline at end of file diff --git a/mobile/lib/ui/cast/choose.dart b/mobile/lib/ui/cast/choose.dart index 0b82ea299e..f3ab39a628 100644 --- a/mobile/lib/ui/cast/choose.dart +++ b/mobile/lib/ui/cast/choose.dart @@ -1,6 +1,6 @@ import "package:flutter/material.dart"; import "package:photos/generated/l10n.dart"; -import "package:photos/service_locator.dart"; +import "package:photos/l10n/l10n.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/components/buttons/button_widget.dart"; import "package:photos/ui/components/models/button_type.dart"; @@ -11,10 +11,10 @@ class CastChooseDialog extends StatefulWidget { }) : super(key: key) {} @override - State createState() => _AutoCastDialogState(); + State createState() => _CastChooseDialogState(); } -class _AutoCastDialogState extends State { +class _CastChooseDialogState extends State { final bool doesUserExist = true; @override @@ -22,7 +22,7 @@ class _AutoCastDialogState extends State { final textStyle = getEnteTextTheme(context); final AlertDialog alert = AlertDialog( title: Text( - "Play album on TV", + context.l10n.playOnTv, style: textStyle.largeBold, ), content: Column( @@ -31,7 +31,7 @@ class _AutoCastDialogState extends State { children: [ const SizedBox(height: 8), Text( - "Auto Pair requires connecting to Google servers and only works with Chromecast supported devices. Google will not receive sensitive data, such as your photos.", + S.of(context).autoPairGoogle, style: textStyle.bodyMuted, ), const SizedBox(height: 12), @@ -50,7 +50,7 @@ class _AutoCastDialogState extends State { ), const SizedBox(height: 36), Text( - "Pair with PIN works for any large screen device you want to play your album on.", + S.of(context).manualPairDesc, style: textStyle.bodyMuted, ), const SizedBox(height: 12), @@ -73,11 +73,4 @@ class _AutoCastDialogState extends State { ); return alert; } - - Future _connectToYourApp( - BuildContext context, - Object castDevice, - ) async { - await castService.connectDevice(context, castDevice); - } } From 864f5c1fd4c33abce43666dc25e766c2ee931054 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Thu, 25 Apr 2024 16:25:04 +0530 Subject: [PATCH 013/240] [mob][photos] Extract strings --- mobile/lib/generated/intl/messages_en.dart | 8 +++++ mobile/lib/generated/l10n.dart | 40 ++++++++++++++++++++++ mobile/lib/l10n/intl_en.arb | 6 +++- mobile/lib/ui/cast/auto.dart | 9 ++--- 4 files changed, 58 insertions(+), 5 deletions(-) diff --git a/mobile/lib/generated/intl/messages_en.dart b/mobile/lib/generated/intl/messages_en.dart index 5c5a9fd4c6..23ea9c5e20 100644 --- a/mobile/lib/generated/intl/messages_en.dart +++ b/mobile/lib/generated/intl/messages_en.dart @@ -357,6 +357,10 @@ class MessageLookup extends MessageLookupByLibrary { "Authentication failed, please try again"), "authenticationSuccessful": MessageLookupByLibrary.simpleMessage("Authentication successful!"), + "autoCastDialogBody": MessageLookupByLibrary.simpleMessage( + "You\'ll see available Cast devices here."), + "autoCastiOSPermission": MessageLookupByLibrary.simpleMessage( + "Make sure Local Network permissions are turned on for the Ente Photos app, in Settings."), "autoPair": MessageLookupByLibrary.simpleMessage("Auto pair"), "autoPairGoogle": MessageLookupByLibrary.simpleMessage( "Auto Pair requires connecting to Google servers and only works with Chromecast supported devices. Google will not receive sensitive data, such as your photos."), @@ -463,6 +467,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Confirm recovery key"), "confirmYourRecoveryKey": MessageLookupByLibrary.simpleMessage("Confirm your recovery key"), + "connectToDevice": + MessageLookupByLibrary.simpleMessage("Connect to device"), "contactFamilyAdmin": m12, "contactSupport": MessageLookupByLibrary.simpleMessage("Contact support"), @@ -941,6 +947,8 @@ class MessageLookup extends MessageLookupByLibrary { "no": MessageLookupByLibrary.simpleMessage("No"), "noAlbumsSharedByYouYet": MessageLookupByLibrary.simpleMessage("No albums shared by you yet"), + "noDeviceFound": + MessageLookupByLibrary.simpleMessage("No device found"), "noDeviceLimit": MessageLookupByLibrary.simpleMessage("None"), "noDeviceThatCanBeDeleted": MessageLookupByLibrary.simpleMessage( "You\'ve no files on this device that can be deleted"), diff --git a/mobile/lib/generated/l10n.dart b/mobile/lib/generated/l10n.dart index 7af032ce1e..d55ab17959 100644 --- a/mobile/lib/generated/l10n.dart +++ b/mobile/lib/generated/l10n.dart @@ -8603,6 +8603,46 @@ class S { args: [], ); } + + /// `Connect to device` + String get connectToDevice { + return Intl.message( + 'Connect to device', + name: 'connectToDevice', + desc: '', + args: [], + ); + } + + /// `You'll see available Cast devices here.` + String get autoCastDialogBody { + return Intl.message( + 'You\'ll see available Cast devices here.', + name: 'autoCastDialogBody', + desc: '', + args: [], + ); + } + + /// `Make sure Local Network permissions are turned on for the Ente Photos app, in Settings.` + String get autoCastiOSPermission { + return Intl.message( + 'Make sure Local Network permissions are turned on for the Ente Photos app, in Settings.', + name: 'autoCastiOSPermission', + desc: '', + args: [], + ); + } + + /// `No device found` + String get noDeviceFound { + return Intl.message( + 'No device found', + name: 'noDeviceFound', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/mobile/lib/l10n/intl_en.arb b/mobile/lib/l10n/intl_en.arb index 0404e33f0f..72afd2a4b3 100644 --- a/mobile/lib/l10n/intl_en.arb +++ b/mobile/lib/l10n/intl_en.arb @@ -1216,5 +1216,9 @@ "createCollaborativeLink": "Create collaborative link", "search": "Search", "autoPairGoogle": "Auto Pair requires connecting to Google servers and only works with Chromecast supported devices. Google will not receive sensitive data, such as your photos.", - "manualPairDesc": "Pair with PIN works for any large screen device you want to play your album on." + "manualPairDesc": "Pair with PIN works for any large screen device you want to play your album on.", + "connectToDevice": "Connect to device", + "autoCastDialogBody": "You'll see available Cast devices here.", + "autoCastiOSPermission": "Make sure Local Network permissions are turned on for the Ente Photos app, in Settings.", + "noDeviceFound": "No device found" } \ No newline at end of file diff --git a/mobile/lib/ui/cast/auto.dart b/mobile/lib/ui/cast/auto.dart index 5d8b64175e..64bae7cf3b 100644 --- a/mobile/lib/ui/cast/auto.dart +++ b/mobile/lib/ui/cast/auto.dart @@ -1,6 +1,7 @@ import "dart:io"; import "package:flutter/material.dart"; +import "package:photos/generated/l10n.dart"; import "package:photos/service_locator.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/common/loading_widget.dart"; @@ -24,7 +25,7 @@ class _AutoCastDialogState extends State { final AlertDialog alert = AlertDialog( title: Text( - "Connect to device", + S.of(context).connectToDevice, style: textStyle.largeBold, ), content: Column( @@ -32,12 +33,12 @@ class _AutoCastDialogState extends State { mainAxisSize: MainAxisSize.min, children: [ Text( - "You'll see available Cast devices here.", + S.of(context).autoCastDialogBody, style: textStyle.bodyMuted, ), if (Platform.isIOS) Text( - "Make sure Local Network permissions are turned on for the Ente Photos app, in Settings.", + S.of(context).autoCastiOSPermission, style: textStyle.bodyMuted, ), const SizedBox(height: 16), @@ -55,7 +56,7 @@ class _AutoCastDialogState extends State { } if (snapshot.data!.isEmpty) { - return const Center(child: Text('No device')); + return const Center(child: Text(S.of(context).noDeviceFound)); } return Column( From 483cfd1f3913109bb5c63e02d5bae8110e6f8149 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Thu, 25 Apr 2024 16:28:30 +0530 Subject: [PATCH 014/240] [mob][photos] Lint suggestions --- mobile/lib/ui/cast/auto.dart | 2 +- mobile/lib/ui/cast/choose.dart | 4 ++-- mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mobile/lib/ui/cast/auto.dart b/mobile/lib/ui/cast/auto.dart index 64bae7cf3b..0b088a9d2d 100644 --- a/mobile/lib/ui/cast/auto.dart +++ b/mobile/lib/ui/cast/auto.dart @@ -56,7 +56,7 @@ class _AutoCastDialogState extends State { } if (snapshot.data!.isEmpty) { - return const Center(child: Text(S.of(context).noDeviceFound)); + return Center(child: Text(S.of(context).noDeviceFound)); } return Column( diff --git a/mobile/lib/ui/cast/choose.dart b/mobile/lib/ui/cast/choose.dart index f3ab39a628..1cfd275c89 100644 --- a/mobile/lib/ui/cast/choose.dart +++ b/mobile/lib/ui/cast/choose.dart @@ -6,9 +6,9 @@ import "package:photos/ui/components/buttons/button_widget.dart"; import "package:photos/ui/components/models/button_type.dart"; class CastChooseDialog extends StatefulWidget { - CastChooseDialog({ + const CastChooseDialog({ Key? key, - }) : super(key: key) {} + }) : super(key: key); @override State createState() => _CastChooseDialogState(); diff --git a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart index 9e53014ed9..d7b1b01907 100644 --- a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart +++ b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart @@ -861,7 +861,7 @@ class _GalleryAppBarWidgetState extends State { context: context, barrierDismissible: true, builder: (BuildContext context) { - return CastChooseDialog(); + return const CastChooseDialog(); }, ); _logger.info("Cast result: $result"); From dddbb959b53e1a1da80750507b01cb3fb01afb64 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Thu, 25 Apr 2024 17:45:03 +0530 Subject: [PATCH 015/240] [mob][photos] Refactor --- .../gallery/gallery_app_bar_widget.dart | 49 +++++++++++-------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart index d7b1b01907..d73662e19f 100644 --- a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart +++ b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart @@ -896,30 +896,37 @@ class _GalleryAppBarWidgetState extends State { alwaysShowSuccessState: false, initialValue: code, onSubmit: (String text) async { - try { - code = text.trim(); - final String? publicKey = await gw.getPublicKey(code); - if (publicKey == null) { - showToast(context, S.of(context).deviceNotFound); - // show _pairPin again - Future.delayed(Duration.zero, () => _pairWithPin(gw, code)); - return; - } - final String castToken = const Uuid().v4().toString(); - final castPayload = CollectionsService.instance - .getCastData(castToken, widget.collection!, publicKey); - await gw.publishCastPayload( - code, - castPayload, - widget.collection!.id, - castToken, - ); - } catch (e, s) { - _logger.severe("Failed to cast album", e, s); - await showGenericErrorDialog(context: context, error: e); + final bool paired = await _castPair(gw, text); + if (!paired) { Future.delayed(Duration.zero, () => _pairWithPin(gw, code)); } }, ); } + + Future _castPair(CastGateway gw, String code) async { + try { + final String? publicKey = await gw.getPublicKey(code); + if (publicKey == null) { + showToast(context, S.of(context).deviceNotFound); + + return false; + } + final String castToken = const Uuid().v4().toString(); + final castPayload = CollectionsService.instance + .getCastData(castToken, widget.collection!, publicKey); + await gw.publishCastPayload( + code, + castPayload, + widget.collection!.id, + castToken, + ); + showToast(context, "Pairing complete"); + return true; + } catch (e, s) { + _logger.severe("Failed to cast album", e, s); + await showGenericErrorDialog(context: context, error: e); + return false; + } + } } From cc94615823f52f774a6fc08e3225ba40f545c176 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 26 Apr 2024 13:46:35 +0530 Subject: [PATCH 016/240] fix --- web/apps/photos/src/components/Upload/Uploader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/apps/photos/src/components/Upload/Uploader.tsx b/web/apps/photos/src/components/Upload/Uploader.tsx index 5cd157afac..9af14c5054 100644 --- a/web/apps/photos/src/components/Upload/Uploader.tsx +++ b/web/apps/photos/src/components/Upload/Uploader.tsx @@ -362,7 +362,7 @@ export default function Uploader(props: Props) { } else if (desktopFilePaths && desktopFilePaths.length > 0) { // File selection from our desktop app fileOrPathsToUpload.current = desktopFilePaths; - setDesktopFilePaths(undefined); + setDesktopFilePaths([]); } fileOrPathsToUpload.current = pruneHiddenFiles( From 88eb0d687e8dcdceef333b851c5a2c3d85a5dab6 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 26 Apr 2024 13:50:19 +0530 Subject: [PATCH 017/240] Fix log --- web/apps/photos/src/utils/native-stream.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/web/apps/photos/src/utils/native-stream.ts b/web/apps/photos/src/utils/native-stream.ts index c1033545bd..75e018b876 100644 --- a/web/apps/photos/src/utils/native-stream.ts +++ b/web/apps/photos/src/utils/native-stream.ts @@ -51,10 +51,11 @@ export const readStream = async ( }; const readNumericHeader = (res: Response, key: string) => { - const value = +res.headers[key]; + const valueText = res.headers[key]; + const value = +valueText; if (isNaN(value)) throw new Error( - `Expected a numeric ${key} when reading a stream response: ${res}`, + `Expected a numeric ${key} when reading a stream response, instead got ${valueText}`, ); return value; }; From faba29b4228bb304a9d9c6ba7aaee80fd7bc261c Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 26 Apr 2024 15:58:47 +0530 Subject: [PATCH 018/240] console.log(Object.fromEntries(res.headers.entries())); --- web/apps/photos/src/utils/native-stream.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/apps/photos/src/utils/native-stream.ts b/web/apps/photos/src/utils/native-stream.ts index 75e018b876..83824e7a9f 100644 --- a/web/apps/photos/src/utils/native-stream.ts +++ b/web/apps/photos/src/utils/native-stream.ts @@ -51,7 +51,7 @@ export const readStream = async ( }; const readNumericHeader = (res: Response, key: string) => { - const valueText = res.headers[key]; + const valueText = res.headers.get(key); const value = +valueText; if (isNaN(value)) throw new Error( From 21ad409c6164cdc9ecad75a04ac07ef540153aa1 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 26 Apr 2024 16:06:45 +0530 Subject: [PATCH 019/240] Don't overwrite what net.fetch already added --- desktop/src/main/stream.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/desktop/src/main/stream.ts b/desktop/src/main/stream.ts index 21104028fb..41cd212480 100644 --- a/desktop/src/main/stream.ts +++ b/desktop/src/main/stream.ts @@ -57,10 +57,12 @@ const handleRead = async (path: string) => { try { const res = await net.fetch(pathToFileURL(path).toString()); if (res.ok) { - // net.fetch defaults to text/plain, which might be fine - // in practice, but as an extra precaution indicate that - // this is binary data. - res.headers.set("Content-Type", "application/octet-stream"); + // `net.fetch` already seems to add "Content-Type" and + // "Last-Modified" headers. But since we already are stat-ting the + // file for the "Content-Length", we explicitly add the + // "X-Last-Modified-Ms" too, (a) guaranteeing its presence, and (b) + // having it be in the exact format we want (no string <-> date + // conversions) and (c) keeping milliseconds. const stat = await fs.stat(path); From 1c59a36c73b187a2884d72fe42652592a8669886 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 26 Apr 2024 16:10:54 +0530 Subject: [PATCH 020/240] Partially revert 699a12cc793e4ab59144310d86a47794c36b80c1 --- desktop/src/main.ts | 12 ++++++------ web/apps/photos/src/utils/native-stream.ts | 1 + 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/desktop/src/main.ts b/desktop/src/main.ts index a8a8a56101..3d7dfc417a 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -15,11 +15,7 @@ import { existsSync } from "node:fs"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { - addAllowOriginHeader, - handleDownloads, - handleExternalLinks, -} from "./main/init"; +import { handleDownloads, handleExternalLinks } from "./main/init"; import { attachFSWatchIPCHandlers, attachIPCHandlers } from "./main/ipc"; import log, { initLogging } from "./main/log"; import { createApplicationMenu, createTrayContextMenu } from "./main/menu"; @@ -349,7 +345,11 @@ const main = () => { registerStreamProtocol(); handleDownloads(mainWindow); handleExternalLinks(mainWindow); - addAllowOriginHeader(mainWindow); + // TODO(MR): Remove or resurrect + // The commit that introduced this header override had the message + // "fix cors issue for uploads". Not sure what that means, so disabling + // it for now to see why exactly this is required. + // addAllowOriginHeader(mainWindow); // Start loading the renderer mainWindow.loadURL(rendererURL); diff --git a/web/apps/photos/src/utils/native-stream.ts b/web/apps/photos/src/utils/native-stream.ts index 83824e7a9f..66d63cc6a4 100644 --- a/web/apps/photos/src/utils/native-stream.ts +++ b/web/apps/photos/src/utils/native-stream.ts @@ -39,6 +39,7 @@ export const readStream = async ( }); const res = await fetch(req); + console.log(Object.fromEntries(res.headers.entries())); if (!res.ok) throw new Error( `Failed to read stream from ${path}: HTTP ${res.status}`, From 9f4153933032ace033ce92313c168a2182b12c1f Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 26 Apr 2024 17:12:35 +0530 Subject: [PATCH 021/240] Enhance --- desktop/src/main.ts | 76 ++++++++++++++++++--- desktop/src/main/init.ts | 44 +----------- desktop/src/main/services/ml-clip.ts | 2 +- desktop/src/main/stream.ts | 14 ++-- web/apps/photos/src/services/detect-type.ts | 5 +- 5 files changed, 78 insertions(+), 63 deletions(-) diff --git a/desktop/src/main.ts b/desktop/src/main.ts index 3d7dfc417a..2774ec730c 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -8,14 +8,15 @@ * * https://www.electronjs.org/docs/latest/tutorial/process-model#the-main-process */ -import { nativeImage } from "electron"; -import { app, BrowserWindow, Menu, protocol, Tray } from "electron/main"; + +import { nativeImage, shell } from "electron/common"; +import type { WebContents } from "electron/main"; +import { BrowserWindow, Menu, Tray, app, protocol } from "electron/main"; import serveNextAt from "next-electron-server"; import { existsSync } from "node:fs"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { handleDownloads, handleExternalLinks } from "./main/init"; import { attachFSWatchIPCHandlers, attachIPCHandlers } from "./main/ipc"; import log, { initLogging } from "./main/log"; import { createApplicationMenu, createTrayContextMenu } from "./main/menu"; @@ -30,7 +31,7 @@ import { isDev } from "./main/utils-electron"; /** * The URL where the renderer HTML is being served from. */ -export const rendererURL = "ente://app"; +const rendererURL = "ente://app"; /** * We want to hide our window instead of closing it when the user presses the @@ -205,7 +206,7 @@ const createMainWindow = async () => { // webContents is not responding to input messages for > 30 seconds." window.webContents.on("unresponsive", () => { log.error( - "Main window's webContents are unresponsive, will restart the renderer process", + "MainWindow's webContents are unresponsive, will restart the renderer process", ); window.webContents.forcefullyCrashRenderer(); }); @@ -236,6 +237,58 @@ const createMainWindow = async () => { return window; }; +/** + * Automatically set the save path for user initiated downloads to the system's + * "downloads" directory instead of asking the user to select a save location. + */ +export const setDownloadPath = (webContents: WebContents) => { + webContents.session.on("will-download", (_, item) => { + item.setSavePath( + uniqueSavePath(app.getPath("downloads"), item.getFilename()), + ); + }); +}; + +const uniqueSavePath = (dirPath: string, fileName: string) => { + const { name, ext } = path.parse(fileName); + + let savePath = path.join(dirPath, fileName); + let n = 1; + while (existsSync(savePath)) { + const suffixedName = [`${name}(${n})`, ext].filter((x) => x).join("."); + savePath = path.join(dirPath, suffixedName); + n++; + } + return savePath; +}; + +/** + * Allow opening external links, e.g. when the user clicks on the "Feature + * requests" button in the sidebar (to open our GitHub repository), or when they + * click the "Support" button to send an email to support. + * + * @param webContents The renderer to configure. + */ +export const allowExternalLinks = (webContents: WebContents) => { + // By default, if the user were open a link, say + // https://github.com/ente-io/ente/discussions, then it would open a _new_ + // BrowserWindow within our app. + // + // This is not the behaviour we want; what we want is to ask the system to + // handle the link (e.g. open the URL in the default browser, or if it is a + // mailto: link, then open the user's mail client). + // + // Returning `action` "deny" accomplishes this. + webContents.setWindowOpenHandler(({ url }) => { + if (!url.startsWith(rendererURL)) { + shell.openExternal(url); + return { action: "deny" }; + } else { + return { action: "allow" }; + } + }); +}; + /** * Add an icon for our app in the system tray. * @@ -338,23 +391,26 @@ const main = () => { // // Note that some Electron APIs can only be used after this event occurs. app.on("ready", async () => { - // Create window and prepare for renderer + // Create window and prepare for the renderer. mainWindow = await createMainWindow(); attachIPCHandlers(); attachFSWatchIPCHandlers(createWatcher(mainWindow)); registerStreamProtocol(); - handleDownloads(mainWindow); - handleExternalLinks(mainWindow); + + // Configure the renderer's environment. + setDownloadPath(mainWindow.webContents); + allowExternalLinks(mainWindow.webContents); + // TODO(MR): Remove or resurrect // The commit that introduced this header override had the message // "fix cors issue for uploads". Not sure what that means, so disabling // it for now to see why exactly this is required. // addAllowOriginHeader(mainWindow); - // Start loading the renderer + // Start loading the renderer. mainWindow.loadURL(rendererURL); - // Continue on with the rest of the startup sequence + // Continue on with the rest of the startup sequence. Menu.setApplicationMenu(await createApplicationMenu(mainWindow)); setupTrayItem(mainWindow); if (!isDev) setupAutoUpdater(mainWindow); diff --git a/desktop/src/main/init.ts b/desktop/src/main/init.ts index 1b078dc98f..d0aee17f8f 100644 --- a/desktop/src/main/init.ts +++ b/desktop/src/main/init.ts @@ -1,46 +1,4 @@ -import { BrowserWindow, app, shell } from "electron"; -import { existsSync } from "node:fs"; -import path from "node:path"; -import { rendererURL } from "../main"; - -export function handleDownloads(mainWindow: BrowserWindow) { - mainWindow.webContents.session.on("will-download", (_, item) => { - item.setSavePath( - getUniqueSavePath(item.getFilename(), app.getPath("downloads")), - ); - }); -} - -function getUniqueSavePath(filename: string, directory: string): string { - let uniqueFileSavePath = path.join(directory, filename); - const { name: filenameWithoutExtension, ext: extension } = - path.parse(filename); - let n = 0; - while (existsSync(uniqueFileSavePath)) { - n++; - // filter need to remove undefined extension from the array - // else [`${fileName}`, undefined].join(".") will lead to `${fileName}.` as joined string - const fileNameWithNumberedSuffix = [ - `${filenameWithoutExtension}(${n})`, - extension, - ] - .filter((x) => x) // filters out undefined/null values - .join(""); - uniqueFileSavePath = path.join(directory, fileNameWithNumberedSuffix); - } - return uniqueFileSavePath; -} - -export function handleExternalLinks(mainWindow: BrowserWindow) { - mainWindow.webContents.setWindowOpenHandler(({ url }) => { - if (!url.startsWith(rendererURL)) { - shell.openExternal(url); - return { action: "deny" }; - } else { - return { action: "allow" }; - } - }); -} +import { BrowserWindow } from "electron"; export function addAllowOriginHeader(mainWindow: BrowserWindow) { mainWindow.webContents.session.webRequest.onHeadersReceived( diff --git a/desktop/src/main/services/ml-clip.ts b/desktop/src/main/services/ml-clip.ts index a5f407f9e7..cdd2baab76 100644 --- a/desktop/src/main/services/ml-clip.ts +++ b/desktop/src/main/services/ml-clip.ts @@ -150,7 +150,7 @@ export const clipTextEmbeddingIfAvailable = async (text: string) => { // Don't wait for the download to complete if (typeof sessionOrStatus == "string") { - console.log( + log.info( "Ignoring CLIP text embedding request because model download is pending", ); return undefined; diff --git a/desktop/src/main/stream.ts b/desktop/src/main/stream.ts index 41cd212480..bae4e6afe0 100644 --- a/desktop/src/main/stream.ts +++ b/desktop/src/main/stream.ts @@ -57,12 +57,14 @@ const handleRead = async (path: string) => { try { const res = await net.fetch(pathToFileURL(path).toString()); if (res.ok) { - // `net.fetch` already seems to add "Content-Type" and - // "Last-Modified" headers. But since we already are stat-ting the - // file for the "Content-Length", we explicitly add the - // "X-Last-Modified-Ms" too, (a) guaranteeing its presence, and (b) - // having it be in the exact format we want (no string <-> date - // conversions) and (c) keeping milliseconds. + // net.fetch already seems to add "Content-Type" and "Last-Modified" + // headers, but I couldn't find documentation for this. In any case, + // since we already are stat-ting the file for the "Content-Length", + // we explicitly add the "X-Last-Modified-Ms" too, + // 1. guaranteeing its presence + // 2. having it be in the exact format we want (no string <-> date + // conversions) + // 3. Retaining milliseconds. const stat = await fs.stat(path); diff --git a/web/apps/photos/src/services/detect-type.ts b/web/apps/photos/src/services/detect-type.ts index 6fd2fd70d2..e92e10bf82 100644 --- a/web/apps/photos/src/services/detect-type.ts +++ b/web/apps/photos/src/services/detect-type.ts @@ -93,8 +93,7 @@ const readInitialChunkOfFile = async (file: File) => { const detectFileTypeFromBuffer = async (buffer: Uint8Array) => { const result = await FileType.fromBuffer(buffer); - if (!result?.ext || !result?.mime) { - throw Error(`Could not deduce file type from buffer`); - } + if (!result) + throw Error("Could not deduce file type from the file's contents"); return result; }; From 2724760d6ccf9945c8db6ec8eee396dd54612a68 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 26 Apr 2024 17:39:00 +0530 Subject: [PATCH 022/240] Remove NodeJS global --- desktop/.eslintrc.js | 5 ----- desktop/src/main/stream.ts | 14 +++++++------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/desktop/.eslintrc.js b/desktop/.eslintrc.js index a47eb483fd..977071a270 100644 --- a/desktop/.eslintrc.js +++ b/desktop/.eslintrc.js @@ -7,11 +7,6 @@ module.exports = { // "plugin:@typescript-eslint/strict-type-checked", // "plugin:@typescript-eslint/stylistic-type-checked", ], - /* Temporarily add a global - Enhancement: Remove me */ - globals: { - NodeJS: "readonly", - }, plugins: ["@typescript-eslint"], parser: "@typescript-eslint/parser", parserOptions: { diff --git a/desktop/src/main/stream.ts b/desktop/src/main/stream.ts index bae4e6afe0..88d85db8e8 100644 --- a/desktop/src/main/stream.ts +++ b/desktop/src/main/stream.ts @@ -61,9 +61,12 @@ const handleRead = async (path: string) => { // headers, but I couldn't find documentation for this. In any case, // since we already are stat-ting the file for the "Content-Length", // we explicitly add the "X-Last-Modified-Ms" too, - // 1. guaranteeing its presence - // 2. having it be in the exact format we want (no string <-> date - // conversions) + // + // 1. Guaranteeing its presence, + // + // 2. Having it be in the exact format we want (no string <-> date + // conversions), + // // 3. Retaining milliseconds. const stat = await fs.stat(path); @@ -137,10 +140,7 @@ const convertWebReadableStreamToNode = (readableStream: ReadableStream) => { return rs; }; -const writeNodeStream = async ( - filePath: string, - fileStream: NodeJS.ReadableStream, -) => { +const writeNodeStream = async (filePath: string, fileStream: Readable) => { const writeable = createWriteStream(filePath); fileStream.on("error", (error) => { From 8ba952c3b8a379696a9ea122b8079826a6cf5eaf Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 26 Apr 2024 17:30:42 +0530 Subject: [PATCH 023/240] Remove unused --- desktop/src/main/dialogs.ts | 20 +++++++++++++++++++- desktop/src/main/ipc.ts | 3 --- desktop/src/main/services/fs.ts | 23 ----------------------- desktop/src/preload.ts | 4 ---- web/packages/next/types/ipc.ts | 1 - 5 files changed, 19 insertions(+), 32 deletions(-) diff --git a/desktop/src/main/dialogs.ts b/desktop/src/main/dialogs.ts index 2f91f5c400..f119e3d133 100644 --- a/desktop/src/main/dialogs.ts +++ b/desktop/src/main/dialogs.ts @@ -1,7 +1,8 @@ import { dialog } from "electron/main"; +import fs from "node:fs/promises"; import path from "node:path"; import type { ElectronFile } from "../types/ipc"; -import { getDirFilePaths, getElectronFile } from "./services/fs"; +import { getElectronFile } from "./services/fs"; import { getElectronFilesFromGoogleZip } from "./services/upload"; export const selectDirectory = async () => { @@ -34,6 +35,23 @@ export const showUploadDirsDialog = async () => { return await Promise.all(filePaths.map(getElectronFile)); }; +// https://stackoverflow.com/a/63111390 +const getDirFilePaths = async (dirPath: string) => { + if (!(await fs.stat(dirPath)).isDirectory()) { + return [dirPath]; + } + + let files: string[] = []; + const filePaths = await fs.readdir(dirPath); + + for (const filePath of filePaths) { + const absolute = path.join(dirPath, filePath); + files = [...files, ...(await getDirFilePaths(absolute))]; + } + + return files; +}; + export const showUploadZipDialog = async () => { const selectedFiles = await dialog.showOpenDialog({ properties: ["openFile", "multiSelections"], diff --git a/desktop/src/main/ipc.ts b/desktop/src/main/ipc.ts index 2475d77892..825a2ed32b 100644 --- a/desktop/src/main/ipc.ts +++ b/desktop/src/main/ipc.ts @@ -40,7 +40,6 @@ import { updateOnNextRestart, } from "./services/app-update"; import { ffmpegExec } from "./services/ffmpeg"; -import { getDirFiles } from "./services/fs"; import { convertToJPEG, generateImageThumbnail } from "./services/image"; import { clipImageEmbedding, @@ -216,8 +215,6 @@ export const attachIPCHandlers = () => { ipcMain.handle("getElectronFilesFromGoogleZip", (_, filePath: string) => getElectronFilesFromGoogleZip(filePath), ); - - ipcMain.handle("getDirFiles", (_, dirPath: string) => getDirFiles(dirPath)); }; /** diff --git a/desktop/src/main/services/fs.ts b/desktop/src/main/services/fs.ts index 30ccf146ba..609fc82d7e 100644 --- a/desktop/src/main/services/fs.ts +++ b/desktop/src/main/services/fs.ts @@ -7,29 +7,6 @@ import log from "../log"; const FILE_STREAM_CHUNK_SIZE: number = 4 * 1024 * 1024; -export async function getDirFiles(dirPath: string) { - const files = await getDirFilePaths(dirPath); - const electronFiles = await Promise.all(files.map(getElectronFile)); - return electronFiles; -} - -// https://stackoverflow.com/a/63111390 -export const getDirFilePaths = async (dirPath: string) => { - if (!(await fs.stat(dirPath)).isDirectory()) { - return [dirPath]; - } - - let files: string[] = []; - const filePaths = await fs.readdir(dirPath); - - for (const filePath of filePaths) { - const absolute = path.join(dirPath, filePath); - files = [...files, ...(await getDirFilePaths(absolute))]; - } - - return files; -}; - const getFileStream = async (filePath: string) => { const file = await fs.open(filePath, "r"); let offset = 0; diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index 68308aea9f..18fb550130 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -261,9 +261,6 @@ const getElectronFilesFromGoogleZip = ( ): Promise => ipcRenderer.invoke("getElectronFilesFromGoogleZip", filePath); -const getDirFiles = (dirPath: string): Promise => - ipcRenderer.invoke("getDirFiles", dirPath); - /** * These objects exposed here will become available to the JS code in our * renderer (the web/ code) as `window.ElectronAPIs.*` @@ -380,5 +377,4 @@ contextBridge.exposeInMainWorld("electron", { // - getElectronFilesFromGoogleZip, - getDirFiles, }); diff --git a/web/packages/next/types/ipc.ts b/web/packages/next/types/ipc.ts index 91a9927f2c..1622a820d9 100644 --- a/web/packages/next/types/ipc.ts +++ b/web/packages/next/types/ipc.ts @@ -503,7 +503,6 @@ export interface Electron { getElectronFilesFromGoogleZip: ( filePath: string, ) => Promise; - getDirFiles: (dirPath: string) => Promise; } /** From 3074bc108f7d2d81e9438c4feaf5bd9207b307ea Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 26 Apr 2024 17:49:50 +0530 Subject: [PATCH 024/240] Fix --- web/apps/photos/src/utils/native-stream.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/web/apps/photos/src/utils/native-stream.ts b/web/apps/photos/src/utils/native-stream.ts index 66d63cc6a4..85d54b7907 100644 --- a/web/apps/photos/src/utils/native-stream.ts +++ b/web/apps/photos/src/utils/native-stream.ts @@ -39,7 +39,6 @@ export const readStream = async ( }); const res = await fetch(req); - console.log(Object.fromEntries(res.headers.entries())); if (!res.ok) throw new Error( `Failed to read stream from ${path}: HTTP ${res.status}`, @@ -103,7 +102,7 @@ export const writeStream = async ( // GET can't have a body method: "POST", body: stream, - // @ts-expect-error TypeScript's libdom.d.ts does not include the + // --@ts-expect-error TypeScript's libdom.d.ts does not include the // "duplex" parameter, e.g. see // https://github.com/node-fetch/node-fetch/issues/1769. duplex: "half", From e786bed078420bd58191e8d3f6576da47a29161d Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 26 Apr 2024 18:16:22 +0530 Subject: [PATCH 025/240] Inline --- web/apps/photos/src/constants/upload.ts | 10 ------ .../src/services/upload/uploadService.ts | 34 +++++++++++++------ .../photos/tests/zip-file-reading.test.ts | 7 ++-- 3 files changed, 27 insertions(+), 24 deletions(-) diff --git a/web/apps/photos/src/constants/upload.ts b/web/apps/photos/src/constants/upload.ts index 2ff01810fc..2b570b3561 100644 --- a/web/apps/photos/src/constants/upload.ts +++ b/web/apps/photos/src/constants/upload.ts @@ -1,15 +1,5 @@ -import { ENCRYPTION_CHUNK_SIZE } from "@ente/shared/crypto/constants"; import { Location } from "types/metadata"; -// this is the chunk size of the un-encrypted file which is read and encrypted before uploading it as a single part. -export const MULTIPART_PART_SIZE = 20 * 1024 * 1024; - -export const FILE_READER_CHUNK_SIZE = ENCRYPTION_CHUNK_SIZE; - -export const FILE_CHUNKS_COMBINED_FOR_A_UPLOAD_PART = Math.floor( - MULTIPART_PART_SIZE / FILE_READER_CHUNK_SIZE, -); - export const RANDOM_PERCENTAGE_PROGRESS_FOR_PUT = () => 90 + 10 * Math.random(); export const NULL_LOCATION: Location = { latitude: null, longitude: null }; diff --git a/web/apps/photos/src/services/upload/uploadService.ts b/web/apps/photos/src/services/upload/uploadService.ts index 1848a2b1e5..4295b6239b 100644 --- a/web/apps/photos/src/services/upload/uploadService.ts +++ b/web/apps/photos/src/services/upload/uploadService.ts @@ -6,6 +6,7 @@ import { basename } from "@/next/file"; import log from "@/next/log"; import { CustomErrorMessage } from "@/next/types/ipc"; import { ensure } from "@/utils/ensure"; +import { ENCRYPTION_CHUNK_SIZE } from "@ente/shared/crypto/constants"; import { DedicatedCryptoWorker } from "@ente/shared/crypto/internal/crypto.worker"; import { B64EncryptionResult, @@ -16,9 +17,6 @@ import { CustomError, handleUploadError } from "@ente/shared/error"; import { isDataStream, type DataStream } from "@ente/shared/utils/data-stream"; import { Remote } from "comlink"; import { - FILE_CHUNKS_COMBINED_FOR_A_UPLOAD_PART, - FILE_READER_CHUNK_SIZE, - MULTIPART_PART_SIZE, NULL_LOCATION, RANDOM_PERCENTAGE_PROGRESS_FOR_PUT, UPLOAD_RESULT, @@ -61,6 +59,18 @@ import { import UploadHttpClient from "./uploadHttpClient"; import type { UploadableFile } from "./uploadManager"; +/** Allow up to 5 ENCRYPTION_CHUNK_SIZE chunks in an upload part */ +const maximumChunksPerUploadPart = 5; + +/** + * The chunk size of the un-encrypted file which is read and encrypted before + * uploading it as a single part of a multipart upload. + * + * ENCRYPTION_CHUNK_SIZE is 4 MB, and maximum number of chunks in a single + * upload part is 5, so this is 20 MB. + * */ +const multipartPartSize = ENCRYPTION_CHUNK_SIZE * maximumChunksPerUploadPart; + /** Upload files to cloud storage */ class UploadService { private uploadURLs: UploadURL[] = []; @@ -464,8 +474,8 @@ const readFileOrPath = async ( fileSize = file.size; lastModifiedMs = file.lastModified; dataOrStream = - fileSize > MULTIPART_PART_SIZE - ? getFileStream(file, FILE_READER_CHUNK_SIZE) + fileSize > multipartPartSize + ? getFileStream(file, ENCRYPTION_CHUNK_SIZE) : new Uint8Array(await file.arrayBuffer()); } else { const path = fileOrPath; @@ -476,8 +486,8 @@ const readFileOrPath = async ( } = await readStream(ensureElectron(), path); fileSize = size; lastModifiedMs = lm; - if (size > MULTIPART_PART_SIZE) { - const chunkCount = Math.ceil(size / FILE_READER_CHUNK_SIZE); + if (size > multipartPartSize) { + const chunkCount = Math.ceil(size / ENCRYPTION_CHUNK_SIZE); dataOrStream = { stream: response.body, chunkCount }; } else { dataOrStream = new Uint8Array(await response.arrayBuffer()); @@ -492,13 +502,13 @@ const readFileOrPathStream = async ( fileOrPath: File | string, ): Promise => { if (fileOrPath instanceof File) { - return getFileStream(fileOrPath, FILE_READER_CHUNK_SIZE); + return getFileStream(fileOrPath, ENCRYPTION_CHUNK_SIZE); } else { const { response, size } = await readStream( ensureElectron(), fileOrPath, ); - const chunkCount = Math.ceil(size / FILE_READER_CHUNK_SIZE); + const chunkCount = Math.ceil(size / ENCRYPTION_CHUNK_SIZE); return { stream: response.body, chunkCount }; } }; @@ -760,6 +770,8 @@ const computeHash = async ( worker: Remote, ) => { const { stream, chunkCount } = await readFileOrPathStream(fileOrPath); + // TODO(MR): ElectronFile + console.log("got stream and chunks", stream, chunkCount); const hashState = await worker.initChunkHashing(); const streamReader = stream.getReader(); @@ -1195,7 +1207,7 @@ async function uploadStreamUsingMultipart( abortIfCancelled: () => void, ) { const uploadPartCount = Math.ceil( - dataStream.chunkCount / FILE_CHUNKS_COMBINED_FOR_A_UPLOAD_PART, + dataStream.chunkCount / maximumChunksPerUploadPart, ); const multipartUploadURLs = await uploadService.fetchMultipartUploadURLs(uploadPartCount); @@ -1255,7 +1267,7 @@ async function combineChunksToFormUploadPart( streamReader: ReadableStreamDefaultReader, ) { const combinedChunks = []; - for (let i = 0; i < FILE_CHUNKS_COMBINED_FOR_A_UPLOAD_PART; i++) { + for (let i = 0; i < maximumChunksPerUploadPart; i++) { const { done, value: chunk } = await streamReader.read(); if (done) { break; diff --git a/web/apps/photos/tests/zip-file-reading.test.ts b/web/apps/photos/tests/zip-file-reading.test.ts index ea7511d0b4..e4fe6bbf3a 100644 --- a/web/apps/photos/tests/zip-file-reading.test.ts +++ b/web/apps/photos/tests/zip-file-reading.test.ts @@ -1,6 +1,7 @@ import { getFileNameSize } from "@/next/file"; +import { ENCRYPTION_CHUNK_SIZE } from "@ente/shared/crypto/constants"; import type { DataStream } from "@ente/shared/utils/data-stream"; -import { FILE_READER_CHUNK_SIZE, PICKED_UPLOAD_TYPE } from "constants/upload"; +import { PICKED_UPLOAD_TYPE } from "constants/upload"; import { getElectronFileStream, getFileStream } from "services/readerService"; import { getImportSuggestion } from "utils/upload"; @@ -35,11 +36,11 @@ export const testZipFileReading = async () => { i++; let filedata: DataStream; if (file instanceof File) { - filedata = getFileStream(file, FILE_READER_CHUNK_SIZE); + filedata = getFileStream(file, ENCRYPTION_CHUNK_SIZE); } else { filedata = await getElectronFileStream( file, - FILE_READER_CHUNK_SIZE, + ENCRYPTION_CHUNK_SIZE, ); } const streamReader = filedata.stream.getReader(); From 31608ab8fa92b10c0725a45a9f1b0aac21b9e6d1 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 26 Apr 2024 18:18:15 +0530 Subject: [PATCH 026/240] Inline --- web/apps/photos/src/constants/upload.ts | 29 ----------------- .../photos/src/services/upload/thumbnail.ts | 32 +++++++++++++++++-- 2 files changed, 30 insertions(+), 31 deletions(-) diff --git a/web/apps/photos/src/constants/upload.ts b/web/apps/photos/src/constants/upload.ts index 2b570b3561..13dee9cf49 100644 --- a/web/apps/photos/src/constants/upload.ts +++ b/web/apps/photos/src/constants/upload.ts @@ -30,32 +30,3 @@ export enum PICKED_UPLOAD_TYPE { FOLDERS = "folders", ZIPS = "zips", } - -export const BLACK_THUMBNAIL_BASE64 = - "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEB" + - "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQ" + - "EBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARC" + - "ACWASwDAREAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUF" + - "BAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk" + - "6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztL" + - "W2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAA" + - "AAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVY" + - "nLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImK" + - "kpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oAD" + - "AMBAAIRAxEAPwD/AD/6ACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA" + - "CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg" + - "AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKAC" + - "gAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo" + - "AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg" + - "AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg" + - "AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA" + - "CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA" + - "CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoA" + - "KACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg" + - "AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo" + - "AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA" + - "CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAK" + - "ACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoA" + - "KACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo" + - "AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo" + - "AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgD/9k="; diff --git a/web/apps/photos/src/services/upload/thumbnail.ts b/web/apps/photos/src/services/upload/thumbnail.ts index 4552d11b36..a44c941f16 100644 --- a/web/apps/photos/src/services/upload/thumbnail.ts +++ b/web/apps/photos/src/services/upload/thumbnail.ts @@ -2,7 +2,6 @@ import { FILE_TYPE, type FileTypeInfo } from "@/media/file-type"; import log from "@/next/log"; import { type Electron } from "@/next/types/ipc"; import { withTimeout } from "@ente/shared/utils"; -import { BLACK_THUMBNAIL_BASE64 } from "constants/upload"; import * as ffmpeg from "services/ffmpeg"; import { heicToJPEG } from "services/heic-convert"; @@ -206,4 +205,33 @@ export const generateThumbnailNative = async ( * fails. */ export const fallbackThumbnail = () => - Uint8Array.from(atob(BLACK_THUMBNAIL_BASE64), (c) => c.charCodeAt(0)); + Uint8Array.from(atob(blackThumbnailB64), (c) => c.charCodeAt(0)); + +const blackThumbnailB64 = + "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEB" + + "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQ" + + "EBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARC" + + "ACWASwDAREAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUF" + + "BAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk" + + "6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztL" + + "W2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAA" + + "AAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVY" + + "nLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImK" + + "kpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oAD" + + "AMBAAIRAxEAPwD/AD/6ACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA" + + "CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg" + + "AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKAC" + + "gAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo" + + "AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg" + + "AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg" + + "AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA" + + "CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA" + + "CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoA" + + "KACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg" + + "AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo" + + "AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA" + + "CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAK" + + "ACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoA" + + "KACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo" + + "AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo" + + "AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgD/9k="; From db30d8d81e7082562fdb645a8f9ac49d332be3d2 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 26 Apr 2024 18:48:33 +0530 Subject: [PATCH 027/240] Read in 4 MB blocks Refs: - https://developer.mozilla.org/en-US/docs/Web/API/TransformStream/TransformStream --- web/apps/photos/src/services/upload/uploadService.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/web/apps/photos/src/services/upload/uploadService.ts b/web/apps/photos/src/services/upload/uploadService.ts index 4295b6239b..15178b4de0 100644 --- a/web/apps/photos/src/services/upload/uploadService.ts +++ b/web/apps/photos/src/services/upload/uploadService.ts @@ -774,9 +774,18 @@ const computeHash = async ( console.log("got stream and chunks", stream, chunkCount); const hashState = await worker.initChunkHashing(); - const streamReader = stream.getReader(); + const chunkedStream = stream.pipeThrough( + new TransformStream( + undefined, + new ByteLengthQueuingStrategy({ + highWaterMark: ENCRYPTION_CHUNK_SIZE, + }), + ), + ); + const streamReader = chunkedStream.getReader(); for (let i = 0; i < chunkCount; i++) { const { done, value: chunk } = await streamReader.read(); + console.log("chunk size", chunk.length); if (done) throw new Error("Less chunks than expected"); await worker.hashFileChunk(hashState, Uint8Array.from(chunk)); } From 4ce6fa790fa6c86a5a4892c63fd545c487860bde Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Sat, 27 Apr 2024 12:16:48 +0530 Subject: [PATCH 028/240] [mob] Add method to close cast and keep track of active casts --- mobile/plugins/ente_cast/lib/ente_cast.dart | 1 + mobile/plugins/ente_cast/lib/src/model.dart | 5 +++ mobile/plugins/ente_cast/lib/src/service.dart | 13 +++++- .../ente_cast_none/lib/src/service.dart | 19 +++++++- .../ente_cast_normal/lib/src/service.dart | 43 +++++++++++++++++-- 5 files changed, 75 insertions(+), 6 deletions(-) create mode 100644 mobile/plugins/ente_cast/lib/src/model.dart diff --git a/mobile/plugins/ente_cast/lib/ente_cast.dart b/mobile/plugins/ente_cast/lib/ente_cast.dart index 66a7132d8d..f421a92970 100644 --- a/mobile/plugins/ente_cast/lib/ente_cast.dart +++ b/mobile/plugins/ente_cast/lib/ente_cast.dart @@ -1 +1,2 @@ +export 'src/model.dart'; export 'src/service.dart'; diff --git a/mobile/plugins/ente_cast/lib/src/model.dart b/mobile/plugins/ente_cast/lib/src/model.dart new file mode 100644 index 0000000000..e86582f76f --- /dev/null +++ b/mobile/plugins/ente_cast/lib/src/model.dart @@ -0,0 +1,5 @@ +// create enum for type of message for cast +enum CastMessageType { + pairCode, + alreadyCasting, +} diff --git a/mobile/plugins/ente_cast/lib/src/service.dart b/mobile/plugins/ente_cast/lib/src/service.dart index 230269bb39..82d8c5978c 100644 --- a/mobile/plugins/ente_cast/lib/src/service.dart +++ b/mobile/plugins/ente_cast/lib/src/service.dart @@ -1,7 +1,18 @@ +import "package:ente_cast/src/model.dart"; import "package:flutter/widgets.dart"; abstract class CastService { bool get isSupported; Future> searchDevices(); - Future connectDevice(BuildContext context, Object device); + Future connectDevice( + BuildContext context, + Object device, { + int? collectionID, + // callback that take a map of string, dynamic + void Function(Map>)? onMessage, + }); + // returns a map of sessionID to deviceNames + Future> getActiveSessions(); + + Future closeActiveCasts(); } diff --git a/mobile/plugins/ente_cast_none/lib/src/service.dart b/mobile/plugins/ente_cast_none/lib/src/service.dart index 166108c528..007a4daaa0 100644 --- a/mobile/plugins/ente_cast_none/lib/src/service.dart +++ b/mobile/plugins/ente_cast_none/lib/src/service.dart @@ -3,7 +3,12 @@ import "package:flutter/widgets.dart"; class CastServiceImpl extends CastService { @override - Future connectDevice(BuildContext context, Object device) { + Future connectDevice( + BuildContext context, + Object device, { + int? collectionID, + void Function(Map>)? onMessage, + }) { throw UnimplementedError(); } @@ -15,4 +20,16 @@ class CastServiceImpl extends CastService { // TODO: implement searchDevices throw UnimplementedError(); } + + @override + Future closeActiveCasts() { + // TODO: implement closeActiveCasts + throw UnimplementedError(); + } + + @override + Future> getActiveSessions() { + // TODO: implement getActiveSessions + throw UnimplementedError(); + } } diff --git a/mobile/plugins/ente_cast_normal/lib/src/service.dart b/mobile/plugins/ente_cast_normal/lib/src/service.dart index 4c4624d8c6..314194b944 100644 --- a/mobile/plugins/ente_cast_normal/lib/src/service.dart +++ b/mobile/plugins/ente_cast_normal/lib/src/service.dart @@ -7,10 +7,15 @@ import "package:flutter/material.dart"; class CastServiceImpl extends CastService { final String _appId = 'F5BCEC64'; final String _pairRequestNamespace = 'urn:x-cast:pair-request'; - final Map sessionIDToDeviceID = {}; + final Map collectionIDToSessions = {}; @override - Future connectDevice(BuildContext context, Object device) async { + Future connectDevice( + BuildContext context, + Object device, { + int? collectionID, + void Function(Map>)? onMessage, + }) async { final CastDevice castDevice = device as CastDevice; final session = await CastSessionManager().startSession(castDevice); session.messageStream.listen((message) { @@ -21,6 +26,13 @@ class CastServiceImpl extends CastService { ); session.sendMessage(_pairRequestNamespace, {}); } else { + if (onMessage != null && message!.containsKey("code")) { + onMessage( + { + CastMessageType.pairCode: message, + }, + ); + } print('receive message: $message'); } }); @@ -29,12 +41,10 @@ class CastServiceImpl extends CastService { if (state == CastSessionState.connected) { const snackBar = SnackBar(content: Text('Connected')); ScaffoldMessenger.of(context).showSnackBar(snackBar); - sessionIDToDeviceID[session.sessionId] = castDevice; debugPrint("Send request to pair"); session.sendMessage(_pairRequestNamespace, {}); } else if (state == CastSessionState.closed) { dev.log('Session closed', name: 'CastServiceImpl'); - sessionIDToDeviceID.remove(session.sessionId); } }); @@ -55,4 +65,29 @@ class CastServiceImpl extends CastService { @override bool get isSupported => true; + + @override + Future closeActiveCasts() { + final sessions = CastSessionManager().sessions; + for (final session in sessions) { + session.sendMessage( + _pairRequestNamespace, + { + "type": "CLOSE", + }, + ); + session.close(); + } + return Future.value(); + } + + @override + Future> getActiveSessions() { + final sessions = CastSessionManager().sessions; + final Map result = {}; + for (final session in sessions) { + result[session.sessionId] = session.state.toString(); + } + return Future.value(result); + } } From 31a70674ff3c875a223016a2c7f76ed0ede18b3b Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Sat, 27 Apr 2024 16:49:22 +0530 Subject: [PATCH 029/240] Revert "Read in 4 MB blocks" This reverts commit db30d8d81e7082562fdb645a8f9ac49d332be3d2. --- web/apps/photos/src/services/upload/uploadService.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/web/apps/photos/src/services/upload/uploadService.ts b/web/apps/photos/src/services/upload/uploadService.ts index 15178b4de0..4295b6239b 100644 --- a/web/apps/photos/src/services/upload/uploadService.ts +++ b/web/apps/photos/src/services/upload/uploadService.ts @@ -774,18 +774,9 @@ const computeHash = async ( console.log("got stream and chunks", stream, chunkCount); const hashState = await worker.initChunkHashing(); - const chunkedStream = stream.pipeThrough( - new TransformStream( - undefined, - new ByteLengthQueuingStrategy({ - highWaterMark: ENCRYPTION_CHUNK_SIZE, - }), - ), - ); - const streamReader = chunkedStream.getReader(); + const streamReader = stream.getReader(); for (let i = 0; i < chunkCount; i++) { const { done, value: chunk } = await streamReader.read(); - console.log("chunk size", chunk.length); if (done) throw new Error("Less chunks than expected"); await worker.hashFileChunk(hashState, Uint8Array.from(chunk)); } From 536bcf1091e8781aa6abac18338948e3928a147f Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Sat, 27 Apr 2024 16:57:40 +0530 Subject: [PATCH 030/240] Add a fixed size block transformer Surprisingly, this is not a primitive. Or maybe I didn't find it. The highWaterMark-ing didn't work, that seems more of a recommendation than an enforcement. --- .../src/services/upload/uploadService.ts | 53 +++++++++++++++---- 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/web/apps/photos/src/services/upload/uploadService.ts b/web/apps/photos/src/services/upload/uploadService.ts index 4295b6239b..9cd8cc0038 100644 --- a/web/apps/photos/src/services/upload/uploadService.ts +++ b/web/apps/photos/src/services/upload/uploadService.ts @@ -501,16 +501,53 @@ const readFileOrPath = async ( const readFileOrPathStream = async ( fileOrPath: File | string, ): Promise => { + const N = ENCRYPTION_CHUNK_SIZE; + + let underlyingStream: ReadableStream; + let chunkCount: number; + if (fileOrPath instanceof File) { - return getFileStream(fileOrPath, ENCRYPTION_CHUNK_SIZE); + const file = fileOrPath; + underlyingStream = file.stream(); + chunkCount = Math.ceil(file.size / N); } else { - const { response, size } = await readStream( - ensureElectron(), - fileOrPath, - ); - const chunkCount = Math.ceil(size / ENCRYPTION_CHUNK_SIZE); - return { stream: response.body, chunkCount }; + const path = fileOrPath; + const { response, size } = await readStream(ensureElectron(), path); + underlyingStream = response.body; + chunkCount = Math.ceil(size / N); } + + // Pipe the underlying stream through a transformer that emits + // ENCRYPTION_CHUNK_SIZE-ed chunks (except the last one, which can be + // smaller). + let pending: Uint8Array | undefined; + const transformer = new TransformStream({ + async transform( + chunk: Uint8Array, + controller: TransformStreamDefaultController, + ) { + let next: Uint8Array; + if (pending) { + next = new Uint8Array(pending.length + chunk.length); + next.set(pending); + next.set(chunk, pending.length); + pending = undefined; + } else { + next = chunk; + } + while (next.length >= N) { + controller.enqueue(next.slice(0, N)); + next = next.slice(N); + } + if (next.length) pending = next; + }, + flush(controller: TransformStreamDefaultController) { + if (pending) controller.enqueue(pending); + }, + }); + + const stream = underlyingStream.pipeThrough(transformer); + return { stream, chunkCount }; }; interface ReadAssetDetailsResult { @@ -770,8 +807,6 @@ const computeHash = async ( worker: Remote, ) => { const { stream, chunkCount } = await readFileOrPathStream(fileOrPath); - // TODO(MR): ElectronFile - console.log("got stream and chunks", stream, chunkCount); const hashState = await worker.initChunkHashing(); const streamReader = stream.getReader(); From 6671a62c78928576acc4eb8d4d910e1354a75c83 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Sat, 27 Apr 2024 17:06:33 +0530 Subject: [PATCH 031/240] duh --- web/apps/photos/src/services/watch.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/apps/photos/src/services/watch.ts b/web/apps/photos/src/services/watch.ts index 1f60836c50..fedf4bf403 100644 --- a/web/apps/photos/src/services/watch.ts +++ b/web/apps/photos/src/services/watch.ts @@ -611,7 +611,7 @@ const isSyncedOrIgnoredPath = (path: string, watch: FolderWatch) => const collectionNameForPath = (path: string, watch: FolderWatch) => watch.collectionMapping == "root" - ? dirname(watch.folderPath) + ? basename(watch.folderPath) : parentDirectoryName(path); const parentDirectoryName = (path: string) => basename(dirname(path)); From c2b55d43897ff557ff46cd7c84cf755f410e8969 Mon Sep 17 00:00:00 2001 From: BifrostTenmei <156937334+BifrostTenmei@users.noreply.github.com> Date: Sat, 27 Apr 2024 13:58:27 +0200 Subject: [PATCH 032/240] Fixed typo --- server/docs/docker.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/docs/docker.md b/server/docs/docker.md index d8f3db9137..a328d734bd 100644 --- a/server/docs/docker.md +++ b/server/docs/docker.md @@ -45,7 +45,7 @@ require you to clone the repository or build any images. + image: ghcr.io/ente-io/server ``` -4. Create an (empty) configuration file. Yyou can later put your custom +4. Create an (empty) configuration file. You can later put your custom configuration in this if needed. ```sh From bb2ddec163718d12298cba4fdf0e86f92bfdf00a Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Sat, 27 Apr 2024 17:42:36 +0530 Subject: [PATCH 033/240] Inline The zip-file tests are no longer directly usable, the way we read the files has changed, these will have to be recreated in a new form. --- .../src/components/Sidebar/DebugSection.tsx | 28 +---- .../src/services/upload/uploadService.ts | 19 +++ .../photos/tests/zip-file-reading.test.ts | 112 ------------------ web/packages/shared/crypto/types.ts | 14 --- web/packages/shared/utils/data-stream.ts | 8 -- 5 files changed, 24 insertions(+), 157 deletions(-) delete mode 100644 web/apps/photos/tests/zip-file-reading.test.ts delete mode 100644 web/packages/shared/utils/data-stream.ts diff --git a/web/apps/photos/src/components/Sidebar/DebugSection.tsx b/web/apps/photos/src/components/Sidebar/DebugSection.tsx index 28c65ca8e0..e336374030 100644 --- a/web/apps/photos/src/components/Sidebar/DebugSection.tsx +++ b/web/apps/photos/src/components/Sidebar/DebugSection.tsx @@ -9,10 +9,6 @@ import { useContext, useEffect, useState } from "react"; import { Trans } from "react-i18next"; import { isInternalUser } from "utils/user"; import { testUpload } from "../../../tests/upload.test"; -import { - testZipFileReading, - testZipWithRootFileReadingTest, -} from "../../../tests/zip-file-reading.test"; export default function DebugSection() { const appContext = useContext(AppContext); @@ -62,25 +58,11 @@ export default function DebugSection() { )} {isInternalUser() && ( - <> - - - - - - + )} ); diff --git a/web/apps/photos/src/services/upload/uploadService.ts b/web/apps/photos/src/services/upload/uploadService.ts index 9cd8cc0038..a9edfc7837 100644 --- a/web/apps/photos/src/services/upload/uploadService.ts +++ b/web/apps/photos/src/services/upload/uploadService.ts @@ -71,6 +71,25 @@ const maximumChunksPerUploadPart = 5; * */ const multipartPartSize = ENCRYPTION_CHUNK_SIZE * maximumChunksPerUploadPart; +interface DataStream { + stream: ReadableStream; + chunkCount: number; +} + +function isDataStream(object: any): object is DataStream { + return "stream" in object; +} + +interface LocalFileAttributes { + encryptedData: T; + decryptionHeader: string; +} + +interface EncryptionResult { + file: LocalFileAttributes; + key: string; +} + /** Upload files to cloud storage */ class UploadService { private uploadURLs: UploadURL[] = []; diff --git a/web/apps/photos/tests/zip-file-reading.test.ts b/web/apps/photos/tests/zip-file-reading.test.ts deleted file mode 100644 index e4fe6bbf3a..0000000000 --- a/web/apps/photos/tests/zip-file-reading.test.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { getFileNameSize } from "@/next/file"; -import { ENCRYPTION_CHUNK_SIZE } from "@ente/shared/crypto/constants"; -import type { DataStream } from "@ente/shared/utils/data-stream"; -import { PICKED_UPLOAD_TYPE } from "constants/upload"; -import { getElectronFileStream, getFileStream } from "services/readerService"; -import { getImportSuggestion } from "utils/upload"; - -// This was for used to verify that converting from the browser readable stream -// to the node readable stream correctly handles files that align on the 4 MB -// data boundary. This expects a zip file containing random files of various -// sizes starting from 1M to 20M. -export const testZipFileReading = async () => { - try { - const electron = globalThis.electron; - if (!electron) { - console.log("testZipFileReading Check is for desktop only"); - return; - } - if (!process.env.NEXT_PUBLIC_FILE_READING_TEST_ZIP_PATH) { - throw Error( - "upload test failed NEXT_PUBLIC_FILE_READING_TEST_ZIP_PATH missing", - ); - } - const files = await electron.getElectronFilesFromGoogleZip( - process.env.NEXT_PUBLIC_FILE_READING_TEST_ZIP_PATH, - ); - if (!files?.length) { - throw Error( - `testZipFileReading Check failed ❌ - No files selected`, - ); - } - console.log("test zip file reading check started"); - let i = 0; - for (const file of files) { - i++; - let filedata: DataStream; - if (file instanceof File) { - filedata = getFileStream(file, ENCRYPTION_CHUNK_SIZE); - } else { - filedata = await getElectronFileStream( - file, - ENCRYPTION_CHUNK_SIZE, - ); - } - const streamReader = filedata.stream.getReader(); - for (let i = 0; i < filedata.chunkCount; i++) { - const { done } = await streamReader.read(); - if (done) { - throw Error( - `testZipFileReading Check failed ❌ - ${getFileNameSize( - file, - )} less than expected chunks, expected: ${ - filedata.chunkCount - }, got ${i - 1}`, - ); - } - } - const { done } = await streamReader.read(); - - if (!done) { - throw Error( - `testZipFileReading Check failed ❌ - ${getFileNameSize( - file, - )} more than expected chunks, expected: ${ - filedata.chunkCount - }`, - ); - } - console.log(`${i}/${files.length} passed ✅`); - } - console.log("test zip file reading check passed ✅"); - } catch (e) { - console.log(e); - } -}; - -// This was used when fixing a bug around handling a zip file that has a photo -// at the root. -export const testZipWithRootFileReadingTest = async () => { - try { - const electron = globalThis.electron; - if (!electron) { - console.log("testZipFileReading Check is for desktop only"); - return; - } - if (!process.env.NEXT_PUBLIC_ZIP_WITH_ROOT_FILE_PATH) { - throw Error( - "upload test failed NEXT_PUBLIC_ZIP_WITH_ROOT_FILE_PATH missing", - ); - } - const files = await electron.getElectronFilesFromGoogleZip( - process.env.NEXT_PUBLIC_ZIP_WITH_ROOT_FILE_PATH, - ); - - const importSuggestion = getImportSuggestion( - PICKED_UPLOAD_TYPE.ZIPS, - files.map((file) => file["path"]), - ); - if (!importSuggestion.rootFolderName) { - throw Error( - `testZipWithRootFileReadingTest Check failed ❌ - rootFolderName is missing`, - ); - } - console.log("testZipWithRootFileReadingTest passed ✅"); - } catch (e) { - console.log(e); - } -}; diff --git a/web/packages/shared/crypto/types.ts b/web/packages/shared/crypto/types.ts index 47bfa8b2ce..e591820f08 100644 --- a/web/packages/shared/crypto/types.ts +++ b/web/packages/shared/crypto/types.ts @@ -1,17 +1,3 @@ -import type { DataStream } from "../utils/data-stream"; - -export interface LocalFileAttributes< - T extends string | Uint8Array | DataStream, -> { - encryptedData: T; - decryptionHeader: string; -} - -export interface EncryptionResult { - file: LocalFileAttributes; - key: string; -} - export interface B64EncryptionResult { encryptedData: string; key: string; diff --git a/web/packages/shared/utils/data-stream.ts b/web/packages/shared/utils/data-stream.ts deleted file mode 100644 index d072dfe7ec..0000000000 --- a/web/packages/shared/utils/data-stream.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface DataStream { - stream: ReadableStream; - chunkCount: number; -} - -export function isDataStream(object: any): object is DataStream { - return "stream" in object; -} From 17275ed29d2d6aceb2955efc3b62f9fc9e94ec19 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Sat, 27 Apr 2024 17:49:47 +0530 Subject: [PATCH 034/240] Inline --- .../photos/src/components/Upload/Uploader.tsx | 117 ++++++++++++++-- .../photos/src/components/WatchFolder.tsx | 12 +- web/apps/photos/src/constants/upload.ts | 6 - .../src/services/deduplicationService.ts | 2 +- .../services/upload/publicUploadHttpClient.ts | 2 +- .../src/services/upload/uploadHttpClient.ts | 30 +++- .../src/services/upload/uploadService.ts | 9 +- web/apps/photos/src/services/watch.ts | 8 +- web/apps/photos/src/utils/upload/index.ts | 128 ------------------ .../photos/src/utils/upload/uploadRetrier.ts | 29 ---- web/packages/media/file.ts | 4 + 11 files changed, 163 insertions(+), 184 deletions(-) delete mode 100644 web/apps/photos/src/utils/upload/index.ts delete mode 100644 web/apps/photos/src/utils/upload/uploadRetrier.ts create mode 100644 web/packages/media/file.ts diff --git a/web/apps/photos/src/components/Upload/Uploader.tsx b/web/apps/photos/src/components/Upload/Uploader.tsx index 9af14c5054..8eb2a736ad 100644 --- a/web/apps/photos/src/components/Upload/Uploader.tsx +++ b/web/apps/photos/src/components/Upload/Uploader.tsx @@ -5,7 +5,7 @@ import { CustomError } from "@ente/shared/error"; import { isPromise } from "@ente/shared/utils"; import DiscFullIcon from "@mui/icons-material/DiscFull"; import UserNameInputDialog from "components/UserNameInputDialog"; -import { PICKED_UPLOAD_TYPE, UPLOAD_STAGES } from "constants/upload"; +import { UPLOAD_STAGES } from "constants/upload"; import { t } from "i18next"; import isElectron from "is-electron"; import { AppContext } from "pages/_app"; @@ -13,6 +13,7 @@ import { GalleryContext } from "pages/gallery"; import { useContext, useEffect, useRef, useState } from "react"; import billingService from "services/billingService"; import { getLatestCollections } from "services/collectionService"; +import { exportMetadataDirectoryName } from "services/export"; import { getPublicCollectionUID, getPublicCollectionUploaderName, @@ -28,6 +29,7 @@ import type { import uploadManager, { setToUploadCollection, } from "services/upload/uploadManager"; +import { fopFileName } from "services/upload/uploadService"; import watcher from "services/watch"; import { NotificationAttributes } from "types/Notification"; import { Collection } from "types/collection"; @@ -45,13 +47,6 @@ import { getDownloadAppMessage, getRootLevelFileWithFolderNotAllowMessage, } from "utils/ui"; -import { - DEFAULT_IMPORT_SUGGESTION, - getImportSuggestion, - groupFilesBasedOnParentFolder, - pruneHiddenFiles, - type ImportSuggestion, -} from "utils/upload"; import { SetCollectionNamerAttributes } from "../Collections/CollectionNamer"; import { CollectionMappingChoiceModal } from "./CollectionMappingChoiceModal"; import UploadProgress from "./UploadProgress"; @@ -59,6 +54,12 @@ import UploadTypeSelector from "./UploadTypeSelector"; const FIRST_ALBUM_NAME = "My First Album"; +enum PICKED_UPLOAD_TYPE { + FILES = "files", + FOLDERS = "folders", + ZIPS = "zips", +} + interface Props { syncWithRemote: (force?: boolean, silent?: boolean) => Promise; closeCollectionSelector?: () => void; @@ -876,3 +877,103 @@ async function waitAndRun( } await task(); } + +// This is used to prompt the user the make upload strategy choice +interface ImportSuggestion { + rootFolderName: string; + hasNestedFolders: boolean; + hasRootLevelFileWithFolder: boolean; +} + +const DEFAULT_IMPORT_SUGGESTION: ImportSuggestion = { + rootFolderName: "", + hasNestedFolders: false, + hasRootLevelFileWithFolder: false, +}; + +function getImportSuggestion( + uploadType: PICKED_UPLOAD_TYPE, + paths: string[], +): ImportSuggestion { + if (isElectron() && uploadType === PICKED_UPLOAD_TYPE.FILES) { + return DEFAULT_IMPORT_SUGGESTION; + } + + const getCharCount = (str: string) => (str.match(/\//g) ?? []).length; + paths.sort((path1, path2) => getCharCount(path1) - getCharCount(path2)); + const firstPath = paths[0]; + const lastPath = paths[paths.length - 1]; + + const L = firstPath.length; + let i = 0; + const firstFileFolder = firstPath.substring(0, firstPath.lastIndexOf("/")); + const lastFileFolder = lastPath.substring(0, lastPath.lastIndexOf("/")); + + while (i < L && firstPath.charAt(i) === lastPath.charAt(i)) i++; + let commonPathPrefix = firstPath.substring(0, i); + + if (commonPathPrefix) { + commonPathPrefix = commonPathPrefix.substring( + 0, + commonPathPrefix.lastIndexOf("/"), + ); + if (commonPathPrefix) { + commonPathPrefix = commonPathPrefix.substring( + commonPathPrefix.lastIndexOf("/") + 1, + ); + } + } + return { + rootFolderName: commonPathPrefix || null, + hasNestedFolders: firstFileFolder !== lastFileFolder, + hasRootLevelFileWithFolder: firstFileFolder === "", + }; +} + +// This function groups files that are that have the same parent folder into collections +// For Example, for user files have a directory structure like this +// a +// / | \ +// b j c +// /|\ / \ +// e f g h i +// +// The files will grouped into 3 collections. +// [a => [j], +// b => [e,f,g], +// c => [h, i]] +const groupFilesBasedOnParentFolder = (fileOrPaths: (File | string)[]) => { + const result = new Map(); + for (const fileOrPath of fileOrPaths) { + const filePath = + /* TODO(MR): ElectronFile */ + typeof fileOrPath == "string" + ? fileOrPath + : (fileOrPath["path"] as string); + + let folderPath = filePath.substring(0, filePath.lastIndexOf("/")); + // If the parent folder of a file is "metadata" + // we consider it to be part of the parent folder + // For Eg,For FileList -> [a/x.png, a/metadata/x.png.json] + // they will both we grouped into the collection "a" + // This is cluster the metadata json files in the same collection as the file it is for + if (folderPath.endsWith(exportMetadataDirectoryName)) { + folderPath = folderPath.substring(0, folderPath.lastIndexOf("/")); + } + const folderName = folderPath.substring( + folderPath.lastIndexOf("/") + 1, + ); + if (!folderName) throw Error("Unexpected empty folder name"); + if (!result.has(folderName)) result.set(folderName, []); + result.get(folderName).push(fileOrPath); + } + return result; +}; + +/** + * Filter out hidden files from amongst {@link fileOrPaths}. + * + * Hidden files are those whose names begin with a "." (dot). + */ +const pruneHiddenFiles = (fileOrPaths: (File | string)[]) => + fileOrPaths.filter((f) => !fopFileName(f).startsWith(".")); diff --git a/web/apps/photos/src/components/WatchFolder.tsx b/web/apps/photos/src/components/WatchFolder.tsx index 738bafde8a..710a541683 100644 --- a/web/apps/photos/src/components/WatchFolder.tsx +++ b/web/apps/photos/src/components/WatchFolder.tsx @@ -1,5 +1,5 @@ import { ensureElectron } from "@/next/electron"; -import { basename } from "@/next/file"; +import { basename, dirname } from "@/next/file"; import type { CollectionMapping, FolderWatch } from "@/next/types/ipc"; import { ensure } from "@/utils/ensure"; import { @@ -32,7 +32,6 @@ import { t } from "i18next"; import { AppContext } from "pages/_app"; import React, { useContext, useEffect, useState } from "react"; import watcher from "services/watch"; -import { areAllInSameDirectory } from "utils/upload"; interface WatchFolderProps { open: boolean; @@ -324,3 +323,12 @@ const EntryOptions: React.FC = ({ confirmStopWatching }) => { ); }; + +/** + * Return true if all the paths in the given list are items that belong to the + * same (arbitrary) directory. + * + * Empty list of paths is considered to be in the same directory. + */ +const areAllInSameDirectory = (paths: string[]) => + new Set(paths.map(dirname)).size == 1; diff --git a/web/apps/photos/src/constants/upload.ts b/web/apps/photos/src/constants/upload.ts index 13dee9cf49..a0103cb6e6 100644 --- a/web/apps/photos/src/constants/upload.ts +++ b/web/apps/photos/src/constants/upload.ts @@ -24,9 +24,3 @@ export enum UPLOAD_RESULT { UPLOADED_WITH_STATIC_THUMBNAIL, ADDED_SYMLINK, } - -export enum PICKED_UPLOAD_TYPE { - FILES = "files", - FOLDERS = "folders", - ZIPS = "zips", -} diff --git a/web/apps/photos/src/services/deduplicationService.ts b/web/apps/photos/src/services/deduplicationService.ts index 19c953de8c..1683e554c4 100644 --- a/web/apps/photos/src/services/deduplicationService.ts +++ b/web/apps/photos/src/services/deduplicationService.ts @@ -1,3 +1,4 @@ +import { hasFileHash } from "@/media/file"; import { FILE_TYPE } from "@/media/file-type"; import type { Metadata } from "@/media/types/file"; import log from "@/next/log"; @@ -5,7 +6,6 @@ import HTTPService from "@ente/shared/network/HTTPService"; import { getEndpoint } from "@ente/shared/network/api"; import { getToken } from "@ente/shared/storage/localStorage/helpers"; import { EnteFile } from "types/file"; -import { hasFileHash } from "utils/upload"; const ENDPOINT = getEndpoint(); diff --git a/web/apps/photos/src/services/upload/publicUploadHttpClient.ts b/web/apps/photos/src/services/upload/publicUploadHttpClient.ts index 12228b822f..8f18a1638b 100644 --- a/web/apps/photos/src/services/upload/publicUploadHttpClient.ts +++ b/web/apps/photos/src/services/upload/publicUploadHttpClient.ts @@ -3,7 +3,7 @@ import { CustomError, handleUploadError } from "@ente/shared/error"; import HTTPService from "@ente/shared/network/HTTPService"; import { getEndpoint } from "@ente/shared/network/api"; import { EnteFile } from "types/file"; -import { retryHTTPCall } from "utils/upload/uploadRetrier"; +import { retryHTTPCall } from "./uploadHttpClient"; import { MultipartUploadURLs, UploadFile, UploadURL } from "./uploadService"; const ENDPOINT = getEndpoint(); diff --git a/web/apps/photos/src/services/upload/uploadHttpClient.ts b/web/apps/photos/src/services/upload/uploadHttpClient.ts index 5757a841ad..e8ae6de977 100644 --- a/web/apps/photos/src/services/upload/uploadHttpClient.ts +++ b/web/apps/photos/src/services/upload/uploadHttpClient.ts @@ -3,8 +3,8 @@ import { CustomError, handleUploadError } from "@ente/shared/error"; import HTTPService from "@ente/shared/network/HTTPService"; import { getEndpoint, getUploadEndpoint } from "@ente/shared/network/api"; import { getToken } from "@ente/shared/storage/localStorage/helpers"; +import { wait } from "@ente/shared/utils"; import { EnteFile } from "types/file"; -import { retryHTTPCall } from "utils/upload/uploadRetrier"; import { MultipartUploadURLs, UploadFile, UploadURL } from "./uploadService"; const ENDPOINT = getEndpoint(); @@ -236,3 +236,31 @@ class UploadHttpClient { } export default new UploadHttpClient(); + +const retrySleepTimeInMilliSeconds = [2000, 5000, 10000]; + +export async function retryHTTPCall( + func: () => Promise, + checkForBreakingError?: (error) => void, +): Promise { + const retrier = async ( + func: () => Promise, + attemptNumber: number = 0, + ) => { + try { + const resp = await func(); + return resp; + } catch (e) { + if (checkForBreakingError) { + checkForBreakingError(e); + } + if (attemptNumber < retrySleepTimeInMilliSeconds.length) { + await wait(retrySleepTimeInMilliSeconds[attemptNumber]); + return await retrier(func, attemptNumber + 1); + } else { + throw e; + } + } + }; + return await retrier(func); +} diff --git a/web/apps/photos/src/services/upload/uploadService.ts b/web/apps/photos/src/services/upload/uploadService.ts index a9edfc7837..13aaf05b1f 100644 --- a/web/apps/photos/src/services/upload/uploadService.ts +++ b/web/apps/photos/src/services/upload/uploadService.ts @@ -1,3 +1,4 @@ +import { hasFileHash } from "@/media/file"; import { FILE_TYPE, type FileTypeInfo } from "@/media/file-type"; import { encodeLivePhoto } from "@/media/live-photo"; import type { Metadata } from "@/media/types/file"; @@ -8,13 +9,8 @@ import { CustomErrorMessage } from "@/next/types/ipc"; import { ensure } from "@/utils/ensure"; import { ENCRYPTION_CHUNK_SIZE } from "@ente/shared/crypto/constants"; import { DedicatedCryptoWorker } from "@ente/shared/crypto/internal/crypto.worker"; -import { - B64EncryptionResult, - EncryptionResult, - LocalFileAttributes, -} from "@ente/shared/crypto/types"; +import { B64EncryptionResult } from "@ente/shared/crypto/types"; import { CustomError, handleUploadError } from "@ente/shared/error"; -import { isDataStream, type DataStream } from "@ente/shared/utils/data-stream"; import { Remote } from "comlink"; import { NULL_LOCATION, @@ -43,7 +39,6 @@ import { updateMagicMetadata, } from "utils/magicMetadata"; import { readStream } from "utils/native-stream"; -import { hasFileHash } from "utils/upload"; import * as convert from "xml-js"; import { detectFileTypeInfoFromChunk } from "../detect-type"; import { getFileStream } from "../readerService"; diff --git a/web/apps/photos/src/services/watch.ts b/web/apps/photos/src/services/watch.ts index fedf4bf403..4de5881aa8 100644 --- a/web/apps/photos/src/services/watch.ts +++ b/web/apps/photos/src/services/watch.ts @@ -20,7 +20,6 @@ import uploadManager, { import { Collection } from "types/collection"; import { EncryptedEnteFile } from "types/file"; import { groupFilesBasedOnCollectionID } from "utils/file"; -import { isHiddenFile } from "utils/upload"; import { removeFromCollection } from "./collectionService"; import { getLocalFiles } from "./fileService"; @@ -596,6 +595,13 @@ const pathsToUpload = (paths: string[], watch: FolderWatch) => // Files that are on disk but not yet synced or ignored. .filter((path) => !isSyncedOrIgnoredPath(path, watch)); +/** + * Return true if the file at the given {@link path} is hidden. + * + * Hidden files are those whose names begin with a "." (dot). + */ +const isHiddenFile = (path: string) => basename(path).startsWith("."); + /** * Return the paths to previously synced files that are no longer on disk and so * must be removed from the Ente collection. diff --git a/web/apps/photos/src/utils/upload/index.ts b/web/apps/photos/src/utils/upload/index.ts deleted file mode 100644 index 7f81408d69..0000000000 --- a/web/apps/photos/src/utils/upload/index.ts +++ /dev/null @@ -1,128 +0,0 @@ -import type { Metadata } from "@/media/types/file"; -import { basename, dirname } from "@/next/file"; -import { PICKED_UPLOAD_TYPE } from "constants/upload"; -import isElectron from "is-electron"; -import { exportMetadataDirectoryName } from "services/export"; -import { fopFileName } from "services/upload/uploadService"; - -export const hasFileHash = (file: Metadata) => - file.hash || (file.imageHash && file.videoHash); - -/** - * Return true if all the paths in the given list are items that belong to the - * same (arbitrary) directory. - * - * Empty list of paths is considered to be in the same directory. - */ -export const areAllInSameDirectory = (paths: string[]) => - new Set(paths.map(dirname)).size == 1; - -// This is used to prompt the user the make upload strategy choice -export interface ImportSuggestion { - rootFolderName: string; - hasNestedFolders: boolean; - hasRootLevelFileWithFolder: boolean; -} - -export const DEFAULT_IMPORT_SUGGESTION: ImportSuggestion = { - rootFolderName: "", - hasNestedFolders: false, - hasRootLevelFileWithFolder: false, -}; - -export function getImportSuggestion( - uploadType: PICKED_UPLOAD_TYPE, - paths: string[], -): ImportSuggestion { - if (isElectron() && uploadType === PICKED_UPLOAD_TYPE.FILES) { - return DEFAULT_IMPORT_SUGGESTION; - } - - const getCharCount = (str: string) => (str.match(/\//g) ?? []).length; - paths.sort((path1, path2) => getCharCount(path1) - getCharCount(path2)); - const firstPath = paths[0]; - const lastPath = paths[paths.length - 1]; - - const L = firstPath.length; - let i = 0; - const firstFileFolder = firstPath.substring(0, firstPath.lastIndexOf("/")); - const lastFileFolder = lastPath.substring(0, lastPath.lastIndexOf("/")); - - while (i < L && firstPath.charAt(i) === lastPath.charAt(i)) i++; - let commonPathPrefix = firstPath.substring(0, i); - - if (commonPathPrefix) { - commonPathPrefix = commonPathPrefix.substring( - 0, - commonPathPrefix.lastIndexOf("/"), - ); - if (commonPathPrefix) { - commonPathPrefix = commonPathPrefix.substring( - commonPathPrefix.lastIndexOf("/") + 1, - ); - } - } - return { - rootFolderName: commonPathPrefix || null, - hasNestedFolders: firstFileFolder !== lastFileFolder, - hasRootLevelFileWithFolder: firstFileFolder === "", - }; -} - -// This function groups files that are that have the same parent folder into collections -// For Example, for user files have a directory structure like this -// a -// / | \ -// b j c -// /|\ / \ -// e f g h i -// -// The files will grouped into 3 collections. -// [a => [j], -// b => [e,f,g], -// c => [h, i]] -export const groupFilesBasedOnParentFolder = ( - fileOrPaths: (File | string)[], -) => { - const result = new Map(); - for (const fileOrPath of fileOrPaths) { - const filePath = - /* TODO(MR): ElectronFile */ - typeof fileOrPath == "string" - ? fileOrPath - : (fileOrPath["path"] as string); - - let folderPath = filePath.substring(0, filePath.lastIndexOf("/")); - // If the parent folder of a file is "metadata" - // we consider it to be part of the parent folder - // For Eg,For FileList -> [a/x.png, a/metadata/x.png.json] - // they will both we grouped into the collection "a" - // This is cluster the metadata json files in the same collection as the file it is for - if (folderPath.endsWith(exportMetadataDirectoryName)) { - folderPath = folderPath.substring(0, folderPath.lastIndexOf("/")); - } - const folderName = folderPath.substring( - folderPath.lastIndexOf("/") + 1, - ); - if (!folderName) throw Error("Unexpected empty folder name"); - if (!result.has(folderName)) result.set(folderName, []); - result.get(folderName).push(fileOrPath); - } - return result; -}; - -/** - * Filter out hidden files from amongst {@link fileOrPaths}. - * - * Hidden files are those whose names begin with a "." (dot). - */ - -export const pruneHiddenFiles = (fileOrPaths: (File | string)[]) => - fileOrPaths.filter((f) => !fopFileName(f).startsWith(".")); - -/** - * Return true if the file at the given {@link path} is hidden. - * - * Hidden files are those whose names begin with a "." (dot). - */ -export const isHiddenFile = (path: string) => basename(path).startsWith("."); diff --git a/web/apps/photos/src/utils/upload/uploadRetrier.ts b/web/apps/photos/src/utils/upload/uploadRetrier.ts deleted file mode 100644 index ca2764f3f1..0000000000 --- a/web/apps/photos/src/utils/upload/uploadRetrier.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { wait } from "@ente/shared/utils"; - -const retrySleepTimeInMilliSeconds = [2000, 5000, 10000]; - -export async function retryHTTPCall( - func: () => Promise, - checkForBreakingError?: (error) => void, -): Promise { - const retrier = async ( - func: () => Promise, - attemptNumber: number = 0, - ) => { - try { - const resp = await func(); - return resp; - } catch (e) { - if (checkForBreakingError) { - checkForBreakingError(e); - } - if (attemptNumber < retrySleepTimeInMilliSeconds.length) { - await wait(retrySleepTimeInMilliSeconds[attemptNumber]); - return await retrier(func, attemptNumber + 1); - } else { - throw e; - } - } - }; - return await retrier(func); -} diff --git a/web/packages/media/file.ts b/web/packages/media/file.ts new file mode 100644 index 0000000000..c840500498 --- /dev/null +++ b/web/packages/media/file.ts @@ -0,0 +1,4 @@ +import type { Metadata } from "./types/file"; + +export const hasFileHash = (file: Metadata) => + !!file.hash || (!!file.imageHash && !!file.videoHash); From 7836562ff1062a75c2c5f3d0d60fbe53c4982367 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Sat, 27 Apr 2024 20:21:54 +0530 Subject: [PATCH 035/240] less noise --- desktop/src/main/utils-electron.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/desktop/src/main/utils-electron.ts b/desktop/src/main/utils-electron.ts index b997d738e3..e8a98f1dfe 100644 --- a/desktop/src/main/utils-electron.ts +++ b/desktop/src/main/utils-electron.ts @@ -33,11 +33,9 @@ export const execAsync = (command: string | string[]) => { ? shellescape(command) : command; const startTime = Date.now(); - log.debug(() => `Running shell command: ${escapedCommand}`); const result = execAsync_(escapedCommand); log.debug( - () => - `Completed in ${Math.round(Date.now() - startTime)} ms (${escapedCommand})`, + () => `${escapedCommand} (${Math.round(Date.now() - startTime)} ms)`, ); return result; }; From 9a37141d291cbcccfc73476ff44807f0453f6820 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Sat, 27 Apr 2024 20:38:49 +0530 Subject: [PATCH 036/240] Not yet sure how but desktop drag drops have the full path --- .../src/services/upload/uploadService.ts | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/web/apps/photos/src/services/upload/uploadService.ts b/web/apps/photos/src/services/upload/uploadService.ts index 13aaf05b1f..fd18ab4ddf 100644 --- a/web/apps/photos/src/services/upload/uploadService.ts +++ b/web/apps/photos/src/services/upload/uploadService.ts @@ -431,9 +431,17 @@ export const uploader = async ( * * The file can be either a web * [File](https://developer.mozilla.org/en-US/docs/Web/API/File) or the absolute - * path to a file on desk. When and why, read on. + * path to a file on desk. * - * This code gets invoked in two contexts: + * tl;dr; There are three cases: + * + * 1. web / File + * 2. desktop / File + * 3. desktop / path + * + * For the when and why, read on. + * + * The code that accesses files (e.g. uplaads) gets invoked in two contexts: * * 1. web: the normal mode, when we're running in as a web app in the browser. * @@ -450,27 +458,31 @@ export const uploader = async ( * * In the desktop context, this can be either a File or a path. * - * 1. If the user provided us this file via some user interaction (say a drag + * 2. If the user provided us this file via some user interaction (say a drag * and a drop), this'll still be a File. * - * 2. However, when running in the desktop app we have the ability to access + * 3. However, when running in the desktop app we have the ability to access * absolute paths on the user's file system. For example, if the user asks us * to watch certain folders on their disk for changes, we'll be able to pick * up new images being added, and in such cases, the parameter here will be a * path. Another example is when resuming an previously interrupted upload - * we'll only have the path at hand in such cases, not the File object. * + * Case 2, when we're provided a path, is simple. We don't have a choice, since + * we cannot still programmatically construct a File object (we can construct it + * on the Node.js layer, but it can't then be transferred over the IPC + * boundary). So all our operations use the path itself. + * + * Case 3 involves a choice on a use-case basis, since (a) such File objects + * also have the full path (unlike the bona-fide web File objects that only have + * a relative path), and (b) neither File nor the path is a better choice that + * would be for all scenarios. + * * The advantage of the File object is that the browser has already read it into * memory for us. The disadvantage comes in the case where we need to * communicate with the native Node.js layer of our desktop app. Since this * communication happens over IPC, the File's contents need to be serialized and * copied, which is a bummer for large videos etc. - * - * So when we do have a path, we first try to see if we can perform IPC using - * the path itself (e.g. when generating thumbnails). Eventually, we'll need to - * read the file once when we need to encrypt and upload it, but if we're smart - * we can do all the rest of the IPC operations using the path itself, and for - * the read during upload using a streaming IPC mechanism. */ const readFileOrPath = async ( fileOrPath: File | string, @@ -1040,7 +1052,7 @@ const withThumbnail = async ( } else { // There isn't a normal scenario where this should happen. // Case 1, should've already worked, and the only known - // reason it'd have been skipped is for image files on + // reason it'd have been skipped is for image files on // Windows, but those should be less than 100 MB. // // So don't risk running out of memory for a case we don't From 85d0a72df3061223a0c3ca48f0714f7435b4fe00 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Sun, 28 Apr 2024 11:11:15 +0530 Subject: [PATCH 037/240] Rework the stream handling --- web/apps/photos/src/services/readerService.ts | 42 -- .../src/services/upload/uploadService.ts | 379 +++++++++--------- web/packages/media/live-photo.ts | 18 +- web/packages/shared/hooks/useFileInput.tsx | 14 + 4 files changed, 224 insertions(+), 229 deletions(-) delete mode 100644 web/apps/photos/src/services/readerService.ts diff --git a/web/apps/photos/src/services/readerService.ts b/web/apps/photos/src/services/readerService.ts deleted file mode 100644 index a1195b35d1..0000000000 --- a/web/apps/photos/src/services/readerService.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { ElectronFile } from "@/next/types/file"; - -export function getFileStream(file: File, chunkSize: number) { - const fileChunkReader = fileChunkReaderMaker(file, chunkSize); - - const stream = new ReadableStream({ - async pull(controller: ReadableStreamDefaultController) { - const chunk = await fileChunkReader.next(); - if (chunk.done) { - controller.close(); - } else { - controller.enqueue(chunk.value); - } - }, - }); - const chunkCount = Math.ceil(file.size / chunkSize); - return { - stream, - chunkCount, - }; -} - -async function* fileChunkReaderMaker(file: File, chunkSize: number) { - let offset = 0; - while (offset < file.size) { - const chunk = file.slice(offset, chunkSize + offset); - yield new Uint8Array(await chunk.arrayBuffer()); - offset += chunkSize; - } - return null; -} - -export async function getElectronFileStream( - file: ElectronFile, - chunkSize: number, -) { - const chunkCount = Math.ceil(file.size / chunkSize); - return { - stream: await file.stream(), - chunkCount, - }; -} diff --git a/web/apps/photos/src/services/upload/uploadService.ts b/web/apps/photos/src/services/upload/uploadService.ts index fd18ab4ddf..d49b32129f 100644 --- a/web/apps/photos/src/services/upload/uploadService.ts +++ b/web/apps/photos/src/services/upload/uploadService.ts @@ -41,7 +41,6 @@ import { import { readStream } from "utils/native-stream"; import * as convert from "xml-js"; import { detectFileTypeInfoFromChunk } from "../detect-type"; -import { getFileStream } from "../readerService"; import { tryParseEpochMicrosecondsFromFileName } from "./date"; import publicUploadHttpClient from "./publicUploadHttpClient"; import type { ParsedMetadataJSON } from "./takeout"; @@ -54,36 +53,50 @@ import { import UploadHttpClient from "./uploadHttpClient"; import type { UploadableFile } from "./uploadManager"; -/** Allow up to 5 ENCRYPTION_CHUNK_SIZE chunks in an upload part */ -const maximumChunksPerUploadPart = 5; +/** + * A readable stream for a file, and its associated size and last modified time. + * + * This is the in-memory representation of the `fileOrPath` type that we usually + * pass around. See: [Note: Reading a fileOrPath] + */ +interface FileStream { + /** + * A stream of the file's contents + * + * This stream is guaranteed to emit data in ENCRYPTION_CHUNK_SIZE chunks + * (except the last chunk which can be smaller since a file would rarely + * align exactly to a ENCRYPTION_CHUNK_SIZE multiple). + * + * Note: A stream can only be read once! + */ + stream: ReadableStream; + /** + * Number of chunks {@link stream} will emit, each ENCRYPTION_CHUNK_SIZE + * sized (except the last one). + */ + chunkCount: number; + /** + * The size in bytes of the underlying file. + */ + fileSize: number; + /** + * The modification time of the file, in epoch milliseconds. + */ + lastModifiedMs: number; + /** + * Set to the underlying {@link File} when we also have access to it. + */ + file?: File; +} /** - * The chunk size of the un-encrypted file which is read and encrypted before - * uploading it as a single part of a multipart upload. + * If the stream we have is more than 5 ENCRYPTION_CHUNK_SIZE chunks, then use + * multipart uploads for it, with each multipart-part containing 5 chunks. * - * ENCRYPTION_CHUNK_SIZE is 4 MB, and maximum number of chunks in a single - * upload part is 5, so this is 20 MB. - * */ -const multipartPartSize = ENCRYPTION_CHUNK_SIZE * maximumChunksPerUploadPart; - -interface DataStream { - stream: ReadableStream; - chunkCount: number; -} - -function isDataStream(object: any): object is DataStream { - return "stream" in object; -} - -interface LocalFileAttributes { - encryptedData: T; - decryptionHeader: string; -} - -interface EncryptionResult { - file: LocalFileAttributes; - key: string; -} + * ENCRYPTION_CHUNK_SIZE is 4 MB, and the number of chunks in a single upload + * part is 5, so each part is (up to) 20 MB. + */ +const multipartChunksPerPart = 5; /** Upload files to cloud storage */ class UploadService { @@ -189,14 +202,14 @@ export const fopSize = async (fileOrPath: File | string): Promise => /* -- Various intermediate type used during upload -- */ -interface UploadAsset2 { +interface UploadAsset { isLivePhoto?: boolean; fileOrPath?: File | string; livePhotoAssets?: LivePhotoAssets; } -interface FileInMemory { - filedata: Uint8Array | DataStream; +interface ThumbnailedFile { + fileStreamOrData: FileStream | Uint8Array; /** The JPEG data of the generated thumbnail */ thumbnail: Uint8Array; /** @@ -206,7 +219,7 @@ interface FileInMemory { hasStaticThumbnail: boolean; } -interface FileWithMetadata extends Omit { +interface FileWithMetadata extends Omit { metadata: Metadata; localID: number; pubMagicMetadata: FilePublicMagicMetadata; @@ -217,8 +230,38 @@ interface EncryptedFile { fileKey: B64EncryptionResult; } +interface EncryptedFileStream { + /** + * A stream of the file's encrypted contents + * + * This stream is guaranteed to emit data in ENCRYPTION_CHUNK_SIZE chunks + * (except the last chunk which can be smaller since a file would rarely + * align exactly to a ENCRYPTION_CHUNK_SIZE multiple). + */ + stream: ReadableStream; + /** + * Number of chunks {@link stream} will emit, each ENCRYPTION_CHUNK_SIZE + * sized (except the last one). + */ + chunkCount: number; +} + +interface LocalFileAttributes< + T extends string | Uint8Array | EncryptedFileStream, +> { + encryptedData: T; + decryptionHeader: string; +} + +interface EncryptionResult< + T extends string | Uint8Array | EncryptedFileStream, +> { + file: LocalFileAttributes; + key: string; +} + interface ProcessedFile { - file: LocalFileAttributes; + file: LocalFileAttributes; thumbnail: LocalFileAttributes; metadata: LocalFileAttributes; pubMagicMetadata: EncryptedMagicMetadata; @@ -349,10 +392,8 @@ export const uploader = async ( abortIfCancelled(); - const { filedata, thumbnail, hasStaticThumbnail } = await readAsset( - fileTypeInfo, - uploadAsset, - ); + const { fileStreamOrData, thumbnail, hasStaticThumbnail } = + await readAsset(fileTypeInfo, uploadAsset); if (hasStaticThumbnail) metadata.hasStaticThumbnail = true; @@ -365,7 +406,7 @@ export const uploader = async ( const fileWithMetadata: FileWithMetadata = { localID, - filedata, + fileStreamOrData, thumbnail, metadata, pubMagicMetadata, @@ -427,7 +468,7 @@ export const uploader = async ( /** * Read the given file or path into an in-memory representation. * - * See: [Note: Reading a fileOrPath] + * [Note: Reading a fileOrPath] * * The file can be either a web * [File](https://developer.mozilla.org/en-US/docs/Web/API/File) or the absolute @@ -473,10 +514,12 @@ export const uploader = async ( * on the Node.js layer, but it can't then be transferred over the IPC * boundary). So all our operations use the path itself. * - * Case 3 involves a choice on a use-case basis, since (a) such File objects - * also have the full path (unlike the bona-fide web File objects that only have - * a relative path), and (b) neither File nor the path is a better choice that - * would be for all scenarios. + * Case 3 involves a choice on a use-case basis, since + * + * (a) unlike in the web context, such File objects also have the full path. + * See: [Note: File paths when running under Electron]. + * + * (b) neither File nor the path is a better choice for all use cases. * * The advantage of the File object is that the browser has already read it into * memory for us. The disadvantage comes in the case where we need to @@ -486,23 +529,17 @@ export const uploader = async ( */ const readFileOrPath = async ( fileOrPath: File | string, -): Promise<{ - dataOrStream: Uint8Array | DataStream; - fileSize: number; - lastModifiedMs: number; -}> => { - let dataOrStream: Uint8Array | DataStream; +): Promise => { + let underlyingStream: ReadableStream; + let file: File | undefined; let fileSize: number; let lastModifiedMs: number; if (fileOrPath instanceof File) { - const file = fileOrPath; + file = fileOrPath; + underlyingStream = file.stream(); fileSize = file.size; lastModifiedMs = file.lastModified; - dataOrStream = - fileSize > multipartPartSize - ? getFileStream(file, ENCRYPTION_CHUNK_SIZE) - : new Uint8Array(await file.arrayBuffer()); } else { const path = fileOrPath; const { @@ -510,38 +547,13 @@ const readFileOrPath = async ( size, lastModifiedMs: lm, } = await readStream(ensureElectron(), path); + underlyingStream = response.body; fileSize = size; lastModifiedMs = lm; - if (size > multipartPartSize) { - const chunkCount = Math.ceil(size / ENCRYPTION_CHUNK_SIZE); - dataOrStream = { stream: response.body, chunkCount }; - } else { - dataOrStream = new Uint8Array(await response.arrayBuffer()); - } } - return { dataOrStream, fileSize, lastModifiedMs }; -}; - -/** A variant of {@readFileOrPath} that always returns an {@link DataStream}. */ -const readFileOrPathStream = async ( - fileOrPath: File | string, -): Promise => { const N = ENCRYPTION_CHUNK_SIZE; - - let underlyingStream: ReadableStream; - let chunkCount: number; - - if (fileOrPath instanceof File) { - const file = fileOrPath; - underlyingStream = file.stream(); - chunkCount = Math.ceil(file.size / N); - } else { - const path = fileOrPath; - const { response, size } = await readStream(ensureElectron(), path); - underlyingStream = response.body; - chunkCount = Math.ceil(size / N); - } + const chunkCount = Math.ceil(fileSize / ENCRYPTION_CHUNK_SIZE); // Pipe the underlying stream through a transformer that emits // ENCRYPTION_CHUNK_SIZE-ed chunks (except the last one, which can be @@ -573,7 +585,8 @@ const readFileOrPathStream = async ( }); const stream = underlyingStream.pipeThrough(transformer); - return { stream, chunkCount }; + + return { stream, chunkCount, fileSize, lastModifiedMs, file }; }; interface ReadAssetDetailsResult { @@ -590,7 +603,7 @@ const readAssetDetails = async ({ isLivePhoto, livePhotoAssets, fileOrPath, -}: UploadAsset2): Promise => +}: UploadAsset): Promise => isLivePhoto ? readLivePhotoDetails(livePhotoAssets) : readImageOrVideoDetails(fileOrPath); @@ -622,18 +635,14 @@ const readLivePhotoDetails = async ({ image, video }: LivePhotoAssets) => { * @param fileOrPath See: [Note: Reading a fileOrPath] */ const readImageOrVideoDetails = async (fileOrPath: File | string) => { - const { dataOrStream, fileSize, lastModifiedMs } = + const { stream, fileSize, lastModifiedMs } = await readFileOrPath(fileOrPath); const fileTypeInfo = await detectFileTypeInfoFromChunk(async () => { - if (dataOrStream instanceof Uint8Array) { - return dataOrStream; - } else { - const reader = dataOrStream.stream.getReader(); - const chunk = ensure((await reader.read()).value); - await reader.cancel(); - return chunk; - } + const reader = stream.getReader(); + const chunk = ensure((await reader.read()).value); + await reader.cancel(); + return chunk; }, fopFileName(fileOrPath)); return { fileTypeInfo, fileSize, lastModifiedMs }; @@ -660,7 +669,7 @@ interface ExtractAssetMetadataResult { * {@link parsedMetadataJSONMap} for the assets. Return the resultant metadatum. */ const extractAssetMetadata = async ( - { isLivePhoto, fileOrPath, livePhotoAssets }: UploadAsset2, + { isLivePhoto, fileOrPath, livePhotoAssets }: UploadAsset, fileTypeInfo: FileTypeInfo, lastModifiedMs: number, collectionID: number, @@ -832,7 +841,7 @@ const computeHash = async ( fileOrPath: File | string, worker: Remote, ) => { - const { stream, chunkCount } = await readFileOrPathStream(fileOrPath); + const { stream, chunkCount } = await readFileOrPath(fileOrPath); const hashState = await worker.initChunkHashing(); const streamReader = stream.getReader(); @@ -901,8 +910,8 @@ const areFilesSameNoHash = (f: Metadata, g: Metadata) => { const readAsset = async ( fileTypeInfo: FileTypeInfo, - { isLivePhoto, fileOrPath, livePhotoAssets }: UploadAsset2, -) => + { isLivePhoto, fileOrPath, livePhotoAssets }: UploadAsset, +): Promise => isLivePhoto ? await readLivePhoto(livePhotoAssets, fileTypeInfo) : await readImageOrVideo(fileOrPath, fileTypeInfo); @@ -911,9 +920,8 @@ const readLivePhoto = async ( livePhotoAssets: LivePhotoAssets, fileTypeInfo: FileTypeInfo, ) => { - const readImage = await readFileOrPath(livePhotoAssets.image); const { - filedata: imageDataOrStream, + fileStreamOrData: imageFileStreamOrData, thumbnail, hasStaticThumbnail, } = await withThumbnail( @@ -922,28 +930,29 @@ const readLivePhoto = async ( extension: fileTypeInfo.imageType, fileType: FILE_TYPE.IMAGE, }, - readImage.dataOrStream, - readImage.fileSize, + await readFileOrPath(livePhotoAssets.image), ); - const readVideo = await readFileOrPath(livePhotoAssets.video); + const videoFileStreamOrData = await readFileOrPath(livePhotoAssets.video); - // We can revisit this later, but the existing code always read the entire - // file into memory here, and to avoid changing the rest of the scaffolding - // retain the same behaviour. + // The JS zip library that encodeLivePhoto uses does not support + // ReadableStreams, so pass the file (blob) if we have one, otherwise read + // the entire stream into memory and pass the resultant data. // - // This is a reasonable assumption too, since the videos corresponding to - // live photos are only a couple of seconds long. - const toData = async (dataOrStream: Uint8Array | DataStream) => - dataOrStream instanceof Uint8Array - ? dataOrStream - : await readEntireStream(dataOrStream.stream); + // This is a reasonable behaviour since the videos corresponding to live + // photos are only a couple of seconds long (we have already done a + // pre-flight check to ensure their size is small in `areLivePhotoAssets`). + const fileOrData = async (sd: FileStream | Uint8Array) => { + const _fs = async ({ file, stream }: FileStream) => + file ? file : await readEntireStream(stream); + return sd instanceof Uint8Array ? sd : _fs(sd); + }; return { - filedata: await encodeLivePhoto({ + fileStreamOrData: await encodeLivePhoto({ imageFileName: fopFileName(livePhotoAssets.image), - imageData: await toData(imageDataOrStream), + imageFileOrData: await fileOrData(imageFileStreamOrData), videoFileName: fopFileName(livePhotoAssets.video), - videoData: await toData(readVideo.dataOrStream), + videoFileOrData: await fileOrData(videoFileStreamOrData), }), thumbnail, hasStaticThumbnail, @@ -954,8 +963,8 @@ const readImageOrVideo = async ( fileOrPath: File | string, fileTypeInfo: FileTypeInfo, ) => { - const { dataOrStream, fileSize } = await readFileOrPath(fileOrPath); - return withThumbnail(fileOrPath, fileTypeInfo, dataOrStream, fileSize); + const fileStream = await readFileOrPath(fileOrPath); + return withThumbnail(fileOrPath, fileTypeInfo, fileStream); }; // TODO(MR): Merge with the uploader @@ -979,17 +988,17 @@ const moduleState = new ModuleState(); * Augment the given {@link dataOrStream} with thumbnail information. * * This is a companion method for {@link readFileOrPath}, and can be used to - * convert the result of {@link readFileOrPath} into an {@link FileInMemory}. + * convert the result of {@link readFileOrPath} into an {@link ThumbnailedFile}. * - * Note: The returned dataOrStream might be different from the one that we - * provide to it. + * Note: The `fileStream` in the returned ThumbnailedFile may be different from + * the one passed to the function. */ const withThumbnail = async ( fileOrPath: File | string, fileTypeInfo: FileTypeInfo, - dataOrStream: Uint8Array | DataStream, - fileSize: number, -): Promise => { + fileStream: FileStream, +): Promise => { + let fileData: Uint8Array | undefined; let thumbnail: Uint8Array | undefined; let hasStaticThumbnail = false; @@ -998,30 +1007,16 @@ const withThumbnail = async ( fileTypeInfo.fileType == FILE_TYPE.IMAGE && moduleState.isNativeImageThumbnailGenerationNotAvailable; - // 1. Native thumbnail generation. + // 1. Native thumbnail generation using file's path. if (electron && !notAvailable) { try { - if (fileOrPath instanceof File) { - if (dataOrStream instanceof Uint8Array) { - thumbnail = await generateThumbnailNative( - electron, - dataOrStream, - fileTypeInfo, - ); - } else { - // This was large enough to need streaming, and trying to - // read it into memory or copying over IPC might cause us to - // run out of memory. So skip the native generation for it, - // instead let it get processed by the browser based - // thumbnailer (case 2). - } - } else { - thumbnail = await generateThumbnailNative( - electron, - fileOrPath, - fileTypeInfo, - ); - } + // When running in the context of our desktop app, File paths will + // be absolute. See: [Note: File paths when running under Electron]. + thumbnail = await generateThumbnailNative( + electron, + fileOrPath instanceof File ? fileOrPath["path"] : fileOrPath, + fileTypeInfo, + ); } catch (e) { if (e.message == CustomErrorMessage.NotAvailable) { moduleState.isNativeImageThumbnailGenerationNotAvailable = true; @@ -1034,38 +1029,47 @@ const withThumbnail = async ( if (!thumbnail) { let blob: Blob | undefined; if (fileOrPath instanceof File) { - // 2. Browser based thumbnail generation for `File`s. + // 2. Browser based thumbnail generation for File (blobs). blob = fileOrPath; } else { // 3. Browser based thumbnail generation for paths. - if (dataOrStream instanceof Uint8Array) { - blob = new Blob([dataOrStream]); + // + // There are two reasons why we could get here: + // + // - We're running under Electron, but thumbnail generation is not + // available. This is currently only a specific scenario for image + // files on Windows. + // + // - We're running under the Electron, but the thumbnail generation + // otherwise failed for some exception. + // + // The fallback in this case involves reading the entire stream into + // memory, and passing that data across the IPC boundary in a single + // go (i.e. not in a streaming manner). This is risky for videos of + // unbounded sizes, plus that isn't the expected scenario. So + // instead of trying to cater for arbitrary exceptions, we only run + // this fallback to cover for the case where thumbnail generation + // was not available for an image file on Windows. If/when we add + // support of native thumbnailing on Windows too, this entire branch + // can be removed. + + if (fileTypeInfo.fileType == FILE_TYPE.IMAGE) { + const data = await readEntireStream(fileStream.stream); + blob = new Blob([data]); + + // The Readable stream cannot be read twice, so use the data + // directly for subsequent steps. + fileData = data; } else { - // Read the stream into memory. Don't try this fallback for huge - // files though lest we run out of memory. - if (fileSize < 100 * 1024 * 1024 /* 100 MB */) { - const data = await readEntireStream(dataOrStream.stream); - // The Readable stream cannot be read twice, so also - // overwrite the stream with the data we read. - dataOrStream = data; - blob = new Blob([data]); - } else { - // There isn't a normal scenario where this should happen. - // Case 1, should've already worked, and the only known - // reason it'd have been skipped is for image files on - // Windows, but those should be less than 100 MB. - // - // So don't risk running out of memory for a case we don't - // comprehend. - log.error( - `Not using browser based thumbnail generation fallback for large file at path ${fileOrPath}`, - ); - } + log.warn( + `Not using browser based thumbnail generation fallback for video at path ${fileOrPath}`, + ); } } try { - thumbnail = await generateThumbnailWeb(blob, fileTypeInfo); + if (blob) + thumbnail = await generateThumbnailWeb(blob, fileTypeInfo); } catch (e) { log.error("Web thumbnail creation failed", e); } @@ -1077,7 +1081,7 @@ const withThumbnail = async ( } return { - filedata: dataOrStream, + fileStreamOrData: fileData ?? fileStream, thumbnail, hasStaticThumbnail, }; @@ -1102,7 +1106,7 @@ const encryptFile = async ( worker: Remote, ): Promise => { const { key: fileKey, file: encryptedFiledata } = await encryptFiledata( - file.filedata, + file.fileStreamOrData, worker, ); @@ -1144,15 +1148,15 @@ const encryptFile = async ( }; const encryptFiledata = async ( - filedata: Uint8Array | DataStream, + fileStreamOrData: FileStream | Uint8Array, worker: Remote, -): Promise> => - isDataStream(filedata) - ? await encryptFileStream(filedata, worker) - : await worker.encryptFile(filedata); +): Promise> => + fileStreamOrData instanceof Uint8Array + ? await worker.encryptFile(fileStreamOrData) + : await encryptFileStream(fileStreamOrData, worker); const encryptFileStream = async ( - fileData: DataStream, + fileData: FileStream, worker: Remote, ) => { const { stream, chunkCount } = fileData; @@ -1193,27 +1197,38 @@ const uploadToBucket = async ( try { let fileObjectKey: string = null; - if (isDataStream(file.file.encryptedData)) { + const encryptedData = file.file.encryptedData; + if ( + !(encryptedData instanceof Uint8Array) && + encryptedData.chunkCount >= multipartChunksPerPart + ) { + // We have a stream, and it is more than multipartChunksPerPart + // chunks long, so use a multipart upload to upload it. fileObjectKey = await uploadStreamUsingMultipart( file.localID, - file.file.encryptedData, + encryptedData, makeProgessTracker, isCFUploadProxyDisabled, abortIfCancelled, ); } else { + const data = + encryptedData instanceof Uint8Array + ? encryptedData + : await readEntireStream(encryptedData.stream); + const progressTracker = makeProgessTracker(file.localID); const fileUploadURL = await uploadService.getUploadURL(); if (!isCFUploadProxyDisabled) { fileObjectKey = await UploadHttpClient.putFileV2( fileUploadURL, - file.file.encryptedData as Uint8Array, + data, progressTracker, ); } else { fileObjectKey = await UploadHttpClient.putFile( fileUploadURL, - file.file.encryptedData as Uint8Array, + data, progressTracker, ); } @@ -1262,13 +1277,13 @@ interface PartEtag { async function uploadStreamUsingMultipart( fileLocalID: number, - dataStream: DataStream, + dataStream: EncryptedFileStream, makeProgessTracker: MakeProgressTracker, isCFUploadProxyDisabled: boolean, abortIfCancelled: () => void, ) { const uploadPartCount = Math.ceil( - dataStream.chunkCount / maximumChunksPerUploadPart, + dataStream.chunkCount / multipartChunksPerPart, ); const multipartUploadURLs = await uploadService.fetchMultipartUploadURLs(uploadPartCount); @@ -1328,7 +1343,7 @@ async function combineChunksToFormUploadPart( streamReader: ReadableStreamDefaultReader, ) { const combinedChunks = []; - for (let i = 0; i < maximumChunksPerUploadPart; i++) { + for (let i = 0; i < multipartChunksPerPart; i++) { const { done, value: chunk } = await streamReader.read(); if (done) { break; diff --git a/web/packages/media/live-photo.ts b/web/packages/media/live-photo.ts index 5cf0291fa1..35a186a418 100644 --- a/web/packages/media/live-photo.ts +++ b/web/packages/media/live-photo.ts @@ -110,6 +110,14 @@ export const decodeLivePhoto = async ( return { imageFileName, imageData, videoFileName, videoData }; }; +/** Variant of {@link LivePhoto}, but one that allows files and data. */ +interface EncodeLivePhotoInput { + imageFileName: string; + imageFileOrData: File | Uint8Array; + videoFileName: string; + videoFileOrData: File | Uint8Array; +} + /** * Return a binary serialized representation of a live photo. * @@ -122,15 +130,15 @@ export const decodeLivePhoto = async ( */ export const encodeLivePhoto = async ({ imageFileName, - imageData, + imageFileOrData, videoFileName, - videoData, -}: LivePhoto) => { + videoFileOrData, +}: EncodeLivePhotoInput) => { const [, imageExt] = nameAndExtension(imageFileName); const [, videoExt] = nameAndExtension(videoFileName); const zip = new JSZip(); - zip.file(fileNameFromComponents(["image", imageExt]), imageData); - zip.file(fileNameFromComponents(["video", videoExt]), videoData); + zip.file(fileNameFromComponents(["image", imageExt]), imageFileOrData); + zip.file(fileNameFromComponents(["video", videoExt]), videoFileOrData); return await zip.generateAsync({ type: "uint8array" }); }; diff --git a/web/packages/shared/hooks/useFileInput.tsx b/web/packages/shared/hooks/useFileInput.tsx index b357d918ee..b53fecb585 100644 --- a/web/packages/shared/hooks/useFileInput.tsx +++ b/web/packages/shared/hooks/useFileInput.tsx @@ -1,5 +1,19 @@ import { useCallback, useRef, useState } from "react"; +/* + * TODO (MR): Understand how this is happening, and validate it further (on + * first glance this is correct). + * + * [Note: File paths when running under Electron] + * + * We have access to the absolute path of the web {@link File} object when we + * are running in the context of our desktop app. + * + * This is in contrast to the `webkitRelativePath` that we get when we're + * running in the browser, which is the relative path to the directory that the + * user selected (or just the name of the file if the user selected or + * drag/dropped a single one). + */ export interface FileWithPath extends File { readonly path?: string; } From e7a4e3e7c2f90c0515e4ca0e2e9388a97c4016a8 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Sun, 28 Apr 2024 12:27:36 +0530 Subject: [PATCH 038/240] fix logger --- web/packages/next/log.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/packages/next/log.ts b/web/packages/next/log.ts index 0f49fea6d2..a0c31c5054 100644 --- a/web/packages/next/log.ts +++ b/web/packages/next/log.ts @@ -34,7 +34,7 @@ const messageWithError = (message: string, e?: unknown) => { if (e instanceof Error) { // In practice, we expect ourselves to be called with Error objects, so // this is the happy path so to say. - return `${e.name}: ${e.message}\n${e.stack}`; + es = `${e.name}: ${e.message}\n${e.stack}`; } else { // For the rest rare cases, use the default string serialization of e. es = String(e); From 05b9f834cfc7bee7beadb9e9f4c442bbb359168e Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Sun, 28 Apr 2024 12:42:34 +0530 Subject: [PATCH 039/240] ffmpeg wasm timings --- web/apps/photos/src/worker/ffmpeg.worker.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/web/apps/photos/src/worker/ffmpeg.worker.ts b/web/apps/photos/src/worker/ffmpeg.worker.ts index 03893efba6..946a2090f0 100644 --- a/web/apps/photos/src/worker/ffmpeg.worker.ts +++ b/web/apps/photos/src/worker/ffmpeg.worker.ts @@ -62,12 +62,16 @@ const ffmpegExec = async ( const inputData = new Uint8Array(await blob.arrayBuffer()); try { - ffmpeg.FS("writeFile", inputPath, inputData); + const startTime = Date.now(); - log.debug(() => `[wasm] ffmpeg ${cmd.join(" ")}`); + ffmpeg.FS("writeFile", inputPath, inputData); await ffmpeg.run(...cmd); - return ffmpeg.FS("readFile", outputPath); + const result = ffmpeg.FS("readFile", outputPath); + + const ms = Math.round(Date.now() - startTime); + log.debug(() => `[wasm] ffmpeg ${cmd.join(" ")} (${ms} ms)`); + return result; } finally { try { ffmpeg.FS("unlink", inputPath); From b66972e8841df95048808b5f42b01a8d68f945f6 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Sun, 28 Apr 2024 12:48:00 +0530 Subject: [PATCH 040/240] Handle missing stacks --- web/packages/next/log.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/packages/next/log.ts b/web/packages/next/log.ts index a0c31c5054..a04520ed3d 100644 --- a/web/packages/next/log.ts +++ b/web/packages/next/log.ts @@ -34,7 +34,7 @@ const messageWithError = (message: string, e?: unknown) => { if (e instanceof Error) { // In practice, we expect ourselves to be called with Error objects, so // this is the happy path so to say. - es = `${e.name}: ${e.message}\n${e.stack}`; + es = [`${e.name}: ${e.message}`, e.stack].filter((x) => x).join("\n"); } else { // For the rest rare cases, use the default string serialization of e. es = String(e); From c90e9b7fd15996501628539634e121f4401a4266 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Sun, 28 Apr 2024 13:26:43 +0530 Subject: [PATCH 041/240] Add a debug inspector --- web/apps/photos/src/components/Upload/Uploader.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web/apps/photos/src/components/Upload/Uploader.tsx b/web/apps/photos/src/components/Upload/Uploader.tsx index 8eb2a736ad..d7485398f5 100644 --- a/web/apps/photos/src/components/Upload/Uploader.tsx +++ b/web/apps/photos/src/components/Upload/Uploader.tsx @@ -366,6 +366,9 @@ export default function Uploader(props: Props) { setDesktopFilePaths([]); } + log.debug(() => "Uploader received:"); + log.debug(() => fileOrPathsToUpload.current); + fileOrPathsToUpload.current = pruneHiddenFiles( fileOrPathsToUpload.current, ); From 69193e374cff92a1b316024ddb24f88b075b4fc4 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Sun, 28 Apr 2024 15:51:08 +0530 Subject: [PATCH 042/240] Wait for i18n loaded before accessing messages --- web/apps/auth/src/pages/_app.tsx | 2 +- web/apps/photos/src/pages/_app.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/apps/auth/src/pages/_app.tsx b/web/apps/auth/src/pages/_app.tsx index bf1093c907..a5aa55f98d 100644 --- a/web/apps/auth/src/pages/_app.tsx +++ b/web/apps/auth/src/pages/_app.tsx @@ -140,7 +140,7 @@ export default function App({ Component, pageProps }: AppProps) { {showNavbar && } - {offline && t("OFFLINE_MSG")} + {isI18nReady && offline && t("OFFLINE_MSG")} diff --git a/web/apps/photos/src/pages/_app.tsx b/web/apps/photos/src/pages/_app.tsx index 4b5fe31071..f3994d0817 100644 --- a/web/apps/photos/src/pages/_app.tsx +++ b/web/apps/photos/src/pages/_app.tsx @@ -352,7 +352,7 @@ export default function App({ Component, pageProps }: AppProps) { {showNavbar && } - {offline && t("OFFLINE_MSG")} + {isI18nReady && offline && t("OFFLINE_MSG")} {sharedFiles && (router.pathname === "/gallery" ? ( From 7179b0a6038dec27103d001597e2b2148db9e057 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Sun, 28 Apr 2024 15:58:15 +0530 Subject: [PATCH 043/240] less line --- web/apps/photos/src/components/PhotoFrame.tsx | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/web/apps/photos/src/components/PhotoFrame.tsx b/web/apps/photos/src/components/PhotoFrame.tsx index 8c935ee274..f7db350daa 100644 --- a/web/apps/photos/src/components/PhotoFrame.tsx +++ b/web/apps/photos/src/components/PhotoFrame.tsx @@ -308,11 +308,7 @@ const PhotoFrame = ({ item: EnteFile, ) => { log.info( - `[${ - item.id - }] getSlideData called for thumbnail:${!!item.msrc} sourceLoaded:${ - item.isSourceLoaded - } fetching:${fetching[item.id]}`, + `[${item.id}] getSlideData called for thumbnail: ${!!item.msrc} sourceLoaded: ${item.isSourceLoaded} fetching:${fetching[item.id]}`, ); if (!item.msrc) { @@ -327,9 +323,7 @@ const PhotoFrame = ({ try { updateURL(index)(item.id, url); log.info( - `[${ - item.id - }] calling invalidateCurrItems for thumbnail msrc :${!!item.msrc}`, + `[${item.id}] calling invalidateCurrItems for thumbnail msrc: ${!!item.msrc}`, ); instance.invalidateCurrItems(); if ((instance as any).isOpen()) { @@ -381,7 +375,7 @@ const PhotoFrame = ({ try { await updateSrcURL(index, item.id, dummyImgSrcUrl); log.info( - `[${item.id}] calling invalidateCurrItems for live photo imgSrc, source loaded :${item.isSourceLoaded}`, + `[${item.id}] calling invalidateCurrItems for live photo imgSrc, source loaded: ${item.isSourceLoaded}`, ); instance.invalidateCurrItems(); if ((instance as any).isOpen()) { @@ -415,7 +409,7 @@ const PhotoFrame = ({ true, ); log.info( - `[${item.id}] calling invalidateCurrItems for live photo complete, source loaded :${item.isSourceLoaded}`, + `[${item.id}] calling invalidateCurrItems for live photo complete, source loaded: ${item.isSourceLoaded}`, ); instance.invalidateCurrItems(); if ((instance as any).isOpen()) { @@ -433,7 +427,7 @@ const PhotoFrame = ({ try { await updateSrcURL(index, item.id, srcURLs); log.info( - `[${item.id}] calling invalidateCurrItems for src, source loaded :${item.isSourceLoaded}`, + `[${item.id}] calling invalidateCurrItems for src, source loaded: ${item.isSourceLoaded}`, ); instance.invalidateCurrItems(); if ((instance as any).isOpen()) { @@ -476,9 +470,7 @@ const PhotoFrame = ({ try { updateURL(index)(item.id, item.msrc, true); log.info( - `[${ - item.id - }] calling invalidateCurrItems for thumbnail msrc :${!!item.msrc}`, + `[${item.id}] calling invalidateCurrItems for thumbnail msrc: ${!!item.msrc}`, ); instance.invalidateCurrItems(); if ((instance as any).isOpen()) { @@ -495,7 +487,7 @@ const PhotoFrame = ({ } try { log.info( - `[${item.id}] new file getConvertedVideo request- ${item.metadata.title}}`, + `[${item.id}] new file getConvertedVideo request ${item.metadata.title}}`, ); fetching[item.id] = true; @@ -504,7 +496,7 @@ const PhotoFrame = ({ try { await updateSrcURL(index, item.id, srcURL, true); log.info( - `[${item.id}] calling invalidateCurrItems for src, source loaded :${item.isSourceLoaded}`, + `[${item.id}] calling invalidateCurrItems for src, source loaded: ${item.isSourceLoaded}`, ); instance.invalidateCurrItems(); if ((instance as any).isOpen()) { From 42b0b6e9bbf837aad684a35591dfdffa32802ed9 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Sun, 28 Apr 2024 16:14:13 +0530 Subject: [PATCH 044/240] convert to mp4 --- .../photos/src/services/download/index.ts | 16 ++-- web/apps/photos/src/services/ffmpeg.ts | 92 ++++++++----------- web/packages/shared/hooks/useFileInput.tsx | 2 +- 3 files changed, 43 insertions(+), 67 deletions(-) diff --git a/web/apps/photos/src/services/download/index.ts b/web/apps/photos/src/services/download/index.ts index 37eeac440d..6440b83300 100644 --- a/web/apps/photos/src/services/download/index.ts +++ b/web/apps/photos/src/services/download/index.ts @@ -10,7 +10,7 @@ import { Events, eventBus } from "@ente/shared/events"; import { isPlaybackPossible } from "@ente/shared/media/video-playback"; import { Remote } from "comlink"; import isElectron from "is-electron"; -import * as ffmpegService from "services/ffmpeg"; +import * as ffmpeg from "services/ffmpeg"; import { EnteFile } from "types/file"; import { generateStreamFromArrayBuffer, getRenderableImage } from "utils/file"; import { PhotosDownloadClient } from "./clients/photos"; @@ -610,17 +610,13 @@ async function getPlayableVideo( if (!forceConvert && !runOnWeb && !isElectron()) { return null; } - log.info( - `video format not supported, converting it name: ${videoNameTitle}`, - ); - const mp4ConvertedVideo = await ffmpegService.convertToMP4( - new File([videoBlob], videoNameTitle), - ); - log.info(`video successfully converted ${videoNameTitle}`); - return new Blob([mp4ConvertedVideo]); + // TODO(MR): This might not work for very large (~ GB) videos. Test. + log.info(`Converting video ${videoNameTitle} to mp4`); + const convertedVideoData = await ffmpeg.convertToMP4(videoBlob); + return new Blob([convertedVideoData]); } } catch (e) { - log.error("video conversion failed", e); + log.error("Video conversion failed", e); return null; } } diff --git a/web/apps/photos/src/services/ffmpeg.ts b/web/apps/photos/src/services/ffmpeg.ts index 6fc2404e2c..6383a8ce0d 100644 --- a/web/apps/photos/src/services/ffmpeg.ts +++ b/web/apps/photos/src/services/ffmpeg.ts @@ -1,4 +1,3 @@ -import { ElectronFile } from "@/next/types/file"; import type { Electron } from "@/next/types/ipc"; import { ComlinkWorker } from "@/next/worker/comlink-worker"; import { validateAndGetCreationUnixTimeInMicroSeconds } from "@ente/shared/time"; @@ -200,23 +199,6 @@ function parseCreationTime(creationTime: string) { return dateTime; } -/** Called when viewing a file */ -export async function convertToMP4(file: File) { - return await ffmpegExec2( - [ - ffmpegPathPlaceholder, - "-i", - inputPathPlaceholder, - "-preset", - "ultrafast", - outputPathPlaceholder, - ], - file, - "mp4", - 30 * 1000, - ); -} - /** * Run the given FFmpeg command using a wasm FFmpeg running in a web worker. * @@ -234,55 +216,53 @@ const ffmpegExecWeb = async ( }; /** - * Run the given FFmpeg command using a native FFmpeg binary bundled with our - * desktop app. + * Convert a video from a format that is not supported in the browser to MP4. + * + * This function is called when the user views a video or a live photo, and we + * want to play it back. The idea is to convert it to MP4 which has much more + * universal support in browsers. + * + * @param blob The video blob. + * + * @returns The mp4 video data. + */ +export const convertToMP4 = async (blob: Blob) => + ffmpegExecNativeOrWeb( + [ + ffmpegPathPlaceholder, + "-i", + inputPathPlaceholder, + "-preset", + "ultrafast", + outputPathPlaceholder, + ], + blob, + "mp4", + 30 * 1000, + ); + +/** + * Run the given FFmpeg command using a native FFmpeg binary when we're running + * in the context of our desktop app, otherwise using the browser based wasm + * FFmpeg implemenation. * * See also: {@link ffmpegExecWeb}. */ -/* -TODO(MR): Remove me -const ffmpegExecNative = async ( - electron: Electron, +const ffmpegExecNativeOrWeb = async ( command: string[], blob: Blob, - timeoutMs: number = 0, -) => { - const electron = globalThis.electron; - if (electron) { - const data = new Uint8Array(await blob.arrayBuffer()); - return await electron.ffmpegExec(command, data, timeoutMs); - } else { - const worker = await workerFactory.lazy(); - return await worker.exec(command, blob, timeoutMs); - } -}; -*/ - -const ffmpegExec2 = async ( - command: string[], - inputFile: File | ElectronFile, outputFileExtension: string, - timeoutMS: number = 0, + timeoutMs: number, ) => { const electron = globalThis.electron; - if (electron || false) { - throw new Error("WIP"); - // return electron.ffmpegExec( - // command, - // /* TODO(MR): ElectronFile changes */ - // inputFile as unknown as string, - // outputFileName, - // timeoutMS, - // ); - } else { - /* TODO(MR): ElectronFile changes */ - return ffmpegExecWeb( + if (electron) + return electron.ffmpegExec( command, - inputFile as File, + new Uint8Array(await blob.arrayBuffer()), outputFileExtension, - timeoutMS, + timeoutMs, ); - } + else return ffmpegExecWeb(command, blob, outputFileExtension, timeoutMs); }; /** Lazily create a singleton instance of our worker */ diff --git a/web/packages/shared/hooks/useFileInput.tsx b/web/packages/shared/hooks/useFileInput.tsx index b53fecb585..ac74ec6957 100644 --- a/web/packages/shared/hooks/useFileInput.tsx +++ b/web/packages/shared/hooks/useFileInput.tsx @@ -1,6 +1,6 @@ import { useCallback, useRef, useState } from "react"; -/* +/** * TODO (MR): Understand how this is happening, and validate it further (on * first glance this is correct). * From 0202f8f38b88b59007acc662ac930ec43ce7df80 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Sun, 28 Apr 2024 16:32:45 +0530 Subject: [PATCH 045/240] More debug --- web/apps/photos/src/components/Upload/Uploader.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/web/apps/photos/src/components/Upload/Uploader.tsx b/web/apps/photos/src/components/Upload/Uploader.tsx index d7485398f5..cf4578fd80 100644 --- a/web/apps/photos/src/components/Upload/Uploader.tsx +++ b/web/apps/photos/src/components/Upload/Uploader.tsx @@ -366,12 +366,13 @@ export default function Uploader(props: Props) { setDesktopFilePaths([]); } - log.debug(() => "Uploader received:"); + log.debug(() => "Uploader invoked"); log.debug(() => fileOrPathsToUpload.current); fileOrPathsToUpload.current = pruneHiddenFiles( fileOrPathsToUpload.current, ); + if (fileOrPathsToUpload.current.length === 0) { props.setLoading(false); return; @@ -386,6 +387,8 @@ export default function Uploader(props: Props) { ); setImportSuggestion(importSuggestion); + log.debug(() => importSuggestion); + handleCollectionCreationAndUpload( importSuggestion, props.isFirstUpload, From 0e9507be34b19b1400e32019c26d65e674de64e3 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Sun, 28 Apr 2024 16:47:03 +0530 Subject: [PATCH 046/240] Understand better --- web/packages/shared/hooks/useFileInput.tsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/web/packages/shared/hooks/useFileInput.tsx b/web/packages/shared/hooks/useFileInput.tsx index ac74ec6957..e2c6d83e29 100644 --- a/web/packages/shared/hooks/useFileInput.tsx +++ b/web/packages/shared/hooks/useFileInput.tsx @@ -18,6 +18,24 @@ export interface FileWithPath extends File { readonly path?: string; } +/** + * Return three things: + * + * - A function that can be called to trigger the showing of the select file / + * directory dialog. + * + * - The list of properties that should be passed to a dummy `input` element + * that needs to be created to anchor the select file dialog. This input HTML + * element is not going to be visible, but it needs to be part of the DOM fro + * the open trigger to have effect. + * + * - The list of files that the user selected. This will be a list even if the + * user selected directories - in that case, it will be the recursive list of + * files within this directory. + * + * @param param0 If {@link directory} is true, the file open dialog will ask the + * user to select directories. Otherwise it'll ask the user to select files. + */ export default function useFileInput({ directory }: { directory?: boolean }) { const [selectedFiles, setSelectedFiles] = useState([]); const inputRef = useRef(); From e65307517db1a92491ff013c63bd3c905b3c7fb8 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Sun, 28 Apr 2024 17:08:22 +0530 Subject: [PATCH 047/240] Scaffold --- desktop/src/main/ipc.ts | 3 +++ desktop/src/main/services/upload.ts | 4 ++++ desktop/src/preload.ts | 4 ++++ web/apps/photos/src/utils/native-stream.ts | 2 ++ web/packages/next/types/ipc.ts | 13 +++++++++++++ 5 files changed, 26 insertions(+) diff --git a/desktop/src/main/ipc.ts b/desktop/src/main/ipc.ts index 825a2ed32b..093724251a 100644 --- a/desktop/src/main/ipc.ts +++ b/desktop/src/main/ipc.ts @@ -53,6 +53,7 @@ import { } from "./services/store"; import { getElectronFilesFromGoogleZip, + lsZip, pendingUploads, setPendingUploadCollection, setPendingUploadFiles, @@ -210,6 +211,8 @@ export const attachIPCHandlers = () => { setPendingUploadFiles(type, filePaths), ); + ipcMain.handle("lsZip", (_, zipPath: string) => lsZip(zipPath)); + // - ipcMain.handle("getElectronFilesFromGoogleZip", (_, filePath: string) => diff --git a/desktop/src/main/services/upload.ts b/desktop/src/main/services/upload.ts index 88c2d88d19..245edafe6c 100644 --- a/desktop/src/main/services/upload.ts +++ b/desktop/src/main/services/upload.ts @@ -69,6 +69,10 @@ const storeKey = (type: PendingUploads["type"]): keyof UploadStatusStore => { } }; +export const lsZip = async (zipPath: string) => { + return [zipPath]; +}; + export const getElectronFilesFromGoogleZip = async (filePath: string) => { const zip = new StreamZip.async({ file: filePath, diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index 18fb550130..244cc9ffce 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -253,6 +253,9 @@ const setPendingUploadFiles = ( ): Promise => ipcRenderer.invoke("setPendingUploadFiles", type, filePaths); +const lsZip = (zipPath: string): Promise => + ipcRenderer.invoke("lsZip", zipPath); + // - TODO: AUDIT below this // - @@ -373,6 +376,7 @@ contextBridge.exposeInMainWorld("electron", { pendingUploads, setPendingUploadCollection, setPendingUploadFiles, + lsZip, // - diff --git a/web/apps/photos/src/utils/native-stream.ts b/web/apps/photos/src/utils/native-stream.ts index 85d54b7907..ed7b16a793 100644 --- a/web/apps/photos/src/utils/native-stream.ts +++ b/web/apps/photos/src/utils/native-stream.ts @@ -2,6 +2,8 @@ * @file Streaming IPC communication with the Node.js layer of our desktop app. * * NOTE: These functions only work when we're running in our desktop app. + * + * See: [Note: IPC streams]. */ import type { Electron } from "@/next/types/ipc"; diff --git a/web/packages/next/types/ipc.ts b/web/packages/next/types/ipc.ts index 1622a820d9..f00409b008 100644 --- a/web/packages/next/types/ipc.ts +++ b/web/packages/next/types/ipc.ts @@ -491,6 +491,19 @@ export interface Electron { filePaths: string[], ) => Promise; + /** + * Get the list of files that are present in the given zip file. + * + * @param zipPath The path of the zip file on the user's local file system. + * + * @returns A list of paths, one for each file in the given zip. Directories + * are traversed recursively, but the directory entries themselves will be + * excluded from the returned list. + * + * To read the contents of the files themselves, see [Note: IPC streams]. + */ + lsZip: (zipPath: string) => Promise; + /* * TODO: AUDIT below this - Some of the types we use below are not copyable * across process boundaries, and such functions will (expectedly) fail at From 243d019e8bea7ebccba37446162f4b6609eb87ef Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Sun, 28 Apr 2024 17:24:11 +0530 Subject: [PATCH 048/240] Potential implementation --- desktop/src/main/stream.ts | 45 ++++++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/desktop/src/main/stream.ts b/desktop/src/main/stream.ts index 88d85db8e8..26e5d1f3f5 100644 --- a/desktop/src/main/stream.ts +++ b/desktop/src/main/stream.ts @@ -2,6 +2,7 @@ * @file stream data to-from renderer using a custom protocol handler. */ import { net, protocol } from "electron/main"; +import StreamZip from "node-stream-zip"; import { createWriteStream, existsSync } from "node:fs"; import fs from "node:fs/promises"; import { Readable } from "node:stream"; @@ -34,17 +35,23 @@ export const registerStreamProtocol = () => { protocol.handle("stream", async (request: Request) => { const url = request.url; // The request URL contains the command to run as the host, and the - // pathname of the file as the path. For example, + // pathname of the file as the path. An additional path can be specified + // as the URL hash. // - // stream://write/path/to/file - // host-pathname----- + // For example, // - const { host, pathname } = new URL(url); + // stream://write/path/to/file#/path/to/another/file + // host[pathname----] [pathname-2---------] + // + const { host, pathname, hash } = new URL(url); // Convert e.g. "%20" to spaces. const path = decodeURIComponent(pathname); + const hashPath = decodeURIComponent(hash); switch (host) { case "read": return handleRead(path); + case "read-zip": + return handleReadZip(path, hashPath); case "write": return handleWrite(path, request); default: @@ -88,6 +95,36 @@ const handleRead = async (path: string) => { } }; +const handleReadZip = async (zipPath: string, zipEntryPath: string) => { + try { + const zip = new StreamZip.async({ + file: zipPath, + }); + const entry = await zip.entry(zipEntryPath); + const stream = await zip.stream(entry); + + return new Response(Readable.toWeb(new Readable(stream)), { + headers: { + // We don't know the exact type, but it doesn't really matter, + // just set it to a generic binary content-type so that the + // browser doesn't tinker with it thinking of it as text. + "Content-Type": "application/octet-stream", + "Content-Length": `${entry.size}`, + // !!TODO(MR): Is this ms + "X-Last-Modified-Ms": `${entry.time}`, + }, + }); + } catch (e) { + log.error( + `Failed to read entry ${zipEntryPath} from zip file at ${zipPath}`, + e, + ); + return new Response(`Failed to read stream: ${e.message}`, { + status: 500, + }); + } +}; + const handleWrite = async (path: string, request: Request) => { try { await writeStream(path, request.body); From a3d06c54afc051bbb1d9134b2dd0010cf59fc727 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Sun, 28 Apr 2024 17:37:39 +0530 Subject: [PATCH 049/240] Prune --- .../src/components/PhotoList/dedupe.tsx | 2 +- .../photos/src/components/PhotoList/index.tsx | 2 +- web/apps/photos/src/services/heic-convert.ts | 27 ++++--------------- web/apps/photos/src/utils/file/index.ts | 13 +++++++++ web/packages/next/file.ts | 26 ------------------ 5 files changed, 20 insertions(+), 50 deletions(-) diff --git a/web/apps/photos/src/components/PhotoList/dedupe.tsx b/web/apps/photos/src/components/PhotoList/dedupe.tsx index 9c86ba24f1..7181f62675 100644 --- a/web/apps/photos/src/components/PhotoList/dedupe.tsx +++ b/web/apps/photos/src/components/PhotoList/dedupe.tsx @@ -1,4 +1,3 @@ -import { convertBytesToHumanReadable } from "@/next/file"; import { FlexWrapper } from "@ente/shared/components/Container"; import { Box, styled } from "@mui/material"; import { @@ -20,6 +19,7 @@ import { } from "react-window"; import { Duplicate } from "services/deduplicationService"; import { EnteFile } from "types/file"; +import { convertBytesToHumanReadable } from "utils/file"; export enum ITEM_TYPE { TIME = "TIME", diff --git a/web/apps/photos/src/components/PhotoList/index.tsx b/web/apps/photos/src/components/PhotoList/index.tsx index 48454fa691..91f712df1b 100644 --- a/web/apps/photos/src/components/PhotoList/index.tsx +++ b/web/apps/photos/src/components/PhotoList/index.tsx @@ -1,4 +1,3 @@ -import { convertBytesToHumanReadable } from "@/next/file"; import { FlexWrapper } from "@ente/shared/components/Container"; import { formatDate, getDate, isSameDay } from "@ente/shared/time/format"; import { Box, Checkbox, Link, Typography, styled } from "@mui/material"; @@ -23,6 +22,7 @@ import { areEqual, } from "react-window"; import { EnteFile } from "types/file"; +import { convertBytesToHumanReadable } from "utils/file"; import { handleSelectCreator } from "utils/photoFrame"; import { PublicCollectionGalleryContext } from "utils/publicCollectionGallery"; diff --git a/web/apps/photos/src/services/heic-convert.ts b/web/apps/photos/src/services/heic-convert.ts index 478cce2185..c2ea198391 100644 --- a/web/apps/photos/src/services/heic-convert.ts +++ b/web/apps/photos/src/services/heic-convert.ts @@ -1,4 +1,3 @@ -import { convertBytesToHumanReadable } from "@/next/file"; import log from "@/next/log"; import { ComlinkWorker } from "@/next/worker/comlink-worker"; import { CustomError } from "@ente/shared/error"; @@ -51,15 +50,10 @@ class HEICConverter { const startTime = Date.now(); const convertedHEIC = await worker.heicToJPEG(fileBlob); - log.info( - `originalFileSize:${convertBytesToHumanReadable( - fileBlob?.size, - )},convertedFileSize:${convertBytesToHumanReadable( - convertedHEIC?.size, - )}, heic conversion time: ${ - Date.now() - startTime - }ms `, + const ms = Math.round( + Date.now() - startTime, ); + log.debug(() => `heic => jpeg (${ms} ms)`); clearTimeout(timeout); resolve(convertedHEIC); } catch (e) { @@ -71,18 +65,7 @@ class HEICConverter { ); if (!convertedHEIC || convertedHEIC?.size === 0) { log.error( - `converted heic fileSize is Zero - ${JSON.stringify( - { - originalFileSize: - convertBytesToHumanReadable( - fileBlob?.size ?? 0, - ), - convertedFileSize: - convertBytesToHumanReadable( - convertedHEIC?.size ?? 0, - ), - }, - )}`, + `Converted HEIC file is empty (original was ${fileBlob?.size} bytes)`, ); } await new Promise((resolve) => { @@ -94,7 +77,7 @@ class HEICConverter { this.workerPool.push(convertWorker); return convertedHEIC; } catch (e) { - log.error("heic conversion failed", e); + log.error("HEIC conversion failed", e); convertWorker.terminate(); this.workerPool.push(createComlinkWorker()); throw e; diff --git a/web/apps/photos/src/utils/file/index.ts b/web/apps/photos/src/utils/file/index.ts index 5d7762abfc..abbc8b0fa3 100644 --- a/web/apps/photos/src/utils/file/index.ts +++ b/web/apps/photos/src/utils/file/index.ts @@ -116,6 +116,19 @@ export async function getUpdatedEXIFFileForDownload( } } +export function convertBytesToHumanReadable( + bytes: number, + precision = 2, +): string { + if (bytes === 0 || isNaN(bytes)) { + return "0 MB"; + } + + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; + return (bytes / Math.pow(1024, i)).toFixed(precision) + " " + sizes[i]; +} + export async function downloadFile(file: EnteFile) { try { const fileReader = new FileReader(); diff --git a/web/packages/next/file.ts b/web/packages/next/file.ts index 56d27b79b5..bd2c043930 100644 --- a/web/packages/next/file.ts +++ b/web/packages/next/file.ts @@ -1,5 +1,3 @@ -import type { ElectronFile } from "./types/file"; - /** * The two parts of a file name - the name itself, and an (optional) extension. * @@ -82,27 +80,3 @@ export const dirname = (path: string) => { } return pathComponents.join("/"); }; - -/** - * Return a short description of the given {@link fileOrPath} suitable for - * helping identify it in log messages. - */ -export const fopLabel = (fileOrPath: File | string) => - fileOrPath instanceof File ? `File(${fileOrPath.name})` : fileOrPath; - -export function getFileNameSize(file: File | ElectronFile) { - return `${file.name}_${convertBytesToHumanReadable(file.size)}`; -} - -export function convertBytesToHumanReadable( - bytes: number, - precision = 2, -): string { - if (bytes === 0 || isNaN(bytes)) { - return "0 MB"; - } - - const i = Math.floor(Math.log(bytes) / Math.log(1024)); - const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; - return (bytes / Math.pow(1024, i)).toFixed(precision) + " " + sizes[i]; -} From 13f0ff3af52d7cc31cd9cfc94342d0eb31f00ea9 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Sun, 28 Apr 2024 20:31:11 +0530 Subject: [PATCH 050/240] wip zip selection on web itself --- .../photos/src/components/Upload/Uploader.tsx | 59 ++++++++++++++++--- .../src/components/UploadSelectorInputs.tsx | 4 ++ web/apps/photos/src/pages/gallery/index.tsx | 11 ++++ .../photos/src/pages/shared-albums/index.tsx | 1 + web/packages/shared/hooks/useFileInput.tsx | 20 ++++++- 5 files changed, 85 insertions(+), 10 deletions(-) diff --git a/web/apps/photos/src/components/Upload/Uploader.tsx b/web/apps/photos/src/components/Upload/Uploader.tsx index cf4578fd80..2bf1e79d20 100644 --- a/web/apps/photos/src/components/Upload/Uploader.tsx +++ b/web/apps/photos/src/components/Upload/Uploader.tsx @@ -76,8 +76,10 @@ interface Props { showSessionExpiredMessage: () => void; showUploadFilesDialog: () => void; showUploadDirsDialog: () => void; + showUploadZipFilesDialog?: () => void; webFolderSelectorFiles: File[]; webFileSelectorFiles: File[]; + webFileSelectorZipFiles?: File[]; dragAndDropFiles: File[]; uploadCollection?: Collection; uploadTypeSelectorIntent: UploadTypeSelectorIntent; @@ -255,24 +257,59 @@ export default function Uploader(props: Props) { ) { log.info(`received file upload request`); setWebFiles(props.webFileSelectorFiles); + } else if ( + pickedUploadType.current === PICKED_UPLOAD_TYPE.ZIPS && + props.webFileSelectorZipFiles?.length > 0 + ) { + if (electron) { + const main = async () => { + const zips: File[] = []; + let electronFiles = [] as ElectronFile[]; + for (const file of props.webFileSelectorZipFiles) { + if (file.name.endsWith(".zip")) { + const zipFiles = await electron.lsZip( + (file as any).path, + ); + log.info( + `zip file - ${file.name} contains ${zipFiles.length} files`, + ); + zips.push(file); + // TODO(MR): This cast is incorrect, but interim. + electronFiles = [ + ...electronFiles, + ...(zipFiles as unknown as ElectronFile[]), + ]; + } + } + // setWebFiles(props.webFileSelectorZipFiles); + zipPaths.current = zips.map((file) => (file as any).path); + setElectronFiles(electronFiles); + }; + main(); + } } else if (props.dragAndDropFiles?.length > 0) { isDragAndDrop.current = true; if (electron) { const main = async () => { try { - log.info(`uploading dropped files from desktop app`); // check and parse dropped files which are zip files + log.info(`uploading dropped files from desktop app`); + const zips: File[] = []; let electronFiles = [] as ElectronFile[]; for (const file of props.dragAndDropFiles) { if (file.name.endsWith(".zip")) { - const zipFiles = - await electron.getElectronFilesFromGoogleZip( - (file as any).path, - ); + const zipFiles = await electron.lsZip( + (file as any).path, + ); log.info( `zip file - ${file.name} contains ${zipFiles.length} files`, ); - electronFiles = [...electronFiles, ...zipFiles]; + zips.push(file); + // TODO(MR): This cast is incorrect, but interim. + electronFiles = [ + ...electronFiles, + ...(zipFiles as unknown as ElectronFile[]), + ]; } else { // type cast to ElectronFile as the file is dropped from desktop app // type file and ElectronFile should be interchangeable, but currently they have some differences. @@ -290,6 +327,9 @@ export default function Uploader(props: Props) { log.info( `uploading dropped files from desktop app - ${electronFiles.length} files found`, ); + zipPaths.current = zips.map( + (file) => (file as any).path, + ); setElectronFiles(electronFiles); } catch (e) { log.error("failed to upload desktop dropped files", e); @@ -306,6 +346,7 @@ export default function Uploader(props: Props) { props.dragAndDropFiles, props.webFileSelectorFiles, props.webFolderSelectorFiles, + props.webFileSelectorZipFiles, ]); useEffect(() => { @@ -768,7 +809,11 @@ export default function Uploader(props: Props) { } else if (type === PICKED_UPLOAD_TYPE.FOLDERS) { props.showUploadDirsDialog(); } else { - appContext.setDialogMessage(getDownloadAppMessage()); + if (props.showUploadZipFilesDialog && electron) { + props.showUploadZipFilesDialog(); + } else { + appContext.setDialogMessage(getDownloadAppMessage()); + } } }; diff --git a/web/apps/photos/src/components/UploadSelectorInputs.tsx b/web/apps/photos/src/components/UploadSelectorInputs.tsx index 1b110d532b..13e33fc6d3 100644 --- a/web/apps/photos/src/components/UploadSelectorInputs.tsx +++ b/web/apps/photos/src/components/UploadSelectorInputs.tsx @@ -2,12 +2,16 @@ export default function UploadSelectorInputs({ getDragAndDropInputProps, getFileSelectorInputProps, getFolderSelectorInputProps, + getZipFileSelectorInputProps, }) { return ( <> + {getZipFileSelectorInputProps && ( + + )} ); } diff --git a/web/apps/photos/src/pages/gallery/index.tsx b/web/apps/photos/src/pages/gallery/index.tsx index 38f559814c..658f62b459 100644 --- a/web/apps/photos/src/pages/gallery/index.tsx +++ b/web/apps/photos/src/pages/gallery/index.tsx @@ -224,6 +224,14 @@ export default function Gallery() { } = useFileInput({ directory: true, }); + const { + selectedFiles: webFileSelectorZipFiles, + open: openZipFileSelector, + getInputProps: getZipFileSelectorInputProps, + } = useFileInput({ + directory: false, + accept: ".zip" + }); const [isInSearchMode, setIsInSearchMode] = useState(false); const [searchResultSummary, setSetSearchResultSummary] = @@ -1023,6 +1031,7 @@ export default function Gallery() { getDragAndDropInputProps={getDragAndDropInputProps} getFileSelectorInputProps={getFileSelectorInputProps} getFolderSelectorInputProps={getFolderSelectorInputProps} + getZipFileSelectorInputProps={getZipFileSelectorInputProps} /> {blockingLoad && ( @@ -1123,10 +1132,12 @@ export default function Gallery() { } webFileSelectorFiles={webFileSelectorFiles} webFolderSelectorFiles={webFolderSelectorFiles} + webFileSelectorZipFiles={webFileSelectorZipFiles} dragAndDropFiles={dragAndDropFiles} uploadTypeSelectorView={uploadTypeSelectorView} showUploadFilesDialog={openFileSelector} showUploadDirsDialog={openFolderSelector} + showUploadZipFilesDialog={openZipFileSelector} showSessionExpiredMessage={showSessionExpiredMessage} /> ([]); const inputRef = useRef(); @@ -66,6 +79,7 @@ export default function useFileInput({ directory }: { directory?: boolean }) { ...(directory ? { directory: "", webkitdirectory: "" } : {}), ref: inputRef, onChange: handleChange, + ...(accept ? { accept } : {}), }), [], ); From 24b64f9522495072119f8f2a3c07135001a76de0 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Sun, 28 Apr 2024 20:38:14 +0530 Subject: [PATCH 051/240] Verify assumption --- desktop/src/main/stream.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/desktop/src/main/stream.ts b/desktop/src/main/stream.ts index 26e5d1f3f5..ddd639c30b 100644 --- a/desktop/src/main/stream.ts +++ b/desktop/src/main/stream.ts @@ -110,7 +110,11 @@ const handleReadZip = async (zipPath: string, zipEntryPath: string) => { // browser doesn't tinker with it thinking of it as text. "Content-Type": "application/octet-stream", "Content-Length": `${entry.size}`, - // !!TODO(MR): Is this ms + // While it is documented that entry.time is the modification + // time, the units are not mentioned. By seeing the source code, + // we can verify that it is indeed epoch milliseconds. See + // `parseZipTime` in the node-stream-zip source, + // https://github.com/antelle/node-stream-zip/blob/master/node_stream_zip.js "X-Last-Modified-Ms": `${entry.time}`, }, }); From d46dbf1e733a82ded2f96b97fe0084336d14d77e Mon Sep 17 00:00:00 2001 From: BifrostTenmei <156937334+BifrostTenmei@users.noreply.github.com> Date: Sun, 28 Apr 2024 18:20:58 +0200 Subject: [PATCH 052/240] Fixed typo --- docs/docs/auth/migration-guides/authy/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/auth/migration-guides/authy/index.md b/docs/docs/auth/migration-guides/authy/index.md index 48ce3965d9..1a92285472 100644 --- a/docs/docs/auth/migration-guides/authy/index.md +++ b/docs/docs/auth/migration-guides/authy/index.md @@ -18,7 +18,7 @@ A guide written by Green, an ente.io lover Migrating from Authy can be tiring, as you cannot export your 2FA codes through the app, meaning that you would have to reconfigure 2FA for all of your accounts for your new 2FA authenticator. However, easier ways exist to export your codes -out of Authy. This guide will cover two of the most used methods for mograting +out of Authy. This guide will cover two of the most used methods for migrating from Authy to Ente Authenticator. > [!CAUTION] From 0f46a25a5d8ea99659f1552319f01b92a080a04e Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 29 Apr 2024 09:25:55 +0530 Subject: [PATCH 053/240] [web] Allow thumbnail cache to be optional See: https://github.com/ente-io/ente/discussions/1449#discussioncomment-9255346 I'm not yet sure what was the case why it was not initialized, hoping to get some logs for the error when initializing the cache to see how we got to this state. But meanwhile, ensure that the code works even without the cache. --- web/apps/photos/src/services/download/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/apps/photos/src/services/download/index.ts b/web/apps/photos/src/services/download/index.ts index 37eeac440d..70934dac06 100644 --- a/web/apps/photos/src/services/download/index.ts +++ b/web/apps/photos/src/services/download/index.ts @@ -150,7 +150,7 @@ class DownloadManagerImpl { this.ensureInitialized(); const key = file.id.toString(); - const cached = await this.thumbnailCache.get(key); + const cached = await this.thumbnailCache?.get(key); if (cached) return new Uint8Array(await cached.arrayBuffer()); if (localOnly) return null; From 75c058fc4c9abbb6acfbf556482adfb869dc5f04 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 29 Apr 2024 09:53:54 +0530 Subject: [PATCH 054/240] This is where it comes from --- web/packages/shared/hooks/useFileInput.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/web/packages/shared/hooks/useFileInput.tsx b/web/packages/shared/hooks/useFileInput.tsx index 3f7f2d4321..4eb346d39c 100644 --- a/web/packages/shared/hooks/useFileInput.tsx +++ b/web/packages/shared/hooks/useFileInput.tsx @@ -1,18 +1,23 @@ import { useCallback, useRef, useState } from "react"; /** - * TODO (MR): Understand how this is happening, and validate it further (on - * first glance this is correct). - * * [Note: File paths when running under Electron] * * We have access to the absolute path of the web {@link File} object when we * are running in the context of our desktop app. * + * https://www.electronjs.org/docs/latest/api/file-object + * * This is in contrast to the `webkitRelativePath` that we get when we're * running in the browser, which is the relative path to the directory that the * user selected (or just the name of the file if the user selected or * drag/dropped a single one). + * + * Note that this is a deprecated approach. From Electron docs: + * + * > Warning: The path property that Electron adds to the File interface is + * > deprecated and will be removed in a future Electron release. We recommend + * > you use `webUtils.getPathForFile` instead. */ export interface FileWithPath extends File { readonly path?: string; @@ -49,7 +54,10 @@ interface UseFileInputParams { * accept can be an extension or a MIME type (See * https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept). */ -export default function useFileInput({ directory, accept }: UseFileInputParams) { +export default function useFileInput({ + directory, + accept, +}: UseFileInputParams) { const [selectedFiles, setSelectedFiles] = useState([]); const inputRef = useRef(); From aa111b2a245e2a5b7f64c41d673d924e6bf16711 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 29 Apr 2024 10:12:53 +0530 Subject: [PATCH 055/240] Outline the plan --- desktop/src/main/stores/upload-status.ts | 27 ++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/desktop/src/main/stores/upload-status.ts b/desktop/src/main/stores/upload-status.ts index 25af7a49e2..e2d1880ce2 100644 --- a/desktop/src/main/stores/upload-status.ts +++ b/desktop/src/main/stores/upload-status.ts @@ -1,12 +1,31 @@ import Store, { Schema } from "electron-store"; export interface UploadStatusStore { - filePaths: string[]; - zipPaths: string[]; + /** + * The name of the collection (when uploading to a singular collection) or + * the root collection (when uploading to separate * albums) to which we + * these uploads are meant to go to. + */ collectionName: string; + /** + * Paths of regular files that need to be uploaded. + */ + filePaths: string[]; + /** + * Paths of zip files that need to be uploaded. + */ + zipPaths: string[]; + /** + * For each zip file, which of its entries (paths) within the zip file that + * need to be uploaded. + */ + zipEntries: Record; } const uploadStatusSchema: Schema = { + collectionName: { + type: "string", + }, filePaths: { type: "array", items: { @@ -19,8 +38,8 @@ const uploadStatusSchema: Schema = { type: "string", }, }, - collectionName: { - type: "string", + zipEntries: { + type: "object", }, }; From 4b97f832b2a1246823863e3aad0c1e25242ac4c0 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 29 Apr 2024 10:23:19 +0530 Subject: [PATCH 056/240] [mob][photos] Finish auto-pair integration --- mobile/lib/ui/cast/auto.dart | 19 +++++++++++++++++-- .../gallery/gallery_app_bar_widget.dart | 6 +++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/mobile/lib/ui/cast/auto.dart b/mobile/lib/ui/cast/auto.dart index 0b088a9d2d..8afacf7db7 100644 --- a/mobile/lib/ui/cast/auto.dart +++ b/mobile/lib/ui/cast/auto.dart @@ -1,5 +1,6 @@ import "dart:io"; +import "package:ente_cast/ente_cast.dart"; import "package:flutter/material.dart"; import "package:photos/generated/l10n.dart"; import "package:photos/service_locator.dart"; @@ -8,7 +9,11 @@ import "package:photos/ui/common/loading_widget.dart"; import "package:photos/utils/dialog_util.dart"; class AutoCastDialog extends StatefulWidget { - AutoCastDialog({ + // async method that takes string as input + // and returns void + final void Function(String) onConnect; + AutoCastDialog( + this.onConnect, { Key? key, }) : super(key: key) {} @@ -88,6 +93,16 @@ class _AutoCastDialogState extends State { BuildContext context, Object castDevice, ) async { - await castService.connectDevice(context, castDevice); + await castService.connectDevice( + context, + castDevice, + onMessage: (message) { + if (message.containsKey(CastMessageType.pairCode)) { + final code = message[CastMessageType.pairCode]!['code']; + widget.onConnect(code); + // Navigator.of(context).pop(); + } + }, + ); } } diff --git a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart index d73662e19f..4365b78130 100644 --- a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart +++ b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart @@ -875,7 +875,11 @@ class _GalleryAppBarWidgetState extends State { context: context, barrierDismissible: true, builder: (BuildContext context) { - return AutoCastDialog(); + return AutoCastDialog( + (device) async { + await _castPair(gw, device); + }, + ); }, ); } From e8687caba2c84afc53b82dda2f20200374fe6c37 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 29 Apr 2024 11:07:57 +0530 Subject: [PATCH 057/240] wip pending --- desktop/src/main/ipc.ts | 5 +++- desktop/src/main/services/upload.ts | 26 ++++++++++++++++--- desktop/src/main/stores/upload-status.ts | 15 ----------- desktop/src/preload.ts | 10 ++++--- web/packages/next/types/ipc.ts | 33 +++++++++++++++++++----- 5 files changed, 59 insertions(+), 30 deletions(-) diff --git a/desktop/src/main/ipc.ts b/desktop/src/main/ipc.ts index 093724251a..bde28d940a 100644 --- a/desktop/src/main/ipc.ts +++ b/desktop/src/main/ipc.ts @@ -52,6 +52,7 @@ import { saveEncryptionKey, } from "./services/store"; import { + clearPendingUploads, getElectronFilesFromGoogleZip, lsZip, pendingUploads, @@ -199,6 +200,8 @@ export const attachIPCHandlers = () => { // - Upload + ipcMain.handle("lsZip", (_, zipPath: string) => lsZip(zipPath)); + ipcMain.handle("pendingUploads", () => pendingUploads()); ipcMain.handle("setPendingUploadCollection", (_, collectionName: string) => @@ -211,7 +214,7 @@ export const attachIPCHandlers = () => { setPendingUploadFiles(type, filePaths), ); - ipcMain.handle("lsZip", (_, zipPath: string) => lsZip(zipPath)); + ipcMain.handle("clearPendingUploads", () => clearPendingUploads()); // - diff --git a/desktop/src/main/services/upload.ts b/desktop/src/main/services/upload.ts index 245edafe6c..1ca7c2bc4e 100644 --- a/desktop/src/main/services/upload.ts +++ b/desktop/src/main/services/upload.ts @@ -8,6 +8,24 @@ import { } from "../stores/upload-status"; import { getElectronFile, getZipFileStream } from "./fs"; +export const lsZip = async (zipPath: string) => { + const zip = new StreamZip.async({ file: zipPath }); + + const entries = await zip.entries(); + const entryPaths: string[] = []; + + for (const entry of Object.values(entries)) { + const basename = path.basename(entry.name); + // Ignore "hidden" files (files whose names begins with a dot). + if (entry.isFile && basename.length > 0 && basename[0] != ".") { + // `entry.name` is the path within the zip. + entryPaths.push(entry.name); + } + } + + return [entryPaths]; +}; + export const pendingUploads = async () => { const collectionName = uploadStatusStore.get("collectionName"); const filePaths = validSavedPaths("files"); @@ -60,6 +78,10 @@ export const setPendingUploadFiles = ( else uploadStatusStore.delete(key); }; +export const clearPendingUploads = () => { + uploadStatusStore.clear(); +}; + const storeKey = (type: PendingUploads["type"]): keyof UploadStatusStore => { switch (type) { case "zips": @@ -69,10 +91,6 @@ const storeKey = (type: PendingUploads["type"]): keyof UploadStatusStore => { } }; -export const lsZip = async (zipPath: string) => { - return [zipPath]; -}; - export const getElectronFilesFromGoogleZip = async (filePath: string) => { const zip = new StreamZip.async({ file: filePath, diff --git a/desktop/src/main/stores/upload-status.ts b/desktop/src/main/stores/upload-status.ts index e2d1880ce2..5b7f7c1127 100644 --- a/desktop/src/main/stores/upload-status.ts +++ b/desktop/src/main/stores/upload-status.ts @@ -1,24 +1,9 @@ import Store, { Schema } from "electron-store"; export interface UploadStatusStore { - /** - * The name of the collection (when uploading to a singular collection) or - * the root collection (when uploading to separate * albums) to which we - * these uploads are meant to go to. - */ collectionName: string; - /** - * Paths of regular files that need to be uploaded. - */ filePaths: string[]; - /** - * Paths of zip files that need to be uploaded. - */ zipPaths: string[]; - /** - * For each zip file, which of its entries (paths) within the zip file that - * need to be uploaded. - */ zipEntries: Record; } diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index 244cc9ffce..ad93a40f6a 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -241,6 +241,9 @@ const watchFindFiles = (folderPath: string): Promise => // - Upload +const lsZip = (zipPath: string): Promise => + ipcRenderer.invoke("lsZip", zipPath); + const pendingUploads = (): Promise => ipcRenderer.invoke("pendingUploads"); @@ -253,8 +256,8 @@ const setPendingUploadFiles = ( ): Promise => ipcRenderer.invoke("setPendingUploadFiles", type, filePaths); -const lsZip = (zipPath: string): Promise => - ipcRenderer.invoke("lsZip", zipPath); +const clearPendingUploads = (): Promise => + ipcRenderer.invoke("clearPendingUploads"); // - TODO: AUDIT below this // - @@ -373,10 +376,11 @@ contextBridge.exposeInMainWorld("electron", { // - Upload + lsZip, pendingUploads, setPendingUploadCollection, setPendingUploadFiles, - lsZip, + clearPendingUploads, // - diff --git a/web/packages/next/types/ipc.ts b/web/packages/next/types/ipc.ts index f00409b008..1056610366 100644 --- a/web/packages/next/types/ipc.ts +++ b/web/packages/next/types/ipc.ts @@ -601,14 +601,33 @@ export interface FolderWatchSyncedFile { } /** - * When the user starts an upload, we remember the files they'd selected or drag - * and dropped so that we can resume (if needed) when the app restarts after - * being stopped in the middle of the uploads. + * State about pending and in-progress uploads. + * + * When the user starts an upload, we remember the files they'd selected (or + * drag-dropped) so that we can resume if they restart the app in before the + * uploads have been completed. This state is kept on the Electron side, and + * this object is the IPC intermediary. */ export interface PendingUploads { - /** The collection to which we're uploading */ + /** + * The collection to which we're uploading, or the root collection. + * + * This is name of the collection (when uploading to a singular collection) + * or the root collection (when uploading to separate * albums) to which we + * these uploads are meant to go to. See {@link CollectionMapping}. + */ collectionName: string; - /* The upload can be either of a Google Takeout zip, or regular files */ - type: "files" | "zips"; - files: ElectronFile[]; + /** + * Paths of regular files that need to be uploaded. + */ + filePaths: string[]; + /** + * Paths of zip files that need to be uploaded. + */ + zipPaths: string[]; + /** + * For each zip file, which of its entries (paths) within the zip file that + * need to be uploaded. + */ + zipEntries: Record; } From 7411125194659af92704f5dcf4892ae24125d5e7 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 29 Apr 2024 11:20:17 +0530 Subject: [PATCH 058/240] [mob][photos] Add support for closing session --- mobile/lib/ui/cast/auto.dart | 2 +- .../gallery/gallery_app_bar_widget.dart | 22 ++++++++++++++++++- mobile/plugins/ente_cast/lib/src/service.dart | 2 +- .../ente_cast_none/lib/src/service.dart | 2 +- .../ente_cast_normal/lib/src/service.dart | 21 ++++++++++-------- 5 files changed, 36 insertions(+), 13 deletions(-) diff --git a/mobile/lib/ui/cast/auto.dart b/mobile/lib/ui/cast/auto.dart index 8afacf7db7..4dc9d5bd01 100644 --- a/mobile/lib/ui/cast/auto.dart +++ b/mobile/lib/ui/cast/auto.dart @@ -100,7 +100,7 @@ class _AutoCastDialogState extends State { if (message.containsKey(CastMessageType.pairCode)) { final code = message[CastMessageType.pairCode]!['code']; widget.onConnect(code); - // Navigator.of(context).pop(); + Navigator.of(context).pop(); } }, ); diff --git a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart index 4365b78130..489d5478ec 100644 --- a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart +++ b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart @@ -583,9 +583,14 @@ class _GalleryAppBarWidgetState extends State { Tooltip( message: "Cast album", child: IconButton( - icon: const Icon(Icons.cast_outlined), + icon: castService.getActiveSessions().isNotEmpty + ? const Icon(Icons.cast_connected_rounded) + : const Icon(Icons.cast_outlined), onPressed: () async { await _castChoiceDialog(); + if (mounted) { + setState(() {}); + } }, ), ), @@ -855,6 +860,21 @@ class _GalleryAppBarWidgetState extends State { Future _castChoiceDialog() async { final gw = CastGateway(NetworkClient.instance.enteDio); + if (castService.getActiveSessions().isNotEmpty) { + await showChoiceDialog( + context, + title: "Stop casting", + firstButtonLabel: "Yes", + secondButtonLabel: "No", + body: "Do you want to stop casting?", + firstButtonOnTap: () async { + gw.revokeAllTokens().ignore(); + await castService.closeActiveCasts(); + }, + ); + return; + } + // stop any existing cast session gw.revokeAllTokens().ignore(); final result = await showDialog( diff --git a/mobile/plugins/ente_cast/lib/src/service.dart b/mobile/plugins/ente_cast/lib/src/service.dart index 82d8c5978c..2ab0961dbd 100644 --- a/mobile/plugins/ente_cast/lib/src/service.dart +++ b/mobile/plugins/ente_cast/lib/src/service.dart @@ -12,7 +12,7 @@ abstract class CastService { void Function(Map>)? onMessage, }); // returns a map of sessionID to deviceNames - Future> getActiveSessions(); + Map getActiveSessions(); Future closeActiveCasts(); } diff --git a/mobile/plugins/ente_cast_none/lib/src/service.dart b/mobile/plugins/ente_cast_none/lib/src/service.dart index 007a4daaa0..c781889733 100644 --- a/mobile/plugins/ente_cast_none/lib/src/service.dart +++ b/mobile/plugins/ente_cast_none/lib/src/service.dart @@ -28,7 +28,7 @@ class CastServiceImpl extends CastService { } @override - Future> getActiveSessions() { + Map getActiveSessions() { // TODO: implement getActiveSessions throw UnimplementedError(); } diff --git a/mobile/plugins/ente_cast_normal/lib/src/service.dart b/mobile/plugins/ente_cast_normal/lib/src/service.dart index 314194b944..a2c4206c87 100644 --- a/mobile/plugins/ente_cast_normal/lib/src/service.dart +++ b/mobile/plugins/ente_cast_normal/lib/src/service.dart @@ -70,24 +70,27 @@ class CastServiceImpl extends CastService { Future closeActiveCasts() { final sessions = CastSessionManager().sessions; for (final session in sessions) { - session.sendMessage( - _pairRequestNamespace, - { - "type": "CLOSE", - }, - ); + debugPrint("send close message for ${session.sessionId}"); + session.sendMessage(CastSession.kNamespaceConnection, { + 'type': 'CLOSE', + }); + debugPrint("close session ${session.sessionId}"); session.close(); } + CastSessionManager().sessions.clear(); + debugPrint("send close message"); return Future.value(); } @override - Future> getActiveSessions() { + Map getActiveSessions() { final sessions = CastSessionManager().sessions; final Map result = {}; for (final session in sessions) { - result[session.sessionId] = session.state.toString(); + if (session.state == CastSessionState.connected) { + result[session.sessionId] = session.state.toString(); + } } - return Future.value(result); + return result; } } From 195ad01f148d7109dbe68647cd05d87e63d3776a Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 29 Apr 2024 11:26:13 +0530 Subject: [PATCH 059/240] [mob][photos] Add timeout for stop casting --- .../plugins/ente_cast_normal/lib/src/service.dart | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/mobile/plugins/ente_cast_normal/lib/src/service.dart b/mobile/plugins/ente_cast_normal/lib/src/service.dart index a2c4206c87..454af58adf 100644 --- a/mobile/plugins/ente_cast_normal/lib/src/service.dart +++ b/mobile/plugins/ente_cast_normal/lib/src/service.dart @@ -71,9 +71,16 @@ class CastServiceImpl extends CastService { final sessions = CastSessionManager().sessions; for (final session in sessions) { debugPrint("send close message for ${session.sessionId}"); - session.sendMessage(CastSession.kNamespaceConnection, { - 'type': 'CLOSE', - }); + Future(() { + session.sendMessage(CastSession.kNamespaceConnection, { + 'type': 'CLOSE', + }); + }).timeout( + const Duration(seconds: 5), + onTimeout: () { + print('sendMessage timed out after 5 seconds'); + }, + ); debugPrint("close session ${session.sessionId}"); session.close(); } From 63841abd301792ecd0df0fa8da8f9be5f36ac9a1 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 29 Apr 2024 11:44:43 +0530 Subject: [PATCH 060/240] Envision --- desktop/src/main/stores/upload-status.ts | 20 ++++-- web/packages/next/types/ipc.ts | 82 +++++++++++------------- 2 files changed, 55 insertions(+), 47 deletions(-) diff --git a/desktop/src/main/stores/upload-status.ts b/desktop/src/main/stores/upload-status.ts index 5b7f7c1127..4a402333a7 100644 --- a/desktop/src/main/stores/upload-status.ts +++ b/desktop/src/main/stores/upload-status.ts @@ -1,10 +1,16 @@ import Store, { Schema } from "electron-store"; export interface UploadStatusStore { + /* The collection to which we're uploading, or the root collection. */ collectionName: string; + /** Paths to regular files that are pending upload */ filePaths: string[]; + /** + * Each item is the path to a zip file and the name of an entry within it. + */ + zipEntries: [zipPath: string, entryName: string][]; + /** Legacy paths to zip files, now subsumed into zipEntries */ zipPaths: string[]; - zipEntries: Record; } const uploadStatusSchema: Schema = { @@ -17,15 +23,21 @@ const uploadStatusSchema: Schema = { type: "string", }, }, + zipEntries: { + type: "array", + items: { + type: "array", + items: { + type: "string", + }, + }, + }, zipPaths: { type: "array", items: { type: "string", }, }, - zipEntries: { - type: "object", - }, }; export const uploadStatusStore = new Store({ diff --git a/web/packages/next/types/ipc.ts b/web/packages/next/types/ipc.ts index 1056610366..e340e7a061 100644 --- a/web/packages/next/types/ipc.ts +++ b/web/packages/next/types/ipc.ts @@ -465,32 +465,6 @@ export interface Electron { // - Upload - /** - * Return any pending uploads that were previously enqueued but haven't yet - * been completed. - * - * The state of pending uploads is persisted in the Node.js layer. - * - * Note that we might have both outstanding zip and regular file uploads at - * the same time. In such cases, the zip file ones get precedence. - */ - pendingUploads: () => Promise; - - /** - * Set or clear the name of the collection where the pending upload is - * directed to. - */ - setPendingUploadCollection: (collectionName: string) => Promise; - - /** - * Update the list of files (of {@link type}) associated with the pending - * upload. - */ - setPendingUploadFiles: ( - type: PendingUploads["type"], - filePaths: string[], - ) => Promise; - /** * Get the list of files that are present in the given zip file. * @@ -498,24 +472,46 @@ export interface Electron { * * @returns A list of paths, one for each file in the given zip. Directories * are traversed recursively, but the directory entries themselves will be - * excluded from the returned list. + * excluded from the returned list. File entries whose file name begins with + * a dot (i.e. "hidden" files) will also be excluded. * * To read the contents of the files themselves, see [Note: IPC streams]. */ lsZip: (zipPath: string) => Promise; - /* - * TODO: AUDIT below this - Some of the types we use below are not copyable - * across process boundaries, and such functions will (expectedly) fail at - * runtime. For such functions, find an efficient alternative or refactor - * the dataflow. + /** + * Return any pending uploads that were previously enqueued but haven't yet + * been completed. + * + * The state of pending uploads is persisted in the Node.js layer. Or app + * start, we read in this data from the Node.js layer via this IPC method. + * The Node.js code returns the persisted data after filtering out any files + * that no longer exist on disk. */ + pendingUploads: () => Promise; - // - + /** + * Set the state of pending uploads. + * + * Typically, this would be called at the start of an upload. Thereafter, as + * each item gets uploaded one by one, we'd call {@link markUploaded}. + * Finally, once the upload completes (or gets cancelled), we'd call + * {@link clearPendingUploads} to complete the circle. + */ + setPendingUploads: (pendingUploads: PendingUploads) => Promise; - getElectronFilesFromGoogleZip: ( - filePath: string, - ) => Promise; + /** + * Update the list of files (of {@link type}) associated with the pending + * upload. + */ + markUploaded: ( + pathOrZipEntry: string | [zipPath: string, entryName: string], + ) => Promise; + + /** + * Clear any pending uploads. + */ + clearPendingUploads: () => Promise; } /** @@ -622,12 +618,12 @@ export interface PendingUploads { */ filePaths: string[]; /** - * Paths of zip files that need to be uploaded. + * When the user uploads a zip file, we create a "zip entry" for each entry + * within that zip file. Such an entry is a tuple containin the path to a + * zip file itself, and the name of an entry within it. + * + * These are the remaining of those zip entries that still need to be + * uploaded. */ - zipPaths: string[]; - /** - * For each zip file, which of its entries (paths) within the zip file that - * need to be uploaded. - */ - zipEntries: Record; + zipEntries: [zipPath: string, entryName: string][]; } From 2d8bcd2530877d41f202658699175436ac6f2426 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 29 Apr 2024 11:55:53 +0530 Subject: [PATCH 061/240] Propagate --- desktop/src/main/ipc.ts | 18 ++++++--------- desktop/src/main/services/upload.ts | 20 +++++++++++------ desktop/src/main/stores/upload-status.ts | 2 +- desktop/src/preload.ts | 28 ++++++------------------ desktop/src/types/ipc.ts | 4 ++-- 5 files changed, 30 insertions(+), 42 deletions(-) diff --git a/desktop/src/main/ipc.ts b/desktop/src/main/ipc.ts index bde28d940a..6e8bbe3f71 100644 --- a/desktop/src/main/ipc.ts +++ b/desktop/src/main/ipc.ts @@ -52,12 +52,11 @@ import { saveEncryptionKey, } from "./services/store"; import { - clearPendingUploads, - getElectronFilesFromGoogleZip, lsZip, pendingUploads, - setPendingUploadCollection, - setPendingUploadFiles, + setPendingUploads, + markUploaded, + clearPendingUploads, } from "./services/upload"; import { watchAdd, @@ -204,14 +203,11 @@ export const attachIPCHandlers = () => { ipcMain.handle("pendingUploads", () => pendingUploads()); - ipcMain.handle("setPendingUploadCollection", (_, collectionName: string) => - setPendingUploadCollection(collectionName), - ); + ipcMain.handle("setPendingUploads", (_, pendingUploads: PendingUploads) => + setPendingUploads(pendingUploads), - ipcMain.handle( - "setPendingUploadFiles", - (_, type: PendingUploads["type"], filePaths: string[]) => - setPendingUploadFiles(type, filePaths), + ipcMain.handle("markUploaded", (_, pathOrZipEntry: string | [zipPath: string, entryName: string]) => + markUploaded(pathOrZipEntry), ); ipcMain.handle("clearPendingUploads", () => clearPendingUploads()); diff --git a/desktop/src/main/services/upload.ts b/desktop/src/main/services/upload.ts index 1ca7c2bc4e..66c97e34c7 100644 --- a/desktop/src/main/services/upload.ts +++ b/desktop/src/main/services/upload.ts @@ -26,7 +26,8 @@ export const lsZip = async (zipPath: string) => { return [entryPaths]; }; -export const pendingUploads = async () => { +export const pendingUploads = async (): Promise => { + /* TODO */ const collectionName = uploadStatusStore.get("collectionName"); const filePaths = validSavedPaths("files"); const zipPaths = validSavedPaths("zips"); @@ -56,7 +57,14 @@ export const pendingUploads = async () => { }; }; -export const validSavedPaths = (type: PendingUploads["type"]) => { +export const setPendingUploads = async (pendingUploads: PendingUploads) => + uploadStatusStore.set(pendingUploads); + +export const markUploaded = async ( + pathOrZipEntry: string | [zipPath: string, entryName: string], +) => {}; + +const validSavedPaths = (type: PendingUploads["type"]) => { const key = storeKey(type); const savedPaths = (uploadStatusStore.get(key) as string[]) ?? []; const paths = savedPaths.filter((p) => existsSync(p)); @@ -64,12 +72,12 @@ export const validSavedPaths = (type: PendingUploads["type"]) => { return paths; }; -export const setPendingUploadCollection = (collectionName: string) => { +const setPendingUploadCollection = (collectionName: string) => { if (collectionName) uploadStatusStore.set("collectionName", collectionName); else uploadStatusStore.delete("collectionName"); }; -export const setPendingUploadFiles = ( +const setPendingUploadFiles = ( type: PendingUploads["type"], filePaths: string[], ) => { @@ -78,9 +86,7 @@ export const setPendingUploadFiles = ( else uploadStatusStore.delete(key); }; -export const clearPendingUploads = () => { - uploadStatusStore.clear(); -}; +export const clearPendingUploads = () => uploadStatusStore.clear(); const storeKey = (type: PendingUploads["type"]): keyof UploadStatusStore => { switch (type) { diff --git a/desktop/src/main/stores/upload-status.ts b/desktop/src/main/stores/upload-status.ts index 4a402333a7..36a7d1fa72 100644 --- a/desktop/src/main/stores/upload-status.ts +++ b/desktop/src/main/stores/upload-status.ts @@ -10,7 +10,7 @@ export interface UploadStatusStore { */ zipEntries: [zipPath: string, entryName: string][]; /** Legacy paths to zip files, now subsumed into zipEntries */ - zipPaths: string[]; + zipPaths?: string[]; } const uploadStatusSchema: Schema = { diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index ad93a40f6a..c3737aceba 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -247,26 +247,16 @@ const lsZip = (zipPath: string): Promise => const pendingUploads = (): Promise => ipcRenderer.invoke("pendingUploads"); -const setPendingUploadCollection = (collectionName: string): Promise => - ipcRenderer.invoke("setPendingUploadCollection", collectionName); +const setPendingUploads = (pendingUploads: PendingUploads): Promise => + ipcRenderer.invoke("setPendingUploads", pendingUploads); -const setPendingUploadFiles = ( - type: PendingUploads["type"], - filePaths: string[], -): Promise => - ipcRenderer.invoke("setPendingUploadFiles", type, filePaths); +const markUploaded = ( + pathOrZipEntry: string | [zipPath: string, entryName: string], +): Promise => ipcRenderer.invoke("markUploaded", pathOrZipEntry); const clearPendingUploads = (): Promise => ipcRenderer.invoke("clearPendingUploads"); -// - TODO: AUDIT below this -// - - -const getElectronFilesFromGoogleZip = ( - filePath: string, -): Promise => - ipcRenderer.invoke("getElectronFilesFromGoogleZip", filePath); - /** * These objects exposed here will become available to the JS code in our * renderer (the web/ code) as `window.ElectronAPIs.*` @@ -378,11 +368,7 @@ contextBridge.exposeInMainWorld("electron", { lsZip, pendingUploads, - setPendingUploadCollection, - setPendingUploadFiles, + setPendingUploads, + markUploaded, clearPendingUploads, - - // - - - getElectronFilesFromGoogleZip, }); diff --git a/desktop/src/types/ipc.ts b/desktop/src/types/ipc.ts index 3fa375eabf..f343e2bba3 100644 --- a/desktop/src/types/ipc.ts +++ b/desktop/src/types/ipc.ts @@ -27,8 +27,8 @@ export interface FolderWatchSyncedFile { export interface PendingUploads { collectionName: string; - type: "files" | "zips"; - files: ElectronFile[]; + filePaths: string[]; + zipEntries: [zipPath: string, entryName: string][]; } /** From b12e6221d431a404029f617bef1f5d7be93258e9 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 29 Apr 2024 13:46:18 +0530 Subject: [PATCH 062/240] [mob][photos] Change button type to neutral --- mobile/lib/ui/cast/choose.dart | 4 ++-- mobile/plugins/ente_cast_normal/lib/src/service.dart | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/mobile/lib/ui/cast/choose.dart b/mobile/lib/ui/cast/choose.dart index 1cfd275c89..7f02887334 100644 --- a/mobile/lib/ui/cast/choose.dart +++ b/mobile/lib/ui/cast/choose.dart @@ -38,7 +38,7 @@ class _CastChooseDialogState extends State { ButtonWidget( labelText: S.of(context).autoPair, icon: Icons.cast_outlined, - buttonType: ButtonType.primary, + buttonType: ButtonType.neutral, buttonSize: ButtonSize.large, shouldStickToDarkTheme: true, buttonAction: ButtonAction.first, @@ -56,7 +56,7 @@ class _CastChooseDialogState extends State { const SizedBox(height: 12), ButtonWidget( labelText: S.of(context).pairWithPin, - buttonType: ButtonType.primary, + buttonType: ButtonType.neutral, // icon for pairing with TV manually icon: Icons.tv_outlined, buttonSize: ButtonSize.large, diff --git a/mobile/plugins/ente_cast_normal/lib/src/service.dart b/mobile/plugins/ente_cast_normal/lib/src/service.dart index 454af58adf..34787734b6 100644 --- a/mobile/plugins/ente_cast_normal/lib/src/service.dart +++ b/mobile/plugins/ente_cast_normal/lib/src/service.dart @@ -78,14 +78,13 @@ class CastServiceImpl extends CastService { }).timeout( const Duration(seconds: 5), onTimeout: () { - print('sendMessage timed out after 5 seconds'); + debugPrint('sendMessage timed out after 5 seconds'); }, ); debugPrint("close session ${session.sessionId}"); session.close(); } CastSessionManager().sessions.clear(); - debugPrint("send close message"); return Future.value(); } From 3b6204f47dd5dc9fad5965bc7416433429af7964 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 29 Apr 2024 13:49:02 +0530 Subject: [PATCH 063/240] Take 2 --- desktop/src/main/ipc.ts | 23 +++++++++++++---------- desktop/src/main/services/upload.ts | 18 +++++++++++++++--- desktop/src/preload.ts | 12 ++++++++---- web/packages/next/types/ipc.ts | 13 +++++++++---- 4 files changed, 45 insertions(+), 21 deletions(-) diff --git a/desktop/src/main/ipc.ts b/desktop/src/main/ipc.ts index 6e8bbe3f71..271577aa04 100644 --- a/desktop/src/main/ipc.ts +++ b/desktop/src/main/ipc.ts @@ -52,11 +52,12 @@ import { saveEncryptionKey, } from "./services/store"; import { + clearPendingUploads, lsZip, + markUploadedFiles, + markUploadedZipEntries, pendingUploads, setPendingUploads, - markUploaded, - clearPendingUploads, } from "./services/upload"; import { watchAdd, @@ -205,18 +206,20 @@ export const attachIPCHandlers = () => { ipcMain.handle("setPendingUploads", (_, pendingUploads: PendingUploads) => setPendingUploads(pendingUploads), + ); - ipcMain.handle("markUploaded", (_, pathOrZipEntry: string | [zipPath: string, entryName: string]) => - markUploaded(pathOrZipEntry), + ipcMain.handle( + "markUploadedFiles", + (_, paths: PendingUploads["filePaths"]) => markUploadedFiles(paths), + ); + + ipcMain.handle( + "markUploadedZipEntries", + (_, zipEntries: PendingUploads["zipEntries"]) => + markUploadedZipEntries(zipEntries), ); ipcMain.handle("clearPendingUploads", () => clearPendingUploads()); - - // - - - ipcMain.handle("getElectronFilesFromGoogleZip", (_, filePath: string) => - getElectronFilesFromGoogleZip(filePath), - ); }; /** diff --git a/desktop/src/main/services/upload.ts b/desktop/src/main/services/upload.ts index 66c97e34c7..ebd1f481f1 100644 --- a/desktop/src/main/services/upload.ts +++ b/desktop/src/main/services/upload.ts @@ -60,9 +60,21 @@ export const pendingUploads = async (): Promise => { export const setPendingUploads = async (pendingUploads: PendingUploads) => uploadStatusStore.set(pendingUploads); -export const markUploaded = async ( - pathOrZipEntry: string | [zipPath: string, entryName: string], -) => {}; +export const markUploadedFiles = async (paths: string[]) => { + const existing = uploadStatusStore.get("filePaths"); + const updated = existing.filter((p) => !paths.includes(p)); + uploadStatusStore.set("filePaths", updated); +}; + +export const markUploadedZipEntries = async ( + entries: [zipPath: string, entryName: string][], +) => { + const existing = uploadStatusStore.get("zipEntries"); + const updated = existing.filter( + (z) => !entries.some((e) => z[0] == e[0] && z[1] == e[1]), + ); + uploadStatusStore.set("zipEntries", updated); +}; const validSavedPaths = (type: PendingUploads["type"]) => { const key = storeKey(type); diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index c3737aceba..484a3bc0e2 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -250,9 +250,12 @@ const pendingUploads = (): Promise => const setPendingUploads = (pendingUploads: PendingUploads): Promise => ipcRenderer.invoke("setPendingUploads", pendingUploads); -const markUploaded = ( - pathOrZipEntry: string | [zipPath: string, entryName: string], -): Promise => ipcRenderer.invoke("markUploaded", pathOrZipEntry); +const markUploadedFiles = (paths: PendingUploads["filePaths"]): Promise => + ipcRenderer.invoke("markUploadedFiles", paths); + +const markUploadedZipEntries = ( + zipEntries: PendingUploads["zipEntries"], +): Promise => ipcRenderer.invoke("markUploadedZipEntries", zipEntries); const clearPendingUploads = (): Promise => ipcRenderer.invoke("clearPendingUploads"); @@ -369,6 +372,7 @@ contextBridge.exposeInMainWorld("electron", { lsZip, pendingUploads, setPendingUploads, - markUploaded, + markUploadedFiles, + markUploadedZipEntries, clearPendingUploads, }); diff --git a/web/packages/next/types/ipc.ts b/web/packages/next/types/ipc.ts index e340e7a061..761cd72f1d 100644 --- a/web/packages/next/types/ipc.ts +++ b/web/packages/next/types/ipc.ts @@ -501,11 +501,16 @@ export interface Electron { setPendingUploads: (pendingUploads: PendingUploads) => Promise; /** - * Update the list of files (of {@link type}) associated with the pending - * upload. + * Mark the given files (given by their {@link paths}) as having been + * uploaded. */ - markUploaded: ( - pathOrZipEntry: string | [zipPath: string, entryName: string], + markUploadedFiles: (paths: PendingUploads["filePaths"]) => Promise; + + /** + * Mark the given zip file entries as having been uploaded. + */ + markUploadedZipEntries: ( + entries: PendingUploads["zipEntries"], ) => Promise; /** From bd07759d8ee2341551a6a596f624172fed276576 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 29 Apr 2024 14:05:41 +0530 Subject: [PATCH 064/240] [mob][photos] Show loading indicator on device tap --- mobile/lib/ui/cast/auto.dart | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/mobile/lib/ui/cast/auto.dart b/mobile/lib/ui/cast/auto.dart index 4dc9d5bd01..ee5974f00c 100644 --- a/mobile/lib/ui/cast/auto.dart +++ b/mobile/lib/ui/cast/auto.dart @@ -23,11 +23,11 @@ class AutoCastDialog extends StatefulWidget { class _AutoCastDialogState extends State { final bool doesUserExist = true; + final Set _isDeviceTapInProgress = {}; @override Widget build(BuildContext context) { final textStyle = getEnteTextTheme(context); - final AlertDialog alert = AlertDialog( title: Text( S.of(context).connectToDevice, @@ -65,19 +65,39 @@ class _AutoCastDialogState extends State { } return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: snapshot.data!.map((result) { final device = result.$2; final name = result.$1; return GestureDetector( onTap: () async { + if (_isDeviceTapInProgress.contains(device)) { + return; + } + setState(() { + _isDeviceTapInProgress.add(device); + }); try { await _connectToYourApp(context, device); } catch (e) { showGenericErrorDialog(context: context, error: e) .ignore(); + } finally { + setState(() { + _isDeviceTapInProgress.remove(device); + }); } }, - child: Text(name), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + children: [ + Expanded(child: Text(name)), + if (_isDeviceTapInProgress.contains(device)) + const EnteLoadingWidget(), + ], + ), + ), ); }).toList(), ); @@ -93,6 +113,8 @@ class _AutoCastDialogState extends State { BuildContext context, Object castDevice, ) async { + // sleep for 10 seconds + await Future.delayed(const Duration(seconds: 10)); await castService.connectDevice( context, castDevice, From 16888c8aadee6a3c078ec300416104f1ca8214c2 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 29 Apr 2024 14:14:23 +0530 Subject: [PATCH 065/240] [mob][photos] Fix lint warning --- mobile/plugins/ente_cast_normal/lib/src/service.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/plugins/ente_cast_normal/lib/src/service.dart b/mobile/plugins/ente_cast_normal/lib/src/service.dart index 34787734b6..1f98fa7464 100644 --- a/mobile/plugins/ente_cast_normal/lib/src/service.dart +++ b/mobile/plugins/ente_cast_normal/lib/src/service.dart @@ -26,7 +26,7 @@ class CastServiceImpl extends CastService { ); session.sendMessage(_pairRequestNamespace, {}); } else { - if (onMessage != null && message!.containsKey("code")) { + if (onMessage != null && message.containsKey("code")) { onMessage( { CastMessageType.pairCode: message, From e258aa352491d8246979de89506894b7bac488fb Mon Sep 17 00:00:00 2001 From: BifrostTenmei <156937334+BifrostTenmei@users.noreply.github.com> Date: Mon, 29 Apr 2024 10:53:20 +0200 Subject: [PATCH 066/240] Fixed typo --- desktop/docs/dependencies.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/docs/dependencies.md b/desktop/docs/dependencies.md index b159b13eb4..5c6b222b08 100644 --- a/desktop/docs/dependencies.md +++ b/desktop/docs/dependencies.md @@ -13,7 +13,7 @@ Electron embeds Chromium and Node.js in the generated app's binary. The generated app thus consists of two separate processes - the _main_ process, and a _renderer_ process. -- The _main_ process is runs the embedded node. This process can deal with the +- The _main_ process runs the embedded node. This process can deal with the host OS - it is conceptually like a `node` repl running on your machine. In our case, the TypeScript code (in the `src/` directory) gets transpiled by `tsc` into JavaScript in the `build/app/` directory, which gets bundled in From 3d298a9cd40c50e4bfc0914002be182323dae779 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 29 Apr 2024 14:23:33 +0530 Subject: [PATCH 067/240] separate type --- desktop/src/main/ipc.ts | 4 +-- desktop/src/main/services/upload.ts | 32 +++++++++++++---- desktop/src/main/stores/upload-status.ts | 4 ++- desktop/src/preload.ts | 7 ++-- desktop/src/types/ipc.ts | 4 ++- web/packages/next/types/ipc.ts | 44 +++++++++++++++--------- 6 files changed, 65 insertions(+), 30 deletions(-) diff --git a/desktop/src/main/ipc.ts b/desktop/src/main/ipc.ts index 271577aa04..d7d4fdc098 100644 --- a/desktop/src/main/ipc.ts +++ b/desktop/src/main/ipc.ts @@ -53,11 +53,11 @@ import { } from "./services/store"; import { clearPendingUploads, - lsZip, markUploadedFiles, markUploadedZipEntries, pendingUploads, setPendingUploads, + zipEntries, } from "./services/upload"; import { watchAdd, @@ -200,7 +200,7 @@ export const attachIPCHandlers = () => { // - Upload - ipcMain.handle("lsZip", (_, zipPath: string) => lsZip(zipPath)); + ipcMain.handle("zipEntries", (_, zipPath: string) => zipEntries(zipPath)); ipcMain.handle("pendingUploads", () => pendingUploads()); diff --git a/desktop/src/main/services/upload.ts b/desktop/src/main/services/upload.ts index ebd1f481f1..c5a987e7bc 100644 --- a/desktop/src/main/services/upload.ts +++ b/desktop/src/main/services/upload.ts @@ -1,35 +1,53 @@ import StreamZip from "node-stream-zip"; import { existsSync } from "original-fs"; import path from "path"; -import { ElectronFile, type PendingUploads } from "../../types/ipc"; +import type { ElectronFile, PendingUploads, ZipEntry } from "../../types/ipc"; import { uploadStatusStore, type UploadStatusStore, } from "../stores/upload-status"; import { getElectronFile, getZipFileStream } from "./fs"; -export const lsZip = async (zipPath: string) => { +export const zipEntries = async (zipPath: string): Promise => { const zip = new StreamZip.async({ file: zipPath }); const entries = await zip.entries(); - const entryPaths: string[] = []; + const entryNames: string[] = []; for (const entry of Object.values(entries)) { const basename = path.basename(entry.name); // Ignore "hidden" files (files whose names begins with a dot). if (entry.isFile && basename.length > 0 && basename[0] != ".") { // `entry.name` is the path within the zip. - entryPaths.push(entry.name); + entryNames.push(entry.name); } } - return [entryPaths]; + return entryNames.map((entryName) => [zipPath, entryName]); }; export const pendingUploads = async (): Promise => { - /* TODO */ const collectionName = uploadStatusStore.get("collectionName"); - const filePaths = validSavedPaths("files"); + if (!collectionName) return undefined; + + const allFilePaths = uploadStatusStore.get("filePaths"); + const filePaths = allFilePaths.filter((f) => existsSync(f)); + + let allZipEntries = uploadStatusStore.get("zipEntries"); + // Migration code - May 2024. Remove after a bit. + // + // The older store formats will not have zipEntries and instead will have + // zipPaths. If we find such a case, read the zipPaths and enqueue all of + // their files as zipEntries in the result. This potentially can be cause us + // to try reuploading an already uploaded file, but the dedup logic will + // kick in at that point so no harm will come off it. + if (allZipEntries === undefined) { + const allZipPaths = uploadStatusStore.get("filePaths"); + const zipPaths = allZipPaths.filter((f) => existsSync(f)); + lsZip(); + } + + if (allZipEntries) "files"; const zipPaths = validSavedPaths("zips"); let files: ElectronFile[] = []; diff --git a/desktop/src/main/stores/upload-status.ts b/desktop/src/main/stores/upload-status.ts index 36a7d1fa72..edd086fbef 100644 --- a/desktop/src/main/stores/upload-status.ts +++ b/desktop/src/main/stores/upload-status.ts @@ -7,8 +7,10 @@ export interface UploadStatusStore { filePaths: string[]; /** * Each item is the path to a zip file and the name of an entry within it. + * + * This is marked optional since legacy stores will not have it. */ - zipEntries: [zipPath: string, entryName: string][]; + zipEntries?: [zipPath: string, entryName: string][]; /** Legacy paths to zip files, now subsumed into zipEntries */ zipPaths?: string[]; } diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index 484a3bc0e2..ac149ad133 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -47,6 +47,7 @@ import type { ElectronFile, FolderWatch, PendingUploads, + ZipEntry, } from "./types/ipc"; // - General @@ -241,8 +242,8 @@ const watchFindFiles = (folderPath: string): Promise => // - Upload -const lsZip = (zipPath: string): Promise => - ipcRenderer.invoke("lsZip", zipPath); +const zipEntries = (zipPath: string): Promise => + ipcRenderer.invoke("zipEntries", zipPath); const pendingUploads = (): Promise => ipcRenderer.invoke("pendingUploads"); @@ -369,7 +370,7 @@ contextBridge.exposeInMainWorld("electron", { // - Upload - lsZip, + zipEntries, pendingUploads, setPendingUploads, markUploadedFiles, diff --git a/desktop/src/types/ipc.ts b/desktop/src/types/ipc.ts index f343e2bba3..307fb7de32 100644 --- a/desktop/src/types/ipc.ts +++ b/desktop/src/types/ipc.ts @@ -25,10 +25,12 @@ export interface FolderWatchSyncedFile { collectionID: number; } +export type ZipEntry = [zipPath: string, entryName: string]; + export interface PendingUploads { collectionName: string; filePaths: string[]; - zipEntries: [zipPath: string, entryName: string][]; + zipEntries: ZipEntry[]; } /** diff --git a/web/packages/next/types/ipc.ts b/web/packages/next/types/ipc.ts index 761cd72f1d..6aa394c6c5 100644 --- a/web/packages/next/types/ipc.ts +++ b/web/packages/next/types/ipc.ts @@ -470,19 +470,22 @@ export interface Electron { * * @param zipPath The path of the zip file on the user's local file system. * - * @returns A list of paths, one for each file in the given zip. Directories - * are traversed recursively, but the directory entries themselves will be - * excluded from the returned list. File entries whose file name begins with - * a dot (i.e. "hidden" files) will also be excluded. + * @returns A list of (zipPath, entryName) tuples, one for each file in the + * given zip. Directories are traversed recursively, but the directory + * entries themselves will be excluded from the returned list. File entries + * whose file name begins with a dot (i.e. "hidden" files) will also be + * excluded. * * To read the contents of the files themselves, see [Note: IPC streams]. */ - lsZip: (zipPath: string) => Promise; + zipEntries : (zipPath: string) => Promise /** * Return any pending uploads that were previously enqueued but haven't yet * been completed. * + * Return undefined if there are no such pending uploads. + * * The state of pending uploads is persisted in the Node.js layer. Or app * start, we read in this data from the Node.js layer via this IPC method. * The Node.js code returns the persisted data after filtering out any files @@ -493,10 +496,13 @@ export interface Electron { /** * Set the state of pending uploads. * - * Typically, this would be called at the start of an upload. Thereafter, as - * each item gets uploaded one by one, we'd call {@link markUploaded}. - * Finally, once the upload completes (or gets cancelled), we'd call - * {@link clearPendingUploads} to complete the circle. + * - Typically, this would be called at the start of an upload. + * + * - Thereafter, as each item gets uploaded one by one, we'd call + * {@link markUploadedFiles} or {@link markUploadedZipEntries}. + * + * - Finally, once the upload completes (or gets cancelled), we'd call + * {@link clearPendingUploads} to complete the circle. */ setPendingUploads: (pendingUploads: PendingUploads) => Promise; @@ -601,6 +607,17 @@ export interface FolderWatchSyncedFile { collectionID: number; } +/** + * When the user uploads a zip file, we create a "zip entry" for each entry + * within that zip file. Such an entry is a tuple containin the path to a zip + * file itself, and the name of an entry within it. + * + * The name of the entry is not just the file name, but rather is the full path + * of the file within the zip. That is, each entry name uniquely identifies a + * particular file within the given zip. + */ +export type ZipEntry = [zipPath: string, entryName: string]; + /** * State about pending and in-progress uploads. * @@ -623,12 +640,7 @@ export interface PendingUploads { */ filePaths: string[]; /** - * When the user uploads a zip file, we create a "zip entry" for each entry - * within that zip file. Such an entry is a tuple containin the path to a - * zip file itself, and the name of an entry within it. - * - * These are the remaining of those zip entries that still need to be - * uploaded. + * {@link ZipEntry} (zip path and entry name) that need to be uploaded. */ - zipEntries: [zipPath: string, entryName: string][]; + zipEntries: ZipEntry[]; } From 2fa1fcac655251b523d55785cdaed8bb222124eb Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 29 Apr 2024 14:34:05 +0530 Subject: [PATCH 068/240] impl --- desktop/src/main/ipc.ts | 6 ++- desktop/src/main/services/upload.ts | 75 +++++------------------------ desktop/src/preload.ts | 6 +-- web/packages/next/types/ipc.ts | 2 +- 4 files changed, 21 insertions(+), 68 deletions(-) diff --git a/desktop/src/main/ipc.ts b/desktop/src/main/ipc.ts index d7d4fdc098..a99a32d097 100644 --- a/desktop/src/main/ipc.ts +++ b/desktop/src/main/ipc.ts @@ -53,11 +53,11 @@ import { } from "./services/store"; import { clearPendingUploads, + listZipEntries, markUploadedFiles, markUploadedZipEntries, pendingUploads, setPendingUploads, - zipEntries, } from "./services/upload"; import { watchAdd, @@ -200,7 +200,9 @@ export const attachIPCHandlers = () => { // - Upload - ipcMain.handle("zipEntries", (_, zipPath: string) => zipEntries(zipPath)); + ipcMain.handle("listZipEntries", (_, zipPath: string) => + listZipEntries(zipPath), + ); ipcMain.handle("pendingUploads", () => pendingUploads()); diff --git a/desktop/src/main/services/upload.ts b/desktop/src/main/services/upload.ts index c5a987e7bc..8487f8327e 100644 --- a/desktop/src/main/services/upload.ts +++ b/desktop/src/main/services/upload.ts @@ -2,13 +2,10 @@ import StreamZip from "node-stream-zip"; import { existsSync } from "original-fs"; import path from "path"; import type { ElectronFile, PendingUploads, ZipEntry } from "../../types/ipc"; -import { - uploadStatusStore, - type UploadStatusStore, -} from "../stores/upload-status"; -import { getElectronFile, getZipFileStream } from "./fs"; +import { uploadStatusStore } from "../stores/upload-status"; +import { getZipFileStream } from "./fs"; -export const zipEntries = async (zipPath: string): Promise => { +export const listZipEntries = async (zipPath: string): Promise => { const zip = new StreamZip.async({ file: zipPath }); const entries = await zip.entries(); @@ -33,7 +30,9 @@ export const pendingUploads = async (): Promise => { const allFilePaths = uploadStatusStore.get("filePaths"); const filePaths = allFilePaths.filter((f) => existsSync(f)); - let allZipEntries = uploadStatusStore.get("zipEntries"); + const allZipEntries = uploadStatusStore.get("zipEntries"); + let zipEntries: typeof allZipEntries; + // Migration code - May 2024. Remove after a bit. // // The older store formats will not have zipEntries and instead will have @@ -44,34 +43,17 @@ export const pendingUploads = async (): Promise => { if (allZipEntries === undefined) { const allZipPaths = uploadStatusStore.get("filePaths"); const zipPaths = allZipPaths.filter((f) => existsSync(f)); - lsZip(); - } - - if (allZipEntries) "files"; - const zipPaths = validSavedPaths("zips"); - - let files: ElectronFile[] = []; - let type: PendingUploads["type"]; - - if (zipPaths.length) { - type = "zips"; - for (const zipPath of zipPaths) { - files = [ - ...files, - ...(await getElectronFilesFromGoogleZip(zipPath)), - ]; - } - const pendingFilePaths = new Set(filePaths); - files = files.filter((file) => pendingFilePaths.has(file.path)); - } else if (filePaths.length) { - type = "files"; - files = await Promise.all(filePaths.map(getElectronFile)); + zipEntries = []; + for (const zip of zipPaths) + zipEntries = zipEntries.concat(await listZipEntries(zip)); + } else { + zipEntries = allZipEntries.filter(([z]) => existsSync(z)); } return { - files, collectionName, - type, + filePaths, + zipEntries, }; }; @@ -94,39 +76,8 @@ export const markUploadedZipEntries = async ( uploadStatusStore.set("zipEntries", updated); }; -const validSavedPaths = (type: PendingUploads["type"]) => { - const key = storeKey(type); - const savedPaths = (uploadStatusStore.get(key) as string[]) ?? []; - const paths = savedPaths.filter((p) => existsSync(p)); - uploadStatusStore.set(key, paths); - return paths; -}; - -const setPendingUploadCollection = (collectionName: string) => { - if (collectionName) uploadStatusStore.set("collectionName", collectionName); - else uploadStatusStore.delete("collectionName"); -}; - -const setPendingUploadFiles = ( - type: PendingUploads["type"], - filePaths: string[], -) => { - const key = storeKey(type); - if (filePaths) uploadStatusStore.set(key, filePaths); - else uploadStatusStore.delete(key); -}; - export const clearPendingUploads = () => uploadStatusStore.clear(); -const storeKey = (type: PendingUploads["type"]): keyof UploadStatusStore => { - switch (type) { - case "zips": - return "zipPaths"; - case "files": - return "filePaths"; - } -}; - export const getElectronFilesFromGoogleZip = async (filePath: string) => { const zip = new StreamZip.async({ file: filePath, diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index ac149ad133..e80625de99 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -242,8 +242,8 @@ const watchFindFiles = (folderPath: string): Promise => // - Upload -const zipEntries = (zipPath: string): Promise => - ipcRenderer.invoke("zipEntries", zipPath); +const listZipEntries = (zipPath: string): Promise => + ipcRenderer.invoke("listZipEntries", zipPath); const pendingUploads = (): Promise => ipcRenderer.invoke("pendingUploads"); @@ -370,7 +370,7 @@ contextBridge.exposeInMainWorld("electron", { // - Upload - zipEntries, + listZipEntries, pendingUploads, setPendingUploads, markUploadedFiles, diff --git a/web/packages/next/types/ipc.ts b/web/packages/next/types/ipc.ts index 6aa394c6c5..7198a2ebce 100644 --- a/web/packages/next/types/ipc.ts +++ b/web/packages/next/types/ipc.ts @@ -478,7 +478,7 @@ export interface Electron { * * To read the contents of the files themselves, see [Note: IPC streams]. */ - zipEntries : (zipPath: string) => Promise + listZipEntries : (zipPath: string) => Promise /** * Return any pending uploads that were previously enqueued but haven't yet From d94f0a0f56c7689039674f9b43683ac6ee1ad720 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 29 Apr 2024 15:08:46 +0530 Subject: [PATCH 069/240] wip --- .../photos/src/components/Upload/Uploader.tsx | 78 ++++++++----------- web/packages/next/types/file.ts | 10 +++ 2 files changed, 44 insertions(+), 44 deletions(-) diff --git a/web/apps/photos/src/components/Upload/Uploader.tsx b/web/apps/photos/src/components/Upload/Uploader.tsx index 2bf1e79d20..1f36216296 100644 --- a/web/apps/photos/src/components/Upload/Uploader.tsx +++ b/web/apps/photos/src/components/Upload/Uploader.tsx @@ -1,6 +1,6 @@ import log from "@/next/log"; -import { ElectronFile } from "@/next/types/file"; -import type { CollectionMapping, Electron } from "@/next/types/ipc"; +import { ElectronFile, type FileAndPath } from "@/next/types/file"; +import type { CollectionMapping, ZipEntry } from "@/next/types/ipc"; import { CustomError } from "@ente/shared/error"; import { isPromise } from "@ente/shared/utils"; import DiscFullIcon from "@mui/icons-material/DiscFull"; @@ -123,21 +123,40 @@ export default function Uploader(props: Props) { * browser. */ const [webFiles, setWebFiles] = useState([]); + /** + * {@link File}s that the user drag-dropped or selected for uploads, + * augmented with their paths. These siblings of {@link webFiles} come into + * play when we are running in the context of our desktop app. + */ + const [desktopFiles, setDesktopFiles] = useState([]); /** * Paths of file to upload that we've received over the IPC bridge from the * code running in the Node.js layer of our desktop app. + * + * Unlike {@link filesWithPaths} which are still user initiated, + * {@link desktopFilePaths} can be set via programmatic action. For example, + * if the user has setup a folder watch, and a new file is added on their + * local filesystem in one of the watched folders, then the relevant path of + * the new file would get added to {@link desktopFilePaths}. */ const [desktopFilePaths, setDesktopFilePaths] = useState([]); /** - * TODO(MR): When? + * (zip file path, entry within zip file) tuples for zip files that the user + * is trying to upload. These are only set when we are running in the + * context of our desktop app. They may be set either on a user action (when + * the user selects or drag-drops zip files) or programmatically (when the + * app is trying to resume pending uploads from a previous session). */ - const [electronFiles, setElectronFiles] = useState([]); + const [desktopZipEntries, setDesktopZipEntries] = useState([]); /** - * Consolidated and cleaned list obtained from {@link webFiles} and - * {@link desktopFilePaths}. + * Consolidated and cleaned list obtained from {@link webFiles}, + * {@link desktopFiles}, {@link desktopFilePaths} and + * {@link desktopZipEntries}. */ - const fileOrPathsToUpload = useRef<(File | string)[]>([]); + const itemsToUpload = useRef<(File | FileAndPath | string | ZipEntry)[]>( + [], + ); /** * If true, then the next upload we'll be processing was initiated by our @@ -151,9 +170,12 @@ export default function Uploader(props: Props) { */ const pendingDesktopUploadCollectionName = useRef(""); - // This is set when the user choses a type to upload from the upload type selector dialog + /** + * This is set to thue user's choice when the user chooses one of the + * predefined type to upload from the upload type selector dialog + */ const pickedUploadType = useRef(null); - const zipPaths = useRef(null); + const currentUploadPromise = useRef>(null); const uploadRunning = useRef(false); const uploaderNameRef = useRef(null); @@ -778,31 +800,11 @@ export default function Uploader(props: Props) { } }; - const handleDesktopUpload = async ( - type: PICKED_UPLOAD_TYPE, - electron: Electron, - ) => { - let files: ElectronFile[]; - pickedUploadType.current = type; - if (type === PICKED_UPLOAD_TYPE.FILES) { - files = await electron.showUploadFilesDialog(); - } else if (type === PICKED_UPLOAD_TYPE.FOLDERS) { - files = await electron.showUploadDirsDialog(); - } else { - const response = await electron.showUploadZipDialog(); - files = response.files; - zipPaths.current = response.zipPaths; - } - if (files?.length > 0) { - log.info( - ` desktop upload for type:${type} and fileCount: ${files?.length} requested`, - ); - setElectronFiles(files); - props.closeUploadTypeSelector(); - } + const cancelUploads = () => { + uploadManager.cancelRunningUpload(); }; - const handleWebUpload = async (type: PICKED_UPLOAD_TYPE) => { + const handleUpload = async (type: PICKED_UPLOAD_TYPE) => { pickedUploadType.current = type; if (type === PICKED_UPLOAD_TYPE.FILES) { props.showUploadFilesDialog(); @@ -817,18 +819,6 @@ export default function Uploader(props: Props) { } }; - const cancelUploads = () => { - uploadManager.cancelRunningUpload(); - }; - - const handleUpload = (type) => () => { - if (electron) { - handleDesktopUpload(type, electron); - } else { - handleWebUpload(type); - } - }; - const handleFileUpload = handleUpload(PICKED_UPLOAD_TYPE.FILES); const handleFolderUpload = handleUpload(PICKED_UPLOAD_TYPE.FOLDERS); const handleZipUpload = handleUpload(PICKED_UPLOAD_TYPE.ZIPS); diff --git a/web/packages/next/types/file.ts b/web/packages/next/types/file.ts index 75641e3a27..5d6b62550d 100644 --- a/web/packages/next/types/file.ts +++ b/web/packages/next/types/file.ts @@ -16,6 +16,16 @@ export interface ElectronFile { arrayBuffer: () => Promise; } +/** + * When we are running in the context of our desktop app, we have access to the + * absolute path of the file under consideration. This type combines these two + * bits of information to remove the need to query it again and again. + */ +export interface FileAndPath { + file: File; + path: string; +} + export interface EventQueueItem { type: "upload" | "trash"; folderPath: string; From 864a53afa2c7f70f14a8cbf30719e6fa9a3b8554 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 29 Apr 2024 15:13:52 +0530 Subject: [PATCH 070/240] more --- .../photos/src/components/Upload/Uploader.tsx | 50 +++++++------------ web/apps/photos/src/pages/gallery/index.tsx | 30 +++++------ .../photos/src/pages/shared-albums/index.tsx | 16 +++--- 3 files changed, 43 insertions(+), 53 deletions(-) diff --git a/web/apps/photos/src/components/Upload/Uploader.tsx b/web/apps/photos/src/components/Upload/Uploader.tsx index 1f36216296..d8087eb5b0 100644 --- a/web/apps/photos/src/components/Upload/Uploader.tsx +++ b/web/apps/photos/src/components/Upload/Uploader.tsx @@ -74,13 +74,13 @@ interface Props { isFirstUpload?: boolean; uploadTypeSelectorView: boolean; showSessionExpiredMessage: () => void; - showUploadFilesDialog: () => void; - showUploadDirsDialog: () => void; - showUploadZipFilesDialog?: () => void; - webFolderSelectorFiles: File[]; - webFileSelectorFiles: File[]; - webFileSelectorZipFiles?: File[]; dragAndDropFiles: File[]; + openFileSelector: () => void; + fileSelectorFiles: File[]; + openFolderSelector: () => void; + folderSelectorFiles: File[]; + openZipFileSelector?: () => void; + fileSelectorZipFiles?: File[]; uploadCollection?: Collection; uploadTypeSelectorIntent: UploadTypeSelectorIntent; activeCollection?: Collection; @@ -239,16 +239,16 @@ export default function Uploader(props: Props) { watcher.init(upload, requestSyncWithRemote); electron.pendingUploads().then((pending) => { - if (pending) { - log.info("Resuming pending desktop upload", pending); - resumeDesktopUpload( - pending.type == "files" - ? PICKED_UPLOAD_TYPE.FILES - : PICKED_UPLOAD_TYPE.ZIPS, - pending.files, - pending.collectionName, - ); - } + if (!pending) return; + + const { collectionName, filePaths, zipEntries } = pending; + if (filePaths.length == 0 && zipEntries.length == 0) return; + + log.info("Resuming pending upload", pending); + isPendingDesktopUpload.current = true; + pendingDesktopUploadCollectionName.current = collectionName; + setDesktopFilePaths(filePaths); + setDesktopZipEntries(zipEntries); }); } }, [ @@ -258,9 +258,8 @@ export default function Uploader(props: Props) { appContext.isCFProxyDisabled, ]); - // this handles the change of selectorFiles changes on web when user selects - // files for upload through the opened file/folder selector or dragAndDrop them - // the webFiles state is update which triggers the upload of those files + // Handle selected files when user selects files for upload through the open + // file / open folder selection dialog, or drag-and-drops them. useEffect(() => { if (appContext.watchFolderView) { // if watch folder dialog is open don't catch the dropped file @@ -463,19 +462,6 @@ export default function Uploader(props: Props) { } }, [webFiles, appContext.sharedFiles, electronFiles, desktopFilePaths]); - const resumeDesktopUpload = async ( - type: PICKED_UPLOAD_TYPE, - electronFiles: ElectronFile[], - collectionName: string, - ) => { - if (electronFiles && electronFiles?.length > 0) { - isPendingDesktopUpload.current = true; - pendingDesktopUploadCollectionName.current = collectionName; - pickedUploadType.current = type; - setElectronFiles(electronFiles); - } - }; - const preCollectionCreationAction = async () => { props.closeCollectionSelector?.(); props.setShouldDisableDropzone(!uploadManager.shouldAllowNewUpload()); diff --git a/web/apps/photos/src/pages/gallery/index.tsx b/web/apps/photos/src/pages/gallery/index.tsx index 658f62b459..70b48c3cc6 100644 --- a/web/apps/photos/src/pages/gallery/index.tsx +++ b/web/apps/photos/src/pages/gallery/index.tsx @@ -211,26 +211,26 @@ export default function Gallery() { disabled: shouldDisableDropzone, }); const { - selectedFiles: webFileSelectorFiles, + selectedFiles: fileSelectorFiles, open: openFileSelector, getInputProps: getFileSelectorInputProps, } = useFileInput({ directory: false, }); const { - selectedFiles: webFolderSelectorFiles, + selectedFiles: folderSelectorFiles, open: openFolderSelector, getInputProps: getFolderSelectorInputProps, } = useFileInput({ directory: true, }); const { - selectedFiles: webFileSelectorZipFiles, + selectedFiles: fileSelectorZipFiles, open: openZipFileSelector, getInputProps: getZipFileSelectorInputProps, } = useFileInput({ directory: false, - accept: ".zip" + accept: ".zip", }); const [isInSearchMode, setIsInSearchMode] = useState(false); @@ -1121,7 +1121,6 @@ export default function Gallery() { null, false, )} - uploadTypeSelectorIntent={uploadTypeSelectorIntent} setLoading={setBlockingLoad} setCollectionNamerAttributes={setCollectionNamerAttributes} setShouldDisableDropzone={setShouldDisableDropzone} @@ -1130,15 +1129,18 @@ export default function Gallery() { isFirstUpload={ !hasNonSystemCollections(collectionSummaries) } - webFileSelectorFiles={webFileSelectorFiles} - webFolderSelectorFiles={webFolderSelectorFiles} - webFileSelectorZipFiles={webFileSelectorZipFiles} - dragAndDropFiles={dragAndDropFiles} - uploadTypeSelectorView={uploadTypeSelectorView} - showUploadFilesDialog={openFileSelector} - showUploadDirsDialog={openFolderSelector} - showUploadZipFilesDialog={openZipFileSelector} - showSessionExpiredMessage={showSessionExpiredMessage} + {...{ + dragAndDropFiles, + openFileSelector, + fileSelectorFiles, + openFolderSelector, + folderSelectorFiles, + openZipFileSelector, + fileSelectorZipFiles, + uploadTypeSelectorIntent, + uploadTypeSelectorView, + showSessionExpiredMessage, + }} /> Date: Mon, 29 Apr 2024 15:43:17 +0530 Subject: [PATCH 071/240] more --- desktop/src/preload.ts | 5 +- .../photos/src/components/Upload/Uploader.tsx | 149 ++++++------------ web/packages/next/types/file.ts | 5 +- web/packages/next/types/ipc.ts | 14 +- 4 files changed, 67 insertions(+), 106 deletions(-) diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index e80625de99..4bb23b9ac6 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -37,7 +37,7 @@ * - [main] desktop/src/main/ipc.ts contains impl */ -import { contextBridge, ipcRenderer } from "electron/renderer"; +import { contextBridge, ipcRenderer, webUtils } from "electron/renderer"; // While we can't import other code, we can import types since they're just // needed when compiling and will not be needed or looked around for at runtime. @@ -242,6 +242,8 @@ const watchFindFiles = (folderPath: string): Promise => // - Upload +const pathForFile = (file: File) => webUtils.getPathForFile(file); + const listZipEntries = (zipPath: string): Promise => ipcRenderer.invoke("listZipEntries", zipPath); @@ -370,6 +372,7 @@ contextBridge.exposeInMainWorld("electron", { // - Upload + pathForFile, listZipEntries, pendingUploads, setPendingUploads, diff --git a/web/apps/photos/src/components/Upload/Uploader.tsx b/web/apps/photos/src/components/Upload/Uploader.tsx index d8087eb5b0..41b9f48540 100644 --- a/web/apps/photos/src/components/Upload/Uploader.tsx +++ b/web/apps/photos/src/components/Upload/Uploader.tsx @@ -1,6 +1,6 @@ import log from "@/next/log"; import { ElectronFile, type FileAndPath } from "@/next/types/file"; -import type { CollectionMapping, ZipEntry } from "@/next/types/ipc"; +import type { CollectionMapping, Electron, ZipEntry } from "@/next/types/ipc"; import { CustomError } from "@ente/shared/error"; import { isPromise } from "@ente/shared/utils"; import DiscFullIcon from "@mui/icons-material/DiscFull"; @@ -86,7 +86,13 @@ interface Props { activeCollection?: Collection; } -export default function Uploader(props: Props) { +export default function Uploader({ + dragAndDropFiles, + fileSelectorFiles, + folderSelectorFiles, + fileSelectorZipFiles, + ...props +}: Props) { const appContext = useContext(AppContext); const galleryContext = useContext(GalleryContext); const publicCollectionGalleryContext = useContext( @@ -266,108 +272,28 @@ export default function Uploader(props: Props) { // as they are folder being dropped for watching return; } - if ( - pickedUploadType.current === PICKED_UPLOAD_TYPE.FOLDERS && - props.webFolderSelectorFiles?.length > 0 - ) { - log.info(`received folder upload request`); - setWebFiles(props.webFolderSelectorFiles); - } else if ( - pickedUploadType.current === PICKED_UPLOAD_TYPE.FILES && - props.webFileSelectorFiles?.length > 0 - ) { - log.info(`received file upload request`); - setWebFiles(props.webFileSelectorFiles); - } else if ( - pickedUploadType.current === PICKED_UPLOAD_TYPE.ZIPS && - props.webFileSelectorZipFiles?.length > 0 - ) { - if (electron) { - const main = async () => { - const zips: File[] = []; - let electronFiles = [] as ElectronFile[]; - for (const file of props.webFileSelectorZipFiles) { - if (file.name.endsWith(".zip")) { - const zipFiles = await electron.lsZip( - (file as any).path, - ); - log.info( - `zip file - ${file.name} contains ${zipFiles.length} files`, - ); - zips.push(file); - // TODO(MR): This cast is incorrect, but interim. - electronFiles = [ - ...electronFiles, - ...(zipFiles as unknown as ElectronFile[]), - ]; - } - } - // setWebFiles(props.webFileSelectorZipFiles); - zipPaths.current = zips.map((file) => (file as any).path); - setElectronFiles(electronFiles); - }; - main(); - } - } else if (props.dragAndDropFiles?.length > 0) { - isDragAndDrop.current = true; - if (electron) { - const main = async () => { - try { - // check and parse dropped files which are zip files - log.info(`uploading dropped files from desktop app`); - const zips: File[] = []; - let electronFiles = [] as ElectronFile[]; - for (const file of props.dragAndDropFiles) { - if (file.name.endsWith(".zip")) { - const zipFiles = await electron.lsZip( - (file as any).path, - ); - log.info( - `zip file - ${file.name} contains ${zipFiles.length} files`, - ); - zips.push(file); - // TODO(MR): This cast is incorrect, but interim. - electronFiles = [ - ...electronFiles, - ...(zipFiles as unknown as ElectronFile[]), - ]; - } else { - // type cast to ElectronFile as the file is dropped from desktop app - // type file and ElectronFile should be interchangeable, but currently they have some differences. - // Typescript is giving error - // Conversion of type 'File' to type 'ElectronFile' may be a mistake because neither type sufficiently - // overlaps with the other. If this was intentional, convert the expression to 'unknown' first. - // Type 'File' is missing the following properties from type 'ElectronFile': path, blob - // for now patching by type casting first to unknown and then to ElectronFile - // TODO: fix types and remove type cast - electronFiles.push( - file as unknown as ElectronFile, - ); - } - } - log.info( - `uploading dropped files from desktop app - ${electronFiles.length} files found`, - ); - zipPaths.current = zips.map( - (file) => (file as any).path, - ); - setElectronFiles(electronFiles); - } catch (e) { - log.error("failed to upload desktop dropped files", e); - setWebFiles(props.dragAndDropFiles); - } - }; - main(); - } else { - log.info(`uploading dropped files from web app`); - setWebFiles(props.dragAndDropFiles); - } + + const files = [ + dragAndDropFiles, + fileSelectorFiles, + folderSelectorFiles, + fileSelectorZipFiles, + ].flat(); + if (electron) { + desktopFilesAndZipEntries(electron, files).then( + ({ fileAndPaths, zipEntries }) => { + setDesktopFiles(fileAndPaths); + setDesktopZipEntries(zipEntries); + }, + ); + } else { + setWebFiles(files); } }, [ - props.dragAndDropFiles, - props.webFileSelectorFiles, - props.webFolderSelectorFiles, - props.webFileSelectorZipFiles, + dragAndDropFiles, + fileSelectorFiles, + folderSelectorFiles, + fileSelectorZipFiles, ]); useEffect(() => { @@ -905,6 +831,25 @@ async function waitAndRun( await task(); } +const desktopFilesAndZipEntries = async ( + electron: Electron, + files: File[], +): Promise<{ fileAndPaths: FileAndPath[]; zipEntries: ZipEntry[] }> => { + const fileAndPaths: FileAndPath[] = []; + const zipEntries: ZipEntry[] = []; + + for (const file of files) { + const path = electron.pathForFile(file); + if (file.name.endsWith(".zip")) { + zipEntries = zipEntries.concat(await electron.listZipEntries(path)); + } else { + fileAndPaths.push({ file, path }); + } + } + + return { fileAndPaths, zipEntries }; +}; + // This is used to prompt the user the make upload strategy choice interface ImportSuggestion { rootFolderName: string; diff --git a/web/packages/next/types/file.ts b/web/packages/next/types/file.ts index 5d6b62550d..6dd1032cdb 100644 --- a/web/packages/next/types/file.ts +++ b/web/packages/next/types/file.ts @@ -18,8 +18,9 @@ export interface ElectronFile { /** * When we are running in the context of our desktop app, we have access to the - * absolute path of the file under consideration. This type combines these two - * bits of information to remove the need to query it again and again. + * absolute path of {@link File} objects. This convenience type clubs these two + * bits of information, saving us the need to query the path again and again + * using the {@link getPathForFile} method of {@link Electron}. */ export interface FileAndPath { file: File; diff --git a/web/packages/next/types/ipc.ts b/web/packages/next/types/ipc.ts index 7198a2ebce..dab10cc8e0 100644 --- a/web/packages/next/types/ipc.ts +++ b/web/packages/next/types/ipc.ts @@ -465,6 +465,18 @@ export interface Electron { // - Upload + /** + * Return the file system path that this File object points to. + * + * This method is a bit different from the other methods on the Electron + * object in the sense that there is no actual IPC happening - the + * implementation of this method is completely in the preload script. Thus + * we can pass it an otherwise unserializable File object. + * + * Consequently, it is also _not_ async. + */ + pathForFile: (file: File) => string; + /** * Get the list of files that are present in the given zip file. * @@ -478,7 +490,7 @@ export interface Electron { * * To read the contents of the files themselves, see [Note: IPC streams]. */ - listZipEntries : (zipPath: string) => Promise + listZipEntries: (zipPath: string) => Promise; /** * Return any pending uploads that were previously enqueued but haven't yet From 0fbafcc4f5f93152128492c3fbd1a31e04e47203 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 29 Apr 2024 16:33:04 +0530 Subject: [PATCH 072/240] Remove unused sharedFiles app context prop setSharedFiles was removed in 3b468cb1545e5ee5065fa89e06f5738c4fd0c06f (years ago). --- .../photos/src/components/Upload/Uploader.tsx | 15 +++++--------- web/apps/photos/src/pages/_app.tsx | 20 ------------------- 2 files changed, 5 insertions(+), 30 deletions(-) diff --git a/web/apps/photos/src/components/Upload/Uploader.tsx b/web/apps/photos/src/components/Upload/Uploader.tsx index 41b9f48540..7f529eef9d 100644 --- a/web/apps/photos/src/components/Upload/Uploader.tsx +++ b/web/apps/photos/src/components/Upload/Uploader.tsx @@ -198,7 +198,6 @@ export default function Uploader({ }; const handleCollectionSelectorCancel = () => { uploadRunning.current = false; - appContext.resetSharedFiles(); }; const handleUserNameInputDialogClose = () => { @@ -298,10 +297,10 @@ export default function Uploader({ useEffect(() => { if ( + webFiles.length > 0 || desktopFilePaths.length > 0 || electronFiles.length > 0 || - webFiles.length > 0 || - appContext.sharedFiles?.length > 0 + ) { log.info( `upload request type: ${ @@ -311,12 +310,11 @@ export default function Uploader({ ? "electronFiles" : webFiles.length > 0 ? "webFiles" - : "sharedFiles" + : "-" } count ${ desktopFilePaths.length + electronFiles.length + - webFiles.length + - (appContext.sharedFiles?.length ?? 0) + webFiles.length }`, ); if (uploadManager.isUploadRunning()) { @@ -340,9 +338,6 @@ export default function Uploader({ // File selection by drag and drop or selection of file. fileOrPathsToUpload.current = webFiles; setWebFiles([]); - } else if (appContext.sharedFiles?.length > 0) { - fileOrPathsToUpload.current = appContext.sharedFiles; - appContext.resetSharedFiles(); } else if (electronFiles?.length > 0) { // File selection from desktop app - deprecated log.warn("Using deprecated code path for ElectronFiles"); @@ -386,7 +381,7 @@ export default function Uploader({ pickedUploadType.current = null; props.setLoading(false); } - }, [webFiles, appContext.sharedFiles, electronFiles, desktopFilePaths]); + }, [webFiles, , electronFiles, desktopFilePaths]); const preCollectionCreationAction = async () => { props.closeCollectionSelector?.(); diff --git a/web/apps/photos/src/pages/_app.tsx b/web/apps/photos/src/pages/_app.tsx index f3994d0817..0e80d0df9f 100644 --- a/web/apps/photos/src/pages/_app.tsx +++ b/web/apps/photos/src/pages/_app.tsx @@ -80,8 +80,6 @@ const redirectMap = new Map([ type AppContextType = { showNavBar: (show: boolean) => void; - sharedFiles: File[]; - resetSharedFiles: () => void; mlSearchEnabled: boolean; mapEnabled: boolean; updateMlSearchEnabled: (enabled: boolean) => Promise; @@ -114,7 +112,6 @@ export default function App({ Component, pageProps }: AppProps) { typeof window !== "undefined" && !window.navigator.onLine, ); const [showNavbar, setShowNavBar] = useState(false); - const [sharedFiles, setSharedFiles] = useState(null); const [redirectName, setRedirectName] = useState(null); const [mlSearchEnabled, setMlSearchEnabled] = useState(false); const [mapEnabled, setMapEnabled] = useState(false); @@ -227,7 +224,6 @@ export default function App({ Component, pageProps }: AppProps) { const setUserOnline = () => setOffline(false); const setUserOffline = () => setOffline(true); - const resetSharedFiles = () => setSharedFiles(null); useEffect(() => { const redirectTo = async (redirect) => { @@ -354,20 +350,6 @@ export default function App({ Component, pageProps }: AppProps) { {isI18nReady && offline && t("OFFLINE_MSG")} - {sharedFiles && - (router.pathname === "/gallery" ? ( - - {t("files_to_be_uploaded", { - count: sharedFiles.length, - })} - - ) : ( - - {t("login_to_upload_files", { - count: sharedFiles.length, - })} - - ))} Date: Mon, 29 Apr 2024 16:44:07 +0530 Subject: [PATCH 073/240] Implement SQLite version of EmbeddingsDB --- mobile/lib/db/embeddings_sqlite_db.dart | 129 ++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 mobile/lib/db/embeddings_sqlite_db.dart diff --git a/mobile/lib/db/embeddings_sqlite_db.dart b/mobile/lib/db/embeddings_sqlite_db.dart new file mode 100644 index 0000000000..cdbe64fb7e --- /dev/null +++ b/mobile/lib/db/embeddings_sqlite_db.dart @@ -0,0 +1,129 @@ +import "dart:io"; +import "dart:typed_data"; + +import "package:path/path.dart"; +import 'package:path_provider/path_provider.dart'; +import "package:photos/core/event_bus.dart"; +import "package:photos/events/embedding_updated_event.dart"; +import "package:photos/models/embedding.dart"; +import "package:sqlite_async/sqlite_async.dart"; + +class EmbeddingsDB { + EmbeddingsDB._privateConstructor(); + + static final EmbeddingsDB instance = EmbeddingsDB._privateConstructor(); + + static const databaseName = "ente.embeddings.db"; + static const tableName = "embeddings"; + static const columnFileID = "file_id"; + static const columnModel = "model"; + static const columnEmbedding = "embedding"; + static const columnUpdationTime = "updation_time"; + + static Future? _dbFuture; + + Future get _database async { + _dbFuture ??= _initDatabase(); + return _dbFuture!; + } + + Future _initDatabase() async { + final Directory documentsDirectory = + await getApplicationDocumentsDirectory(); + final String path = join(documentsDirectory.path, databaseName); + final migrations = SqliteMigrations() + ..add( + SqliteMigration( + 1, + (tx) async { + await tx.execute( + 'CREATE TABLE $tableName IF NOT EXISTS ($columnFileID INTEGER NOT NULL, $columnModel TEXT NOT NULL, $columnEmbedding BLOB NOT NULL, $columnUpdationTime INTEGER, UNIQUE ($columnFileID, $columnModel))', + ); + }, + ), + ); + final database = SqliteDatabase(path: path); + await migrations.migrate(database); + return database; + } + + Future clearTable() async { + final db = await _database; + await db.execute('DELETE * FROM $tableName'); + } + + Future> getAll(Model model) async { + final db = await _database; + final results = await db.getAll('SELECT * FROM $tableName'); + return _convertToEmbeddings(results); + } + + Future put(Embedding embedding) async { + final db = await _database; + await db.execute( + 'INSERT OR REPLACE INTO $tableName ($columnFileID, $columnModel, $columnEmbedding) VALUES (?, ?, ?, ?)', + _getRowFromEmbedding(embedding), + ); + Bus.instance.fire(EmbeddingUpdatedEvent()); + } + + Future putMany(List embeddings) async { + final db = await _database; + final inputs = embeddings.map((e) => _getRowFromEmbedding(e)).toList(); + await db.executeBatch( + 'INSERT OR REPLACE INTO $tableName ($columnFileID, $columnModel, $columnEmbedding) values(?, ?, ?, ?)', + inputs, + ); + Bus.instance.fire(EmbeddingUpdatedEvent()); + } + + Future> getUnsyncedEmbeddings() async { + final db = await _database; + final results = await db.getAll( + 'SELECT * FROM $tableName WHERE $columnUpdationTime IS NULL', + ); + return _convertToEmbeddings(results); + } + + Future deleteEmbeddings(List fileIDs) async { + final db = await _database; + await db.execute( + 'DELETE FROM $tableName WHERE $columnFileID IN (${fileIDs.join(", ")})', + ); + Bus.instance.fire(EmbeddingUpdatedEvent()); + } + + Future deleteAllForModel(Model model) async { + final db = await _database; + await db.execute( + 'DELETE FROM $tableName WHERE $columnModel = ?', + [serialize(model)], + ); + Bus.instance.fire(EmbeddingUpdatedEvent()); + } + + List _convertToEmbeddings(List> results) { + final List embeddings = []; + for (final result in results) { + embeddings.add(_getEmbeddingFromRow(result)); + } + return embeddings; + } + + Embedding _getEmbeddingFromRow(Map row) { + final fileID = row[columnFileID]; + final model = deserialize(row[columnModel]); + final bytes = row[columnEmbedding] as Uint8List; + final list = Float32List.view(bytes.buffer); + return Embedding(fileID: fileID, model: model, embedding: list); + } + + List _getRowFromEmbedding(Embedding embedding) { + return [ + embedding.fileID, + serialize(embedding.model), + Float32List.fromList(embedding.embedding).buffer.asUint8List(), + embedding.updationTime, + ]; + } +} From 7c9160478dee0cf5461bd052f9a96bd233c176fa Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 29 Apr 2024 16:45:43 +0530 Subject: [PATCH 074/240] [mob][photos] Extract strings --- mobile/lib/generated/intl/messages_en.dart | 4 ++++ mobile/lib/generated/l10n.dart | 20 +++++++++++++++++++ mobile/lib/l10n/intl_en.arb | 4 +++- mobile/lib/ui/cast/auto.dart | 2 -- .../gallery/gallery_app_bar_widget.dart | 9 ++++----- .../ente_cast_normal/lib/src/service.dart | 2 -- 6 files changed, 31 insertions(+), 10 deletions(-) diff --git a/mobile/lib/generated/intl/messages_en.dart b/mobile/lib/generated/intl/messages_en.dart index 23ea9c5e20..265e4819b3 100644 --- a/mobile/lib/generated/intl/messages_en.dart +++ b/mobile/lib/generated/intl/messages_en.dart @@ -1342,6 +1342,10 @@ class MessageLookup extends MessageLookupByLibrary { "sparkleSuccess": MessageLookupByLibrary.simpleMessage("✨ Success"), "startBackup": MessageLookupByLibrary.simpleMessage("Start backup"), "status": MessageLookupByLibrary.simpleMessage("Status"), + "stopCastingBody": MessageLookupByLibrary.simpleMessage( + "Do you want to stop casting?"), + "stopCastingTitle": + MessageLookupByLibrary.simpleMessage("Stop casting"), "storage": MessageLookupByLibrary.simpleMessage("Storage"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Family"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("You"), diff --git a/mobile/lib/generated/l10n.dart b/mobile/lib/generated/l10n.dart index d55ab17959..a142fb78ec 100644 --- a/mobile/lib/generated/l10n.dart +++ b/mobile/lib/generated/l10n.dart @@ -8643,6 +8643,26 @@ class S { args: [], ); } + + /// `Stop casting` + String get stopCastingTitle { + return Intl.message( + 'Stop casting', + name: 'stopCastingTitle', + desc: '', + args: [], + ); + } + + /// `Do you want to stop casting?` + String get stopCastingBody { + return Intl.message( + 'Do you want to stop casting?', + name: 'stopCastingBody', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/mobile/lib/l10n/intl_en.arb b/mobile/lib/l10n/intl_en.arb index 72afd2a4b3..f365f50e86 100644 --- a/mobile/lib/l10n/intl_en.arb +++ b/mobile/lib/l10n/intl_en.arb @@ -1220,5 +1220,7 @@ "connectToDevice": "Connect to device", "autoCastDialogBody": "You'll see available Cast devices here.", "autoCastiOSPermission": "Make sure Local Network permissions are turned on for the Ente Photos app, in Settings.", - "noDeviceFound": "No device found" + "noDeviceFound": "No device found", + "stopCastingTitle": "Stop casting", + "stopCastingBody": "Do you want to stop casting?" } \ No newline at end of file diff --git a/mobile/lib/ui/cast/auto.dart b/mobile/lib/ui/cast/auto.dart index ee5974f00c..aed8ee0a50 100644 --- a/mobile/lib/ui/cast/auto.dart +++ b/mobile/lib/ui/cast/auto.dart @@ -113,8 +113,6 @@ class _AutoCastDialogState extends State { BuildContext context, Object castDevice, ) async { - // sleep for 10 seconds - await Future.delayed(const Duration(seconds: 10)); await castService.connectDevice( context, castDevice, diff --git a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart index 489d5478ec..0fc20ab1eb 100644 --- a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart +++ b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart @@ -863,10 +863,10 @@ class _GalleryAppBarWidgetState extends State { if (castService.getActiveSessions().isNotEmpty) { await showChoiceDialog( context, - title: "Stop casting", - firstButtonLabel: "Yes", - secondButtonLabel: "No", - body: "Do you want to stop casting?", + title: S.of(context).stopCastingTitle, + firstButtonLabel: S.of(context).yes, + secondButtonLabel: S.of(context).no, + body: S.of(context).stopCastingBody, firstButtonOnTap: () async { gw.revokeAllTokens().ignore(); await castService.closeActiveCasts(); @@ -884,7 +884,6 @@ class _GalleryAppBarWidgetState extends State { return const CastChooseDialog(); }, ); - _logger.info("Cast result: $result"); if (result == null) { return; } diff --git a/mobile/plugins/ente_cast_normal/lib/src/service.dart b/mobile/plugins/ente_cast_normal/lib/src/service.dart index 1f98fa7464..04c501666a 100644 --- a/mobile/plugins/ente_cast_normal/lib/src/service.dart +++ b/mobile/plugins/ente_cast_normal/lib/src/service.dart @@ -39,8 +39,6 @@ class CastServiceImpl extends CastService { session.stateStream.listen((state) { if (state == CastSessionState.connected) { - const snackBar = SnackBar(content: Text('Connected')); - ScaffoldMessenger.of(context).showSnackBar(snackBar); debugPrint("Send request to pair"); session.sendMessage(_pairRequestNamespace, {}); } else if (state == CastSessionState.closed) { From 5c645d50f052e979aafd46b4b8ebfbb77a6fea43 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 29 Apr 2024 16:56:53 +0530 Subject: [PATCH 075/240] [mob][photos] Show custom error on ip mismatch --- mobile/lib/gateways/cast_gw.dart | 16 +++++++++++---- mobile/lib/generated/intl/messages_en.dart | 4 ++++ mobile/lib/generated/l10n.dart | 20 +++++++++++++++++++ mobile/lib/l10n/intl_en.arb | 4 +++- .../gallery/gallery_app_bar_widget.dart | 10 +++++++++- 5 files changed, 48 insertions(+), 6 deletions(-) diff --git a/mobile/lib/gateways/cast_gw.dart b/mobile/lib/gateways/cast_gw.dart index fb342c1a90..63735d6782 100644 --- a/mobile/lib/gateways/cast_gw.dart +++ b/mobile/lib/gateways/cast_gw.dart @@ -12,10 +12,14 @@ class CastGateway { ); return response.data["publicKey"]; } catch (e) { - if (e is DioError && - e.response != null && - e.response!.statusCode == 404) { - return null; + if (e is DioError && e.response != null) { + if (e.response!.statusCode == 404) { + return null; + } else if (e.response!.statusCode == 403) { + throw CastIPMismatchException(); + } else { + rethrow; + } } rethrow; } @@ -48,3 +52,7 @@ class CastGateway { } } } + +class CastIPMismatchException implements Exception { + CastIPMismatchException(); +} diff --git a/mobile/lib/generated/intl/messages_en.dart b/mobile/lib/generated/intl/messages_en.dart index 265e4819b3..6c91e4c7c1 100644 --- a/mobile/lib/generated/intl/messages_en.dart +++ b/mobile/lib/generated/intl/messages_en.dart @@ -394,6 +394,10 @@ class MessageLookup extends MessageLookupByLibrary { "cannotAddMorePhotosAfterBecomingViewer": m9, "cannotDeleteSharedFiles": MessageLookupByLibrary.simpleMessage("Cannot delete shared files"), + "castIPMismatchBody": MessageLookupByLibrary.simpleMessage( + "Please make sure you are on the same network as the TV."), + "castIPMismatchTitle": + MessageLookupByLibrary.simpleMessage("Failed to cast album"), "castInstruction": MessageLookupByLibrary.simpleMessage( "Visit cast.ente.io on the device you want to pair.\n\nEnter the code below to play the album on your TV."), "centerPoint": MessageLookupByLibrary.simpleMessage("Center point"), diff --git a/mobile/lib/generated/l10n.dart b/mobile/lib/generated/l10n.dart index a142fb78ec..0acdf14df1 100644 --- a/mobile/lib/generated/l10n.dart +++ b/mobile/lib/generated/l10n.dart @@ -8663,6 +8663,26 @@ class S { args: [], ); } + + /// `Failed to cast album` + String get castIPMismatchTitle { + return Intl.message( + 'Failed to cast album', + name: 'castIPMismatchTitle', + desc: '', + args: [], + ); + } + + /// `Please make sure you are on the same network as the TV.` + String get castIPMismatchBody { + return Intl.message( + 'Please make sure you are on the same network as the TV.', + name: 'castIPMismatchBody', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/mobile/lib/l10n/intl_en.arb b/mobile/lib/l10n/intl_en.arb index f365f50e86..dbfa1448c9 100644 --- a/mobile/lib/l10n/intl_en.arb +++ b/mobile/lib/l10n/intl_en.arb @@ -1222,5 +1222,7 @@ "autoCastiOSPermission": "Make sure Local Network permissions are turned on for the Ente Photos app, in Settings.", "noDeviceFound": "No device found", "stopCastingTitle": "Stop casting", - "stopCastingBody": "Do you want to stop casting?" + "stopCastingBody": "Do you want to stop casting?", + "castIPMismatchTitle": "Failed to cast album", + "castIPMismatchBody": "Please make sure you are on the same network as the TV." } \ No newline at end of file diff --git a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart index 0fc20ab1eb..c363ebd2e0 100644 --- a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart +++ b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart @@ -948,7 +948,15 @@ class _GalleryAppBarWidgetState extends State { return true; } catch (e, s) { _logger.severe("Failed to cast album", e, s); - await showGenericErrorDialog(context: context, error: e); + if (e is CastIPMismatchException) { + await showErrorDialog( + context, + S.of(context).castIPMismatchTitle, + S.of(context).castIPMismatchBody, + ); + } else { + await showGenericErrorDialog(context: context, error: e); + } return false; } } From 9e7c82d5b9c57f95638aa76912e42a8c56d3a350 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 29 Apr 2024 16:59:28 +0530 Subject: [PATCH 076/240] [mob][photos] Extract string --- mobile/lib/generated/intl/messages_en.dart | 2 ++ mobile/lib/generated/l10n.dart | 10 ++++++++++ mobile/lib/l10n/intl_en.arb | 3 ++- .../lib/ui/viewer/gallery/gallery_app_bar_widget.dart | 2 +- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/mobile/lib/generated/intl/messages_en.dart b/mobile/lib/generated/intl/messages_en.dart index 6c91e4c7c1..efa91ffb74 100644 --- a/mobile/lib/generated/intl/messages_en.dart +++ b/mobile/lib/generated/intl/messages_en.dart @@ -1000,6 +1000,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Or pick an existing one"), "pair": MessageLookupByLibrary.simpleMessage("Pair"), "pairWithPin": MessageLookupByLibrary.simpleMessage("Pair with PIN"), + "pairingComplete": + MessageLookupByLibrary.simpleMessage("Pairing complete"), "passkey": MessageLookupByLibrary.simpleMessage("Passkey"), "passkeyAuthTitle": MessageLookupByLibrary.simpleMessage("Passkey verification"), diff --git a/mobile/lib/generated/l10n.dart b/mobile/lib/generated/l10n.dart index 0acdf14df1..fcf9265115 100644 --- a/mobile/lib/generated/l10n.dart +++ b/mobile/lib/generated/l10n.dart @@ -8683,6 +8683,16 @@ class S { args: [], ); } + + /// `Pairing complete` + String get pairingComplete { + return Intl.message( + 'Pairing complete', + name: 'pairingComplete', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/mobile/lib/l10n/intl_en.arb b/mobile/lib/l10n/intl_en.arb index dbfa1448c9..7139a7098a 100644 --- a/mobile/lib/l10n/intl_en.arb +++ b/mobile/lib/l10n/intl_en.arb @@ -1224,5 +1224,6 @@ "stopCastingTitle": "Stop casting", "stopCastingBody": "Do you want to stop casting?", "castIPMismatchTitle": "Failed to cast album", - "castIPMismatchBody": "Please make sure you are on the same network as the TV." + "castIPMismatchBody": "Please make sure you are on the same network as the TV.", + "pairingComplete": "Pairing complete" } \ No newline at end of file diff --git a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart index c363ebd2e0..fc925b4c7d 100644 --- a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart +++ b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart @@ -944,7 +944,7 @@ class _GalleryAppBarWidgetState extends State { widget.collection!.id, castToken, ); - showToast(context, "Pairing complete"); + showToast(context, S.of(context).pairingComplete); return true; } catch (e, s) { _logger.severe("Failed to cast album", e, s); From 104a7a5f003dc2b519d0121102f3fd9e37edaf59 Mon Sep 17 00:00:00 2001 From: vishnukvmd Date: Mon, 29 Apr 2024 17:10:53 +0530 Subject: [PATCH 077/240] Fix queries --- mobile/lib/db/embeddings_sqlite_db.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mobile/lib/db/embeddings_sqlite_db.dart b/mobile/lib/db/embeddings_sqlite_db.dart index cdbe64fb7e..1a101d4ebf 100644 --- a/mobile/lib/db/embeddings_sqlite_db.dart +++ b/mobile/lib/db/embeddings_sqlite_db.dart @@ -37,7 +37,7 @@ class EmbeddingsDB { 1, (tx) async { await tx.execute( - 'CREATE TABLE $tableName IF NOT EXISTS ($columnFileID INTEGER NOT NULL, $columnModel TEXT NOT NULL, $columnEmbedding BLOB NOT NULL, $columnUpdationTime INTEGER, UNIQUE ($columnFileID, $columnModel))', + 'CREATE TABLE $tableName ($columnFileID INTEGER NOT NULL, $columnModel TEXT NOT NULL, $columnEmbedding BLOB NOT NULL, $columnUpdationTime INTEGER, UNIQUE ($columnFileID, $columnModel))', ); }, ), @@ -61,7 +61,7 @@ class EmbeddingsDB { Future put(Embedding embedding) async { final db = await _database; await db.execute( - 'INSERT OR REPLACE INTO $tableName ($columnFileID, $columnModel, $columnEmbedding) VALUES (?, ?, ?, ?)', + 'INSERT OR REPLACE INTO $tableName ($columnFileID, $columnModel, $columnEmbedding, $columnUpdationTime) VALUES (?, ?, ?, ?)', _getRowFromEmbedding(embedding), ); Bus.instance.fire(EmbeddingUpdatedEvent()); @@ -71,7 +71,7 @@ class EmbeddingsDB { final db = await _database; final inputs = embeddings.map((e) => _getRowFromEmbedding(e)).toList(); await db.executeBatch( - 'INSERT OR REPLACE INTO $tableName ($columnFileID, $columnModel, $columnEmbedding) values(?, ?, ?, ?)', + 'INSERT OR REPLACE INTO $tableName ($columnFileID, $columnModel, $columnEmbedding, $columnUpdationTime) values(?, ?, ?, ?)', inputs, ); Bus.instance.fire(EmbeddingUpdatedEvent()); From c1ef0199bea129dfba447c96bc7e1b0b70d6b9a3 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 29 Apr 2024 17:13:15 +0530 Subject: [PATCH 078/240] Partial integration --- .../photos/src/components/Upload/Uploader.tsx | 177 ++++++++---------- 1 file changed, 78 insertions(+), 99 deletions(-) diff --git a/web/apps/photos/src/components/Upload/Uploader.tsx b/web/apps/photos/src/components/Upload/Uploader.tsx index 7f529eef9d..8929bb60f3 100644 --- a/web/apps/photos/src/components/Upload/Uploader.tsx +++ b/web/apps/photos/src/components/Upload/Uploader.tsx @@ -1,3 +1,4 @@ +import { basename } from "@/next/file"; import log from "@/next/log"; import { ElectronFile, type FileAndPath } from "@/next/types/file"; import type { CollectionMapping, Electron, ZipEntry } from "@/next/types/ipc"; @@ -29,7 +30,6 @@ import type { import uploadManager, { setToUploadCollection, } from "services/upload/uploadManager"; -import { fopFileName } from "services/upload/uploadService"; import watcher from "services/watch"; import { NotificationAttributes } from "types/Notification"; import { Collection } from "types/collection"; @@ -88,8 +88,11 @@ interface Props { export default function Uploader({ dragAndDropFiles, + openFileSelector, fileSelectorFiles, + openFolderSelector, folderSelectorFiles, + openZipFileSelector, fileSelectorZipFiles, ...props }: Props) { @@ -164,6 +167,9 @@ export default function Uploader({ [], ); + // TODO(MR): temp, doesn't have zips + const fileOrPathsToUpload = useRef<(File | string)[]>([]); + /** * If true, then the next upload we'll be processing was initiated by our * desktop app. @@ -295,93 +301,74 @@ export default function Uploader({ fileSelectorZipFiles, ]); + // Trigger an upload when any of the dependencies change. useEffect(() => { - if ( - webFiles.length > 0 || - desktopFilePaths.length > 0 || - electronFiles.length > 0 || + const itemAndPaths = [ + /* TODO(MR): use webkitRelativePath || name here */ + webFiles.map((f) => [f, f["path"]]), + desktopFiles.map((fp) => [fp, fp.path]), + desktopFilePaths.map((p) => [p, p]), + desktopZipEntries.map((ze) => [ze, ze[1]]), + ].flat(); - ) { - log.info( - `upload request type: ${ - desktopFilePaths.length > 0 - ? "desktopFilePaths" - : electronFiles.length > 0 - ? "electronFiles" - : webFiles.length > 0 - ? "webFiles" - : "-" - } count ${ - desktopFilePaths.length + - electronFiles.length + - webFiles.length - }`, - ); - if (uploadManager.isUploadRunning()) { - if (watcher.isUploadRunning()) { - // Pause watch folder sync on user upload - log.info( - "Folder watcher was uploading, pausing it to first run user upload", - ); - watcher.pauseRunningSync(); - } else { - log.info( - "Ignoring new upload request because an upload is already running", - ); - return; - } - } - uploadRunning.current = true; - props.closeUploadTypeSelector(); - props.setLoading(true); - if (webFiles?.length > 0) { - // File selection by drag and drop or selection of file. - fileOrPathsToUpload.current = webFiles; - setWebFiles([]); - } else if (electronFiles?.length > 0) { - // File selection from desktop app - deprecated - log.warn("Using deprecated code path for ElectronFiles"); - fileOrPathsToUpload.current = electronFiles.map((f) => f.path); - setElectronFiles([]); - } else if (desktopFilePaths && desktopFilePaths.length > 0) { - // File selection from our desktop app - fileOrPathsToUpload.current = desktopFilePaths; - setDesktopFilePaths([]); - } + if (itemAndPaths.length == 0) return; - log.debug(() => "Uploader invoked"); - log.debug(() => fileOrPathsToUpload.current); - - fileOrPathsToUpload.current = pruneHiddenFiles( - fileOrPathsToUpload.current, - ); - - if (fileOrPathsToUpload.current.length === 0) { - props.setLoading(false); + if (uploadManager.isUploadRunning()) { + if (watcher.isUploadRunning()) { + log.info("Pausing watch folder sync to prioritize user upload"); + watcher.pauseRunningSync(); + } else { + log.info( + "Ignoring new upload request when upload is already running", + ); return; } - - const importSuggestion = getImportSuggestion( - pickedUploadType.current, - fileOrPathsToUpload.current.map((file) => - /** TODO(MR): Is path valid for Web files? */ - typeof file == "string" ? file : file["path"], - ), - ); - setImportSuggestion(importSuggestion); - - log.debug(() => importSuggestion); - - handleCollectionCreationAndUpload( - importSuggestion, - props.isFirstUpload, - pickedUploadType.current, - publicCollectionGalleryContext.accessedThroughSharedURL, - ); - pickedUploadType.current = null; - props.setLoading(false); } - }, [webFiles, , electronFiles, desktopFilePaths]); + uploadRunning.current = true; + props.closeUploadTypeSelector(); + props.setLoading(true); + + setWebFiles([]); + setDesktopFiles([]); + setDesktopFilePaths([]); + setDesktopZipEntries([]); + + // Remove hidden files (files whose names begins with a "."). + const prunedItemAndPaths = itemAndPaths.filter( + ([_, p]) => !basename(p).startsWith("."), + ); + + itemsToUpload.current = prunedItemAndPaths.map(([i]) => i); + fileOrPathsToUpload.current = itemsToUpload.current.map((i) => { + if (typeof i == "string" || i instanceof File) return i; + if (Array.isArray(i)) return undefined; + return i.file; + }).filter((x) => x); + itemsToUpload.current = []; + if (fileOrPathsToUpload.current.length === 0) { + props.setLoading(false); + return; + } + + const importSuggestion = getImportSuggestion( + pickedUploadType.current, + prunedItemAndPaths.map(([_, p]) => p), + ); + setImportSuggestion(importSuggestion); + + log.debug(() => "Uploader invoked:"); + log.debug(() => fileOrPathsToUpload.current); + log.debug(() => importSuggestion); + + handleCollectionCreationAndUpload( + importSuggestion, + props.isFirstUpload, + pickedUploadType.current, + publicCollectionGalleryContext.accessedThroughSharedURL, + ); + pickedUploadType.current = null; + props.setLoading(false); + }, [webFiles, desktopFiles, desktopFilePaths, desktopZipEntries]); const preCollectionCreationAction = async () => { props.closeCollectionSelector?.(); @@ -711,24 +698,24 @@ export default function Uploader({ uploadManager.cancelRunningUpload(); }; - const handleUpload = async (type: PICKED_UPLOAD_TYPE) => { + const handleUpload = (type: PICKED_UPLOAD_TYPE) => { pickedUploadType.current = type; if (type === PICKED_UPLOAD_TYPE.FILES) { - props.showUploadFilesDialog(); + openFileSelector(); } else if (type === PICKED_UPLOAD_TYPE.FOLDERS) { - props.showUploadDirsDialog(); + openFolderSelector(); } else { - if (props.showUploadZipFilesDialog && electron) { - props.showUploadZipFilesDialog(); + if (openZipFileSelector && electron) { + openZipFileSelector(); } else { appContext.setDialogMessage(getDownloadAppMessage()); } } }; - const handleFileUpload = handleUpload(PICKED_UPLOAD_TYPE.FILES); - const handleFolderUpload = handleUpload(PICKED_UPLOAD_TYPE.FOLDERS); - const handleZipUpload = handleUpload(PICKED_UPLOAD_TYPE.ZIPS); + const handleFileUpload = () => handleUpload(PICKED_UPLOAD_TYPE.FILES); + const handleFolderUpload = () => handleUpload(PICKED_UPLOAD_TYPE.FOLDERS); + const handleZipUpload = () => handleUpload(PICKED_UPLOAD_TYPE.ZIPS); const handlePublicUpload = async ( uploaderName: string, @@ -831,7 +818,7 @@ const desktopFilesAndZipEntries = async ( files: File[], ): Promise<{ fileAndPaths: FileAndPath[]; zipEntries: ZipEntry[] }> => { const fileAndPaths: FileAndPath[] = []; - const zipEntries: ZipEntry[] = []; + let zipEntries: ZipEntry[] = []; for (const file of files) { const path = electron.pathForFile(file); @@ -936,11 +923,3 @@ const groupFilesBasedOnParentFolder = (fileOrPaths: (File | string)[]) => { } return result; }; - -/** - * Filter out hidden files from amongst {@link fileOrPaths}. - * - * Hidden files are those whose names begin with a "." (dot). - */ -const pruneHiddenFiles = (fileOrPaths: (File | string)[]) => - fileOrPaths.filter((f) => !fopFileName(f).startsWith(".")); From 64f2be09e65ca795e56cb3f009f0975fb0c071dd Mon Sep 17 00:00:00 2001 From: vishnukvmd Date: Mon, 29 Apr 2024 17:30:56 +0530 Subject: [PATCH 079/240] Replace Isar with SQLite --- mobile/lib/core/configuration.dart | 2 +- .../machine_learning/semantic_search/embedding_store.dart | 4 ++-- .../semantic_search/semantic_search_service.dart | 3 +-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/mobile/lib/core/configuration.dart b/mobile/lib/core/configuration.dart index cde766b1e0..8b7ecbad53 100644 --- a/mobile/lib/core/configuration.dart +++ b/mobile/lib/core/configuration.dart @@ -11,7 +11,7 @@ import 'package:photos/core/constants.dart'; import 'package:photos/core/error-reporting/super_logging.dart'; import 'package:photos/core/event_bus.dart'; import 'package:photos/db/collections_db.dart'; -import "package:photos/db/embeddings_db.dart"; +import "package:photos/db/embeddings_sqlite_db.dart"; import 'package:photos/db/files_db.dart'; import 'package:photos/db/memories_db.dart'; import 'package:photos/db/trash_db.dart'; diff --git a/mobile/lib/services/machine_learning/semantic_search/embedding_store.dart b/mobile/lib/services/machine_learning/semantic_search/embedding_store.dart index f7d17f8b86..b8cd539e41 100644 --- a/mobile/lib/services/machine_learning/semantic_search/embedding_store.dart +++ b/mobile/lib/services/machine_learning/semantic_search/embedding_store.dart @@ -5,7 +5,7 @@ import "dart:typed_data"; import "package:computer/computer.dart"; import "package:logging/logging.dart"; import "package:photos/core/network/network.dart"; -import "package:photos/db/embeddings_db.dart"; +import "package:photos/db/embeddings_sqlite_db.dart"; import "package:photos/db/files_db.dart"; import "package:photos/models/embedding.dart"; import "package:photos/models/file/file.dart"; @@ -19,7 +19,7 @@ class EmbeddingStore { static final EmbeddingStore instance = EmbeddingStore._privateConstructor(); - static const kEmbeddingsSyncTimeKey = "sync_time_embeddings_v2"; + static const kEmbeddingsSyncTimeKey = "sync_time_embeddings_v3-test"; final _logger = Logger("EmbeddingStore"); final _dio = NetworkClient.instance.enteDio; diff --git a/mobile/lib/services/machine_learning/semantic_search/semantic_search_service.dart b/mobile/lib/services/machine_learning/semantic_search/semantic_search_service.dart index d1074053a2..0587ff5223 100644 --- a/mobile/lib/services/machine_learning/semantic_search/semantic_search_service.dart +++ b/mobile/lib/services/machine_learning/semantic_search/semantic_search_service.dart @@ -7,7 +7,7 @@ import "package:logging/logging.dart"; import "package:photos/core/cache/lru_map.dart"; import "package:photos/core/configuration.dart"; import "package:photos/core/event_bus.dart"; -import "package:photos/db/embeddings_db.dart"; +import "package:photos/db/embeddings_sqlite_db.dart"; import "package:photos/db/files_db.dart"; import "package:photos/events/diff_sync_complete_event.dart"; import 'package:photos/events/embedding_updated_event.dart'; @@ -72,7 +72,6 @@ class SemanticSearchService { _mlFramework = _currentModel == Model.onnxClip ? ONNX(shouldDownloadOverMobileData) : GGML(shouldDownloadOverMobileData); - await EmbeddingsDB.instance.init(); await EmbeddingStore.instance.init(); await _loadEmbeddings(); Bus.instance.on().listen((event) { From 0f5007b8d23cfead2f8ea50f4b3240865147352f Mon Sep 17 00:00:00 2001 From: vishnukvmd Date: Mon, 29 Apr 2024 17:32:42 +0530 Subject: [PATCH 080/240] Update key --- .../machine_learning/semantic_search/embedding_store.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/lib/services/machine_learning/semantic_search/embedding_store.dart b/mobile/lib/services/machine_learning/semantic_search/embedding_store.dart index b8cd539e41..b3795db4e9 100644 --- a/mobile/lib/services/machine_learning/semantic_search/embedding_store.dart +++ b/mobile/lib/services/machine_learning/semantic_search/embedding_store.dart @@ -19,7 +19,7 @@ class EmbeddingStore { static final EmbeddingStore instance = EmbeddingStore._privateConstructor(); - static const kEmbeddingsSyncTimeKey = "sync_time_embeddings_v3-test"; + static const kEmbeddingsSyncTimeKey = "sync_time_embeddings_v3"; final _logger = Logger("EmbeddingStore"); final _dio = NetworkClient.instance.enteDio; From 8ecb7710de66bcfeae340d67b59928ecd3e88a01 Mon Sep 17 00:00:00 2001 From: vishnukvmd Date: Mon, 29 Apr 2024 17:34:00 +0530 Subject: [PATCH 081/240] v0.8.85 --- mobile/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 9c96bc7621..d479374044 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -12,7 +12,7 @@ description: ente photos application # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.8.84+604 +version: 0.8.85+605 publish_to: none environment: From cca33074fbed075aebe031cd5a35053668f73b00 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 29 Apr 2024 19:29:52 +0530 Subject: [PATCH 082/240] Pending uploads --- desktop/src/main/services/upload.ts | 5 +- desktop/src/main/stores/upload-status.ts | 4 +- .../photos/src/components/Upload/Uploader.tsx | 81 ++++++++++++------- 3 files changed, 58 insertions(+), 32 deletions(-) diff --git a/desktop/src/main/services/upload.ts b/desktop/src/main/services/upload.ts index 8487f8327e..1f52fe1e7c 100644 --- a/desktop/src/main/services/upload.ts +++ b/desktop/src/main/services/upload.ts @@ -25,9 +25,8 @@ export const listZipEntries = async (zipPath: string): Promise => { export const pendingUploads = async (): Promise => { const collectionName = uploadStatusStore.get("collectionName"); - if (!collectionName) return undefined; - const allFilePaths = uploadStatusStore.get("filePaths"); + const allFilePaths = uploadStatusStore.get("filePaths") ?? []; const filePaths = allFilePaths.filter((f) => existsSync(f)); const allZipEntries = uploadStatusStore.get("zipEntries"); @@ -50,6 +49,8 @@ export const pendingUploads = async (): Promise => { zipEntries = allZipEntries.filter(([z]) => existsSync(z)); } + if (filePaths.length == 0 && zipEntries.length == 0) return undefined; + return { collectionName, filePaths, diff --git a/desktop/src/main/stores/upload-status.ts b/desktop/src/main/stores/upload-status.ts index edd086fbef..20a431fd9b 100644 --- a/desktop/src/main/stores/upload-status.ts +++ b/desktop/src/main/stores/upload-status.ts @@ -2,9 +2,9 @@ import Store, { Schema } from "electron-store"; export interface UploadStatusStore { /* The collection to which we're uploading, or the root collection. */ - collectionName: string; + collectionName?: string; /** Paths to regular files that are pending upload */ - filePaths: string[]; + filePaths?: string[]; /** * Each item is the path to a zip file and the name of an entry within it. * diff --git a/web/apps/photos/src/components/Upload/Uploader.tsx b/web/apps/photos/src/components/Upload/Uploader.tsx index 8929bb60f3..672c418b26 100644 --- a/web/apps/photos/src/components/Upload/Uploader.tsx +++ b/web/apps/photos/src/components/Upload/Uploader.tsx @@ -1,6 +1,6 @@ import { basename } from "@/next/file"; import log from "@/next/log"; -import { ElectronFile, type FileAndPath } from "@/next/types/file"; +import { type FileAndPath } from "@/next/types/file"; import type { CollectionMapping, Electron, ZipEntry } from "@/next/types/ipc"; import { CustomError } from "@ente/shared/error"; import { isPromise } from "@ente/shared/utils"; @@ -27,9 +27,7 @@ import type { UploadCounter, UploadFileNames, } from "services/upload/uploadManager"; -import uploadManager, { - setToUploadCollection, -} from "services/upload/uploadManager"; +import uploadManager from "services/upload/uploadManager"; import watcher from "services/watch"; import { NotificationAttributes } from "types/Notification"; import { Collection } from "types/collection"; @@ -253,8 +251,6 @@ export default function Uploader({ if (!pending) return; const { collectionName, filePaths, zipEntries } = pending; - if (filePaths.length == 0 && zipEntries.length == 0) return; - log.info("Resuming pending upload", pending); isPendingDesktopUpload.current = true; pendingDesktopUploadCollectionName.current = collectionName; @@ -339,11 +335,13 @@ export default function Uploader({ ); itemsToUpload.current = prunedItemAndPaths.map(([i]) => i); - fileOrPathsToUpload.current = itemsToUpload.current.map((i) => { - if (typeof i == "string" || i instanceof File) return i; - if (Array.isArray(i)) return undefined; - return i.file; - }).filter((x) => x); + fileOrPathsToUpload.current = itemsToUpload.current + .map((i) => { + if (typeof i == "string" || i instanceof File) return i; + if (Array.isArray(i)) return undefined; + return i.file; + }) + .filter((x) => x); itemsToUpload.current = []; if (fileOrPathsToUpload.current.length === 0) { props.setLoading(false); @@ -515,23 +513,10 @@ export default function Uploader({ !isPendingDesktopUpload.current && !watcher.isUploadRunning() ) { - await setToUploadCollection(collections); - if (zipPaths.current) { - await electron.setPendingUploadFiles( - "zips", - zipPaths.current, - ); - zipPaths.current = null; - } - await electron.setPendingUploadFiles( - "files", - filesWithCollectionToUploadIn.map( - // TODO(MR): ElectronFile - ({ fileOrPath }) => - typeof fileOrPath == "string" - ? fileOrPath - : (fileOrPath as any as ElectronFile).path, - ), + setPendingUploads( + electron, + collections, + filesWithCollectionToUploadIn, ); } const wereFilesProcessed = await uploadManager.uploadFiles( @@ -923,3 +908,43 @@ const groupFilesBasedOnParentFolder = (fileOrPaths: (File | string)[]) => { } return result; }; + +export const setPendingUploads = async ( + electron: Electron, + collections: Collection[], + filesWithCollectionToUploadIn: FileWithCollection[], +) => { + let collectionName: string | undefined; + /* collection being one suggest one of two things + 1. Either the user has upload to a single existing collection + 2. Created a new single collection to upload to + may have had multiple folder, but chose to upload + to one album + hence saving the collection name when upload collection count is 1 + helps the info of user choosing this options + and on next upload we can directly start uploading to this collection + */ + if (collections.length === 1) { + collectionName = collections[0].name; + } + + const filePaths: string[] = []; + const zipEntries: ZipEntry[] = []; + for (const file of filesWithCollectionToUploadIn) { + if (file instanceof File) { + throw new Error("Unexpected web file for a desktop pending upload"); + } else if (typeof file == "string") { + filePaths.push(file); + } else if (Array.isArray(file)) { + zipEntries.push(file); + } else { + filePaths.push(file.path); + } + } + + await electron.setPendingUploads({ + collectionName, + filePaths, + zipEntries, + }); +}; From 3ef727537c7a0057cafd802c2f553efe66cfbb55 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 29 Apr 2024 19:46:50 +0530 Subject: [PATCH 083/240] UploadItem --- .../photos/src/components/Upload/Uploader.tsx | 4 +- .../src/services/upload/uploadManager.ts | 47 +++++++++++-------- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/web/apps/photos/src/components/Upload/Uploader.tsx b/web/apps/photos/src/components/Upload/Uploader.tsx index 672c418b26..14b0c79c69 100644 --- a/web/apps/photos/src/components/Upload/Uploader.tsx +++ b/web/apps/photos/src/components/Upload/Uploader.tsx @@ -161,7 +161,7 @@ export default function Uploader({ * {@link desktopFiles}, {@link desktopFilePaths} and * {@link desktopZipEntries}. */ - const itemsToUpload = useRef<(File | FileAndPath | string | ZipEntry)[]>( + const itemsToUpload = useRef( [], ); @@ -924,7 +924,7 @@ export const setPendingUploads = async ( helps the info of user choosing this options and on next upload we can directly start uploading to this collection */ - if (collections.length === 1) { + if (collections.length == 1) { collectionName = collections[0].name; } diff --git a/web/apps/photos/src/services/upload/uploadManager.ts b/web/apps/photos/src/services/upload/uploadManager.ts index 665cd76c87..9a8cb6c6d3 100644 --- a/web/apps/photos/src/services/upload/uploadManager.ts +++ b/web/apps/photos/src/services/upload/uploadManager.ts @@ -3,8 +3,8 @@ import { potentialFileTypeFromExtension } from "@/media/live-photo"; import { ensureElectron } from "@/next/electron"; import { lowercaseExtension, nameAndExtension } from "@/next/file"; import log from "@/next/log"; -import { ElectronFile } from "@/next/types/file"; -import type { Electron } from "@/next/types/ipc"; +import { ElectronFile, type FileAndPath } from "@/next/types/file"; +import type { Electron, ZipEntry } from "@/next/types/ipc"; import { ComlinkWorker } from "@/next/worker/comlink-worker"; import { ensure } from "@/utils/ensure"; import { getDedicatedCryptoWorker } from "@ente/shared/crypto"; @@ -83,6 +83,32 @@ export interface ProgressUpdater { /** The number of uploads to process in parallel. */ const maxConcurrentUploads = 4; +/** + * An item to upload is one of the following: + * + * 1. A file drag-and-dropped or selected by the user when we are running in the + * web browser. These is the {@link File} case. + * + * 2. A file drag-and-dropped or selected by the user when we are running in the + * context of our desktop app. In such cases, we also have the absolute path + * of the file in the user's local filesystem. this is the + * {@link FileAndPath} case. + * + * 3. A file path programmatically requested by the desktop app. For example, we + * might be resuming a previously interrupted upload after an app restart + * (thus we no longer have access to the {@link File} from case 2). Or we + * could be uploading a file this is in one of the folders the user has asked + * us to watch for changes. This is the {@link string} case. + * + * 4. A file within a zip file. This too is only possible when we are running in + * the context of our desktop app. The user might have drag-and-dropped or + * selected the zip file, or it might be a zip file that they'd previously + * selected but we now are resuming an interrupted upload. Either ways, what + * we have is a path to zip file, and the name of an entry within that zip + * file. This is the {@link ZipEntry} case. + */ +export type UploadItem = File | FileAndPath | string | ZipEntry; + export interface FileWithCollection { localID: number; collectionID: number; @@ -806,23 +832,6 @@ const splitMetadataAndMediaFiles = ( [[], []], ); -export const setToUploadCollection = async (collections: Collection[]) => { - let collectionName: string = null; - /* collection being one suggest one of two things - 1. Either the user has upload to a single existing collection - 2. Created a new single collection to upload to - may have had multiple folder, but chose to upload - to one album - hence saving the collection name when upload collection count is 1 - helps the info of user choosing this options - and on next upload we can directly start uploading to this collection - */ - if (collections.length === 1) { - collectionName = collections[0].name; - } - await ensureElectron().setPendingUploadCollection(collectionName); -}; - const updatePendingUploads = async ( electron: Electron, files: ClusteredFile[], From 61de0c9c9c02ba55ed6548c9ad67aab9a3da6653 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 29 Apr 2024 19:55:04 +0530 Subject: [PATCH 084/240] Before the changes --- .../photos/src/components/Upload/Uploader.tsx | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/web/apps/photos/src/components/Upload/Uploader.tsx b/web/apps/photos/src/components/Upload/Uploader.tsx index 14b0c79c69..f3a6968af6 100644 --- a/web/apps/photos/src/components/Upload/Uploader.tsx +++ b/web/apps/photos/src/components/Upload/Uploader.tsx @@ -26,6 +26,7 @@ import type { SegregatedFinishedUploads, UploadCounter, UploadFileNames, + UploadItem, } from "services/upload/uploadManager"; import uploadManager from "services/upload/uploadManager"; import watcher from "services/watch"; @@ -160,10 +161,10 @@ export default function Uploader({ * Consolidated and cleaned list obtained from {@link webFiles}, * {@link desktopFiles}, {@link desktopFilePaths} and * {@link desktopZipEntries}. + * + * See the documentation of {@link UploadItem} for more details. */ - const itemsToUpload = useRef( - [], - ); + const uploadItems = useRef([]); // TODO(MR): temp, doesn't have zips const fileOrPathsToUpload = useRef<(File | string)[]>([]); @@ -251,6 +252,7 @@ export default function Uploader({ if (!pending) return; const { collectionName, filePaths, zipEntries } = pending; + log.info("Resuming pending upload", pending); isPendingDesktopUpload.current = true; pendingDesktopUploadCollectionName.current = collectionName; @@ -300,7 +302,7 @@ export default function Uploader({ // Trigger an upload when any of the dependencies change. useEffect(() => { const itemAndPaths = [ - /* TODO(MR): use webkitRelativePath || name here */ + /* TODO(MR): ElectronFile | use webkitRelativePath || name here */ webFiles.map((f) => [f, f["path"]]), desktopFiles.map((fp) => [fp, fp.path]), desktopFilePaths.map((p) => [p, p]), @@ -320,6 +322,7 @@ export default function Uploader({ return; } } + uploadRunning.current = true; props.closeUploadTypeSelector(); props.setLoading(true); @@ -334,15 +337,15 @@ export default function Uploader({ ([_, p]) => !basename(p).startsWith("."), ); - itemsToUpload.current = prunedItemAndPaths.map(([i]) => i); - fileOrPathsToUpload.current = itemsToUpload.current + uploadItems.current = prunedItemAndPaths.map(([i]) => i); + fileOrPathsToUpload.current = uploadItems.current .map((i) => { if (typeof i == "string" || i instanceof File) return i; if (Array.isArray(i)) return undefined; return i.file; }) .filter((x) => x); - itemsToUpload.current = []; + uploadItems.current = []; if (fileOrPathsToUpload.current.length === 0) { props.setLoading(false); return; @@ -912,7 +915,7 @@ const groupFilesBasedOnParentFolder = (fileOrPaths: (File | string)[]) => { export const setPendingUploads = async ( electron: Electron, collections: Collection[], - filesWithCollectionToUploadIn: FileWithCollection[], + uploadItems: UploadItem[], ) => { let collectionName: string | undefined; /* collection being one suggest one of two things @@ -930,15 +933,15 @@ export const setPendingUploads = async ( const filePaths: string[] = []; const zipEntries: ZipEntry[] = []; - for (const file of filesWithCollectionToUploadIn) { - if (file instanceof File) { + for (const item of uploadItems) { + if (item instanceof File) { throw new Error("Unexpected web file for a desktop pending upload"); - } else if (typeof file == "string") { - filePaths.push(file); - } else if (Array.isArray(file)) { - zipEntries.push(file); + } else if (typeof item == "string") { + filePaths.push(item); + } else if (Array.isArray(item)) { + zipEntries.push(item); } else { - filePaths.push(file.path); + filePaths.push(item.path); } } From 2c62f983a8318fbb5910a217854a33b16e19c708 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 29 Apr 2024 20:35:00 +0530 Subject: [PATCH 085/240] wipx --- .../photos/src/components/Upload/Uploader.tsx | 357 ++++++++---------- .../src/services/upload/uploadManager.ts | 18 +- web/apps/photos/src/services/watch.ts | 59 +-- 3 files changed, 197 insertions(+), 237 deletions(-) diff --git a/web/apps/photos/src/components/Upload/Uploader.tsx b/web/apps/photos/src/components/Upload/Uploader.tsx index f3a6968af6..dd90bb98c9 100644 --- a/web/apps/photos/src/components/Upload/Uploader.tsx +++ b/web/apps/photos/src/components/Upload/Uploader.tsx @@ -21,12 +21,12 @@ import { savePublicCollectionUploaderName, } from "services/publicCollectionService"; import type { - FileWithCollection, InProgressUpload, SegregatedFinishedUploads, UploadCounter, UploadFileNames, UploadItem, + UploadItemWithCollection, } from "services/upload/uploadManager"; import uploadManager from "services/upload/uploadManager"; import watcher from "services/watch"; @@ -86,6 +86,7 @@ interface Props { } export default function Uploader({ + isFirstUpload, dragAndDropFiles, openFileSelector, fileSelectorFiles, @@ -162,12 +163,14 @@ export default function Uploader({ * {@link desktopFiles}, {@link desktopFilePaths} and * {@link desktopZipEntries}. * + * Augment each {@link UploadItem} with its "path" (relative path or name in + * the case of {@link webFiles}, absolute path in the case of + * {@link desktopFiles}, {@link desktopFilePaths}, and the path within the + * zip file for {@link desktopZipEntries}). + * * See the documentation of {@link UploadItem} for more details. */ - const uploadItems = useRef([]); - - // TODO(MR): temp, doesn't have zips - const fileOrPathsToUpload = useRef<(File | string)[]>([]); + const uploadItemsAndPaths = useRef<[UploadItem, string][]>([]); /** * If true, then the next upload we'll be processing was initiated by our @@ -301,15 +304,15 @@ export default function Uploader({ // Trigger an upload when any of the dependencies change. useEffect(() => { - const itemAndPaths = [ + const allItemAndPaths = [ /* TODO(MR): ElectronFile | use webkitRelativePath || name here */ - webFiles.map((f) => [f, f["path"]]), + webFiles.map((f) => [f, f["path"] ?? f.name]), desktopFiles.map((fp) => [fp, fp.path]), desktopFilePaths.map((p) => [p, p]), desktopZipEntries.map((ze) => [ze, ze[1]]), - ].flat(); + ].flat() as [UploadItem, string][]; - if (itemAndPaths.length == 0) return; + if (allItemAndPaths.length == 0) return; if (uploadManager.isUploadRunning()) { if (watcher.isUploadRunning()) { @@ -333,42 +336,93 @@ export default function Uploader({ setDesktopZipEntries([]); // Remove hidden files (files whose names begins with a "."). - const prunedItemAndPaths = itemAndPaths.filter( + const prunedItemAndPaths = allItemAndPaths.filter( + // eslint-disable-next-line @typescript-eslint/no-unused-vars ([_, p]) => !basename(p).startsWith("."), ); - uploadItems.current = prunedItemAndPaths.map(([i]) => i); - fileOrPathsToUpload.current = uploadItems.current - .map((i) => { - if (typeof i == "string" || i instanceof File) return i; - if (Array.isArray(i)) return undefined; - return i.file; - }) - .filter((x) => x); - uploadItems.current = []; - if (fileOrPathsToUpload.current.length === 0) { + uploadItemsAndPaths.current = prunedItemAndPaths; + if (uploadItemsAndPaths.current.length === 0) { props.setLoading(false); return; } const importSuggestion = getImportSuggestion( pickedUploadType.current, + // eslint-disable-next-line @typescript-eslint/no-unused-vars prunedItemAndPaths.map(([_, p]) => p), ); setImportSuggestion(importSuggestion); log.debug(() => "Uploader invoked:"); - log.debug(() => fileOrPathsToUpload.current); + log.debug(() => uploadItemsAndPaths.current); log.debug(() => importSuggestion); - handleCollectionCreationAndUpload( - importSuggestion, - props.isFirstUpload, - pickedUploadType.current, - publicCollectionGalleryContext.accessedThroughSharedURL, - ); + const _pickedUploadType = pickedUploadType.current; pickedUploadType.current = null; props.setLoading(false); + + (async () => { + if (publicCollectionGalleryContext.accessedThroughSharedURL) { + const uploaderName = await getPublicCollectionUploaderName( + getPublicCollectionUID( + publicCollectionGalleryContext.token, + ), + ); + uploaderNameRef.current = uploaderName; + showUserNameInputDialog(); + return; + } + + if (isPendingDesktopUpload.current) { + isPendingDesktopUpload.current = false; + if (pendingDesktopUploadCollectionName.current) { + uploadFilesToNewCollections( + "root", + pendingDesktopUploadCollectionName.current, + ); + pendingDesktopUploadCollectionName.current = null; + } else { + uploadFilesToNewCollections("parent"); + } + return; + } + + if (electron && _pickedUploadType === PICKED_UPLOAD_TYPE.ZIPS) { + uploadFilesToNewCollections("parent"); + return; + } + + if (isFirstUpload && !importSuggestion.rootFolderName) { + importSuggestion.rootFolderName = FIRST_ALBUM_NAME; + } + + if (isDragAndDrop.current) { + isDragAndDrop.current = false; + if ( + props.activeCollection && + props.activeCollection.owner.id === galleryContext.user?.id + ) { + uploadFilesToExistingCollection(props.activeCollection); + return; + } + } + + let showNextModal = () => {}; + if (importSuggestion.hasNestedFolders) { + showNextModal = () => setChoiceModalView(true); + } else { + showNextModal = () => + showCollectionCreateModal(importSuggestion.rootFolderName); + } + + props.setCollectionSelectorAttributes({ + callback: uploadFilesToExistingCollection, + onCancel: handleCollectionSelectorCancel, + showNextModal, + intent: CollectionSelectorIntent.upload, + }); + })(); }, [webFiles, desktopFiles, desktopFilePaths, desktopZipEntries]); const preCollectionCreationAction = async () => { @@ -382,100 +436,78 @@ export default function Uploader({ collection: Collection, uploaderName?: string, ) => { - try { - log.info( - `Uploading files existing collection id ${collection.id} (${collection.name})`, - ); - await preCollectionCreationAction(); - const filesWithCollectionToUpload = fileOrPathsToUpload.current.map( - (fileOrPath, index) => ({ - fileOrPath, - localID: index, - collectionID: collection.id, - }), - ); - await waitInQueueAndUploadFiles( - filesWithCollectionToUpload, - [collection], - uploaderName, - ); - } catch (e) { - log.error("Failed to upload files to existing collection", e); - } + await preCollectionCreationAction(); + const uploadItemsWithCollection = uploadItemsAndPaths.current.map( + ([uploadItem], index) => ({ + uploadItem, + localID: index, + collectionID: collection.id, + }), + ); + await waitInQueueAndUploadFiles( + uploadItemsWithCollection, + [collection], + uploaderName, + ); + uploadItemsAndPaths.current = null; }; const uploadFilesToNewCollections = async ( mapping: CollectionMapping, collectionName?: string, ) => { - try { - log.info( - `Uploading files to collection using ${mapping} mapping (${collectionName ?? ""})`, + await preCollectionCreationAction(); + let uploadItemsWithCollection: UploadItemWithCollection[] = []; + const collections: Collection[] = []; + let collectionNameToUploadItems = new Map(); + if (mapping == "root") { + collectionNameToUploadItems.set( + collectionName, + uploadItemsAndPaths.current.map(([i]) => i), ); - await preCollectionCreationAction(); - let filesWithCollectionToUpload: FileWithCollection[] = []; - const collections: Collection[] = []; - let collectionNameToFileOrPaths = new Map< - string, - (File | string)[] - >(); - if (mapping == "root") { - collectionNameToFileOrPaths.set( - collectionName, - fileOrPathsToUpload.current, - ); - } else { - collectionNameToFileOrPaths = groupFilesBasedOnParentFolder( - fileOrPathsToUpload.current, - ); - } - try { - const existingCollections = await getLatestCollections(); - let index = 0; - for (const [ - collectionName, - fileOrPaths, - ] of collectionNameToFileOrPaths) { - const collection = await getOrCreateAlbum( - collectionName, - existingCollections, - ); - collections.push(collection); - props.setCollections([ - ...existingCollections, - ...collections, - ]); - filesWithCollectionToUpload = [ - ...filesWithCollectionToUpload, - ...fileOrPaths.map((fileOrPath) => ({ - localID: index++, - collectionID: collection.id, - fileOrPath, - })), - ]; - } - } catch (e) { - closeUploadProgress(); - log.error("Failed to create album", e); - appContext.setDialogMessage({ - title: t("ERROR"), - close: { variant: "critical" }, - content: t("CREATE_ALBUM_FAILED"), - }); - throw e; - } - await waitInQueueAndUploadFiles( - filesWithCollectionToUpload, - collections, + } else { + collectionNameToUploadItems = groupFilesBasedOnParentFolder( + uploadItemsAndPaths.current, ); - fileOrPathsToUpload.current = null; - } catch (e) { - log.error("Failed to upload files to new collections", e); } + try { + const existingCollections = await getLatestCollections(); + let index = 0; + for (const [ + collectionName, + fileOrPaths, + ] of collectionNameToUploadItems) { + const collection = await getOrCreateAlbum( + collectionName, + existingCollections, + ); + collections.push(collection); + props.setCollections([...existingCollections, ...collections]); + uploadItemsWithCollection = [ + ...uploadItemsWithCollection, + ...fileOrPaths.map((fileOrPath) => ({ + localID: index++, + collectionID: collection.id, + fileOrPath, + })), + ]; + } + } catch (e) { + closeUploadProgress(); + log.error("Failed to create album", e); + appContext.setDialogMessage({ + title: t("ERROR"), + close: { variant: "critical" }, + content: t("CREATE_ALBUM_FAILED"), + }); + throw e; + } + await waitInQueueAndUploadFiles(uploadItemsWithCollection, collections); + uploadItemsAndPaths.current = null; }; const waitInQueueAndUploadFiles = async ( - filesWithCollectionToUploadIn: FileWithCollection[], + uploadItemsWithCollection: UploadItemWithCollection[], collections: Collection[], uploaderName?: string, ) => { @@ -484,7 +516,7 @@ export default function Uploader({ currentPromise, async () => await uploadFiles( - filesWithCollectionToUploadIn, + uploadItemsWithCollection, collections, uploaderName, ), @@ -505,7 +537,7 @@ export default function Uploader({ } const uploadFiles = async ( - filesWithCollectionToUploadIn: FileWithCollection[], + uploadItemsWithCollection: UploadItemWithCollection[], collections: Collection[], uploaderName?: string, ) => { @@ -519,11 +551,13 @@ export default function Uploader({ setPendingUploads( electron, collections, - filesWithCollectionToUploadIn, + uploadItemsWithCollection + .map(({ uploadItem }) => uploadItem) + .filter((x) => x), ); } const wereFilesProcessed = await uploadManager.uploadFiles( - filesWithCollectionToUploadIn, + uploadItemsWithCollection, collections, uploaderName, ); @@ -531,11 +565,12 @@ export default function Uploader({ if (isElectron()) { if (watcher.isUploadRunning()) { await watcher.allFileUploadsDone( - filesWithCollectionToUploadIn, + uploadItemsWithCollection, collections, ); } else if (watcher.isSyncPaused()) { - // resume the service after user upload is done + // Resume folder watch after the user upload that + // interrupted it is done. watcher.resumePausedSync(); } } @@ -610,78 +645,6 @@ export default function Uploader({ }); }; - const handleCollectionCreationAndUpload = async ( - importSuggestion: ImportSuggestion, - isFirstUpload: boolean, - pickedUploadType: PICKED_UPLOAD_TYPE, - accessedThroughSharedURL?: boolean, - ) => { - try { - if (accessedThroughSharedURL) { - const uploaderName = await getPublicCollectionUploaderName( - getPublicCollectionUID( - publicCollectionGalleryContext.token, - ), - ); - uploaderNameRef.current = uploaderName; - showUserNameInputDialog(); - return; - } - - if (isPendingDesktopUpload.current) { - isPendingDesktopUpload.current = false; - if (pendingDesktopUploadCollectionName.current) { - uploadFilesToNewCollections( - "root", - pendingDesktopUploadCollectionName.current, - ); - pendingDesktopUploadCollectionName.current = null; - } else { - uploadFilesToNewCollections("parent"); - } - return; - } - - if (isElectron() && pickedUploadType === PICKED_UPLOAD_TYPE.ZIPS) { - uploadFilesToNewCollections("parent"); - return; - } - - if (isFirstUpload && !importSuggestion.rootFolderName) { - importSuggestion.rootFolderName = FIRST_ALBUM_NAME; - } - - if (isDragAndDrop.current) { - isDragAndDrop.current = false; - if ( - props.activeCollection && - props.activeCollection.owner.id === galleryContext.user?.id - ) { - uploadFilesToExistingCollection(props.activeCollection); - return; - } - } - - let showNextModal = () => {}; - if (importSuggestion.hasNestedFolders) { - showNextModal = () => setChoiceModalView(true); - } else { - showNextModal = () => - showCollectionCreateModal(importSuggestion.rootFolderName); - } - - props.setCollectionSelectorAttributes({ - callback: uploadFilesToExistingCollection, - onCancel: handleCollectionSelectorCancel, - showNextModal, - intent: CollectionSelectorIntent.upload, - }); - } catch (e) { - // TODO(MR): Why? - log.warn("Ignoring error in handleCollectionCreationAndUpload", e); - } - }; - const cancelUploads = () => { uploadManager.cancelRunningUpload(); }; @@ -784,7 +747,7 @@ export default function Uploader({ open={userNameInputDialogView} onClose={handleUserNameInputDialogClose} onNameSubmit={handlePublicUpload} - toUploadFilesCount={fileOrPathsToUpload.current?.length} + toUploadFilesCount={uploadItemsAndPaths.current?.length} uploaderName={uploaderNameRef.current} /> @@ -884,16 +847,12 @@ function getImportSuggestion( // [a => [j], // b => [e,f,g], // c => [h, i]] -const groupFilesBasedOnParentFolder = (fileOrPaths: (File | string)[]) => { - const result = new Map(); - for (const fileOrPath of fileOrPaths) { - const filePath = - /* TODO(MR): ElectronFile */ - typeof fileOrPath == "string" - ? fileOrPath - : (fileOrPath["path"] as string); - - let folderPath = filePath.substring(0, filePath.lastIndexOf("/")); +const groupFilesBasedOnParentFolder = ( + uploadItemsAndPaths: [UploadItem, string][], +) => { + const result = new Map(); + for (const [uploadItem, pathOrName] of uploadItemsAndPaths) { + let folderPath = pathOrName.substring(0, pathOrName.lastIndexOf("/")); // If the parent folder of a file is "metadata" // we consider it to be part of the parent folder // For Eg,For FileList -> [a/x.png, a/metadata/x.png.json] @@ -907,7 +866,7 @@ const groupFilesBasedOnParentFolder = (fileOrPaths: (File | string)[]) => { ); if (!folderName) throw Error("Unexpected empty folder name"); if (!result.has(folderName)) result.set(folderName, []); - result.get(folderName).push(fileOrPath); + result.get(folderName).push(uploadItem); } return result; }; diff --git a/web/apps/photos/src/services/upload/uploadManager.ts b/web/apps/photos/src/services/upload/uploadManager.ts index 9a8cb6c6d3..00741843c8 100644 --- a/web/apps/photos/src/services/upload/uploadManager.ts +++ b/web/apps/photos/src/services/upload/uploadManager.ts @@ -109,17 +109,17 @@ const maxConcurrentUploads = 4; */ export type UploadItem = File | FileAndPath | string | ZipEntry; -export interface FileWithCollection { +export interface UploadItemWithCollection { localID: number; collectionID: number; isLivePhoto?: boolean; - fileOrPath?: File | string; + uploadItem?: UploadItem; livePhotoAssets?: LivePhotoAssets; } export interface LivePhotoAssets { - image: File | string; - video: File | string; + image: UploadItem; + video: UploadItem; } export interface PublicUploadProps { @@ -419,7 +419,7 @@ class UploadManager { * @returns `true` if at least one file was processed */ public async uploadFiles( - filesWithCollectionToUploadIn: FileWithCollection[], + filesWithCollectionToUploadIn: UploadItemWithCollection[], collections: Collection[], uploaderName?: string, ) { @@ -735,8 +735,8 @@ export default new UploadManager(); * As files progress through stages, they get more and more bits tacked on to * them. These types document the journey. * - * - The input is {@link FileWithCollection}. This can either be a new - * {@link FileWithCollection}, in which case it'll only have a + * - The input is {@link UploadItemWithCollection}. This can either be a new + * {@link UploadItemWithCollection}, in which case it'll only have a * {@link localID}, {@link collectionID} and a {@link fileOrPath}. Or it could * be a retry, in which case it'll not have a {@link fileOrPath} but instead * will have data from a previous stage (concretely, it'll just be a @@ -772,9 +772,9 @@ type FileWithCollectionIDAndName = { }; const makeFileWithCollectionIDAndName = ( - f: FileWithCollection, + f: UploadItemWithCollection, ): FileWithCollectionIDAndName => { - const fileOrPath = f.fileOrPath; + const fileOrPath = f.uploadItem; /* TODO(MR): ElectronFile */ if (!(fileOrPath instanceof File || typeof fileOrPath == "string")) throw new Error(`Unexpected file ${f}`); diff --git a/web/apps/photos/src/services/watch.ts b/web/apps/photos/src/services/watch.ts index 4de5881aa8..82d3b2f4ec 100644 --- a/web/apps/photos/src/services/watch.ts +++ b/web/apps/photos/src/services/watch.ts @@ -15,7 +15,7 @@ import { ensureString } from "@/utils/ensure"; import { UPLOAD_RESULT } from "constants/upload"; import debounce from "debounce"; import uploadManager, { - type FileWithCollection, + type UploadItemWithCollection, } from "services/upload/uploadManager"; import { Collection } from "types/collection"; import { EncryptedEnteFile } from "types/file"; @@ -317,16 +317,17 @@ class FolderWatcher { } /** - * Callback invoked by the uploader whenever a file we requested to + * Callback invoked by the uploader whenever a item we requested to * {@link upload} gets uploaded. */ async onFileUpload( fileUploadResult: UPLOAD_RESULT, - fileWithCollection: FileWithCollection, + item: UploadItemWithCollection, file: EncryptedEnteFile, ) { - // The files we get here will have fileWithCollection.file as a string, - // not as a File or a ElectronFile + // Re the usage of ensureString: For desktop watch, the only possibility + // for a UploadItem is for it to be a string (the absolute path to a + // file on disk). if ( [ UPLOAD_RESULT.ADDED_SYMLINK, @@ -335,18 +336,18 @@ class FolderWatcher { UPLOAD_RESULT.ALREADY_UPLOADED, ].includes(fileUploadResult) ) { - if (fileWithCollection.isLivePhoto) { + if (item.isLivePhoto) { this.uploadedFileForPath.set( - ensureString(fileWithCollection.livePhotoAssets.image), + ensureString(item.livePhotoAssets.image), file, ); this.uploadedFileForPath.set( - ensureString(fileWithCollection.livePhotoAssets.video), + ensureString(item.livePhotoAssets.video), file, ); } else { this.uploadedFileForPath.set( - ensureString(fileWithCollection.fileOrPath), + ensureString(item.uploadItem), file, ); } @@ -355,17 +356,15 @@ class FolderWatcher { fileUploadResult, ) ) { - if (fileWithCollection.isLivePhoto) { + if (item.isLivePhoto) { this.unUploadableFilePaths.add( - ensureString(fileWithCollection.livePhotoAssets.image), + ensureString(item.livePhotoAssets.image), ); this.unUploadableFilePaths.add( - ensureString(fileWithCollection.livePhotoAssets.video), + ensureString(item.livePhotoAssets.video), ); } else { - this.unUploadableFilePaths.add( - ensureString(fileWithCollection.fileOrPath), - ); + this.unUploadableFilePaths.add(ensureString(item.uploadItem)); } } } @@ -375,7 +374,7 @@ class FolderWatcher { * {@link upload} get uploaded. */ async allFileUploadsDone( - filesWithCollection: FileWithCollection[], + uploadItemsWithCollection: UploadItemWithCollection[], collections: Collection[], ) { const electron = ensureElectron(); @@ -384,14 +383,15 @@ class FolderWatcher { log.debug(() => JSON.stringify({ f: "watch/allFileUploadsDone", - filesWithCollection, + uploadItemsWithCollection, collections, watch, }), ); - const { syncedFiles, ignoredFiles } = - this.deduceSyncedAndIgnored(filesWithCollection); + const { syncedFiles, ignoredFiles } = this.deduceSyncedAndIgnored( + uploadItemsWithCollection, + ); if (syncedFiles.length > 0) await electron.watch.updateSyncedFiles( @@ -411,7 +411,9 @@ class FolderWatcher { this.debouncedRunNextEvent(); } - private deduceSyncedAndIgnored(filesWithCollection: FileWithCollection[]) { + private deduceSyncedAndIgnored( + uploadItemsWithCollection: UploadItemWithCollection[], + ) { const syncedFiles: FolderWatch["syncedFiles"] = []; const ignoredFiles: FolderWatch["ignoredFiles"] = []; @@ -430,14 +432,13 @@ class FolderWatcher { this.unUploadableFilePaths.delete(path); }; - for (const fileWithCollection of filesWithCollection) { - if (fileWithCollection.isLivePhoto) { - const imagePath = ensureString( - fileWithCollection.livePhotoAssets.image, - ); - const videoPath = ensureString( - fileWithCollection.livePhotoAssets.video, - ); + for (const item of uploadItemsWithCollection) { + // Re the usage of ensureString: For desktop watch, the only + // possibility for a UploadItem is for it to be a string (the + // absolute path to a file on disk). + if (item.isLivePhoto) { + const imagePath = ensureString(item.livePhotoAssets.image); + const videoPath = ensureString(item.livePhotoAssets.video); const imageFile = this.uploadedFileForPath.get(imagePath); const videoFile = this.uploadedFileForPath.get(videoPath); @@ -453,7 +454,7 @@ class FolderWatcher { markIgnored(videoPath); } } else { - const path = ensureString(fileWithCollection.fileOrPath); + const path = ensureString(item.uploadItem); const file = this.uploadedFileForPath.get(path); if (file) { markSynced(file, path); From 38094f317a068630d14cb52b174f8f86cbbdb01b Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 29 Apr 2024 20:58:58 +0530 Subject: [PATCH 086/240] wipx --- .../PhotoViewer/ImageEditorOverlay/index.tsx | 2 +- .../photos/src/components/Upload/Uploader.tsx | 14 ++-- .../src/services/upload/uploadManager.ts | 72 +++++++++++-------- .../src/services/upload/uploadService.ts | 45 ++++++------ 4 files changed, 75 insertions(+), 58 deletions(-) diff --git a/web/apps/photos/src/components/PhotoViewer/ImageEditorOverlay/index.tsx b/web/apps/photos/src/components/PhotoViewer/ImageEditorOverlay/index.tsx index ff795aca78..3c7b6a9cab 100644 --- a/web/apps/photos/src/components/PhotoViewer/ImageEditorOverlay/index.tsx +++ b/web/apps/photos/src/components/PhotoViewer/ImageEditorOverlay/index.tsx @@ -514,7 +514,7 @@ const ImageEditorOverlay = (props: IProps) => { uploadManager.prepareForNewUpload(); uploadManager.showUploadProgressDialog(); - uploadManager.uploadFiles([file], [collection]); + uploadManager.uploadItems([file], [collection]); setFileURL(null); props.onClose(); props.closePhotoViewer(); diff --git a/web/apps/photos/src/components/Upload/Uploader.tsx b/web/apps/photos/src/components/Upload/Uploader.tsx index dd90bb98c9..c895894ccc 100644 --- a/web/apps/photos/src/components/Upload/Uploader.tsx +++ b/web/apps/photos/src/components/Upload/Uploader.tsx @@ -475,7 +475,7 @@ export default function Uploader({ let index = 0; for (const [ collectionName, - fileOrPaths, + uploadItems, ] of collectionNameToUploadItems) { const collection = await getOrCreateAlbum( collectionName, @@ -485,10 +485,10 @@ export default function Uploader({ props.setCollections([...existingCollections, ...collections]); uploadItemsWithCollection = [ ...uploadItemsWithCollection, - ...fileOrPaths.map((fileOrPath) => ({ + ...uploadItems.map((uploadItem) => ({ localID: index++, collectionID: collection.id, - fileOrPath, + uploadItem, })), ]; } @@ -556,7 +556,7 @@ export default function Uploader({ .filter((x) => x), ); } - const wereFilesProcessed = await uploadManager.uploadFiles( + const wereFilesProcessed = await uploadManager.uploadItems( uploadItemsWithCollection, collections, uploaderName, @@ -586,11 +586,11 @@ export default function Uploader({ const retryFailed = async () => { try { log.info("Retrying failed uploads"); - const { files, collections } = - uploadManager.getFailedFilesWithCollections(); + const { items, collections } = + uploadManager.getFailedItemsWithCollections(); const uploaderName = uploadManager.getUploaderName(); await preUploadAction(); - await uploadManager.uploadFiles(files, collections, uploaderName); + await uploadManager.uploadItems(items, collections, uploaderName); } catch (e) { log.error("Retrying failed uploads failed", e); showUserFacingError(e.message); diff --git a/web/apps/photos/src/services/upload/uploadManager.ts b/web/apps/photos/src/services/upload/uploadManager.ts index 00741843c8..3ae22e4ae4 100644 --- a/web/apps/photos/src/services/upload/uploadManager.ts +++ b/web/apps/photos/src/services/upload/uploadManager.ts @@ -36,7 +36,11 @@ import { tryParseTakeoutMetadataJSON, type ParsedMetadataJSON, } from "./takeout"; -import UploadService, { fopFileName, fopSize, uploader } from "./uploadService"; +import UploadService, { + uploadItemFileName, + uploadItemSize, + uploader, +} from "./uploadService"; export type FileID = number; @@ -413,28 +417,28 @@ class UploadManager { * It is an error to call this method when there is already an in-progress * upload. * - * @param filesWithCollectionToUploadIn The files to upload, each paired - * with the id of the collection that they should be uploaded into. + * @param itemsWithCollection The items to upload, each paired with the id + * of the collection that they should be uploaded into. * * @returns `true` if at least one file was processed */ - public async uploadFiles( - filesWithCollectionToUploadIn: UploadItemWithCollection[], + public async uploadItems( + itemsWithCollection: UploadItemWithCollection[], collections: Collection[], uploaderName?: string, ) { if (this.uploadInProgress) throw new Error("Cannot run multiple uploads at once"); - log.info(`Uploading ${filesWithCollectionToUploadIn.length} files`); + log.info(`Uploading ${itemsWithCollection.length} files`); this.uploadInProgress = true; this.uploaderName = uploaderName; try { await this.updateExistingFilesAndCollections(collections); - const namedFiles = filesWithCollectionToUploadIn.map( - makeFileWithCollectionIDAndName, + const namedFiles = itemsWithCollection.map( + makeUploadItemWithCollectionIDAndName, ); this.uiService.setFiles(namedFiles); @@ -505,10 +509,16 @@ class UploadManager { ); } - private async parseMetadataJSONFiles(files: FileWithCollectionIDAndName[]) { + private async parseMetadataJSONFiles( + files: UploadItemWithCollectionIDAndName[], + ) { this.uiService.reset(files.length); - for (const { fileOrPath, fileName, collectionID } of files) { + for (const { + uploadItem: fileOrPath, + fileName, + collectionID, + } of files) { this.abortIfCancelled(); log.info(`Parsing metadata JSON ${fileName}`); @@ -687,9 +697,9 @@ class UploadManager { uploadCancelService.requestUploadCancelation(); } - public getFailedFilesWithCollections() { + public getFailedItemsWithCollections() { return { - files: this.failedFiles, + items: this.failedFiles, collections: [...this.collections.values()], }; } @@ -742,7 +752,7 @@ export default new UploadManager(); * will have data from a previous stage (concretely, it'll just be a * relabelled {@link ClusteredFile}), like a snake eating its tail. * - * - Immediately we convert it to {@link FileWithCollectionIDAndName}. This is + * - Immediately we convert it to {@link UploadItemWithCollectionIDAndName}. This is * to mostly systematize what we have, and also attach a {@link fileName}. * * - These then get converted to "assets", whereby both parts of a live photo @@ -752,7 +762,7 @@ export default new UploadManager(); * {@link collection}, giving us {@link UploadableFile}. This is what gets * queued and then passed to the {@link uploader}. */ -type FileWithCollectionIDAndName = { +type UploadItemWithCollectionIDAndName = { /** A unique ID for the duration of the upload */ localID: number; /** The ID of the collection to which this file should be uploaded. */ @@ -766,14 +776,14 @@ type FileWithCollectionIDAndName = { /** `true` if this is a live photo. */ isLivePhoto?: boolean; /* Valid for non-live photos */ - fileOrPath?: File | string; + uploadItem?: UploadItem; /* Valid for live photos */ livePhotoAssets?: LivePhotoAssets; }; -const makeFileWithCollectionIDAndName = ( +const makeUploadItemWithCollectionIDAndName = ( f: UploadItemWithCollection, -): FileWithCollectionIDAndName => { +): UploadItemWithCollectionIDAndName => { const fileOrPath = f.uploadItem; /* TODO(MR): ElectronFile */ if (!(fileOrPath instanceof File || typeof fileOrPath == "string")) @@ -784,11 +794,11 @@ const makeFileWithCollectionIDAndName = ( collectionID: ensure(f.collectionID), fileName: ensure( f.isLivePhoto - ? fopFileName(f.livePhotoAssets.image) - : fopFileName(fileOrPath), + ? uploadItemFileName(f.livePhotoAssets.image) + : uploadItemFileName(fileOrPath), ), isLivePhoto: f.isLivePhoto, - fileOrPath: fileOrPath, + uploadItem: fileOrPath, livePhotoAssets: f.livePhotoAssets, }; }; @@ -818,10 +828,10 @@ export type UploadableFile = ClusteredFile & { }; const splitMetadataAndMediaFiles = ( - files: FileWithCollectionIDAndName[], + files: UploadItemWithCollectionIDAndName[], ): [ - metadata: FileWithCollectionIDAndName[], - media: FileWithCollectionIDAndName[], + metadata: UploadItemWithCollectionIDAndName[], + media: UploadItemWithCollectionIDAndName[], ] => files.reduce( ([metadata, media], f) => { @@ -865,7 +875,9 @@ const cancelRemainingUploads = async () => { * Go through the given files, combining any sibling image + video assets into a * single live photo when appropriate. */ -const clusterLivePhotos = async (files: FileWithCollectionIDAndName[]) => { +const clusterLivePhotos = async ( + files: UploadItemWithCollectionIDAndName[], +) => { const result: ClusteredFile[] = []; files .sort((f, g) => @@ -884,13 +896,13 @@ const clusterLivePhotos = async (files: FileWithCollectionIDAndName[]) => { fileName: f.fileName, fileType: fFileType, collectionID: f.collectionID, - fileOrPath: f.fileOrPath, + fileOrPath: f.uploadItem, }; const ga: PotentialLivePhotoAsset = { fileName: g.fileName, fileType: gFileType, collectionID: g.collectionID, - fileOrPath: g.fileOrPath, + fileOrPath: g.uploadItem, }; if (await areLivePhotoAssets(fa, ga)) { const [image, video] = @@ -901,8 +913,8 @@ const clusterLivePhotos = async (files: FileWithCollectionIDAndName[]) => { fileName: image.fileName, isLivePhoto: true, livePhotoAssets: { - image: image.fileOrPath, - video: video.fileOrPath, + image: image.uploadItem, + video: video.uploadItem, }, }); index += 2; @@ -970,8 +982,8 @@ const areLivePhotoAssets = async ( // we use doesn't support stream as a input. const maxAssetSize = 20 * 1024 * 1024; /* 20MB */ - const fSize = await fopSize(f.fileOrPath); - const gSize = await fopSize(g.fileOrPath); + const fSize = await uploadItemSize(f.fileOrPath); + const gSize = await uploadItemSize(g.fileOrPath); if (fSize > maxAssetSize || gSize > maxAssetSize) { log.info( `Not classifying assets with too large sizes ${[fSize, gSize]} as a live photo`, diff --git a/web/apps/photos/src/services/upload/uploadService.ts b/web/apps/photos/src/services/upload/uploadService.ts index d49b32129f..a93d98975d 100644 --- a/web/apps/photos/src/services/upload/uploadService.ts +++ b/web/apps/photos/src/services/upload/uploadService.ts @@ -51,7 +51,7 @@ import { generateThumbnailWeb, } from "./thumbnail"; import UploadHttpClient from "./uploadHttpClient"; -import type { UploadableFile } from "./uploadManager"; +import type { UploadItem, UploadableFile } from "./uploadManager"; /** * A readable stream for a file, and its associated size and last modified time. @@ -181,24 +181,29 @@ const uploadService = new UploadService(); export default uploadService; /** - * Return the file name for the given {@link fileOrPath}. - * - * @param fileOrPath The {@link File}, or the path to it. Note that it is only - * valid to specify a path if we are running in the context of our desktop app. + * Return the file name for the given {@link uploadItem}. */ -export const fopFileName = (fileOrPath: File | string) => - typeof fileOrPath == "string" ? basename(fileOrPath) : fileOrPath.name; +export const uploadItemFileName = (uploadItem: UploadItem) => { + if (uploadItem instanceof File) return uploadItem.name; + if (typeof uploadItem == "string") return basename(uploadItem); + if (Array.isArray(uploadItem)) return basename(uploadItem[1]); + return uploadItem.file.name; +}; /** - * Return the size of the given {@link fileOrPath}. - * - * @param fileOrPath The {@link File}, or the path to it. Note that it is only - * valid to specify a path if we are running in the context of our desktop app. + * Return the size of the given {@link uploadItem}. */ -export const fopSize = async (fileOrPath: File | string): Promise => - fileOrPath instanceof File - ? fileOrPath.size - : await ensureElectron().fs.size(fileOrPath); +export const uploadItemSize = async ( + uploadItem: UploadItem, +): Promise => { + if (uploadItem instanceof File) return uploadItem.size; + if (typeof uploadItem == "string") return basename(uploadItem); + if (Array.isArray(uploadItem)) return basename(uploadItem[1]); + return uploadItem.file.size; +}; +uploadItem instanceof File + ? uploadItem.size + : await ensureElectron().fs.size(uploadItem); /* -- Various intermediate type used during upload -- */ @@ -643,7 +648,7 @@ const readImageOrVideoDetails = async (fileOrPath: File | string) => { const chunk = ensure((await reader.read()).value); await reader.cancel(); return chunk; - }, fopFileName(fileOrPath)); + }, uploadItemFileName(fileOrPath)); return { fileTypeInfo, fileSize, lastModifiedMs }; }; @@ -721,7 +726,7 @@ const extractLivePhotoMetadata = async ( return { metadata: { ...imageMetadata, - title: fopFileName(livePhotoAssets.image), + title: uploadItemFileName(livePhotoAssets.image), fileType: FILE_TYPE.LIVE_PHOTO, imageHash: imageMetadata.hash, videoHash: videoHash, @@ -739,7 +744,7 @@ const extractImageOrVideoMetadata = async ( parsedMetadataJSONMap: Map, worker: Remote, ) => { - const fileName = fopFileName(fileOrPath); + const fileName = uploadItemFileName(fileOrPath); const { fileType } = fileTypeInfo; let extractedMetadata: ParsedExtractedMetadata; @@ -949,9 +954,9 @@ const readLivePhoto = async ( return { fileStreamOrData: await encodeLivePhoto({ - imageFileName: fopFileName(livePhotoAssets.image), + imageFileName: uploadItemFileName(livePhotoAssets.image), imageFileOrData: await fileOrData(imageFileStreamOrData), - videoFileName: fopFileName(livePhotoAssets.video), + videoFileName: uploadItemFileName(livePhotoAssets.video), videoFileOrData: await fileOrData(videoFileStreamOrData), }), thumbnail, From fca398f296a835873e755681be7623382714849b Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 29 Apr 2024 21:06:20 +0530 Subject: [PATCH 087/240] impl --- desktop/src/main/fs.ts | 2 -- desktop/src/main/ipc.ts | 11 ++++++++--- desktop/src/main/services/upload.ts | 15 +++++++++++++++ desktop/src/preload.ts | 9 +++++---- .../photos/src/services/upload/uploadService.ts | 9 ++++----- web/packages/next/types/ipc.ts | 11 ++++++----- 6 files changed, 38 insertions(+), 19 deletions(-) diff --git a/desktop/src/main/fs.ts b/desktop/src/main/fs.ts index fc181cf46c..2428d3a80c 100644 --- a/desktop/src/main/fs.ts +++ b/desktop/src/main/fs.ts @@ -27,5 +27,3 @@ export const fsIsDir = async (dirPath: string) => { const stat = await fs.stat(dirPath); return stat.isDirectory(); }; - -export const fsSize = (path: string) => fs.stat(path).then((s) => s.size); diff --git a/desktop/src/main/ipc.ts b/desktop/src/main/ipc.ts index a99a32d097..01f481f8eb 100644 --- a/desktop/src/main/ipc.ts +++ b/desktop/src/main/ipc.ts @@ -14,6 +14,7 @@ import type { CollectionMapping, FolderWatch, PendingUploads, + ZipEntry, } from "../types/ipc"; import { selectDirectory, @@ -29,7 +30,6 @@ import { fsRename, fsRm, fsRmdir, - fsSize, fsWriteFile, } from "./fs"; import { logToDisk } from "./log"; @@ -54,6 +54,7 @@ import { import { clearPendingUploads, listZipEntries, + pathOrZipEntrySize, markUploadedFiles, markUploadedZipEntries, pendingUploads, @@ -141,8 +142,6 @@ export const attachIPCHandlers = () => { ipcMain.handle("fsIsDir", (_, dirPath: string) => fsIsDir(dirPath)); - ipcMain.handle("fsSize", (_, path: string) => fsSize(path)); - // - Conversion ipcMain.handle("convertToJPEG", (_, imageData: Uint8Array) => @@ -204,6 +203,12 @@ export const attachIPCHandlers = () => { listZipEntries(zipPath), ); + ipcMain.handle( + "pathOrZipEntrySize", + (_, pathOrZipEntry: string | ZipEntry) => + pathOrZipEntrySize(pathOrZipEntry), + ); + ipcMain.handle("pendingUploads", () => pendingUploads()); ipcMain.handle("setPendingUploads", (_, pendingUploads: PendingUploads) => diff --git a/desktop/src/main/services/upload.ts b/desktop/src/main/services/upload.ts index 1f52fe1e7c..a26722cb80 100644 --- a/desktop/src/main/services/upload.ts +++ b/desktop/src/main/services/upload.ts @@ -1,4 +1,5 @@ import StreamZip from "node-stream-zip"; +import fs from "node:fs/promises"; import { existsSync } from "original-fs"; import path from "path"; import type { ElectronFile, PendingUploads, ZipEntry } from "../../types/ipc"; @@ -23,6 +24,20 @@ export const listZipEntries = async (zipPath: string): Promise => { return entryNames.map((entryName) => [zipPath, entryName]); }; +export const pathOrZipEntrySize = async ( + pathOrZipEntry: string | ZipEntry, +): Promise => { + if (typeof pathOrZipEntry == "string") { + const stat = await fs.stat(pathOrZipEntry); + return stat.size; + } else { + const [zipPath, entryName] = pathOrZipEntry; + const zip = new StreamZip.async({ file: zipPath }); + const entry = await zip.entry(entryName); + return entry.size; + } +}; + export const pendingUploads = async (): Promise => { const collectionName = uploadStatusStore.get("collectionName"); diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index 4bb23b9ac6..226a80767a 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -123,9 +123,6 @@ const fsWriteFile = (path: string, contents: string): Promise => const fsIsDir = (dirPath: string): Promise => ipcRenderer.invoke("fsIsDir", dirPath); -const fsSize = (path: string): Promise => - ipcRenderer.invoke("fsSize", path); - // - Conversion const convertToJPEG = (imageData: Uint8Array): Promise => @@ -247,6 +244,10 @@ const pathForFile = (file: File) => webUtils.getPathForFile(file); const listZipEntries = (zipPath: string): Promise => ipcRenderer.invoke("listZipEntries", zipPath); +const pathOrZipEntrySize = ( + pathOrZipEntry: string | ZipEntry, +): Promise => ipcRenderer.invoke("pathOrZipEntrySize", pathOrZipEntry); + const pendingUploads = (): Promise => ipcRenderer.invoke("pendingUploads"); @@ -333,7 +334,6 @@ contextBridge.exposeInMainWorld("electron", { readTextFile: fsReadTextFile, writeFile: fsWriteFile, isDir: fsIsDir, - size: fsSize, }, // - Conversion @@ -374,6 +374,7 @@ contextBridge.exposeInMainWorld("electron", { pathForFile, listZipEntries, + pathOrZipEntrySize, pendingUploads, setPendingUploads, markUploadedFiles, diff --git a/web/apps/photos/src/services/upload/uploadService.ts b/web/apps/photos/src/services/upload/uploadService.ts index a93d98975d..954f171f9a 100644 --- a/web/apps/photos/src/services/upload/uploadService.ts +++ b/web/apps/photos/src/services/upload/uploadService.ts @@ -197,13 +197,12 @@ export const uploadItemSize = async ( uploadItem: UploadItem, ): Promise => { if (uploadItem instanceof File) return uploadItem.size; - if (typeof uploadItem == "string") return basename(uploadItem); - if (Array.isArray(uploadItem)) return basename(uploadItem[1]); + if (typeof uploadItem == "string") + return ensureElectron().pathOrZipEntrySize(uploadItem); + if (Array.isArray(uploadItem)) + return ensureElectron().pathOrZipEntrySize(uploadItem); return uploadItem.file.size; }; -uploadItem instanceof File - ? uploadItem.size - : await ensureElectron().fs.size(uploadItem); /* -- Various intermediate type used during upload -- */ diff --git a/web/packages/next/types/ipc.ts b/web/packages/next/types/ipc.ts index dab10cc8e0..34bb9196a2 100644 --- a/web/packages/next/types/ipc.ts +++ b/web/packages/next/types/ipc.ts @@ -189,11 +189,6 @@ export interface Electron { * directory. */ isDir: (dirPath: string) => Promise; - - /** - * Return the size in bytes of the file at {@link path}. - */ - size: (path: string) => Promise; }; // - Conversion @@ -492,6 +487,12 @@ export interface Electron { */ listZipEntries: (zipPath: string) => Promise; + /** + * Return the size in bytes of the file at the given path or of a particular + * entry within a zip file. + */ + pathOrZipEntrySize: (pathOrZipEntry: string | ZipEntry) => Promise; + /** * Return any pending uploads that were previously enqueued but haven't yet * been completed. From eb608f4bdd53db426f1704f37d42b06b8ff67c27 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 29 Apr 2024 21:12:47 +0530 Subject: [PATCH 088/240] ren --- .../src/services/upload/uploadManager.ts | 53 ++++++++++--------- .../src/services/upload/uploadService.ts | 6 +-- 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/web/apps/photos/src/services/upload/uploadManager.ts b/web/apps/photos/src/services/upload/uploadManager.ts index 3ae22e4ae4..53b6c7abff 100644 --- a/web/apps/photos/src/services/upload/uploadManager.ts +++ b/web/apps/photos/src/services/upload/uploadManager.ts @@ -350,9 +350,9 @@ class UploadManager { ComlinkWorker >(maxConcurrentUploads); private parsedMetadataJSONMap: Map; - private filesToBeUploaded: ClusteredFile[]; - private remainingFiles: ClusteredFile[] = []; - private failedFiles: ClusteredFile[]; + private filesToBeUploaded: ClusteredUploadItem[]; + private remainingFiles: ClusteredUploadItem[] = []; + private failedFiles: ClusteredUploadItem[]; private existingFiles: EnteFile[]; private setFiles: SetFiles; private collections: Map; @@ -533,7 +533,7 @@ class UploadManager { } } - private async uploadMediaFiles(mediaFiles: ClusteredFile[]) { + private async uploadMediaFiles(mediaFiles: ClusteredUploadItem[]) { this.filesToBeUploaded = [...this.filesToBeUploaded, ...mediaFiles]; if (isElectron()) { @@ -608,7 +608,7 @@ class UploadManager { } private async postUploadTask( - uploadableFile: UploadableFile, + uploadableFile: UploadableUploadItem, uploadResult: UPLOAD_RESULT, uploadedFile: EncryptedEnteFile | EnteFile | undefined, ) { @@ -655,7 +655,7 @@ class UploadManager { eventBus.emit(Events.FILE_UPLOADED, { enteFile: decryptedFile, localFile: - uploadableFile.fileOrPath ?? + uploadableFile.uploadItem ?? uploadableFile.livePhotoAssets.image, }); } catch (e) { @@ -677,7 +677,7 @@ class UploadManager { private async watchFolderCallback( fileUploadResult: UPLOAD_RESULT, - fileWithCollection: ClusteredFile, + fileWithCollection: ClusteredUploadItem, uploadedFile: EncryptedEnteFile, ) { if (isElectron()) { @@ -720,7 +720,7 @@ class UploadManager { this.setFiles((files) => sortFiles([...files, decryptedFile])); } - private async removeFromPendingUploads({ localID }: ClusteredFile) { + private async removeFromPendingUploads({ localID }: ClusteredUploadItem) { const electron = globalThis.electron; if (electron) { this.remainingFiles = this.remainingFiles.filter( @@ -747,20 +747,21 @@ export default new UploadManager(); * * - The input is {@link UploadItemWithCollection}. This can either be a new * {@link UploadItemWithCollection}, in which case it'll only have a - * {@link localID}, {@link collectionID} and a {@link fileOrPath}. Or it could - * be a retry, in which case it'll not have a {@link fileOrPath} but instead + * {@link localID}, {@link collectionID} and a {@link uploadItem}. Or it could + * be a retry, in which case it'll not have a {@link uploadItem} but instead * will have data from a previous stage (concretely, it'll just be a - * relabelled {@link ClusteredFile}), like a snake eating its tail. + * relabelled {@link ClusteredUploadItem}), like a snake eating its tail. * - * - Immediately we convert it to {@link UploadItemWithCollectionIDAndName}. This is - * to mostly systematize what we have, and also attach a {@link fileName}. + * - Immediately we convert it to {@link UploadItemWithCollectionIDAndName}. + * This is to mostly systematize what we have, and also attach a + * {@link fileName}. * * - These then get converted to "assets", whereby both parts of a live photo - * are combined. This is a {@link ClusteredFile}. + * are combined. This is a {@link ClusteredUploadItem}. * - * - On to the {@link ClusteredFile} we attach the corresponding - * {@link collection}, giving us {@link UploadableFile}. This is what gets - * queued and then passed to the {@link uploader}. + * - On to the {@link ClusteredUploadItem} we attach the corresponding + * {@link collection}, giving us {@link UploadableUploadItem}. This is what + * gets queued and then passed to the {@link uploader}. */ type UploadItemWithCollectionIDAndName = { /** A unique ID for the duration of the upload */ @@ -804,26 +805,26 @@ const makeUploadItemWithCollectionIDAndName = ( }; /** - * A file with both parts of a live photo clubbed together. + * An upload item with both parts of a live photo clubbed together. * * See: [Note: Intermediate file types during upload]. */ -type ClusteredFile = { +type ClusteredUploadItem = { localID: number; collectionID: number; fileName: string; isLivePhoto: boolean; - fileOrPath?: File | string; + uploadItem?: UploadItem; livePhotoAssets?: LivePhotoAssets; }; /** - * The file that we hand off to the uploader. Essentially {@link ClusteredFile} - * with the {@link collection} attached to it. + * The file that we hand off to the uploader. Essentially + * {@link ClusteredUploadItem} with the {@link collection} attached to it. * * See: [Note: Intermediate file types during upload]. */ -export type UploadableFile = ClusteredFile & { +export type UploadableUploadItem = ClusteredUploadItem & { collection: Collection; }; @@ -844,13 +845,13 @@ const splitMetadataAndMediaFiles = ( const updatePendingUploads = async ( electron: Electron, - files: ClusteredFile[], + files: ClusteredUploadItem[], ) => { const paths = files .map((file) => file.isLivePhoto ? [file.livePhotoAssets.image, file.livePhotoAssets.video] - : [file.fileOrPath], + : [file.uploadItem], ) .flat() .map((f) => getFilePathElectron(f)); @@ -878,7 +879,7 @@ const cancelRemainingUploads = async () => { const clusterLivePhotos = async ( files: UploadItemWithCollectionIDAndName[], ) => { - const result: ClusteredFile[] = []; + const result: ClusteredUploadItem[] = []; files .sort((f, g) => nameAndExtension(f.fileName)[0].localeCompare( diff --git a/web/apps/photos/src/services/upload/uploadService.ts b/web/apps/photos/src/services/upload/uploadService.ts index 954f171f9a..f5ae6b650b 100644 --- a/web/apps/photos/src/services/upload/uploadService.ts +++ b/web/apps/photos/src/services/upload/uploadService.ts @@ -51,7 +51,7 @@ import { generateThumbnailWeb, } from "./thumbnail"; import UploadHttpClient from "./uploadHttpClient"; -import type { UploadItem, UploadableFile } from "./uploadManager"; +import type { UploadItem, UploadableUploadItem } from "./uploadManager"; /** * A readable stream for a file, and its associated size and last modified time. @@ -315,14 +315,14 @@ interface UploadResponse { } /** - * Upload the given {@link UploadableFile} + * Upload the given {@link UploadableUploadItem} * * This is lower layer implementation of the upload. It is invoked by * {@link UploadManager} after it has assembled all the relevant bits we need to * go forth and upload. */ export const uploader = async ( - { collection, localID, fileName, ...uploadAsset }: UploadableFile, + { collection, localID, fileName, ...uploadAsset }: UploadableUploadItem, uploaderName: string, existingFiles: EnteFile[], parsedMetadataJSONMap: Map, From 6bcf98539025cf17df9fe3219fabdded16d52734 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 29 Apr 2024 21:27:45 +0530 Subject: [PATCH 089/240] or can it --- .../src/services/upload/uploadManager.ts | 102 ++++++++++-------- 1 file changed, 59 insertions(+), 43 deletions(-) diff --git a/web/apps/photos/src/services/upload/uploadManager.ts b/web/apps/photos/src/services/upload/uploadManager.ts index 53b6c7abff..d5a7b4caff 100644 --- a/web/apps/photos/src/services/upload/uploadManager.ts +++ b/web/apps/photos/src/services/upload/uploadManager.ts @@ -720,13 +720,15 @@ class UploadManager { this.setFiles((files) => sortFiles([...files, decryptedFile])); } - private async removeFromPendingUploads({ localID }: ClusteredUploadItem) { + private async removeFromPendingUploads( + clusteredUploadItem: ClusteredUploadItem, + ) { const electron = globalThis.electron; if (electron) { this.remainingFiles = this.remainingFiles.filter( - (f) => f.localID != localID, + (f) => f.localID != clusteredUploadItem.localID, ); - await updatePendingUploads(electron, this.remainingFiles); + await markUploaded(electron, clusteredUploadItem); } } @@ -784,25 +786,18 @@ type UploadItemWithCollectionIDAndName = { const makeUploadItemWithCollectionIDAndName = ( f: UploadItemWithCollection, -): UploadItemWithCollectionIDAndName => { - const fileOrPath = f.uploadItem; - /* TODO(MR): ElectronFile */ - if (!(fileOrPath instanceof File || typeof fileOrPath == "string")) - throw new Error(`Unexpected file ${f}`); - - return { - localID: ensure(f.localID), - collectionID: ensure(f.collectionID), - fileName: ensure( - f.isLivePhoto - ? uploadItemFileName(f.livePhotoAssets.image) - : uploadItemFileName(fileOrPath), - ), - isLivePhoto: f.isLivePhoto, - uploadItem: fileOrPath, - livePhotoAssets: f.livePhotoAssets, - }; -}; +): UploadItemWithCollectionIDAndName => ({ + localID: ensure(f.localID), + collectionID: ensure(f.collectionID), + fileName: ensure( + f.isLivePhoto + ? uploadItemFileName(f.livePhotoAssets.image) + : uploadItemFileName(f.uploadItem), + ), + isLivePhoto: f.isLivePhoto, + uploadItem: f.uploadItem, + livePhotoAssets: f.livePhotoAssets, +}); /** * An upload item with both parts of a live photo clubbed together. @@ -829,12 +824,12 @@ export type UploadableUploadItem = ClusteredUploadItem & { }; const splitMetadataAndMediaFiles = ( - files: UploadItemWithCollectionIDAndName[], + items: UploadItemWithCollectionIDAndName[], ): [ metadata: UploadItemWithCollectionIDAndName[], media: UploadItemWithCollectionIDAndName[], ] => - files.reduce( + items.reduce( ([metadata, media], f) => { if (lowercaseExtension(f.fileName) == "json") metadata.push(f); else media.push(f); @@ -843,19 +838,45 @@ const splitMetadataAndMediaFiles = ( [[], []], ); -const updatePendingUploads = async ( - electron: Electron, - files: ClusteredUploadItem[], -) => { - const paths = files - .map((file) => - file.isLivePhoto - ? [file.livePhotoAssets.image, file.livePhotoAssets.video] - : [file.uploadItem], - ) - .flat() - .map((f) => getFilePathElectron(f)); - await electron.setPendingUploadFiles("files", paths); +const markUploaded = async (electron: Electron, item: ClusteredUploadItem) => { + // TODO: This can be done better + if (item.isLivePhoto) { + const [p0, p1] = [ + item.livePhotoAssets.image, + item.livePhotoAssets.video, + ]; + if (Array.isArray(p0) && Array.isArray(p1)) { + electron.markUploadedZipEntries([p0, p1]); + } else if (typeof p0 == "string" && typeof p1 == "string") { + electron.markUploadedFiles([p0, p1]); + } else if ( + p0 && + typeof p0 == "object" && + "path" in p0 && + p1 && + typeof p1 == "object" && + "path" in p1 + ) { + electron.markUploadedFiles([p0.path, p1.path]); + } else { + throw new Error( + "Attempting to mark upload completion of unexpected desktop upload items", + ); + } + } else { + const p = ensure(item.uploadItem); + if (Array.isArray(p)) { + electron.markUploadedZipEntries([p]); + } else if (typeof p == "string") { + electron.markUploadedFiles([p]); + } else if (p && typeof p == "object" && "path" in p) { + electron.markUploadedFiles([p]); + } else { + throw new Error( + "Attempting to mark upload completion of unexpected desktop upload items", + ); + } + } }; /** @@ -865,12 +886,7 @@ const updatePendingUploads = async ( export const getFilePathElectron = (file: File | ElectronFile | string) => typeof file == "string" ? file : (file as ElectronFile).path; -const cancelRemainingUploads = async () => { - const electron = ensureElectron(); - await electron.setPendingUploadCollection(undefined); - await electron.setPendingUploadFiles("zips", []); - await electron.setPendingUploadFiles("files", []); -}; +const cancelRemainingUploads = () => ensureElectron().clearPendingUploads(); /** * Go through the given files, combining any sibling image + video assets into a From 39737b985b2df396f096f6c0b2b93f702d865f7c Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 29 Apr 2024 21:38:21 +0530 Subject: [PATCH 090/240] teach readstream about zips --- .../photos/src/services/upload/takeout.ts | 24 ++++++--- .../src/services/upload/uploadManager.ts | 53 ++++++++----------- web/apps/photos/src/utils/native-stream.ts | 24 ++++++--- 3 files changed, 56 insertions(+), 45 deletions(-) diff --git a/web/apps/photos/src/services/upload/takeout.ts b/web/apps/photos/src/services/upload/takeout.ts index 5cd16130ef..2a71e420a0 100644 --- a/web/apps/photos/src/services/upload/takeout.ts +++ b/web/apps/photos/src/services/upload/takeout.ts @@ -5,6 +5,8 @@ import { nameAndExtension } from "@/next/file"; import log from "@/next/log"; import { NULL_LOCATION } from "constants/upload"; import type { Location } from "types/metadata"; +import { readStream } from "utils/native-stream"; +import type { UploadItem } from "./uploadManager"; export interface ParsedMetadataJSON { creationTime: number; @@ -75,21 +77,29 @@ function getFileOriginalName(fileName: string) { /** Try to parse the contents of a metadata JSON file from a Google Takeout. */ export const tryParseTakeoutMetadataJSON = async ( - fileOrPath: File | string, + uploadItem: UploadItem, ): Promise => { try { - const text = - fileOrPath instanceof File - ? await fileOrPath.text() - : await ensureElectron().fs.readTextFile(fileOrPath); - - return parseMetadataJSONText(text); + return parseMetadataJSONText(await uploadItemText(uploadItem)); } catch (e) { log.error("Failed to parse takeout metadata JSON", e); return undefined; } }; +const uploadItemText = async (uploadItem: UploadItem) => { + if (uploadItem instanceof File) { + return await uploadItem.text(); + } else if (typeof uploadItem == "string") { + return await ensureElectron().fs.readTextFile(uploadItem); + } else if (Array.isArray(uploadItem)) { + const { response } = await readStream(ensureElectron(), uploadItem); + return await response.text(); + } else { + return await uploadItem.file.text(); + } +}; + const NULL_PARSED_METADATA_JSON: ParsedMetadataJSON = { creationTime: null, modificationTime: null, diff --git a/web/apps/photos/src/services/upload/uploadManager.ts b/web/apps/photos/src/services/upload/uploadManager.ts index d5a7b4caff..b561cb02ff 100644 --- a/web/apps/photos/src/services/upload/uploadManager.ts +++ b/web/apps/photos/src/services/upload/uploadManager.ts @@ -3,7 +3,7 @@ import { potentialFileTypeFromExtension } from "@/media/live-photo"; import { ensureElectron } from "@/next/electron"; import { lowercaseExtension, nameAndExtension } from "@/next/file"; import log from "@/next/log"; -import { ElectronFile, type FileAndPath } from "@/next/types/file"; +import { type FileAndPath } from "@/next/types/file"; import type { Electron, ZipEntry } from "@/next/types/ipc"; import { ComlinkWorker } from "@/next/worker/comlink-worker"; import { ensure } from "@/utils/ensure"; @@ -437,25 +437,25 @@ class UploadManager { try { await this.updateExistingFilesAndCollections(collections); - const namedFiles = itemsWithCollection.map( + const namedItems = itemsWithCollection.map( makeUploadItemWithCollectionIDAndName, ); - this.uiService.setFiles(namedFiles); + this.uiService.setFiles(namedItems); - const [metadataFiles, mediaFiles] = - splitMetadataAndMediaFiles(namedFiles); + const [metadataItems, mediaItems] = + splitMetadataAndMediaItems(namedItems); - if (metadataFiles.length) { + if (metadataItems.length) { this.uiService.setUploadStage( UPLOAD_STAGES.READING_GOOGLE_METADATA_FILES, ); - await this.parseMetadataJSONFiles(metadataFiles); + await this.parseMetadataJSONFiles(metadataItems); } - if (mediaFiles.length) { - const clusteredMediaFiles = await clusterLivePhotos(mediaFiles); + if (mediaItems.length) { + const clusteredMediaFiles = await clusterLivePhotos(mediaItems); this.abortIfCancelled(); @@ -464,7 +464,7 @@ class UploadManager { this.uiService.setFiles(clusteredMediaFiles); this.uiService.setHasLivePhoto( - mediaFiles.length != clusteredMediaFiles.length, + mediaItems.length != clusteredMediaFiles.length, ); await this.uploadMediaFiles(clusteredMediaFiles); @@ -510,19 +510,17 @@ class UploadManager { } private async parseMetadataJSONFiles( - files: UploadItemWithCollectionIDAndName[], + items: UploadItemWithCollectionIDAndName[], ) { - this.uiService.reset(files.length); + this.uiService.reset(items.length); - for (const { - uploadItem: fileOrPath, - fileName, - collectionID, - } of files) { + for (const { uploadItem, fileName, collectionID } of items) { this.abortIfCancelled(); log.info(`Parsing metadata JSON ${fileName}`); - const metadataJSON = await tryParseTakeoutMetadataJSON(fileOrPath); + const metadataJSON = await tryParseTakeoutMetadataJSON( + ensure(uploadItem), + ); if (metadataJSON) { this.parsedMetadataJSONMap.set( getMetadataJSONMapKeyForJSON(collectionID, fileName), @@ -823,7 +821,7 @@ export type UploadableUploadItem = ClusteredUploadItem & { collection: Collection; }; -const splitMetadataAndMediaFiles = ( +const splitMetadataAndMediaItems = ( items: UploadItemWithCollectionIDAndName[], ): [ metadata: UploadItemWithCollectionIDAndName[], @@ -879,13 +877,6 @@ const markUploaded = async (electron: Electron, item: ClusteredUploadItem) => { } }; -/** - * NOTE: a stop gap measure, only meant to be called by code that is running in - * the context of a desktop app initiated upload - */ -export const getFilePathElectron = (file: File | ElectronFile | string) => - typeof file == "string" ? file : (file as ElectronFile).path; - const cancelRemainingUploads = () => ensureElectron().clearPendingUploads(); /** @@ -913,13 +904,13 @@ const clusterLivePhotos = async ( fileName: f.fileName, fileType: fFileType, collectionID: f.collectionID, - fileOrPath: f.uploadItem, + uploadItem: f.uploadItem, }; const ga: PotentialLivePhotoAsset = { fileName: g.fileName, fileType: gFileType, collectionID: g.collectionID, - fileOrPath: g.uploadItem, + uploadItem: g.uploadItem, }; if (await areLivePhotoAssets(fa, ga)) { const [image, video] = @@ -956,7 +947,7 @@ interface PotentialLivePhotoAsset { fileName: string; fileType: FILE_TYPE; collectionID: number; - fileOrPath: File | string; + uploadItem: UploadItem; } const areLivePhotoAssets = async ( @@ -999,8 +990,8 @@ const areLivePhotoAssets = async ( // we use doesn't support stream as a input. const maxAssetSize = 20 * 1024 * 1024; /* 20MB */ - const fSize = await uploadItemSize(f.fileOrPath); - const gSize = await uploadItemSize(g.fileOrPath); + const fSize = await uploadItemSize(f.uploadItem); + const gSize = await uploadItemSize(g.uploadItem); if (fSize > maxAssetSize || gSize > maxAssetSize) { log.info( `Not classifying assets with too large sizes ${[fSize, gSize]} as a live photo`, diff --git a/web/apps/photos/src/utils/native-stream.ts b/web/apps/photos/src/utils/native-stream.ts index ed7b16a793..c882d5031a 100644 --- a/web/apps/photos/src/utils/native-stream.ts +++ b/web/apps/photos/src/utils/native-stream.ts @@ -6,10 +6,10 @@ * See: [Note: IPC streams]. */ -import type { Electron } from "@/next/types/ipc"; +import type { Electron, ZipEntry } from "@/next/types/ipc"; /** - * Stream the given file from the user's local filesystem. + * Stream the given file or zip entry from the user's local filesystem. * * This only works when we're running in our desktop app since it uses the * "stream://" protocol handler exposed by our custom code in the Node.js layer. @@ -18,8 +18,9 @@ import type { Electron } from "@/next/types/ipc"; * To avoid accidentally invoking it in a non-desktop app context, it requires * the {@link Electron} object as a parameter (even though it doesn't use it). * - * @param path The path on the file on the user's local filesystem whose - * contents we want to stream. + * @param pathOrZipEntry Either the path on the file on the user's local + * filesystem whose contents we want to stream. Or a tuple containing the path + * to a zip file and the name of the entry within it. * * @return A ({@link Response}, size, lastModifiedMs) triple. * @@ -34,16 +35,25 @@ import type { Electron } from "@/next/types/ipc"; */ export const readStream = async ( _: Electron, - path: string, + pathOrZipEntry: string | ZipEntry, ): Promise<{ response: Response; size: number; lastModifiedMs: number }> => { - const req = new Request(`stream://read${path}`, { + let url: URL; + if (typeof pathOrZipEntry == "string") { + url = new URL(`stream://read${pathOrZipEntry}`); + } else { + const [zipPath, entryName] = pathOrZipEntry; + url = new URL(`stream://read${zipPath}`); + url.hash = entryName; + } + + const req = new Request(url, { method: "GET", }); const res = await fetch(req); if (!res.ok) throw new Error( - `Failed to read stream from ${path}: HTTP ${res.status}`, + `Failed to read stream from ${url}: HTTP ${res.status}`, ); const size = readNumericHeader(res, "Content-Length"); From ff8aba816a0a76271b09374e78898e29390dfbeb Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 29 Apr 2024 21:41:47 +0530 Subject: [PATCH 091/240] cont --- .../src/services/upload/uploadManager.ts | 106 ++++++++++-------- .../src/services/upload/uploadService.ts | 21 +--- 2 files changed, 61 insertions(+), 66 deletions(-) diff --git a/web/apps/photos/src/services/upload/uploadManager.ts b/web/apps/photos/src/services/upload/uploadManager.ts index b561cb02ff..06772b2d29 100644 --- a/web/apps/photos/src/services/upload/uploadManager.ts +++ b/web/apps/photos/src/services/upload/uploadManager.ts @@ -36,11 +36,7 @@ import { tryParseTakeoutMetadataJSON, type ParsedMetadataJSON, } from "./takeout"; -import UploadService, { - uploadItemFileName, - uploadItemSize, - uploader, -} from "./uploadService"; +import UploadService, { uploadItemFileName, uploader } from "./uploadService"; export type FileID = number; @@ -350,9 +346,9 @@ class UploadManager { ComlinkWorker >(maxConcurrentUploads); private parsedMetadataJSONMap: Map; - private filesToBeUploaded: ClusteredUploadItem[]; - private remainingFiles: ClusteredUploadItem[] = []; - private failedFiles: ClusteredUploadItem[]; + private itemsToBeUploaded: ClusteredUploadItem[]; + private remainingItems: ClusteredUploadItem[] = []; + private failedItems: ClusteredUploadItem[]; private existingFiles: EnteFile[]; private setFiles: SetFiles; private collections: Map; @@ -389,9 +385,9 @@ class UploadManager { } private resetState() { - this.filesToBeUploaded = []; - this.remainingFiles = []; - this.failedFiles = []; + this.itemsToBeUploaded = []; + this.remainingItems = []; + this.failedItems = []; this.parsedMetadataJSONMap = new Map(); this.uploaderName = null; @@ -455,24 +451,24 @@ class UploadManager { } if (mediaItems.length) { - const clusteredMediaFiles = await clusterLivePhotos(mediaItems); + const clusteredMediaItems = await clusterLivePhotos(mediaItems); this.abortIfCancelled(); // Live photos might've been clustered together, reset the list // of files to reflect that. - this.uiService.setFiles(clusteredMediaFiles); + this.uiService.setFiles(clusteredMediaItems); this.uiService.setHasLivePhoto( - mediaItems.length != clusteredMediaFiles.length, + mediaItems.length != clusteredMediaItems.length, ); - await this.uploadMediaFiles(clusteredMediaFiles); + await this.uploadMediaItems(clusteredMediaItems); } } catch (e) { if (e.message === CustomError.UPLOAD_CANCELLED) { if (isElectron()) { - this.remainingFiles = []; + this.remainingItems = []; await cancelRemainingUploads(); } } else { @@ -531,48 +527,48 @@ class UploadManager { } } - private async uploadMediaFiles(mediaFiles: ClusteredUploadItem[]) { - this.filesToBeUploaded = [...this.filesToBeUploaded, ...mediaFiles]; + private async uploadMediaItems(mediaItems: ClusteredUploadItem[]) { + this.itemsToBeUploaded = [...this.itemsToBeUploaded, ...mediaItems]; if (isElectron()) { - this.remainingFiles = [...this.remainingFiles, ...mediaFiles]; + this.remainingItems = [...this.remainingItems, ...mediaItems]; } - this.uiService.reset(mediaFiles.length); + this.uiService.reset(mediaItems.length); - await UploadService.setFileCount(mediaFiles.length); + await UploadService.setFileCount(mediaItems.length); this.uiService.setUploadStage(UPLOAD_STAGES.UPLOADING); const uploadProcesses = []; for ( let i = 0; - i < maxConcurrentUploads && this.filesToBeUploaded.length > 0; + i < maxConcurrentUploads && this.itemsToBeUploaded.length > 0; i++ ) { this.cryptoWorkers[i] = getDedicatedCryptoWorker(); const worker = await this.cryptoWorkers[i].remote; - uploadProcesses.push(this.uploadNextFileInQueue(worker)); + uploadProcesses.push(this.uploadNextItemInQueue(worker)); } await Promise.all(uploadProcesses); } - private async uploadNextFileInQueue(worker: Remote) { + private async uploadNextItemInQueue(worker: Remote) { const uiService = this.uiService; - while (this.filesToBeUploaded.length > 0) { + while (this.itemsToBeUploaded.length > 0) { this.abortIfCancelled(); - const clusteredFile = this.filesToBeUploaded.pop(); - const { localID, collectionID } = clusteredFile; + const clusteredItem = this.itemsToBeUploaded.pop(); + const { localID, collectionID } = clusteredItem; const collection = this.collections.get(collectionID); - const uploadableFile = { ...clusteredFile, collection }; + const uploadableItem = { ...clusteredItem, collection }; uiService.setFileProgress(localID, 0); await wait(0); const { uploadResult, uploadedFile } = await uploader( - uploadableFile, + uploadableItem, this.uploaderName, this.existingFiles, this.parsedMetadataJSONMap, @@ -594,7 +590,7 @@ class UploadManager { ); const finalUploadResult = await this.postUploadTask( - uploadableFile, + uploadableItem, uploadResult, uploadedFile, ); @@ -606,20 +602,20 @@ class UploadManager { } private async postUploadTask( - uploadableFile: UploadableUploadItem, + uploadableItem: UploadableUploadItem, uploadResult: UPLOAD_RESULT, uploadedFile: EncryptedEnteFile | EnteFile | undefined, ) { log.info( - `Uploaded ${uploadableFile.fileName} with result ${uploadResult}`, + `Uploaded ${uploadableItem.fileName} with result ${uploadResult}`, ); try { let decryptedFile: EnteFile; - await this.removeFromPendingUploads(uploadableFile); + await this.removeFromPendingUploads(uploadableItem); switch (uploadResult) { case UPLOAD_RESULT.FAILED: case UPLOAD_RESULT.BLOCKED: - this.failedFiles.push(uploadableFile); + this.failedItems.push(uploadableItem); break; case UPLOAD_RESULT.ALREADY_UPLOADED: decryptedFile = uploadedFile as EnteFile; @@ -632,7 +628,7 @@ class UploadManager { case UPLOAD_RESULT.UPLOADED_WITH_STATIC_THUMBNAIL: decryptedFile = await decryptFile( uploadedFile as EncryptedEnteFile, - uploadableFile.collection.key, + uploadableItem.collection.key, ); break; case UPLOAD_RESULT.UNSUPPORTED: @@ -653,8 +649,8 @@ class UploadManager { eventBus.emit(Events.FILE_UPLOADED, { enteFile: decryptedFile, localFile: - uploadableFile.uploadItem ?? - uploadableFile.livePhotoAssets.image, + uploadableItem.uploadItem ?? + uploadableItem.livePhotoAssets.image, }); } catch (e) { log.warn("Ignoring error in fileUploaded handlers", e); @@ -663,7 +659,7 @@ class UploadManager { } await this.watchFolderCallback( uploadResult, - uploadableFile, + uploadableItem, uploadedFile as EncryptedEnteFile, ); return uploadResult; @@ -697,7 +693,7 @@ class UploadManager { public getFailedItemsWithCollections() { return { - items: this.failedFiles, + items: this.failedItems, collections: [...this.collections.values()], }; } @@ -723,7 +719,7 @@ class UploadManager { ) { const electron = globalThis.electron; if (electron) { - this.remainingFiles = this.remainingFiles.filter( + this.remainingItems = this.remainingItems.filter( (f) => f.localID != clusteredUploadItem.localID, ); await markUploaded(electron, clusteredUploadItem); @@ -868,7 +864,7 @@ const markUploaded = async (electron: Electron, item: ClusteredUploadItem) => { } else if (typeof p == "string") { electron.markUploadedFiles([p]); } else if (p && typeof p == "object" && "path" in p) { - electron.markUploadedFiles([p]); + electron.markUploadedFiles([p.path]); } else { throw new Error( "Attempting to mark upload completion of unexpected desktop upload items", @@ -884,10 +880,10 @@ const cancelRemainingUploads = () => ensureElectron().clearPendingUploads(); * single live photo when appropriate. */ const clusterLivePhotos = async ( - files: UploadItemWithCollectionIDAndName[], + items: UploadItemWithCollectionIDAndName[], ) => { const result: ClusteredUploadItem[] = []; - files + items .sort((f, g) => nameAndExtension(f.fileName)[0].localeCompare( nameAndExtension(g.fileName)[0], @@ -895,9 +891,9 @@ const clusterLivePhotos = async ( ) .sort((f, g) => f.collectionID - g.collectionID); let index = 0; - while (index < files.length - 1) { - const f = files[index]; - const g = files[index + 1]; + while (index < items.length - 1) { + const f = items[index]; + const g = items[index + 1]; const fFileType = potentialFileTypeFromExtension(f.fileName); const gFileType = potentialFileTypeFromExtension(g.fileName); const fa: PotentialLivePhotoAsset = { @@ -934,9 +930,9 @@ const clusterLivePhotos = async ( index += 1; } } - if (index === files.length - 1) { + if (index === items.length - 1) { result.push({ - ...files[index], + ...items[index], isLivePhoto: false, }); } @@ -994,7 +990,7 @@ const areLivePhotoAssets = async ( const gSize = await uploadItemSize(g.uploadItem); if (fSize > maxAssetSize || gSize > maxAssetSize) { log.info( - `Not classifying assets with too large sizes ${[fSize, gSize]} as a live photo`, + `Not classifying files with too large sizes (${fSize} and ${gSize} bytes) as a live photo`, ); return false; } @@ -1027,3 +1023,15 @@ const removePotentialLivePhotoSuffix = (name: string, suffix?: string) => { return foundSuffix ? name.slice(0, foundSuffix.length * -1) : name; }; + +/** + * Return the size of the given {@link uploadItem}. + */ +const uploadItemSize = async (uploadItem: UploadItem): Promise => { + if (uploadItem instanceof File) return uploadItem.size; + if (typeof uploadItem == "string") + return ensureElectron().pathOrZipEntrySize(uploadItem); + if (Array.isArray(uploadItem)) + return ensureElectron().pathOrZipEntrySize(uploadItem); + return uploadItem.file.size; +}; diff --git a/web/apps/photos/src/services/upload/uploadService.ts b/web/apps/photos/src/services/upload/uploadService.ts index f5ae6b650b..275c001648 100644 --- a/web/apps/photos/src/services/upload/uploadService.ts +++ b/web/apps/photos/src/services/upload/uploadService.ts @@ -190,25 +190,12 @@ export const uploadItemFileName = (uploadItem: UploadItem) => { return uploadItem.file.name; }; -/** - * Return the size of the given {@link uploadItem}. - */ -export const uploadItemSize = async ( - uploadItem: UploadItem, -): Promise => { - if (uploadItem instanceof File) return uploadItem.size; - if (typeof uploadItem == "string") - return ensureElectron().pathOrZipEntrySize(uploadItem); - if (Array.isArray(uploadItem)) - return ensureElectron().pathOrZipEntrySize(uploadItem); - return uploadItem.file.size; -}; /* -- Various intermediate type used during upload -- */ interface UploadAsset { isLivePhoto?: boolean; - fileOrPath?: File | string; + uploadItem?: UploadItem; livePhotoAssets?: LivePhotoAssets; } @@ -606,7 +593,7 @@ interface ReadAssetDetailsResult { const readAssetDetails = async ({ isLivePhoto, livePhotoAssets, - fileOrPath, + uploadItem: fileOrPath, }: UploadAsset): Promise => isLivePhoto ? readLivePhotoDetails(livePhotoAssets) @@ -673,7 +660,7 @@ interface ExtractAssetMetadataResult { * {@link parsedMetadataJSONMap} for the assets. Return the resultant metadatum. */ const extractAssetMetadata = async ( - { isLivePhoto, fileOrPath, livePhotoAssets }: UploadAsset, + { isLivePhoto, uploadItem: fileOrPath, livePhotoAssets }: UploadAsset, fileTypeInfo: FileTypeInfo, lastModifiedMs: number, collectionID: number, @@ -914,7 +901,7 @@ const areFilesSameNoHash = (f: Metadata, g: Metadata) => { const readAsset = async ( fileTypeInfo: FileTypeInfo, - { isLivePhoto, fileOrPath, livePhotoAssets }: UploadAsset, + { isLivePhoto, uploadItem: fileOrPath, livePhotoAssets }: UploadAsset, ): Promise => isLivePhoto ? await readLivePhoto(livePhotoAssets, fileTypeInfo) From baf491c62445f9a25fae57cddc603a767ca7ae84 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 09:10:27 +0530 Subject: [PATCH 092/240] up --- web/apps/photos/src/services/ffmpeg.ts | 2 +- .../src/services/upload/uploadService.ts | 64 ++++++++++--------- 2 files changed, 35 insertions(+), 31 deletions(-) diff --git a/web/apps/photos/src/services/ffmpeg.ts b/web/apps/photos/src/services/ffmpeg.ts index 6383a8ce0d..a8b9bc3671 100644 --- a/web/apps/photos/src/services/ffmpeg.ts +++ b/web/apps/photos/src/services/ffmpeg.ts @@ -50,7 +50,7 @@ const _generateVideoThumbnail = async ( * for the new files that the user is adding. * * @param dataOrPath The input video's data or the path to the video on the - * user's local filesystem. See: [Note: Reading a fileOrPath]. + * user's local filesystem. See: [Note: Reading a UploadItem]. * * @returns JPEG data of the generated thumbnail. * diff --git a/web/apps/photos/src/services/upload/uploadService.ts b/web/apps/photos/src/services/upload/uploadService.ts index 275c001648..35404eb2b5 100644 --- a/web/apps/photos/src/services/upload/uploadService.ts +++ b/web/apps/photos/src/services/upload/uploadService.ts @@ -57,7 +57,7 @@ import type { UploadItem, UploadableUploadItem } from "./uploadManager"; * A readable stream for a file, and its associated size and last modified time. * * This is the in-memory representation of the `fileOrPath` type that we usually - * pass around. See: [Note: Reading a fileOrPath] + * pass around. See: [Note: Reading a UploadItem] */ interface FileStream { /** @@ -190,7 +190,6 @@ export const uploadItemFileName = (uploadItem: UploadItem) => { return uploadItem.file.name; }; - /* -- Various intermediate type used during upload -- */ interface UploadAsset { @@ -457,19 +456,21 @@ export const uploader = async ( }; /** - * Read the given file or path into an in-memory representation. + * Read the given file or path or zip entry into an in-memory representation. * - * [Note: Reading a fileOrPath] + * [Note: Reading a UploadItem] * * The file can be either a web - * [File](https://developer.mozilla.org/en-US/docs/Web/API/File) or the absolute - * path to a file on desk. + * [File](https://developer.mozilla.org/en-US/docs/Web/API/File), the absolute + * path to a file on desk, a combination of these two, or a entry in a zip file + * on the user's local filesystem. * - * tl;dr; There are three cases: + * tl;dr; There are four cases: * * 1. web / File - * 2. desktop / File + * 2. desktop / File (+ path) * 3. desktop / path + * 4. desktop / ZipEntry * * For the when and why, read on. * @@ -488,10 +489,13 @@ export const uploader = async ( * So in the web context, this will always be a File we get as a result of an * explicit user interaction (e.g. drag and drop). * - * In the desktop context, this can be either a File or a path. + * In the desktop context, this can be either a File (+ path), or a path, or an + * entry within a zip file. * * 2. If the user provided us this file via some user interaction (say a drag - * and a drop), this'll still be a File. + * and a drop), this'll still be a File. Note that unlike in the web context, + * such File objects also have the full path. See: [Note: File paths when + * running under Electron]. * * 3. However, when running in the desktop app we have the ability to access * absolute paths on the user's file system. For example, if the user asks us @@ -500,17 +504,17 @@ export const uploader = async ( * path. Another example is when resuming an previously interrupted upload - * we'll only have the path at hand in such cases, not the File object. * - * Case 2, when we're provided a path, is simple. We don't have a choice, since - * we cannot still programmatically construct a File object (we can construct it - * on the Node.js layer, but it can't then be transferred over the IPC - * boundary). So all our operations use the path itself. + * 4. The user might've also initiated an upload of a zip file. In this case we + * will get a tuple (path to the zip file on the local file system, and the + * name of the entry within that zip file). * - * Case 3 involves a choice on a use-case basis, since + * Case 3 and 4, when we're provided a path, are simple. We don't have a choice, + * since we cannot still programmatically construct a File object (we can + * construct it on the Node.js layer, but it can't then be transferred over the + * IPC boundary). So all our operations use the path itself. * - * (a) unlike in the web context, such File objects also have the full path. - * See: [Note: File paths when running under Electron]. - * - * (b) neither File nor the path is a better choice for all use cases. + * Case 2 involves a choice on a use-case basis as neither File nor the path is + * a better choice for all use cases. * * The advantage of the File object is that the browser has already read it into * memory for us. The disadvantage comes in the case where we need to @@ -518,7 +522,7 @@ export const uploader = async ( * communication happens over IPC, the File's contents need to be serialized and * copied, which is a bummer for large videos etc. */ -const readFileOrPath = async ( +const readUploadItem = async ( fileOrPath: File | string, ): Promise => { let underlyingStream: ReadableStream; @@ -623,18 +627,18 @@ const readLivePhotoDetails = async ({ image, video }: LivePhotoAssets) => { * While we're at it, also return the size of the file, and its last modified * time (expressed as epoch milliseconds). * - * @param fileOrPath See: [Note: Reading a fileOrPath] + * @param uploadItem See: [Note: Reading a UploadItem] */ -const readImageOrVideoDetails = async (fileOrPath: File | string) => { +const readImageOrVideoDetails = async (uploadItem: UploadItem) => { const { stream, fileSize, lastModifiedMs } = - await readFileOrPath(fileOrPath); + await readUploadItem(uploadItem); const fileTypeInfo = await detectFileTypeInfoFromChunk(async () => { const reader = stream.getReader(); const chunk = ensure((await reader.read()).value); await reader.cancel(); return chunk; - }, uploadItemFileName(fileOrPath)); + }, uploadItemFileName(uploadItem)); return { fileTypeInfo, fileSize, lastModifiedMs }; }; @@ -832,7 +836,7 @@ const computeHash = async ( fileOrPath: File | string, worker: Remote, ) => { - const { stream, chunkCount } = await readFileOrPath(fileOrPath); + const { stream, chunkCount } = await readUploadItem(fileOrPath); const hashState = await worker.initChunkHashing(); const streamReader = stream.getReader(); @@ -921,9 +925,9 @@ const readLivePhoto = async ( extension: fileTypeInfo.imageType, fileType: FILE_TYPE.IMAGE, }, - await readFileOrPath(livePhotoAssets.image), + await readUploadItem(livePhotoAssets.image), ); - const videoFileStreamOrData = await readFileOrPath(livePhotoAssets.video); + const videoFileStreamOrData = await readUploadItem(livePhotoAssets.video); // The JS zip library that encodeLivePhoto uses does not support // ReadableStreams, so pass the file (blob) if we have one, otherwise read @@ -954,7 +958,7 @@ const readImageOrVideo = async ( fileOrPath: File | string, fileTypeInfo: FileTypeInfo, ) => { - const fileStream = await readFileOrPath(fileOrPath); + const fileStream = await readUploadItem(fileOrPath); return withThumbnail(fileOrPath, fileTypeInfo, fileStream); }; @@ -978,8 +982,8 @@ const moduleState = new ModuleState(); /** * Augment the given {@link dataOrStream} with thumbnail information. * - * This is a companion method for {@link readFileOrPath}, and can be used to - * convert the result of {@link readFileOrPath} into an {@link ThumbnailedFile}. + * This is a companion method for {@link readUploadItem}, and can be used to + * convert the result of {@link readUploadItem} into an {@link ThumbnailedFile}. * * Note: The `fileStream` in the returned ThumbnailedFile may be different from * the one passed to the function. From 93991c3a7f9120c62029b9e42e26daaefe4d2f54 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 09:16:10 +0530 Subject: [PATCH 093/240] up --- .../src/services/upload/uploadService.ts | 96 ++++++++++--------- 1 file changed, 51 insertions(+), 45 deletions(-) diff --git a/web/apps/photos/src/services/upload/uploadService.ts b/web/apps/photos/src/services/upload/uploadService.ts index 35404eb2b5..97848eeac2 100644 --- a/web/apps/photos/src/services/upload/uploadService.ts +++ b/web/apps/photos/src/services/upload/uploadService.ts @@ -522,29 +522,30 @@ export const uploader = async ( * communication happens over IPC, the File's contents need to be serialized and * copied, which is a bummer for large videos etc. */ -const readUploadItem = async ( - fileOrPath: File | string, -): Promise => { +const readUploadItem = async (uploadItem: UploadItem): Promise => { let underlyingStream: ReadableStream; let file: File | undefined; let fileSize: number; let lastModifiedMs: number; - if (fileOrPath instanceof File) { - file = fileOrPath; - underlyingStream = file.stream(); - fileSize = file.size; - lastModifiedMs = file.lastModified; - } else { - const path = fileOrPath; + if (typeof uploadItem == "string" || Array.isArray(uploadItem)) { const { response, size, lastModifiedMs: lm, - } = await readStream(ensureElectron(), path); + } = await readStream(ensureElectron(), uploadItem); underlyingStream = response.body; fileSize = size; lastModifiedMs = lm; + } else { + if (uploadItem instanceof File) { + file = uploadItem; + } else { + file = uploadItem.file; + } + underlyingStream = file.stream(); + fileSize = file.size; + lastModifiedMs = file.lastModified; } const N = ENCRYPTION_CHUNK_SIZE; @@ -591,8 +592,8 @@ interface ReadAssetDetailsResult { } /** - * Read the file(s) to determine the type, size and last modified time of the - * given {@link asset}. + * Read the associated file(s) to determine the type, size and last modified + * time of the given {@link asset}. */ const readAssetDetails = async ({ isLivePhoto, @@ -664,7 +665,7 @@ interface ExtractAssetMetadataResult { * {@link parsedMetadataJSONMap} for the assets. Return the resultant metadatum. */ const extractAssetMetadata = async ( - { isLivePhoto, uploadItem: fileOrPath, livePhotoAssets }: UploadAsset, + { isLivePhoto, uploadItem, livePhotoAssets }: UploadAsset, fileTypeInfo: FileTypeInfo, lastModifiedMs: number, collectionID: number, @@ -681,7 +682,7 @@ const extractAssetMetadata = async ( worker, ) : await extractImageOrVideoMetadata( - fileOrPath, + uploadItem, fileTypeInfo, lastModifiedMs, collectionID, @@ -727,33 +728,33 @@ const extractLivePhotoMetadata = async ( }; const extractImageOrVideoMetadata = async ( - fileOrPath: File | string, + uploadItem: UploadItem, fileTypeInfo: FileTypeInfo, lastModifiedMs: number, collectionID: number, parsedMetadataJSONMap: Map, worker: Remote, ) => { - const fileName = uploadItemFileName(fileOrPath); + const fileName = uploadItemFileName(uploadItem); const { fileType } = fileTypeInfo; let extractedMetadata: ParsedExtractedMetadata; if (fileType === FILE_TYPE.IMAGE) { extractedMetadata = (await tryExtractImageMetadata( - fileOrPath, + uploadItem, fileTypeInfo, lastModifiedMs, )) ?? NULL_EXTRACTED_METADATA; } else if (fileType === FILE_TYPE.VIDEO) { extractedMetadata = - (await tryExtractVideoMetadata(fileOrPath)) ?? + (await tryExtractVideoMetadata(uploadItem)) ?? NULL_EXTRACTED_METADATA; } else { - throw new Error(`Unexpected file type ${fileType} for ${fileOrPath}`); + throw new Error(`Unexpected file type ${fileType} for ${uploadItem}`); } - const hash = await computeHash(fileOrPath, worker); + const hash = await computeHash(uploadItem, worker); const modificationTime = lastModifiedMs * 1000; const creationTime = @@ -797,46 +798,48 @@ const NULL_EXTRACTED_METADATA: ParsedExtractedMetadata = { }; async function tryExtractImageMetadata( - fileOrPath: File | string, + uploadItem: UploadItem, fileTypeInfo: FileTypeInfo, lastModifiedMs: number, ): Promise { let file: File; - if (fileOrPath instanceof File) { - file = fileOrPath; - } else { - const path = fileOrPath; + if (typeof uploadItem == "string" || Array.isArray(uploadItem)) { // The library we use for extracting EXIF from images, exifr, doesn't // support streams. But unlike videos, for images it is reasonable to // read the entire stream into memory here. - const { response } = await readStream(ensureElectron(), path); + const { response } = await readStream(ensureElectron(), uploadItem); + const path = typeof uploadItem == "string" ? uploadItem : uploadItem[1]; file = new File([await response.arrayBuffer()], basename(path), { lastModified: lastModifiedMs, }); + } else if (uploadItem instanceof File) { + file = uploadItem; + } else { + file = uploadItem.file; } try { return await parseImageMetadata(file, fileTypeInfo); } catch (e) { - log.error(`Failed to extract image metadata for ${fileOrPath}`, e); + log.error(`Failed to extract image metadata for ${uploadItem}`, e); return undefined; } } -const tryExtractVideoMetadata = async (fileOrPath: File | string) => { +const tryExtractVideoMetadata = async (uploadItem: UploadItem) => { try { - return await ffmpeg.extractVideoMetadata(fileOrPath); + return await ffmpeg.extractVideoMetadata(uploadItem); } catch (e) { - log.error(`Failed to extract video metadata for ${fileOrPath}`, e); + log.error(`Failed to extract video metadata for ${uploadItem}`, e); return undefined; } }; const computeHash = async ( - fileOrPath: File | string, + uploadItem: UploadItem, worker: Remote, ) => { - const { stream, chunkCount } = await readUploadItem(fileOrPath); + const { stream, chunkCount } = await readUploadItem(uploadItem); const hashState = await worker.initChunkHashing(); const streamReader = stream.getReader(); @@ -905,11 +908,11 @@ const areFilesSameNoHash = (f: Metadata, g: Metadata) => { const readAsset = async ( fileTypeInfo: FileTypeInfo, - { isLivePhoto, uploadItem: fileOrPath, livePhotoAssets }: UploadAsset, + { isLivePhoto, uploadItem, livePhotoAssets }: UploadAsset, ): Promise => isLivePhoto ? await readLivePhoto(livePhotoAssets, fileTypeInfo) - : await readImageOrVideo(fileOrPath, fileTypeInfo); + : await readImageOrVideo(uploadItem, fileTypeInfo); const readLivePhoto = async ( livePhotoAssets: LivePhotoAssets, @@ -955,11 +958,11 @@ const readLivePhoto = async ( }; const readImageOrVideo = async ( - fileOrPath: File | string, + uploadItem: UploadItem, fileTypeInfo: FileTypeInfo, ) => { - const fileStream = await readUploadItem(fileOrPath); - return withThumbnail(fileOrPath, fileTypeInfo, fileStream); + const fileStream = await readUploadItem(uploadItem); + return withThumbnail(uploadItem, fileTypeInfo, fileStream); }; // TODO(MR): Merge with the uploader @@ -985,11 +988,14 @@ const moduleState = new ModuleState(); * This is a companion method for {@link readUploadItem}, and can be used to * convert the result of {@link readUploadItem} into an {@link ThumbnailedFile}. * - * Note: The `fileStream` in the returned ThumbnailedFile may be different from - * the one passed to the function. + * @param uploadItem The {@link UploadItem} where the given {@link fileStream} + * came from. + * + * Note: The `fileStream` in the returned {@link ThumbnailedFile} may be + * different from the one passed to the function. */ const withThumbnail = async ( - fileOrPath: File | string, + uploadItem: UploadItem, fileTypeInfo: FileTypeInfo, fileStream: FileStream, ): Promise => { @@ -1009,7 +1015,7 @@ const withThumbnail = async ( // be absolute. See: [Note: File paths when running under Electron]. thumbnail = await generateThumbnailNative( electron, - fileOrPath instanceof File ? fileOrPath["path"] : fileOrPath, + uploadItem instanceof File ? uploadItem["path"] : uploadItem, fileTypeInfo, ); } catch (e) { @@ -1023,9 +1029,9 @@ const withThumbnail = async ( if (!thumbnail) { let blob: Blob | undefined; - if (fileOrPath instanceof File) { + if (uploadItem instanceof File) { // 2. Browser based thumbnail generation for File (blobs). - blob = fileOrPath; + blob = uploadItem; } else { // 3. Browser based thumbnail generation for paths. // @@ -1057,7 +1063,7 @@ const withThumbnail = async ( fileData = data; } else { log.warn( - `Not using browser based thumbnail generation fallback for video at path ${fileOrPath}`, + `Not using browser based thumbnail generation fallback for video at path ${uploadItem}`, ); } } From 77fe4f9f03aa4615c4f42a3b7dd8773e4b9a241e Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 09:39:38 +0530 Subject: [PATCH 094/240] wip ze ipc --- desktop/src/main/ipc.ts | 17 ++++++++++++----- desktop/src/main/services/image.ts | 29 +++++++++++++++++++++-------- desktop/src/main/stream.ts | 4 +--- desktop/src/preload.ts | 8 ++++---- web/packages/next/types/ipc.ts | 24 +++++++++++++++--------- 5 files changed, 53 insertions(+), 29 deletions(-) diff --git a/desktop/src/main/ipc.ts b/desktop/src/main/ipc.ts index 01f481f8eb..1a95828627 100644 --- a/desktop/src/main/ipc.ts +++ b/desktop/src/main/ipc.ts @@ -54,9 +54,9 @@ import { import { clearPendingUploads, listZipEntries, - pathOrZipEntrySize, markUploadedFiles, markUploadedZipEntries, + pathOrZipEntrySize, pendingUploads, setPendingUploads, } from "./services/upload"; @@ -152,10 +152,11 @@ export const attachIPCHandlers = () => { "generateImageThumbnail", ( _, - dataOrPath: Uint8Array | string, + dataOrPathOrZipEntry: Uint8Array | string | ZipEntry, maxDimension: number, maxSize: number, - ) => generateImageThumbnail(dataOrPath, maxDimension, maxSize), + ) => + generateImageThumbnail(dataOrPathOrZipEntry, maxDimension, maxSize), ); ipcMain.handle( @@ -163,10 +164,16 @@ export const attachIPCHandlers = () => { ( _, command: string[], - dataOrPath: Uint8Array | string, + dataOrPathOrZipEntry: Uint8Array | string | ZipEntry, outputFileExtension: string, timeoutMS: number, - ) => ffmpegExec(command, dataOrPath, outputFileExtension, timeoutMS), + ) => + ffmpegExec( + command, + dataOrPathOrZipEntry, + outputFileExtension, + timeoutMS, + ), ); // - ML diff --git a/desktop/src/main/services/image.ts b/desktop/src/main/services/image.ts index 26b4b351e5..894ff34049 100644 --- a/desktop/src/main/services/image.ts +++ b/desktop/src/main/services/image.ts @@ -1,8 +1,9 @@ /** @file Image format conversions and thumbnail generation */ +import StreamZip from "node-stream-zip"; import fs from "node:fs/promises"; import path from "path"; -import { CustomErrorMessage } from "../../types/ipc"; +import { CustomErrorMessage, type ZipEntry } from "../../types/ipc"; import log from "../log"; import { execAsync, isDev } from "../utils-electron"; import { deleteTempFile, makeTempFilePath } from "../utils-temp"; @@ -63,18 +64,31 @@ const imageMagickPath = () => path.join(isDev ? "build" : process.resourcesPath, "image-magick"); export const generateImageThumbnail = async ( - dataOrPath: Uint8Array | string, + dataOrPathOrZipEntry: Uint8Array | string | ZipEntry, maxDimension: number, maxSize: number, ): Promise => { let inputFilePath: string; let isInputFileTemporary: boolean; - if (dataOrPath instanceof Uint8Array) { + let writeToTemporaryInputFile = async () => {}; + if (typeof dataOrPathOrZipEntry == "string") { + inputFilePath = dataOrPathOrZipEntry; + isInputFileTemporary = false; + } else { inputFilePath = await makeTempFilePath(); isInputFileTemporary = true; - } else { - inputFilePath = dataOrPath; - isInputFileTemporary = false; + if (dataOrPathOrZipEntry instanceof Uint8Array) { + writeToTemporaryInputFile = async () => { + await fs.writeFile(inputFilePath, dataOrPathOrZipEntry); + }; + } else { + writeToTemporaryInputFile = async () => { + const [zipPath, entryName] = dataOrPathOrZipEntry; + const zip = new StreamZip.async({ file: zipPath }); + await zip.extract(entryName, inputFilePath); + zip.close(); + }; + } } const outputFilePath = await makeTempFilePath("jpeg"); @@ -89,8 +103,7 @@ export const generateImageThumbnail = async ( ); try { - if (dataOrPath instanceof Uint8Array) - await fs.writeFile(inputFilePath, dataOrPath); + writeToTemporaryInputFile(); let thumbnail: Uint8Array; do { diff --git a/desktop/src/main/stream.ts b/desktop/src/main/stream.ts index ddd639c30b..c518874494 100644 --- a/desktop/src/main/stream.ts +++ b/desktop/src/main/stream.ts @@ -97,9 +97,7 @@ const handleRead = async (path: string) => { const handleReadZip = async (zipPath: string, zipEntryPath: string) => { try { - const zip = new StreamZip.async({ - file: zipPath, - }); + const zip = new StreamZip.async({ file: zipPath }); const entry = await zip.entry(zipEntryPath); const stream = await zip.stream(entry); diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index 226a80767a..76d44591e0 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -129,27 +129,27 @@ const convertToJPEG = (imageData: Uint8Array): Promise => ipcRenderer.invoke("convertToJPEG", imageData); const generateImageThumbnail = ( - dataOrPath: Uint8Array | string, + dataOrPathOrZipEntry: Uint8Array | string | ZipEntry, maxDimension: number, maxSize: number, ): Promise => ipcRenderer.invoke( "generateImageThumbnail", - dataOrPath, + dataOrPathOrZipEntry, maxDimension, maxSize, ); const ffmpegExec = ( command: string[], - dataOrPath: Uint8Array | string, + dataOrPathOrZipEntry: Uint8Array | string | ZipEntry, outputFileExtension: string, timeoutMS: number, ): Promise => ipcRenderer.invoke( "ffmpegExec", command, - dataOrPath, + dataOrPathOrZipEntry, outputFileExtension, timeoutMS, ); diff --git a/web/packages/next/types/ipc.ts b/web/packages/next/types/ipc.ts index 34bb9196a2..4b3d97dd32 100644 --- a/web/packages/next/types/ipc.ts +++ b/web/packages/next/types/ipc.ts @@ -221,22 +221,27 @@ export interface Electron { * not yet possible, this function will throw an error with the * {@link CustomErrorMessage.NotAvailable} message. * - * @param dataOrPath The raw image data (the contents of the image file), or - * the path to the image file, whose thumbnail we want to generate. + * @param dataOrPathOrZipEntry The file whose thumbnail we want to generate. + * It can be provided as raw image data (the contents of the image file), or + * the path to the image file, or a tuple containing the path of the zip + * file along with the name of an entry in it. + * * @param maxDimension The maximum width or height of the generated * thumbnail. + * * @param maxSize Maximum size (in bytes) of the generated thumbnail. * * @returns JPEG data of the generated thumbnail. */ generateImageThumbnail: ( - dataOrPath: Uint8Array | string, + dataOrPathOrZipEntry: Uint8Array | string | ZipEntry, maxDimension: number, maxSize: number, ) => Promise; /** - * Execute a FFmpeg {@link command} on the given {@link dataOrPath}. + * Execute a FFmpeg {@link command} on the given + * {@link dataOrPathOrZipEntry}. * * This executes the command using a FFmpeg executable we bundle with our * desktop app. We also have a wasm FFmpeg wasm implementation that we use @@ -249,10 +254,11 @@ export interface Electron { * (respectively {@link inputPathPlaceholder}, * {@link outputPathPlaceholder}, {@link ffmpegPathPlaceholder}). * - * @param dataOrPath The bytes of the input file, or the path to the input - * file on the user's local disk. In both cases, the data gets serialized to - * a temporary file, and then that path gets substituted in the FFmpeg - * {@link command} in lieu of {@link inputPathPlaceholder}. + * @param dataOrPathOrZipEntry The bytes of the input file, or the path to + * the input file on the user's local disk, or the path to a zip file on the + * user's disk and the name of an entry in it. In all three cases, the data + * gets serialized to a temporary file, and then that path gets substituted + * in the FFmpeg {@link command} in lieu of {@link inputPathPlaceholder}. * * @param outputFileExtension The extension (without the dot, e.g. "jpeg") * to use for the output file that we ask FFmpeg to create in @@ -268,7 +274,7 @@ export interface Electron { */ ffmpegExec: ( command: string[], - dataOrPath: Uint8Array | string, + dataOrPathOrZipEntry: Uint8Array | string | ZipEntry, outputFileExtension: string, timeoutMS: number, ) => Promise; From e9bf26e421921ff8ae859f3d892938530ccf6ae7 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 09:55:45 +0530 Subject: [PATCH 095/240] Extract --- desktop/src/main/services/ffmpeg.ts | 33 ++++++++-------- desktop/src/main/services/image.ts | 36 ++++++----------- desktop/src/main/services/upload.ts | 8 +++- desktop/src/main/stream.ts | 1 + desktop/src/main/utils-temp.ts | 61 +++++++++++++++++++++++++++++ 5 files changed, 97 insertions(+), 42 deletions(-) diff --git a/desktop/src/main/services/ffmpeg.ts b/desktop/src/main/services/ffmpeg.ts index ed3542f6ad..ea9ceaf761 100644 --- a/desktop/src/main/services/ffmpeg.ts +++ b/desktop/src/main/services/ffmpeg.ts @@ -1,9 +1,14 @@ import pathToFfmpeg from "ffmpeg-static"; import fs from "node:fs/promises"; +import type { ZipEntry } from "../../types/ipc"; import log from "../log"; import { withTimeout } from "../utils"; import { execAsync } from "../utils-electron"; -import { deleteTempFile, makeTempFilePath } from "../utils-temp"; +import { + deleteTempFile, + makeFileForDataOrPathOrZipEntry, + makeTempFilePath, +} from "../utils-temp"; /* Duplicated in the web app's code (used by the WASM FFmpeg implementation). */ const ffmpegPathPlaceholder = "FFMPEG"; @@ -39,28 +44,24 @@ const outputPathPlaceholder = "OUTPUT"; */ export const ffmpegExec = async ( command: string[], - dataOrPath: Uint8Array | string, + dataOrPathOrZipEntry: Uint8Array | string | ZipEntry, outputFileExtension: string, timeoutMS: number, ): Promise => { - // TODO (MR): This currently copies files for both input and output. This - // needs to be tested extremely large video files when invoked downstream of - // `convertToMP4` in the web code. + // TODO (MR): This currently copies files for both input (when + // dataOrPathOrZipEntry is data) and output. This needs to be tested + // extremely large video files when invoked downstream of `convertToMP4` in + // the web code. - let inputFilePath: string; - let isInputFileTemporary: boolean; - if (dataOrPath instanceof Uint8Array) { - inputFilePath = await makeTempFilePath(); - isInputFileTemporary = true; - } else { - inputFilePath = dataOrPath; - isInputFileTemporary = false; - } + const { + path: inputFilePath, + isFileTemporary: isInputFileTemporary, + writeToTemporaryFile: writeToTemporaryInputFile, + } = await makeFileForDataOrPathOrZipEntry(dataOrPathOrZipEntry); const outputFilePath = await makeTempFilePath(outputFileExtension); try { - if (dataOrPath instanceof Uint8Array) - await fs.writeFile(inputFilePath, dataOrPath); + await writeToTemporaryInputFile(); const cmd = substitutePlaceholders( command, diff --git a/desktop/src/main/services/image.ts b/desktop/src/main/services/image.ts index 894ff34049..c55bacdffe 100644 --- a/desktop/src/main/services/image.ts +++ b/desktop/src/main/services/image.ts @@ -1,12 +1,15 @@ /** @file Image format conversions and thumbnail generation */ -import StreamZip from "node-stream-zip"; import fs from "node:fs/promises"; import path from "path"; import { CustomErrorMessage, type ZipEntry } from "../../types/ipc"; import log from "../log"; import { execAsync, isDev } from "../utils-electron"; -import { deleteTempFile, makeTempFilePath } from "../utils-temp"; +import { + deleteTempFile, + makeFileForDataOrPathOrZipEntry, + makeTempFilePath, +} from "../utils-temp"; export const convertToJPEG = async (imageData: Uint8Array) => { const inputFilePath = await makeTempFilePath(); @@ -68,28 +71,11 @@ export const generateImageThumbnail = async ( maxDimension: number, maxSize: number, ): Promise => { - let inputFilePath: string; - let isInputFileTemporary: boolean; - let writeToTemporaryInputFile = async () => {}; - if (typeof dataOrPathOrZipEntry == "string") { - inputFilePath = dataOrPathOrZipEntry; - isInputFileTemporary = false; - } else { - inputFilePath = await makeTempFilePath(); - isInputFileTemporary = true; - if (dataOrPathOrZipEntry instanceof Uint8Array) { - writeToTemporaryInputFile = async () => { - await fs.writeFile(inputFilePath, dataOrPathOrZipEntry); - }; - } else { - writeToTemporaryInputFile = async () => { - const [zipPath, entryName] = dataOrPathOrZipEntry; - const zip = new StreamZip.async({ file: zipPath }); - await zip.extract(entryName, inputFilePath); - zip.close(); - }; - } - } + const { + path: inputFilePath, + isFileTemporary: isInputFileTemporary, + writeToTemporaryFile: writeToTemporaryInputFile, + } = await makeFileForDataOrPathOrZipEntry(dataOrPathOrZipEntry); const outputFilePath = await makeTempFilePath("jpeg"); @@ -103,7 +89,7 @@ export const generateImageThumbnail = async ( ); try { - writeToTemporaryInputFile(); + await writeToTemporaryInputFile(); let thumbnail: Uint8Array; do { diff --git a/desktop/src/main/services/upload.ts b/desktop/src/main/services/upload.ts index a26722cb80..804a84736f 100644 --- a/desktop/src/main/services/upload.ts +++ b/desktop/src/main/services/upload.ts @@ -21,6 +21,8 @@ export const listZipEntries = async (zipPath: string): Promise => { } } + zip.close(); + return entryNames.map((entryName) => [zipPath, entryName]); }; @@ -34,7 +36,9 @@ export const pathOrZipEntrySize = async ( const [zipPath, entryName] = pathOrZipEntry; const zip = new StreamZip.async({ file: zipPath }); const entry = await zip.entry(entryName); - return entry.size; + const size = entry.size; + zip.close(); + return size; } }; @@ -110,6 +114,8 @@ export const getElectronFilesFromGoogleZip = async (filePath: string) => { } } + zip.close(); + return files; }; diff --git a/desktop/src/main/stream.ts b/desktop/src/main/stream.ts index c518874494..bcffe2cc50 100644 --- a/desktop/src/main/stream.ts +++ b/desktop/src/main/stream.ts @@ -100,6 +100,7 @@ const handleReadZip = async (zipPath: string, zipEntryPath: string) => { const zip = new StreamZip.async({ file: zipPath }); const entry = await zip.entry(zipEntryPath); const stream = await zip.stream(entry); + // TODO(MR): when to call zip.close() return new Response(Readable.toWeb(new Readable(stream)), { headers: { diff --git a/desktop/src/main/utils-temp.ts b/desktop/src/main/utils-temp.ts index a52daf619d..2e416bd652 100644 --- a/desktop/src/main/utils-temp.ts +++ b/desktop/src/main/utils-temp.ts @@ -1,7 +1,9 @@ import { app } from "electron/main"; +import StreamZip from "node-stream-zip"; import { existsSync } from "node:fs"; import fs from "node:fs/promises"; import path from "path"; +import type { ZipEntry } from "../types/ipc"; /** * Our very own directory within the system temp directory. Go crazy, but @@ -61,3 +63,62 @@ export const deleteTempFile = async (tempFilePath: string) => { throw new Error(`Attempting to delete a non-temp file ${tempFilePath}`); await fs.rm(tempFilePath, { force: true }); }; + +/** The result of {@link makeFileForDataOrPathOrZipEntry}. */ +interface FileForDataOrPathOrZipEntry { + /** The path to the file (possibly temporary) */ + path: string; + /** + * `true` if {@link path} points to a temporary file which should be deleted + * once we are done processing. + */ + isFileTemporary: boolean; + /** + * If set, this'll be a function that can be called to actually write the + * contents of the source `Uint8Array | string | ZipEntry` into the file at + * {@link path}. + * + * It will be undefined if the source is already a path since nothing needs + * to be written in that case. In the other two cases this function will + * write the data or zip entry into the file at {@link path}. + */ + writeToTemporaryFile?: () => Promise; +} + +/** + * Return the path to a file, a boolean indicating if this is a temporary path + * that needs to be deleted after processing, and a function to write the given + * {@link dataOrPathOrZipEntry} into that temporary file if needed. + * + * @param dataOrPathOrZipEntry The contents of the file, or the path to an + * existing file, or a (path to a zip file, name of an entry within that zip + * file) tuple. + */ +export const makeFileForDataOrPathOrZipEntry = async ( + dataOrPathOrZipEntry: Uint8Array | string | ZipEntry, +): Promise => { + let path: string; + let isFileTemporary: boolean; + let writeToTemporaryFile: () => Promise | undefined; + + if (typeof dataOrPathOrZipEntry == "string") { + path = dataOrPathOrZipEntry; + isFileTemporary = false; + } else { + path = await makeTempFilePath(); + isFileTemporary = true; + if (dataOrPathOrZipEntry instanceof Uint8Array) { + writeToTemporaryFile = () => + fs.writeFile(path, dataOrPathOrZipEntry); + } else { + writeToTemporaryFile = async () => { + const [zipPath, entryName] = dataOrPathOrZipEntry; + const zip = new StreamZip.async({ file: zipPath }); + await zip.extract(entryName, path); + zip.close(); + }; + } + } + + return { path, isFileTemporary, writeToTemporaryFile }; +}; From 73baf5a375272d9387c76084553e19f27885e612 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 10:08:31 +0530 Subject: [PATCH 096/240] Uncollide with ZipEntry from StreamZip --- desktop/src/main/ipc.ts | 21 +++++++-------- desktop/src/main/services/ffmpeg.ts | 10 +++---- desktop/src/main/services/image.ts | 8 +++--- desktop/src/main/services/upload.ts | 16 +++++------ desktop/src/main/stores/upload-status.ts | 21 +++++++++++---- desktop/src/main/stream.ts | 6 ++--- desktop/src/main/utils-temp.ts | 34 +++++++++++++----------- desktop/src/preload.ts | 21 +++++++-------- desktop/src/types/ipc.ts | 4 +-- 9 files changed, 75 insertions(+), 66 deletions(-) diff --git a/desktop/src/main/ipc.ts b/desktop/src/main/ipc.ts index 1a95828627..abbe54705d 100644 --- a/desktop/src/main/ipc.ts +++ b/desktop/src/main/ipc.ts @@ -14,7 +14,7 @@ import type { CollectionMapping, FolderWatch, PendingUploads, - ZipEntry, + ZipItem, } from "../types/ipc"; import { selectDirectory, @@ -56,7 +56,7 @@ import { listZipEntries, markUploadedFiles, markUploadedZipEntries, - pathOrZipEntrySize, + pathOrZipItemSize, pendingUploads, setPendingUploads, } from "./services/upload"; @@ -152,11 +152,10 @@ export const attachIPCHandlers = () => { "generateImageThumbnail", ( _, - dataOrPathOrZipEntry: Uint8Array | string | ZipEntry, + dataOrPathOrZipItem: Uint8Array | string | ZipItem, maxDimension: number, maxSize: number, - ) => - generateImageThumbnail(dataOrPathOrZipEntry, maxDimension, maxSize), + ) => generateImageThumbnail(dataOrPathOrZipItem, maxDimension, maxSize), ); ipcMain.handle( @@ -164,13 +163,13 @@ export const attachIPCHandlers = () => { ( _, command: string[], - dataOrPathOrZipEntry: Uint8Array | string | ZipEntry, + dataOrPathOrZipItem: Uint8Array | string | ZipItem, outputFileExtension: string, timeoutMS: number, ) => ffmpegExec( command, - dataOrPathOrZipEntry, + dataOrPathOrZipItem, outputFileExtension, timeoutMS, ), @@ -210,10 +209,8 @@ export const attachIPCHandlers = () => { listZipEntries(zipPath), ); - ipcMain.handle( - "pathOrZipEntrySize", - (_, pathOrZipEntry: string | ZipEntry) => - pathOrZipEntrySize(pathOrZipEntry), + ipcMain.handle("pathOrZipItemSize", (_, pathOrZipItem: string | ZipItem) => + pathOrZipItemSize(pathOrZipItem), ); ipcMain.handle("pendingUploads", () => pendingUploads()); @@ -229,7 +226,7 @@ export const attachIPCHandlers = () => { ipcMain.handle( "markUploadedZipEntries", - (_, zipEntries: PendingUploads["zipEntries"]) => + (_, zipEntries: PendingUploads["zipItems"]) => markUploadedZipEntries(zipEntries), ); diff --git a/desktop/src/main/services/ffmpeg.ts b/desktop/src/main/services/ffmpeg.ts index ea9ceaf761..35977409ae 100644 --- a/desktop/src/main/services/ffmpeg.ts +++ b/desktop/src/main/services/ffmpeg.ts @@ -1,12 +1,12 @@ import pathToFfmpeg from "ffmpeg-static"; import fs from "node:fs/promises"; -import type { ZipEntry } from "../../types/ipc"; +import type { ZipItem } from "../../types/ipc"; import log from "../log"; import { withTimeout } from "../utils"; import { execAsync } from "../utils-electron"; import { deleteTempFile, - makeFileForDataOrPathOrZipEntry, + makeFileForDataOrPathOrZipItem, makeTempFilePath, } from "../utils-temp"; @@ -44,12 +44,12 @@ const outputPathPlaceholder = "OUTPUT"; */ export const ffmpegExec = async ( command: string[], - dataOrPathOrZipEntry: Uint8Array | string | ZipEntry, + dataOrPathOrZipItem: Uint8Array | string | ZipItem, outputFileExtension: string, timeoutMS: number, ): Promise => { // TODO (MR): This currently copies files for both input (when - // dataOrPathOrZipEntry is data) and output. This needs to be tested + // dataOrPathOrZipItem is data) and output. This needs to be tested // extremely large video files when invoked downstream of `convertToMP4` in // the web code. @@ -57,7 +57,7 @@ export const ffmpegExec = async ( path: inputFilePath, isFileTemporary: isInputFileTemporary, writeToTemporaryFile: writeToTemporaryInputFile, - } = await makeFileForDataOrPathOrZipEntry(dataOrPathOrZipEntry); + } = await makeFileForDataOrPathOrZipItem(dataOrPathOrZipItem); const outputFilePath = await makeTempFilePath(outputFileExtension); try { diff --git a/desktop/src/main/services/image.ts b/desktop/src/main/services/image.ts index c55bacdffe..c48e87c5bf 100644 --- a/desktop/src/main/services/image.ts +++ b/desktop/src/main/services/image.ts @@ -2,12 +2,12 @@ import fs from "node:fs/promises"; import path from "path"; -import { CustomErrorMessage, type ZipEntry } from "../../types/ipc"; +import { CustomErrorMessage, type ZipItem } from "../../types/ipc"; import log from "../log"; import { execAsync, isDev } from "../utils-electron"; import { deleteTempFile, - makeFileForDataOrPathOrZipEntry, + makeFileForDataOrPathOrZipItem, makeTempFilePath, } from "../utils-temp"; @@ -67,7 +67,7 @@ const imageMagickPath = () => path.join(isDev ? "build" : process.resourcesPath, "image-magick"); export const generateImageThumbnail = async ( - dataOrPathOrZipEntry: Uint8Array | string | ZipEntry, + dataOrPathOrZipItem: Uint8Array | string | ZipItem, maxDimension: number, maxSize: number, ): Promise => { @@ -75,7 +75,7 @@ export const generateImageThumbnail = async ( path: inputFilePath, isFileTemporary: isInputFileTemporary, writeToTemporaryFile: writeToTemporaryInputFile, - } = await makeFileForDataOrPathOrZipEntry(dataOrPathOrZipEntry); + } = await makeFileForDataOrPathOrZipItem(dataOrPathOrZipItem); const outputFilePath = await makeTempFilePath("jpeg"); diff --git a/desktop/src/main/services/upload.ts b/desktop/src/main/services/upload.ts index 804a84736f..1a1d343c5c 100644 --- a/desktop/src/main/services/upload.ts +++ b/desktop/src/main/services/upload.ts @@ -2,11 +2,11 @@ import StreamZip from "node-stream-zip"; import fs from "node:fs/promises"; import { existsSync } from "original-fs"; import path from "path"; -import type { ElectronFile, PendingUploads, ZipEntry } from "../../types/ipc"; +import type { ElectronFile, PendingUploads, ZipItem } from "../../types/ipc"; import { uploadStatusStore } from "../stores/upload-status"; import { getZipFileStream } from "./fs"; -export const listZipEntries = async (zipPath: string): Promise => { +export const listZipEntries = async (zipPath: string): Promise => { const zip = new StreamZip.async({ file: zipPath }); const entries = await zip.entries(); @@ -26,14 +26,14 @@ export const listZipEntries = async (zipPath: string): Promise => { return entryNames.map((entryName) => [zipPath, entryName]); }; -export const pathOrZipEntrySize = async ( - pathOrZipEntry: string | ZipEntry, +export const pathOrZipItemSize = async ( + pathOrZipItem: string | ZipItem, ): Promise => { - if (typeof pathOrZipEntry == "string") { - const stat = await fs.stat(pathOrZipEntry); + if (typeof pathOrZipItem == "string") { + const stat = await fs.stat(pathOrZipItem); return stat.size; } else { - const [zipPath, entryName] = pathOrZipEntry; + const [zipPath, entryName] = pathOrZipItem; const zip = new StreamZip.async({ file: zipPath }); const entry = await zip.entry(entryName); const size = entry.size; @@ -73,7 +73,7 @@ export const pendingUploads = async (): Promise => { return { collectionName, filePaths, - zipEntries, + zipItems: zipEntries, }; }; diff --git a/desktop/src/main/stores/upload-status.ts b/desktop/src/main/stores/upload-status.ts index 20a431fd9b..4fd6c9860e 100644 --- a/desktop/src/main/stores/upload-status.ts +++ b/desktop/src/main/stores/upload-status.ts @@ -1,17 +1,28 @@ import Store, { Schema } from "electron-store"; export interface UploadStatusStore { - /* The collection to which we're uploading, or the root collection. */ + /** + * The collection to which we're uploading, or the root collection. + * + * Not all pending uploads will have an associated collection. + */ collectionName?: string; - /** Paths to regular files that are pending upload */ + /** + * Paths to regular files that are pending upload. + * + * This should generally be present, albeit empty, but it is marked optional + * in sympathy with its siblings. + */ filePaths?: string[]; /** * Each item is the path to a zip file and the name of an entry within it. * * This is marked optional since legacy stores will not have it. */ - zipEntries?: [zipPath: string, entryName: string][]; - /** Legacy paths to zip files, now subsumed into zipEntries */ + zipItems?: [zipPath: string, entryName: string][]; + /** + * @deprecated Legacy paths to zip files, now subsumed into zipEntries. + */ zipPaths?: string[]; } @@ -25,7 +36,7 @@ const uploadStatusSchema: Schema = { type: "string", }, }, - zipEntries: { + zipItems: { type: "array", items: { type: "array", diff --git a/desktop/src/main/stream.ts b/desktop/src/main/stream.ts index bcffe2cc50..b37970cfae 100644 --- a/desktop/src/main/stream.ts +++ b/desktop/src/main/stream.ts @@ -95,10 +95,10 @@ const handleRead = async (path: string) => { } }; -const handleReadZip = async (zipPath: string, zipEntryPath: string) => { +const handleReadZip = async (zipPath: string, entryName: string) => { try { const zip = new StreamZip.async({ file: zipPath }); - const entry = await zip.entry(zipEntryPath); + const entry = await zip.entry(entryName); const stream = await zip.stream(entry); // TODO(MR): when to call zip.close() @@ -119,7 +119,7 @@ const handleReadZip = async (zipPath: string, zipEntryPath: string) => { }); } catch (e) { log.error( - `Failed to read entry ${zipEntryPath} from zip file at ${zipPath}`, + `Failed to read entry ${entryName} from zip file at ${zipPath}`, e, ); return new Response(`Failed to read stream: ${e.message}`, { diff --git a/desktop/src/main/utils-temp.ts b/desktop/src/main/utils-temp.ts index 2e416bd652..3f3a6081e4 100644 --- a/desktop/src/main/utils-temp.ts +++ b/desktop/src/main/utils-temp.ts @@ -3,7 +3,7 @@ import StreamZip from "node-stream-zip"; import { existsSync } from "node:fs"; import fs from "node:fs/promises"; import path from "path"; -import type { ZipEntry } from "../types/ipc"; +import type { ZipItem } from "../types/ipc"; /** * Our very own directory within the system temp directory. Go crazy, but @@ -64,9 +64,11 @@ export const deleteTempFile = async (tempFilePath: string) => { await fs.rm(tempFilePath, { force: true }); }; -/** The result of {@link makeFileForDataOrPathOrZipEntry}. */ -interface FileForDataOrPathOrZipEntry { - /** The path to the file (possibly temporary) */ +/** The result of {@link makeFileForDataOrPathOrZipItem}. */ +interface FileForDataOrPathOrZipItem { + /** + * The path to the file (possibly temporary). + */ path: string; /** * `true` if {@link path} points to a temporary file which should be deleted @@ -75,12 +77,12 @@ interface FileForDataOrPathOrZipEntry { isFileTemporary: boolean; /** * If set, this'll be a function that can be called to actually write the - * contents of the source `Uint8Array | string | ZipEntry` into the file at + * contents of the source `Uint8Array | string | ZipItem` into the file at * {@link path}. * * It will be undefined if the source is already a path since nothing needs * to be written in that case. In the other two cases this function will - * write the data or zip entry into the file at {@link path}. + * write the data or zip item into the file at {@link path}. */ writeToTemporaryFile?: () => Promise; } @@ -88,31 +90,31 @@ interface FileForDataOrPathOrZipEntry { /** * Return the path to a file, a boolean indicating if this is a temporary path * that needs to be deleted after processing, and a function to write the given - * {@link dataOrPathOrZipEntry} into that temporary file if needed. + * {@link dataOrPathOrZipItem} into that temporary file if needed. * - * @param dataOrPathOrZipEntry The contents of the file, or the path to an + * @param dataOrPathOrZipItem The contents of the file, or the path to an * existing file, or a (path to a zip file, name of an entry within that zip * file) tuple. */ -export const makeFileForDataOrPathOrZipEntry = async ( - dataOrPathOrZipEntry: Uint8Array | string | ZipEntry, -): Promise => { +export const makeFileForDataOrPathOrZipItem = async ( + dataOrPathOrZipItem: Uint8Array | string | ZipItem, +): Promise => { let path: string; let isFileTemporary: boolean; let writeToTemporaryFile: () => Promise | undefined; - if (typeof dataOrPathOrZipEntry == "string") { - path = dataOrPathOrZipEntry; + if (typeof dataOrPathOrZipItem == "string") { + path = dataOrPathOrZipItem; isFileTemporary = false; } else { path = await makeTempFilePath(); isFileTemporary = true; - if (dataOrPathOrZipEntry instanceof Uint8Array) { + if (dataOrPathOrZipItem instanceof Uint8Array) { writeToTemporaryFile = () => - fs.writeFile(path, dataOrPathOrZipEntry); + fs.writeFile(path, dataOrPathOrZipItem); } else { writeToTemporaryFile = async () => { - const [zipPath, entryName] = dataOrPathOrZipEntry; + const [zipPath, entryName] = dataOrPathOrZipItem; const zip = new StreamZip.async({ file: zipPath }); await zip.extract(entryName, path); zip.close(); diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index 76d44591e0..48e4d1448c 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -47,7 +47,7 @@ import type { ElectronFile, FolderWatch, PendingUploads, - ZipEntry, + ZipItem, } from "./types/ipc"; // - General @@ -129,27 +129,27 @@ const convertToJPEG = (imageData: Uint8Array): Promise => ipcRenderer.invoke("convertToJPEG", imageData); const generateImageThumbnail = ( - dataOrPathOrZipEntry: Uint8Array | string | ZipEntry, + dataOrPathOrZipItem: Uint8Array | string | ZipItem, maxDimension: number, maxSize: number, ): Promise => ipcRenderer.invoke( "generateImageThumbnail", - dataOrPathOrZipEntry, + dataOrPathOrZipItem, maxDimension, maxSize, ); const ffmpegExec = ( command: string[], - dataOrPathOrZipEntry: Uint8Array | string | ZipEntry, + dataOrPathOrZipItem: Uint8Array | string | ZipItem, outputFileExtension: string, timeoutMS: number, ): Promise => ipcRenderer.invoke( "ffmpegExec", command, - dataOrPathOrZipEntry, + dataOrPathOrZipItem, outputFileExtension, timeoutMS, ); @@ -241,12 +241,11 @@ const watchFindFiles = (folderPath: string): Promise => const pathForFile = (file: File) => webUtils.getPathForFile(file); -const listZipEntries = (zipPath: string): Promise => +const listZipEntries = (zipPath: string): Promise => ipcRenderer.invoke("listZipEntries", zipPath); -const pathOrZipEntrySize = ( - pathOrZipEntry: string | ZipEntry, -): Promise => ipcRenderer.invoke("pathOrZipEntrySize", pathOrZipEntry); +const pathOrZipItemSize = (pathOrZipItem: string | ZipItem): Promise => + ipcRenderer.invoke("pathOrZipItemSize", pathOrZipItem); const pendingUploads = (): Promise => ipcRenderer.invoke("pendingUploads"); @@ -258,7 +257,7 @@ const markUploadedFiles = (paths: PendingUploads["filePaths"]): Promise => ipcRenderer.invoke("markUploadedFiles", paths); const markUploadedZipEntries = ( - zipEntries: PendingUploads["zipEntries"], + zipEntries: PendingUploads["zipItems"], ): Promise => ipcRenderer.invoke("markUploadedZipEntries", zipEntries); const clearPendingUploads = (): Promise => @@ -374,7 +373,7 @@ contextBridge.exposeInMainWorld("electron", { pathForFile, listZipEntries, - pathOrZipEntrySize, + pathOrZipItemSize, pendingUploads, setPendingUploads, markUploadedFiles, diff --git a/desktop/src/types/ipc.ts b/desktop/src/types/ipc.ts index 307fb7de32..6e47b7a3a6 100644 --- a/desktop/src/types/ipc.ts +++ b/desktop/src/types/ipc.ts @@ -25,12 +25,12 @@ export interface FolderWatchSyncedFile { collectionID: number; } -export type ZipEntry = [zipPath: string, entryName: string]; +export type ZipItem = [zipPath: string, entryName: string]; export interface PendingUploads { collectionName: string; filePaths: string[]; - zipEntries: ZipEntry[]; + zipItems: ZipItem[]; } /** From afb0e1aff3e0a2544fbf88eab50cfcad559b7472 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 10:11:27 +0530 Subject: [PATCH 097/240] web --- desktop/src/main/ipc.ts | 13 ++-- desktop/src/main/services/upload.ts | 38 +++++------ desktop/src/main/stores/upload-status.ts | 2 +- desktop/src/preload.ts | 14 ++--- .../photos/src/components/Upload/Uploader.tsx | 63 ++++++++++--------- .../src/services/upload/uploadManager.ts | 14 ++--- web/apps/photos/src/utils/native-stream.ts | 4 +- web/packages/next/types/ipc.ts | 36 +++++------ 8 files changed, 95 insertions(+), 89 deletions(-) diff --git a/desktop/src/main/ipc.ts b/desktop/src/main/ipc.ts index abbe54705d..df6ab7c8ea 100644 --- a/desktop/src/main/ipc.ts +++ b/desktop/src/main/ipc.ts @@ -53,9 +53,9 @@ import { } from "./services/store"; import { clearPendingUploads, - listZipEntries, + listZipItems, markUploadedFiles, - markUploadedZipEntries, + markUploadedZipItems, pathOrZipItemSize, pendingUploads, setPendingUploads, @@ -205,8 +205,8 @@ export const attachIPCHandlers = () => { // - Upload - ipcMain.handle("listZipEntries", (_, zipPath: string) => - listZipEntries(zipPath), + ipcMain.handle("listZipItems", (_, zipPath: string) => + listZipItems(zipPath), ); ipcMain.handle("pathOrZipItemSize", (_, pathOrZipItem: string | ZipItem) => @@ -225,9 +225,8 @@ export const attachIPCHandlers = () => { ); ipcMain.handle( - "markUploadedZipEntries", - (_, zipEntries: PendingUploads["zipItems"]) => - markUploadedZipEntries(zipEntries), + "markUploadedZipItems", + (_, items: PendingUploads["zipItems"]) => markUploadedZipItems(items), ); ipcMain.handle("clearPendingUploads", () => clearPendingUploads()); diff --git a/desktop/src/main/services/upload.ts b/desktop/src/main/services/upload.ts index 1a1d343c5c..9b24cc0ead 100644 --- a/desktop/src/main/services/upload.ts +++ b/desktop/src/main/services/upload.ts @@ -6,7 +6,7 @@ import type { ElectronFile, PendingUploads, ZipItem } from "../../types/ipc"; import { uploadStatusStore } from "../stores/upload-status"; import { getZipFileStream } from "./fs"; -export const listZipEntries = async (zipPath: string): Promise => { +export const listZipItems = async (zipPath: string): Promise => { const zip = new StreamZip.async({ file: zipPath }); const entries = await zip.entries(); @@ -48,32 +48,34 @@ export const pendingUploads = async (): Promise => { const allFilePaths = uploadStatusStore.get("filePaths") ?? []; const filePaths = allFilePaths.filter((f) => existsSync(f)); - const allZipEntries = uploadStatusStore.get("zipEntries"); - let zipEntries: typeof allZipEntries; + const allZipItems = uploadStatusStore.get("zipItems"); + let zipItems: typeof allZipItems; // Migration code - May 2024. Remove after a bit. // - // The older store formats will not have zipEntries and instead will have + // The older store formats will not have zipItems and instead will have // zipPaths. If we find such a case, read the zipPaths and enqueue all of - // their files as zipEntries in the result. This potentially can be cause us - // to try reuploading an already uploaded file, but the dedup logic will - // kick in at that point so no harm will come off it. - if (allZipEntries === undefined) { + // their files as zipItems in the result. + // + // This potentially can be cause us to try reuploading an already uploaded + // file, but the dedup logic will kick in at that point so no harm will come + // off it. + if (allZipItems === undefined) { const allZipPaths = uploadStatusStore.get("filePaths"); const zipPaths = allZipPaths.filter((f) => existsSync(f)); - zipEntries = []; + zipItems = []; for (const zip of zipPaths) - zipEntries = zipEntries.concat(await listZipEntries(zip)); + zipItems = zipItems.concat(await listZipItems(zip)); } else { - zipEntries = allZipEntries.filter(([z]) => existsSync(z)); + zipItems = allZipItems.filter(([z]) => existsSync(z)); } - if (filePaths.length == 0 && zipEntries.length == 0) return undefined; + if (filePaths.length == 0 && zipItems.length == 0) return undefined; return { collectionName, filePaths, - zipItems: zipEntries, + zipItems, }; }; @@ -86,14 +88,14 @@ export const markUploadedFiles = async (paths: string[]) => { uploadStatusStore.set("filePaths", updated); }; -export const markUploadedZipEntries = async ( - entries: [zipPath: string, entryName: string][], +export const markUploadedZipItems = async ( + items: [zipPath: string, entryName: string][], ) => { - const existing = uploadStatusStore.get("zipEntries"); + const existing = uploadStatusStore.get("zipItems"); const updated = existing.filter( - (z) => !entries.some((e) => z[0] == e[0] && z[1] == e[1]), + (z) => !items.some((e) => z[0] == e[0] && z[1] == e[1]), ); - uploadStatusStore.set("zipEntries", updated); + uploadStatusStore.set("zipItems", updated); }; export const clearPendingUploads = () => uploadStatusStore.clear(); diff --git a/desktop/src/main/stores/upload-status.ts b/desktop/src/main/stores/upload-status.ts index 4fd6c9860e..472f38a7f9 100644 --- a/desktop/src/main/stores/upload-status.ts +++ b/desktop/src/main/stores/upload-status.ts @@ -21,7 +21,7 @@ export interface UploadStatusStore { */ zipItems?: [zipPath: string, entryName: string][]; /** - * @deprecated Legacy paths to zip files, now subsumed into zipEntries. + * @deprecated Legacy paths to zip files, now subsumed into zipItems. */ zipPaths?: string[]; } diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index 48e4d1448c..61955b5240 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -241,8 +241,8 @@ const watchFindFiles = (folderPath: string): Promise => const pathForFile = (file: File) => webUtils.getPathForFile(file); -const listZipEntries = (zipPath: string): Promise => - ipcRenderer.invoke("listZipEntries", zipPath); +const listZipItems = (zipPath: string): Promise => + ipcRenderer.invoke("listZipItems", zipPath); const pathOrZipItemSize = (pathOrZipItem: string | ZipItem): Promise => ipcRenderer.invoke("pathOrZipItemSize", pathOrZipItem); @@ -256,9 +256,9 @@ const setPendingUploads = (pendingUploads: PendingUploads): Promise => const markUploadedFiles = (paths: PendingUploads["filePaths"]): Promise => ipcRenderer.invoke("markUploadedFiles", paths); -const markUploadedZipEntries = ( - zipEntries: PendingUploads["zipItems"], -): Promise => ipcRenderer.invoke("markUploadedZipEntries", zipEntries); +const markUploadedZipItems = ( + items: PendingUploads["zipItems"], +): Promise => ipcRenderer.invoke("markUploadedZipItems", items); const clearPendingUploads = (): Promise => ipcRenderer.invoke("clearPendingUploads"); @@ -372,11 +372,11 @@ contextBridge.exposeInMainWorld("electron", { // - Upload pathForFile, - listZipEntries, + listZipItems, pathOrZipItemSize, pendingUploads, setPendingUploads, markUploadedFiles, - markUploadedZipEntries, + markUploadedZipItems, clearPendingUploads, }); diff --git a/web/apps/photos/src/components/Upload/Uploader.tsx b/web/apps/photos/src/components/Upload/Uploader.tsx index c895894ccc..cd31d2d382 100644 --- a/web/apps/photos/src/components/Upload/Uploader.tsx +++ b/web/apps/photos/src/components/Upload/Uploader.tsx @@ -1,7 +1,7 @@ import { basename } from "@/next/file"; import log from "@/next/log"; import { type FileAndPath } from "@/next/types/file"; -import type { CollectionMapping, Electron, ZipEntry } from "@/next/types/ipc"; +import type { CollectionMapping, Electron, ZipItem } from "@/next/types/ipc"; import { CustomError } from "@ente/shared/error"; import { isPromise } from "@ente/shared/utils"; import DiscFullIcon from "@mui/icons-material/DiscFull"; @@ -127,15 +127,18 @@ export default function Uploader({ ); /** - * {@link File}s that the user drag-dropped or selected for uploads. This is - * the only type of selection that is possible when we're running in the - * browser. + * {@link File}s that the user drag-dropped or selected for uploads (web). + * + * This is the only type of selection that is possible when we're running in + * the browser. */ const [webFiles, setWebFiles] = useState([]); /** * {@link File}s that the user drag-dropped or selected for uploads, - * augmented with their paths. These siblings of {@link webFiles} come into - * play when we are running in the context of our desktop app. + * augmented with their paths (desktop). + * + * These siblings of {@link webFiles} come into play when we are running in + * the context of our desktop app. */ const [desktopFiles, setDesktopFiles] = useState([]); /** @@ -151,22 +154,24 @@ export default function Uploader({ const [desktopFilePaths, setDesktopFilePaths] = useState([]); /** * (zip file path, entry within zip file) tuples for zip files that the user - * is trying to upload. These are only set when we are running in the - * context of our desktop app. They may be set either on a user action (when - * the user selects or drag-drops zip files) or programmatically (when the - * app is trying to resume pending uploads from a previous session). + * is trying to upload. + * + * These are only set when we are running in the context of our desktop app. + * They may be set either on a user action (when the user selects or + * drag-drops zip files) or programmatically (when the app is trying to + * resume pending uploads from a previous session). */ - const [desktopZipEntries, setDesktopZipEntries] = useState([]); + const [desktopZipItems, setDesktopZipItems] = useState([]); /** * Consolidated and cleaned list obtained from {@link webFiles}, * {@link desktopFiles}, {@link desktopFilePaths} and - * {@link desktopZipEntries}. + * {@link desktopZipItems}. * * Augment each {@link UploadItem} with its "path" (relative path or name in * the case of {@link webFiles}, absolute path in the case of * {@link desktopFiles}, {@link desktopFilePaths}, and the path within the - * zip file for {@link desktopZipEntries}). + * zip file for {@link desktopZipItems}). * * See the documentation of {@link UploadItem} for more details. */ @@ -254,13 +259,13 @@ export default function Uploader({ electron.pendingUploads().then((pending) => { if (!pending) return; - const { collectionName, filePaths, zipEntries } = pending; + const { collectionName, filePaths, zipItems } = pending; log.info("Resuming pending upload", pending); isPendingDesktopUpload.current = true; pendingDesktopUploadCollectionName.current = collectionName; setDesktopFilePaths(filePaths); - setDesktopZipEntries(zipEntries); + setDesktopZipItems(zipItems); }); } }, [ @@ -286,10 +291,10 @@ export default function Uploader({ fileSelectorZipFiles, ].flat(); if (electron) { - desktopFilesAndZipEntries(electron, files).then( - ({ fileAndPaths, zipEntries }) => { + desktopFilesAndZipItems(electron, files).then( + ({ fileAndPaths, zipItems }) => { setDesktopFiles(fileAndPaths); - setDesktopZipEntries(zipEntries); + setDesktopZipItems(zipItems); }, ); } else { @@ -309,7 +314,7 @@ export default function Uploader({ webFiles.map((f) => [f, f["path"] ?? f.name]), desktopFiles.map((fp) => [fp, fp.path]), desktopFilePaths.map((p) => [p, p]), - desktopZipEntries.map((ze) => [ze, ze[1]]), + desktopZipItems.map((ze) => [ze, ze[1]]), ].flat() as [UploadItem, string][]; if (allItemAndPaths.length == 0) return; @@ -333,7 +338,7 @@ export default function Uploader({ setWebFiles([]); setDesktopFiles([]); setDesktopFilePaths([]); - setDesktopZipEntries([]); + setDesktopZipItems([]); // Remove hidden files (files whose names begins with a "."). const prunedItemAndPaths = allItemAndPaths.filter( @@ -423,7 +428,7 @@ export default function Uploader({ intent: CollectionSelectorIntent.upload, }); })(); - }, [webFiles, desktopFiles, desktopFilePaths, desktopZipEntries]); + }, [webFiles, desktopFiles, desktopFilePaths, desktopZipItems]); const preCollectionCreationAction = async () => { props.closeCollectionSelector?.(); @@ -764,23 +769,23 @@ async function waitAndRun( await task(); } -const desktopFilesAndZipEntries = async ( +const desktopFilesAndZipItems = async ( electron: Electron, files: File[], -): Promise<{ fileAndPaths: FileAndPath[]; zipEntries: ZipEntry[] }> => { +): Promise<{ fileAndPaths: FileAndPath[]; zipItems: ZipItem[] }> => { const fileAndPaths: FileAndPath[] = []; - let zipEntries: ZipEntry[] = []; + let zipItems: ZipItem[] = []; for (const file of files) { const path = electron.pathForFile(file); if (file.name.endsWith(".zip")) { - zipEntries = zipEntries.concat(await electron.listZipEntries(path)); + zipItems = zipItems.concat(await electron.listZipItems(path)); } else { fileAndPaths.push({ file, path }); } } - return { fileAndPaths, zipEntries }; + return { fileAndPaths, zipItems }; }; // This is used to prompt the user the make upload strategy choice @@ -891,14 +896,14 @@ export const setPendingUploads = async ( } const filePaths: string[] = []; - const zipEntries: ZipEntry[] = []; + const zipItems: ZipItem[] = []; for (const item of uploadItems) { if (item instanceof File) { throw new Error("Unexpected web file for a desktop pending upload"); } else if (typeof item == "string") { filePaths.push(item); } else if (Array.isArray(item)) { - zipEntries.push(item); + zipItems.push(item); } else { filePaths.push(item.path); } @@ -907,6 +912,6 @@ export const setPendingUploads = async ( await electron.setPendingUploads({ collectionName, filePaths, - zipEntries, + zipItems: zipItems, }); }; diff --git a/web/apps/photos/src/services/upload/uploadManager.ts b/web/apps/photos/src/services/upload/uploadManager.ts index 06772b2d29..44bfef92f5 100644 --- a/web/apps/photos/src/services/upload/uploadManager.ts +++ b/web/apps/photos/src/services/upload/uploadManager.ts @@ -4,7 +4,7 @@ import { ensureElectron } from "@/next/electron"; import { lowercaseExtension, nameAndExtension } from "@/next/file"; import log from "@/next/log"; import { type FileAndPath } from "@/next/types/file"; -import type { Electron, ZipEntry } from "@/next/types/ipc"; +import type { Electron, ZipItem } from "@/next/types/ipc"; import { ComlinkWorker } from "@/next/worker/comlink-worker"; import { ensure } from "@/utils/ensure"; import { getDedicatedCryptoWorker } from "@ente/shared/crypto"; @@ -105,9 +105,9 @@ const maxConcurrentUploads = 4; * selected the zip file, or it might be a zip file that they'd previously * selected but we now are resuming an interrupted upload. Either ways, what * we have is a path to zip file, and the name of an entry within that zip - * file. This is the {@link ZipEntry} case. + * file. This is the {@link ZipItem} case. */ -export type UploadItem = File | FileAndPath | string | ZipEntry; +export type UploadItem = File | FileAndPath | string | ZipItem; export interface UploadItemWithCollection { localID: number; @@ -840,7 +840,7 @@ const markUploaded = async (electron: Electron, item: ClusteredUploadItem) => { item.livePhotoAssets.video, ]; if (Array.isArray(p0) && Array.isArray(p1)) { - electron.markUploadedZipEntries([p0, p1]); + electron.markUploadedZipItems([p0, p1]); } else if (typeof p0 == "string" && typeof p1 == "string") { electron.markUploadedFiles([p0, p1]); } else if ( @@ -860,7 +860,7 @@ const markUploaded = async (electron: Electron, item: ClusteredUploadItem) => { } else { const p = ensure(item.uploadItem); if (Array.isArray(p)) { - electron.markUploadedZipEntries([p]); + electron.markUploadedZipItems([p]); } else if (typeof p == "string") { electron.markUploadedFiles([p]); } else if (p && typeof p == "object" && "path" in p) { @@ -1030,8 +1030,8 @@ const removePotentialLivePhotoSuffix = (name: string, suffix?: string) => { const uploadItemSize = async (uploadItem: UploadItem): Promise => { if (uploadItem instanceof File) return uploadItem.size; if (typeof uploadItem == "string") - return ensureElectron().pathOrZipEntrySize(uploadItem); + return ensureElectron().pathOrZipItemSize(uploadItem); if (Array.isArray(uploadItem)) - return ensureElectron().pathOrZipEntrySize(uploadItem); + return ensureElectron().pathOrZipItemSize(uploadItem); return uploadItem.file.size; }; diff --git a/web/apps/photos/src/utils/native-stream.ts b/web/apps/photos/src/utils/native-stream.ts index c882d5031a..d33d41e2fe 100644 --- a/web/apps/photos/src/utils/native-stream.ts +++ b/web/apps/photos/src/utils/native-stream.ts @@ -6,7 +6,7 @@ * See: [Note: IPC streams]. */ -import type { Electron, ZipEntry } from "@/next/types/ipc"; +import type { Electron, ZipItem } from "@/next/types/ipc"; /** * Stream the given file or zip entry from the user's local filesystem. @@ -35,7 +35,7 @@ import type { Electron, ZipEntry } from "@/next/types/ipc"; */ export const readStream = async ( _: Electron, - pathOrZipEntry: string | ZipEntry, + pathOrZipEntry: string | ZipItem, ): Promise<{ response: Response; size: number; lastModifiedMs: number }> => { let url: URL; if (typeof pathOrZipEntry == "string") { diff --git a/web/packages/next/types/ipc.ts b/web/packages/next/types/ipc.ts index 4b3d97dd32..6efa32f4c6 100644 --- a/web/packages/next/types/ipc.ts +++ b/web/packages/next/types/ipc.ts @@ -221,7 +221,7 @@ export interface Electron { * not yet possible, this function will throw an error with the * {@link CustomErrorMessage.NotAvailable} message. * - * @param dataOrPathOrZipEntry The file whose thumbnail we want to generate. + * @param dataOrPathOrZipItem The file whose thumbnail we want to generate. * It can be provided as raw image data (the contents of the image file), or * the path to the image file, or a tuple containing the path of the zip * file along with the name of an entry in it. @@ -234,14 +234,14 @@ export interface Electron { * @returns JPEG data of the generated thumbnail. */ generateImageThumbnail: ( - dataOrPathOrZipEntry: Uint8Array | string | ZipEntry, + dataOrPathOrZipItem: Uint8Array | string | ZipItem, maxDimension: number, maxSize: number, ) => Promise; /** * Execute a FFmpeg {@link command} on the given - * {@link dataOrPathOrZipEntry}. + * {@link dataOrPathOrZipItem}. * * This executes the command using a FFmpeg executable we bundle with our * desktop app. We also have a wasm FFmpeg wasm implementation that we use @@ -254,7 +254,7 @@ export interface Electron { * (respectively {@link inputPathPlaceholder}, * {@link outputPathPlaceholder}, {@link ffmpegPathPlaceholder}). * - * @param dataOrPathOrZipEntry The bytes of the input file, or the path to + * @param dataOrPathOrZipItem The bytes of the input file, or the path to * the input file on the user's local disk, or the path to a zip file on the * user's disk and the name of an entry in it. In all three cases, the data * gets serialized to a temporary file, and then that path gets substituted @@ -274,7 +274,7 @@ export interface Electron { */ ffmpegExec: ( command: string[], - dataOrPathOrZipEntry: Uint8Array | string | ZipEntry, + dataOrPathOrZipItem: Uint8Array | string | ZipItem, outputFileExtension: string, timeoutMS: number, ) => Promise; @@ -491,13 +491,13 @@ export interface Electron { * * To read the contents of the files themselves, see [Note: IPC streams]. */ - listZipEntries: (zipPath: string) => Promise; + listZipItems: (zipPath: string) => Promise; /** * Return the size in bytes of the file at the given path or of a particular * entry within a zip file. */ - pathOrZipEntrySize: (pathOrZipEntry: string | ZipEntry) => Promise; + pathOrZipItemSize: (pathOrZipItem: string | ZipItem) => Promise; /** * Return any pending uploads that were previously enqueued but haven't yet @@ -518,7 +518,7 @@ export interface Electron { * - Typically, this would be called at the start of an upload. * * - Thereafter, as each item gets uploaded one by one, we'd call - * {@link markUploadedFiles} or {@link markUploadedZipEntries}. + * {@link markUploadedFiles} or {@link markUploadedZipItems}. * * - Finally, once the upload completes (or gets cancelled), we'd call * {@link clearPendingUploads} to complete the circle. @@ -532,11 +532,9 @@ export interface Electron { markUploadedFiles: (paths: PendingUploads["filePaths"]) => Promise; /** - * Mark the given zip file entries as having been uploaded. + * Mark the given {@link ZipItem}s as having been uploaded. */ - markUploadedZipEntries: ( - entries: PendingUploads["zipEntries"], - ) => Promise; + markUploadedZipItems: (items: PendingUploads["zipItems"]) => Promise; /** * Clear any pending uploads. @@ -627,15 +625,17 @@ export interface FolderWatchSyncedFile { } /** - * When the user uploads a zip file, we create a "zip entry" for each entry - * within that zip file. Such an entry is a tuple containin the path to a zip - * file itself, and the name of an entry within it. + * A particular file within a zip file. + * + * When the user uploads a zip file, we create a "zip item" for each entry + * within the zip file. Each such entry is a tuple containing the (path to a zip + * file itself, and the name of an entry within it). * * The name of the entry is not just the file name, but rather is the full path * of the file within the zip. That is, each entry name uniquely identifies a * particular file within the given zip. */ -export type ZipEntry = [zipPath: string, entryName: string]; +export type ZipItem = [zipPath: string, entryName: string]; /** * State about pending and in-progress uploads. @@ -659,7 +659,7 @@ export interface PendingUploads { */ filePaths: string[]; /** - * {@link ZipEntry} (zip path and entry name) that need to be uploaded. + * {@link ZipItem} (zip path and entry name) that need to be uploaded. */ - zipEntries: ZipEntry[]; + zipItems: ZipItem[]; } From 7ad4069b999e9cdc16491bc5f2ae967be4610a02 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 10:23:33 +0530 Subject: [PATCH 098/240] hobgoblins --- web/apps/photos/src/components/Upload/Uploader.tsx | 4 ++-- web/apps/photos/src/services/export/index.ts | 8 ++++---- web/apps/photos/src/services/ffmpeg.ts | 2 +- web/apps/photos/src/services/upload/thumbnail.ts | 2 +- web/apps/photos/src/services/upload/uploadManager.ts | 2 +- web/apps/photos/src/services/upload/uploadService.ts | 10 +++++----- web/apps/photos/src/utils/native-fs.ts | 2 +- web/apps/photos/src/utils/native-stream.ts | 8 ++++---- web/docs/storage.md | 2 +- web/packages/next/types/ipc.ts | 8 ++++---- 10 files changed, 24 insertions(+), 24 deletions(-) diff --git a/web/apps/photos/src/components/Upload/Uploader.tsx b/web/apps/photos/src/components/Upload/Uploader.tsx index cd31d2d382..f5cb19e8c1 100644 --- a/web/apps/photos/src/components/Upload/Uploader.tsx +++ b/web/apps/photos/src/components/Upload/Uploader.tsx @@ -148,8 +148,8 @@ export default function Uploader({ * Unlike {@link filesWithPaths} which are still user initiated, * {@link desktopFilePaths} can be set via programmatic action. For example, * if the user has setup a folder watch, and a new file is added on their - * local filesystem in one of the watched folders, then the relevant path of - * the new file would get added to {@link desktopFilePaths}. + * local file system in one of the watched folders, then the relevant path + * of the new file would get added to {@link desktopFilePaths}. */ const [desktopFilePaths, setDesktopFilePaths] = useState([]); /** diff --git a/web/apps/photos/src/services/export/index.ts b/web/apps/photos/src/services/export/index.ts index 5a732658a6..82dfdbf8bf 100644 --- a/web/apps/photos/src/services/export/index.ts +++ b/web/apps/photos/src/services/export/index.ts @@ -46,13 +46,13 @@ const exportRecordFileName = "export_status.json"; /** * Name of the top level directory which we create underneath the selected - * directory when the user starts an export to the filesystem. + * directory when the user starts an export to the file system. */ const exportDirectoryName = "Ente Photos"; /** - * Name of the directory in which we put our metadata when exporting to the - * filesystem. + * Name of the directory in which we put our metadata when exporting to the file + * system. */ export const exportMetadataDirectoryName = "metadata"; @@ -1378,7 +1378,7 @@ const isExportInProgress = (exportStage: ExportStage) => * * Also move its associated metadata JSON to Trash. * - * @param exportDir The root directory on the user's filesystem where we are + * @param exportDir The root directory on the user's file system where we are * exporting to. * */ const moveToTrash = async ( diff --git a/web/apps/photos/src/services/ffmpeg.ts b/web/apps/photos/src/services/ffmpeg.ts index a8b9bc3671..00d9a97351 100644 --- a/web/apps/photos/src/services/ffmpeg.ts +++ b/web/apps/photos/src/services/ffmpeg.ts @@ -50,7 +50,7 @@ const _generateVideoThumbnail = async ( * for the new files that the user is adding. * * @param dataOrPath The input video's data or the path to the video on the - * user's local filesystem. See: [Note: Reading a UploadItem]. + * user's local file system. See: [Note: Reading a UploadItem]. * * @returns JPEG data of the generated thumbnail. * diff --git a/web/apps/photos/src/services/upload/thumbnail.ts b/web/apps/photos/src/services/upload/thumbnail.ts index a44c941f16..9cd9a339ca 100644 --- a/web/apps/photos/src/services/upload/thumbnail.ts +++ b/web/apps/photos/src/services/upload/thumbnail.ts @@ -178,7 +178,7 @@ const percentageSizeDiff = ( * object which we use to perform IPC with the Node.js side of our desktop app. * * @param dataOrPath Contents of an image or video file, or the path to the - * image or video file on the user's local filesystem, whose thumbnail we want + * image or video file on the user's local file system, whose thumbnail we want * to generate. * * @param fileTypeInfo The type information for {@link dataOrPath}. diff --git a/web/apps/photos/src/services/upload/uploadManager.ts b/web/apps/photos/src/services/upload/uploadManager.ts index 44bfef92f5..be24d7da72 100644 --- a/web/apps/photos/src/services/upload/uploadManager.ts +++ b/web/apps/photos/src/services/upload/uploadManager.ts @@ -91,7 +91,7 @@ const maxConcurrentUploads = 4; * * 2. A file drag-and-dropped or selected by the user when we are running in the * context of our desktop app. In such cases, we also have the absolute path - * of the file in the user's local filesystem. this is the + * of the file in the user's local file system. this is the * {@link FileAndPath} case. * * 3. A file path programmatically requested by the desktop app. For example, we diff --git a/web/apps/photos/src/services/upload/uploadService.ts b/web/apps/photos/src/services/upload/uploadService.ts index 97848eeac2..a88e5c3aad 100644 --- a/web/apps/photos/src/services/upload/uploadService.ts +++ b/web/apps/photos/src/services/upload/uploadService.ts @@ -456,21 +456,21 @@ export const uploader = async ( }; /** - * Read the given file or path or zip entry into an in-memory representation. + * Read the given file or path or zip item into an in-memory representation. * * [Note: Reading a UploadItem] * * The file can be either a web * [File](https://developer.mozilla.org/en-US/docs/Web/API/File), the absolute * path to a file on desk, a combination of these two, or a entry in a zip file - * on the user's local filesystem. + * on the user's local file system. * * tl;dr; There are four cases: * * 1. web / File * 2. desktop / File (+ path) * 3. desktop / path - * 4. desktop / ZipEntry + * 4. desktop / ZipItem * * For the when and why, read on. * @@ -482,9 +482,9 @@ export const uploader = async ( * * In the web context, we'll always get a File, since within the browser we * cannot programmatically construct paths to or arbitrarily access files on the - * user's filesystem. Note that even if we were to have an absolute path at + * user's file system. Note that even if we were to have an absolute path at * hand, we cannot programmatically create such File objects to arbitrary - * absolute paths on user's local filesystem for security reasons. + * absolute paths on user's local file system for security reasons. * * So in the web context, this will always be a File we get as a result of an * explicit user interaction (e.g. drag and drop). diff --git a/web/apps/photos/src/utils/native-fs.ts b/web/apps/photos/src/utils/native-fs.ts index 2ef8963022..27ebdd1c12 100644 --- a/web/apps/photos/src/utils/native-fs.ts +++ b/web/apps/photos/src/utils/native-fs.ts @@ -1,5 +1,5 @@ /** - * @file Utilities for native filesystem access. + * @file Utilities for native file system access. * * While they don't have any direct dependencies to our desktop app, they were * written for use by the code that runs in our desktop app. diff --git a/web/apps/photos/src/utils/native-stream.ts b/web/apps/photos/src/utils/native-stream.ts index d33d41e2fe..1ecae06241 100644 --- a/web/apps/photos/src/utils/native-stream.ts +++ b/web/apps/photos/src/utils/native-stream.ts @@ -9,7 +9,7 @@ import type { Electron, ZipItem } from "@/next/types/ipc"; /** - * Stream the given file or zip entry from the user's local filesystem. + * Stream the given file or zip entry from the user's local file system. * * This only works when we're running in our desktop app since it uses the * "stream://" protocol handler exposed by our custom code in the Node.js layer. @@ -18,9 +18,9 @@ import type { Electron, ZipItem } from "@/next/types/ipc"; * To avoid accidentally invoking it in a non-desktop app context, it requires * the {@link Electron} object as a parameter (even though it doesn't use it). * - * @param pathOrZipEntry Either the path on the file on the user's local - * filesystem whose contents we want to stream. Or a tuple containing the path - * to a zip file and the name of the entry within it. + * @param pathOrZipEntry Either the path on the file on the user's local file + * system whose contents we want to stream. Or a tuple containing the path to a + * zip file and the name of the entry within it. * * @return A ({@link Response}, size, lastModifiedMs) triple. * diff --git a/web/docs/storage.md b/web/docs/storage.md index d01654b234..9f19a6a46d 100644 --- a/web/docs/storage.md +++ b/web/docs/storage.md @@ -34,6 +34,6 @@ meant for larger, tabular data. OPFS is used for caching entire files when we're running under Electron (the Web Cache API is used in the browser). -As it name suggests, it is an entire filesystem, private for us ("origin"). In +As it name suggests, it is an entire file system, private for us ("origin"). In is not undbounded though, and the storage is not guaranteed to be persistent (at least with the APIs we use), hence the cache designation. diff --git a/web/packages/next/types/ipc.ts b/web/packages/next/types/ipc.ts index 6efa32f4c6..173b12b17c 100644 --- a/web/packages/next/types/ipc.ts +++ b/web/packages/next/types/ipc.ts @@ -123,17 +123,17 @@ export interface Electron { skipAppUpdate: (version: string) => void; /** - * A subset of filesystem access APIs. + * A subset of file system access APIs. * * The renderer process, being a web process, does not have full access to - * the local filesystem apart from files explicitly dragged and dropped (or + * the local file system apart from files explicitly dragged and dropped (or * selected by the user in a native file open dialog). * - * The main process, however, has full filesystem access (limited only be an + * The main process, however, has full fil system access (limited only be an * OS level sandbox on the entire process). * * When we're running in the desktop app, we want to better utilize the - * local filesystem access to provide more integrated features to the user - + * local file system access to provide more integrated features to the user; * things that are not currently possible using web technologies. For * example, continuous exports to an arbitrary user chosen location on disk, * or watching some folders for changes and syncing them automatically. From 5f0103682b91dc0d966aa38bd1e56f0b9442b96a Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 10:24:41 +0530 Subject: [PATCH 099/240] entries --- web/apps/photos/src/utils/native-stream.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/web/apps/photos/src/utils/native-stream.ts b/web/apps/photos/src/utils/native-stream.ts index 1ecae06241..8ada6070cd 100644 --- a/web/apps/photos/src/utils/native-stream.ts +++ b/web/apps/photos/src/utils/native-stream.ts @@ -18,7 +18,7 @@ import type { Electron, ZipItem } from "@/next/types/ipc"; * To avoid accidentally invoking it in a non-desktop app context, it requires * the {@link Electron} object as a parameter (even though it doesn't use it). * - * @param pathOrZipEntry Either the path on the file on the user's local file + * @param pathOrZipItem Either the path on the file on the user's local file * system whose contents we want to stream. Or a tuple containing the path to a * zip file and the name of the entry within it. * @@ -35,20 +35,18 @@ import type { Electron, ZipItem } from "@/next/types/ipc"; */ export const readStream = async ( _: Electron, - pathOrZipEntry: string | ZipItem, + pathOrZipItem: string | ZipItem, ): Promise<{ response: Response; size: number; lastModifiedMs: number }> => { let url: URL; - if (typeof pathOrZipEntry == "string") { - url = new URL(`stream://read${pathOrZipEntry}`); + if (typeof pathOrZipItem == "string") { + url = new URL(`stream://read${pathOrZipItem}`); } else { - const [zipPath, entryName] = pathOrZipEntry; + const [zipPath, entryName] = pathOrZipItem; url = new URL(`stream://read${zipPath}`); url.hash = entryName; } - const req = new Request(url, { - method: "GET", - }); + const req = new Request(url, { method: "GET" }); const res = await fetch(req); if (!res.ok) From c1a3fb489621579f8cf2a9937a9d87f61a52121b Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 10:31:33 +0530 Subject: [PATCH 100/240] docs --- .../src/services/upload/uploadManager.ts | 19 ++++--- .../src/services/upload/uploadService.ts | 49 ++++++++++--------- 2 files changed, 37 insertions(+), 31 deletions(-) diff --git a/web/apps/photos/src/services/upload/uploadManager.ts b/web/apps/photos/src/services/upload/uploadManager.ts index be24d7da72..bbf0f827ad 100644 --- a/web/apps/photos/src/services/upload/uploadManager.ts +++ b/web/apps/photos/src/services/upload/uploadManager.ts @@ -91,21 +91,24 @@ const maxConcurrentUploads = 4; * * 2. A file drag-and-dropped or selected by the user when we are running in the * context of our desktop app. In such cases, we also have the absolute path - * of the file in the user's local file system. this is the + * of the file in the user's local file system. This is the * {@link FileAndPath} case. * * 3. A file path programmatically requested by the desktop app. For example, we * might be resuming a previously interrupted upload after an app restart * (thus we no longer have access to the {@link File} from case 2). Or we * could be uploading a file this is in one of the folders the user has asked - * us to watch for changes. This is the {@link string} case. + * us to watch for changes. This is the `string` case. * - * 4. A file within a zip file. This too is only possible when we are running in - * the context of our desktop app. The user might have drag-and-dropped or - * selected the zip file, or it might be a zip file that they'd previously - * selected but we now are resuming an interrupted upload. Either ways, what - * we have is a path to zip file, and the name of an entry within that zip - * file. This is the {@link ZipItem} case. + * 4. A file within a zip file on the user's local file system. This too is only + * possible when we are running in the context of our desktop app. The user + * might have drag-and-dropped or selected a zip file, or it might be a zip + * file that they'd previously selected but we now are resuming an + * interrupted upload of. Either ways, what we have is a tuple containing the + * (path to zip file, and the name of an entry within that zip file). This is + * the {@link ZipItem} case. + * + * Also see: [Note: Reading a UploadItem]. */ export type UploadItem = File | FileAndPath | string | ZipItem; diff --git a/web/apps/photos/src/services/upload/uploadService.ts b/web/apps/photos/src/services/upload/uploadService.ts index a88e5c3aad..5aadb25644 100644 --- a/web/apps/photos/src/services/upload/uploadService.ts +++ b/web/apps/photos/src/services/upload/uploadService.ts @@ -482,45 +482,48 @@ export const uploader = async ( * * In the web context, we'll always get a File, since within the browser we * cannot programmatically construct paths to or arbitrarily access files on the - * user's file system. Note that even if we were to have an absolute path at - * hand, we cannot programmatically create such File objects to arbitrary - * absolute paths on user's local file system for security reasons. + * user's file system. + * + * > Note that even if we were to somehow have an absolute path at hand, we + * cannot programmatically create such File objects to arbitrary absolute + * paths on user's local file system for security reasons. * * So in the web context, this will always be a File we get as a result of an - * explicit user interaction (e.g. drag and drop). + * explicit user interaction (e.g. drag and drop or using a file selector). * * In the desktop context, this can be either a File (+ path), or a path, or an * entry within a zip file. * * 2. If the user provided us this file via some user interaction (say a drag - * and a drop), this'll still be a File. Note that unlike in the web context, - * such File objects also have the full path. See: [Note: File paths when - * running under Electron]. + * and a drop), this'll still be a File. But unlike in the web context, we + * also have access to the full path of this file. * - * 3. However, when running in the desktop app we have the ability to access - * absolute paths on the user's file system. For example, if the user asks us - * to watch certain folders on their disk for changes, we'll be able to pick - * up new images being added, and in such cases, the parameter here will be a - * path. Another example is when resuming an previously interrupted upload - - * we'll only have the path at hand in such cases, not the File object. + * 3. In addition, when running in the desktop app we have the ability to + * initate programmatic access absolute paths on the user's file system. For + * example, if the user asks us to watch certain folders on their disk for + * changes, we'll be able to pick up new images being added, and in such + * cases, the parameter here will be a path. Another example is when resuming + * an previously interrupted upload - we'll only have the path at hand in + * such cases, not the original File object since the app subsequently + * restarted. * - * 4. The user might've also initiated an upload of a zip file. In this case we - * will get a tuple (path to the zip file on the local file system, and the - * name of the entry within that zip file). + * 4. The user might've also initiated an upload of a zip file (or we might be + * resuming one). In such cases we will get a tuple (path to the zip file on + * the local file system, and the name of the entry within that zip file). * * Case 3 and 4, when we're provided a path, are simple. We don't have a choice, * since we cannot still programmatically construct a File object (we can * construct it on the Node.js layer, but it can't then be transferred over the * IPC boundary). So all our operations use the path itself. * - * Case 2 involves a choice on a use-case basis as neither File nor the path is - * a better choice for all use cases. + * Case 2 involves a choice on a use-case basis. Neither File nor the path is a + * better choice for all use cases. * - * The advantage of the File object is that the browser has already read it into - * memory for us. The disadvantage comes in the case where we need to - * communicate with the native Node.js layer of our desktop app. Since this - * communication happens over IPC, the File's contents need to be serialized and - * copied, which is a bummer for large videos etc. + * > The advantage of the File object is that the browser has already read it + * into memory for us. The disadvantage comes in the case where we need to + * communicate with the native Node.js layer of our desktop app. Since this + * communication happens over IPC, the File's contents need to be serialized + * and copied, which is a bummer for large videos etc. */ const readUploadItem = async (uploadItem: UploadItem): Promise => { let underlyingStream: ReadableStream; From 761fd560a18932e1ec5c5500d43ab22622ff17f1 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 10:43:12 +0530 Subject: [PATCH 101/240] Separate file --- web/apps/photos/src/services/ffmpeg.ts | 11 ++++--- .../photos/src/services/upload/takeout.ts | 2 +- web/apps/photos/src/services/upload/types.ts | 31 +++++++++++++++++ .../src/services/upload/uploadManager.ts | 33 ++----------------- .../src/services/upload/uploadService.ts | 3 +- 5 files changed, 42 insertions(+), 38 deletions(-) create mode 100644 web/apps/photos/src/services/upload/types.ts diff --git a/web/apps/photos/src/services/ffmpeg.ts b/web/apps/photos/src/services/ffmpeg.ts index 00d9a97351..f637b5bd2f 100644 --- a/web/apps/photos/src/services/ffmpeg.ts +++ b/web/apps/photos/src/services/ffmpeg.ts @@ -10,6 +10,7 @@ import { import { NULL_LOCATION } from "constants/upload"; import type { ParsedExtractedMetadata } from "types/metadata"; import type { DedicatedFFmpegWorker } from "worker/ffmpeg.worker"; +import type { UploadItem } from "./upload/types"; /** * Generate a thumbnail for the given video using a wasm FFmpeg running in a web @@ -92,18 +93,18 @@ const makeGenThumbnailCommand = (seekTime: number) => [ * This function is called during upload, when we need to extract the metadata * of videos that the user is uploading. * - * @param fileOrPath A {@link File}, or the absolute path to a file on the + * @param uploadItem A {@link File}, or the absolute path to a file on the * user's local filesytem. A path can only be provided when we're running in the * context of our desktop app. */ export const extractVideoMetadata = async ( - fileOrPath: File | string, + uploadItem: UploadItem, ): Promise => { const command = extractVideoMetadataCommand; const outputData = - fileOrPath instanceof File - ? await ffmpegExecWeb(command, fileOrPath, "txt", 0) - : await electron.ffmpegExec(command, fileOrPath, "txt", 0); + uploadItem instanceof File + ? await ffmpegExecWeb(command, uploadItem, "txt", 0) + : await electron.ffmpegExec(command, uploadItem, "txt", 0); return parseFFmpegExtractedMetadata(outputData); }; diff --git a/web/apps/photos/src/services/upload/takeout.ts b/web/apps/photos/src/services/upload/takeout.ts index 2a71e420a0..24c0a9d267 100644 --- a/web/apps/photos/src/services/upload/takeout.ts +++ b/web/apps/photos/src/services/upload/takeout.ts @@ -6,7 +6,7 @@ import log from "@/next/log"; import { NULL_LOCATION } from "constants/upload"; import type { Location } from "types/metadata"; import { readStream } from "utils/native-stream"; -import type { UploadItem } from "./uploadManager"; +import type { UploadItem } from "./types"; export interface ParsedMetadataJSON { creationTime: number; diff --git a/web/apps/photos/src/services/upload/types.ts b/web/apps/photos/src/services/upload/types.ts new file mode 100644 index 0000000000..25a79de1a2 --- /dev/null +++ b/web/apps/photos/src/services/upload/types.ts @@ -0,0 +1,31 @@ +import type { FileAndPath } from "@/next/types/file"; +import type { ZipItem } from "@/next/types/ipc"; + +/** + * An item to upload is one of the following: + * + * 1. A file drag-and-dropped or selected by the user when we are running in the + * web browser. These is the {@link File} case. + * + * 2. A file drag-and-dropped or selected by the user when we are running in the + * context of our desktop app. In such cases, we also have the absolute path + * of the file in the user's local file system. This is the + * {@link FileAndPath} case. + * + * 3. A file path programmatically requested by the desktop app. For example, we + * might be resuming a previously interrupted upload after an app restart + * (thus we no longer have access to the {@link File} from case 2). Or we + * could be uploading a file this is in one of the folders the user has asked + * us to watch for changes. This is the `string` case. + * + * 4. A file within a zip file on the user's local file system. This too is only + * possible when we are running in the context of our desktop app. The user + * might have drag-and-dropped or selected a zip file, or it might be a zip + * file that they'd previously selected but we now are resuming an + * interrupted upload of. Either ways, what we have is a tuple containing the + * (path to zip file, and the name of an entry within that zip file). This is + * the {@link ZipItem} case. + * + * Also see: [Note: Reading a UploadItem]. + */ +export type UploadItem = File | FileAndPath | string | ZipItem; diff --git a/web/apps/photos/src/services/upload/uploadManager.ts b/web/apps/photos/src/services/upload/uploadManager.ts index bbf0f827ad..99fe6ced39 100644 --- a/web/apps/photos/src/services/upload/uploadManager.ts +++ b/web/apps/photos/src/services/upload/uploadManager.ts @@ -3,8 +3,7 @@ import { potentialFileTypeFromExtension } from "@/media/live-photo"; import { ensureElectron } from "@/next/electron"; import { lowercaseExtension, nameAndExtension } from "@/next/file"; import log from "@/next/log"; -import { type FileAndPath } from "@/next/types/file"; -import type { Electron, ZipItem } from "@/next/types/ipc"; +import type { Electron } from "@/next/types/ipc"; import { ComlinkWorker } from "@/next/worker/comlink-worker"; import { ensure } from "@/utils/ensure"; import { getDedicatedCryptoWorker } from "@ente/shared/crypto"; @@ -36,6 +35,7 @@ import { tryParseTakeoutMetadataJSON, type ParsedMetadataJSON, } from "./takeout"; +import type { UploadItem } from "./types"; import UploadService, { uploadItemFileName, uploader } from "./uploadService"; export type FileID = number; @@ -83,35 +83,6 @@ export interface ProgressUpdater { /** The number of uploads to process in parallel. */ const maxConcurrentUploads = 4; -/** - * An item to upload is one of the following: - * - * 1. A file drag-and-dropped or selected by the user when we are running in the - * web browser. These is the {@link File} case. - * - * 2. A file drag-and-dropped or selected by the user when we are running in the - * context of our desktop app. In such cases, we also have the absolute path - * of the file in the user's local file system. This is the - * {@link FileAndPath} case. - * - * 3. A file path programmatically requested by the desktop app. For example, we - * might be resuming a previously interrupted upload after an app restart - * (thus we no longer have access to the {@link File} from case 2). Or we - * could be uploading a file this is in one of the folders the user has asked - * us to watch for changes. This is the `string` case. - * - * 4. A file within a zip file on the user's local file system. This too is only - * possible when we are running in the context of our desktop app. The user - * might have drag-and-dropped or selected a zip file, or it might be a zip - * file that they'd previously selected but we now are resuming an - * interrupted upload of. Either ways, what we have is a tuple containing the - * (path to zip file, and the name of an entry within that zip file). This is - * the {@link ZipItem} case. - * - * Also see: [Note: Reading a UploadItem]. - */ -export type UploadItem = File | FileAndPath | string | ZipItem; - export interface UploadItemWithCollection { localID: number; collectionID: number; diff --git a/web/apps/photos/src/services/upload/uploadService.ts b/web/apps/photos/src/services/upload/uploadService.ts index 5aadb25644..8c042ccaf3 100644 --- a/web/apps/photos/src/services/upload/uploadService.ts +++ b/web/apps/photos/src/services/upload/uploadService.ts @@ -50,8 +50,9 @@ import { generateThumbnailNative, generateThumbnailWeb, } from "./thumbnail"; +import type { UploadItem } from "./types"; import UploadHttpClient from "./uploadHttpClient"; -import type { UploadItem, UploadableUploadItem } from "./uploadManager"; +import type { UploadableUploadItem } from "./uploadManager"; /** * A readable stream for a file, and its associated size and last modified time. From a5177a37423c51441dd6b9071746014b85acf3c4 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 10:51:20 +0530 Subject: [PATCH 102/240] fore --- web/apps/photos/src/services/ffmpeg.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/web/apps/photos/src/services/ffmpeg.ts b/web/apps/photos/src/services/ffmpeg.ts index f637b5bd2f..dbdc53a3c5 100644 --- a/web/apps/photos/src/services/ffmpeg.ts +++ b/web/apps/photos/src/services/ffmpeg.ts @@ -104,11 +104,21 @@ export const extractVideoMetadata = async ( const outputData = uploadItem instanceof File ? await ffmpegExecWeb(command, uploadItem, "txt", 0) - : await electron.ffmpegExec(command, uploadItem, "txt", 0); + : await electron.ffmpegExec(command, forE(uploadItem), "txt", 0); return parseFFmpegExtractedMetadata(outputData); }; +/** + * For each of cases of {@link UploadItem} that apply when we're running in the + * context of our desktop app, return a value that can be passed to + * {@link Electron}'s {@link ffmpegExec} over IPC. + */ +const forE = (desktopUploadItem: Exclude) => + typeof desktopUploadItem == "string" || Array.isArray(desktopUploadItem) + ? desktopUploadItem + : desktopUploadItem.path; + // Options: // // - `-c [short for codex] copy` From 68f3f1e714e2c508aedcdd64a54fa8050c409427 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 11:01:50 +0530 Subject: [PATCH 103/240] more --- web/apps/photos/src/services/ffmpeg.ts | 27 +++++++++---------- .../photos/src/services/upload/thumbnail.ts | 7 ++--- web/apps/photos/src/services/upload/types.ts | 16 +++++++++++ .../src/services/upload/uploadService.ts | 22 +++++++-------- 4 files changed, 44 insertions(+), 28 deletions(-) diff --git a/web/apps/photos/src/services/ffmpeg.ts b/web/apps/photos/src/services/ffmpeg.ts index dbdc53a3c5..4dfdb3f641 100644 --- a/web/apps/photos/src/services/ffmpeg.ts +++ b/web/apps/photos/src/services/ffmpeg.ts @@ -10,7 +10,11 @@ import { import { NULL_LOCATION } from "constants/upload"; import type { ParsedExtractedMetadata } from "types/metadata"; import type { DedicatedFFmpegWorker } from "worker/ffmpeg.worker"; -import type { UploadItem } from "./upload/types"; +import { + toDataOrPathOrZipEntry, + type DesktopUploadItem, + type UploadItem, +} from "./upload/types"; /** * Generate a thumbnail for the given video using a wasm FFmpeg running in a web @@ -59,12 +63,12 @@ const _generateVideoThumbnail = async ( */ export const generateVideoThumbnailNative = async ( electron: Electron, - dataOrPath: Uint8Array | string, + desktopUploadItem: DesktopUploadItem, ) => _generateVideoThumbnail((seekTime: number) => electron.ffmpegExec( makeGenThumbnailCommand(seekTime), - dataOrPath, + toDataOrPathOrZipEntry(desktopUploadItem), "jpeg", 0, ), @@ -104,21 +108,16 @@ export const extractVideoMetadata = async ( const outputData = uploadItem instanceof File ? await ffmpegExecWeb(command, uploadItem, "txt", 0) - : await electron.ffmpegExec(command, forE(uploadItem), "txt", 0); + : await electron.ffmpegExec( + command, + toDataOrPathOrZipEntry(uploadItem), + "txt", + 0, + ); return parseFFmpegExtractedMetadata(outputData); }; -/** - * For each of cases of {@link UploadItem} that apply when we're running in the - * context of our desktop app, return a value that can be passed to - * {@link Electron}'s {@link ffmpegExec} over IPC. - */ -const forE = (desktopUploadItem: Exclude) => - typeof desktopUploadItem == "string" || Array.isArray(desktopUploadItem) - ? desktopUploadItem - : desktopUploadItem.path; - // Options: // // - `-c [short for codex] copy` diff --git a/web/apps/photos/src/services/upload/thumbnail.ts b/web/apps/photos/src/services/upload/thumbnail.ts index 9cd9a339ca..1dd448376e 100644 --- a/web/apps/photos/src/services/upload/thumbnail.ts +++ b/web/apps/photos/src/services/upload/thumbnail.ts @@ -4,6 +4,7 @@ import { type Electron } from "@/next/types/ipc"; import { withTimeout } from "@ente/shared/utils"; import * as ffmpeg from "services/ffmpeg"; import { heicToJPEG } from "services/heic-convert"; +import { toDataOrPathOrZipEntry, type DesktopUploadItem } from "./types"; /** Maximum width or height of the generated thumbnail */ const maxThumbnailDimension = 720; @@ -189,16 +190,16 @@ const percentageSizeDiff = ( */ export const generateThumbnailNative = async ( electron: Electron, - dataOrPath: Uint8Array | string, + desktopUploadItem: DesktopUploadItem, fileTypeInfo: FileTypeInfo, ): Promise => fileTypeInfo.fileType === FILE_TYPE.IMAGE ? await electron.generateImageThumbnail( - dataOrPath, + toDataOrPathOrZipEntry(desktopUploadItem), maxThumbnailDimension, maxThumbnailSize, ) - : ffmpeg.generateVideoThumbnailNative(electron, dataOrPath); + : ffmpeg.generateVideoThumbnailNative(electron, desktopUploadItem); /** * A fallback, black, thumbnail for use in cases where thumbnail generation diff --git a/web/apps/photos/src/services/upload/types.ts b/web/apps/photos/src/services/upload/types.ts index 25a79de1a2..05ad332d4a 100644 --- a/web/apps/photos/src/services/upload/types.ts +++ b/web/apps/photos/src/services/upload/types.ts @@ -29,3 +29,19 @@ import type { ZipItem } from "@/next/types/ipc"; * Also see: [Note: Reading a UploadItem]. */ export type UploadItem = File | FileAndPath | string | ZipItem; + +/** + * The of cases of {@link UploadItem} that apply when we're running in the + * context of our desktop app. + */ +export type DesktopUploadItem = Exclude; + +/** + * For each of cases of {@link UploadItem} that apply when we're running in the + * context of our desktop app, return a value that can be passed to + * {@link Electron} functions over IPC. + */ +export const toDataOrPathOrZipEntry = (desktopUploadItem: DesktopUploadItem) => + typeof desktopUploadItem == "string" || Array.isArray(desktopUploadItem) + ? desktopUploadItem + : desktopUploadItem.path; diff --git a/web/apps/photos/src/services/upload/uploadService.ts b/web/apps/photos/src/services/upload/uploadService.ts index 8c042ccaf3..6dc1bbd493 100644 --- a/web/apps/photos/src/services/upload/uploadService.ts +++ b/web/apps/photos/src/services/upload/uploadService.ts @@ -1012,14 +1012,12 @@ const withThumbnail = async ( fileTypeInfo.fileType == FILE_TYPE.IMAGE && moduleState.isNativeImageThumbnailGenerationNotAvailable; - // 1. Native thumbnail generation using file's path. - if (electron && !notAvailable) { + // 1. Native thumbnail generation using items's (effective) path. + if (electron && !notAvailable && !(uploadItem instanceof File)) { try { - // When running in the context of our desktop app, File paths will - // be absolute. See: [Note: File paths when running under Electron]. thumbnail = await generateThumbnailNative( electron, - uploadItem instanceof File ? uploadItem["path"] : uploadItem, + uploadItem, fileTypeInfo, ); } catch (e) { @@ -1051,12 +1049,14 @@ const withThumbnail = async ( // The fallback in this case involves reading the entire stream into // memory, and passing that data across the IPC boundary in a single // go (i.e. not in a streaming manner). This is risky for videos of - // unbounded sizes, plus that isn't the expected scenario. So - // instead of trying to cater for arbitrary exceptions, we only run - // this fallback to cover for the case where thumbnail generation - // was not available for an image file on Windows. If/when we add - // support of native thumbnailing on Windows too, this entire branch - // can be removed. + // unbounded sizes, plus we shouldn't even be getting here unless + // something went wrong. + // + // So instead of trying to cater for arbitrary exceptions, we only + // run this fallback to cover for the case where thumbnail + // generation was not available for an image file on Windows. + // If/when we add support of native thumbnailing on Windows too, + // this entire branch can be removed. if (fileTypeInfo.fileType == FILE_TYPE.IMAGE) { const data = await readEntireStream(fileStream.stream); From 4ed194f397fd4fdf0daf0238f93b08171f3528e5 Mon Sep 17 00:00:00 2001 From: daviddeepan Date: Tue, 30 Apr 2024 11:05:27 +0530 Subject: [PATCH 104/240] search-bar component added --- .../Collections/CollectionSelector/index.tsx | 38 ++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/web/apps/photos/src/components/Collections/CollectionSelector/index.tsx b/web/apps/photos/src/components/Collections/CollectionSelector/index.tsx index b9ea88639f..794c97606f 100644 --- a/web/apps/photos/src/components/Collections/CollectionSelector/index.tsx +++ b/web/apps/photos/src/components/Collections/CollectionSelector/index.tsx @@ -39,6 +39,7 @@ interface Props { collections: Collection[]; collectionSummaries: CollectionSummaries; } + function CollectionSelector({ attributes, collectionSummaries, @@ -46,7 +47,7 @@ function CollectionSelector({ ...props }: Props) { const appContext = useContext(AppContext); - + const [searchQuery, setSearchQuery] = useState(""); const [collectionsToShow, setCollectionsToShow] = useState< CollectionSummary[] >([]); @@ -81,6 +82,7 @@ function CollectionSelector({ return isMoveToAllowedCollection(type); } }) + .sort((a, b) => { return a.name.localeCompare(b.name); }) @@ -89,15 +91,24 @@ function CollectionSelector({ COLLECTION_SORT_ORDER.get(a.type) - COLLECTION_SORT_ORDER.get(b.type) ); - }); + }) + .filter((collection) => + collection.name + .toLowerCase() + .includes(searchQuery.toLowerCase()), + ); if (collectionsToShow.length === 0) { - props.onClose(); - attributes.showNextModal(); + if (searchQuery !== "") { + return console.log("No Albums with that name..."); + } else { + props.onClose(); + attributes.showNextModal(); + } } setCollectionsToShow(collectionsToShow); }; main(); - }, [collectionSummaries, attributes, props.open]); + }, [collectionSummaries, attributes, props.open, searchQuery]); if (!collectionsToShow?.length) { return <>; @@ -121,6 +132,12 @@ function CollectionSelector({ props.onClose(); }; + const handleSearchInputChange = (e) => { + if (e) { + setSearchQuery(e.target.value); + } else console.log("No collections to show....."); + }; + return ( + Date: Tue, 30 Apr 2024 11:05:53 +0530 Subject: [PATCH 105/240] connect --- .../PhotoViewer/ImageEditorOverlay/index.tsx | 2 +- .../photos/src/components/Upload/Uploader.tsx | 2 +- .../photos/src/services/upload/uploadService.ts | 16 ++++++++-------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/web/apps/photos/src/components/PhotoViewer/ImageEditorOverlay/index.tsx b/web/apps/photos/src/components/PhotoViewer/ImageEditorOverlay/index.tsx index 3c7b6a9cab..42edddbf11 100644 --- a/web/apps/photos/src/components/PhotoViewer/ImageEditorOverlay/index.tsx +++ b/web/apps/photos/src/components/PhotoViewer/ImageEditorOverlay/index.tsx @@ -507,7 +507,7 @@ const ImageEditorOverlay = (props: IProps) => { const editedFile = await getEditedFile(); const file = { - fileOrPath: editedFile, + uploadItem: editedFile, localID: 1, collectionID: props.file.collectionID, }; diff --git a/web/apps/photos/src/components/Upload/Uploader.tsx b/web/apps/photos/src/components/Upload/Uploader.tsx index f5cb19e8c1..eb7595050b 100644 --- a/web/apps/photos/src/components/Upload/Uploader.tsx +++ b/web/apps/photos/src/components/Upload/Uploader.tsx @@ -20,12 +20,12 @@ import { getPublicCollectionUploaderName, savePublicCollectionUploaderName, } from "services/publicCollectionService"; +import type { UploadItem } from "services/upload/types"; import type { InProgressUpload, SegregatedFinishedUploads, UploadCounter, UploadFileNames, - UploadItem, UploadItemWithCollection, } from "services/upload/uploadManager"; import uploadManager from "services/upload/uploadManager"; diff --git a/web/apps/photos/src/services/upload/uploadService.ts b/web/apps/photos/src/services/upload/uploadService.ts index 6dc1bbd493..7d33038842 100644 --- a/web/apps/photos/src/services/upload/uploadService.ts +++ b/web/apps/photos/src/services/upload/uploadService.ts @@ -57,8 +57,8 @@ import type { UploadableUploadItem } from "./uploadManager"; /** * A readable stream for a file, and its associated size and last modified time. * - * This is the in-memory representation of the `fileOrPath` type that we usually - * pass around. See: [Note: Reading a UploadItem] + * This is the in-memory representation of the {@link UploadItem} type that we + * usually pass around. See: [Note: Reading a UploadItem] */ interface FileStream { /** @@ -602,11 +602,11 @@ interface ReadAssetDetailsResult { const readAssetDetails = async ({ isLivePhoto, livePhotoAssets, - uploadItem: fileOrPath, + uploadItem, }: UploadAsset): Promise => isLivePhoto ? readLivePhotoDetails(livePhotoAssets) - : readImageOrVideoDetails(fileOrPath); + : readImageOrVideoDetails(uploadItem); const readLivePhotoDetails = async ({ image, video }: LivePhotoAssets) => { const img = await readImageOrVideoDetails(image); @@ -941,12 +941,12 @@ const readLivePhoto = async ( // the entire stream into memory and pass the resultant data. // // This is a reasonable behaviour since the videos corresponding to live - // photos are only a couple of seconds long (we have already done a - // pre-flight check to ensure their size is small in `areLivePhotoAssets`). + // photos are only a couple of seconds long (we've already done a pre-flight + // check during areLivePhotoAssets to ensure their size is small). const fileOrData = async (sd: FileStream | Uint8Array) => { - const _fs = async ({ file, stream }: FileStream) => + const fos = async ({ file, stream }: FileStream) => file ? file : await readEntireStream(stream); - return sd instanceof Uint8Array ? sd : _fs(sd); + return sd instanceof Uint8Array ? sd : fos(sd); }; return { From 8ee9b2be32734d0dda8e398867270262f3fcf625 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 11:32:25 +0530 Subject: [PATCH 106/240] Use only the currently uploaded items --- .../photos/src/components/Upload/Uploader.tsx | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/web/apps/photos/src/components/Upload/Uploader.tsx b/web/apps/photos/src/components/Upload/Uploader.tsx index eb7595050b..62d06971e3 100644 --- a/web/apps/photos/src/components/Upload/Uploader.tsx +++ b/web/apps/photos/src/components/Upload/Uploader.tsx @@ -284,12 +284,26 @@ export default function Uploader({ return; } - const files = [ - dragAndDropFiles, - fileSelectorFiles, - folderSelectorFiles, - fileSelectorZipFiles, - ].flat(); + let files: File[]; + + switch (pickedUploadType.current) { + case PICKED_UPLOAD_TYPE.FILES: + files = fileSelectorFiles; + break; + + case PICKED_UPLOAD_TYPE.FOLDERS: + files = folderSelectorFiles; + break; + + case PICKED_UPLOAD_TYPE.ZIPS: + files = fileSelectorZipFiles; + break; + + default: + files = dragAndDropFiles; + break; + } + if (electron) { desktopFilesAndZipItems(electron, files).then( ({ fileAndPaths, zipItems }) => { From ab95b4daeea46c1df323ae38fec7d2cc1ca74324 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 11:50:15 +0530 Subject: [PATCH 107/240] Inline --- .../photos/src/components/Upload/Uploader.tsx | 27 +++++++------------ 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/web/apps/photos/src/components/Upload/Uploader.tsx b/web/apps/photos/src/components/Upload/Uploader.tsx index 62d06971e3..f99b768ca9 100644 --- a/web/apps/photos/src/components/Upload/Uploader.tsx +++ b/web/apps/photos/src/components/Upload/Uploader.tsx @@ -209,6 +209,7 @@ export default function Uploader({ setChoiceModalView(false); uploadRunning.current = false; }; + const handleCollectionSelectorCancel = () => { uploadRunning.current = false; }; @@ -234,6 +235,7 @@ export default function Uploader({ publicCollectionGalleryContext, appContext.isCFProxyDisabled, ); + if (uploadManager.isUploadRunning()) { setUploadProgressView(true); } @@ -709,28 +711,19 @@ export default function Uploader({ } }; - const handleUploadToSingleCollection = () => { - uploadToSingleNewCollection(importSuggestion.rootFolderName); - }; - - const handleUploadToMultipleCollections = () => { - if (importSuggestion.hasRootLevelFileWithFolder) { - appContext.setDialogMessage( - getRootLevelFileWithFolderNotAllowMessage(), - ); - return; - } - uploadFilesToNewCollections("parent"); - }; - const didSelectCollectionMapping = (mapping: CollectionMapping) => { switch (mapping) { case "root": - handleUploadToSingleCollection(); + uploadToSingleNewCollection(importSuggestion.rootFolderName); break; case "parent": - handleUploadToMultipleCollections(); - break; + if (importSuggestion.hasRootLevelFileWithFolder) { + appContext.setDialogMessage( + getRootLevelFileWithFolderNotAllowMessage(), + ); + } else { + uploadFilesToNewCollections("parent"); + } } }; From e2cd1ea380299a593a20bc30d94a36069a4c5ef3 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 12:03:05 +0530 Subject: [PATCH 108/240] Fallback --- .../photos/src/components/Upload/Uploader.tsx | 22 +++++++++++++++---- .../next/locales/en-US/translation.json | 4 +++- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/web/apps/photos/src/components/Upload/Uploader.tsx b/web/apps/photos/src/components/Upload/Uploader.tsx index f99b768ca9..fdc6ee9329 100644 --- a/web/apps/photos/src/components/Upload/Uploader.tsx +++ b/web/apps/photos/src/components/Upload/Uploader.tsx @@ -51,8 +51,6 @@ import { CollectionMappingChoiceModal } from "./CollectionMappingChoiceModal"; import UploadProgress from "./UploadProgress"; import UploadTypeSelector from "./UploadTypeSelector"; -const FIRST_ALBUM_NAME = "My First Album"; - enum PICKED_UPLOAD_TYPE { FILES = "files", FOLDERS = "folders", @@ -415,7 +413,9 @@ export default function Uploader({ } if (isFirstUpload && !importSuggestion.rootFolderName) { - importSuggestion.rootFolderName = FIRST_ALBUM_NAME; + importSuggestion.rootFolderName = t( + "autogenerated_first_album_name", + ); } if (isDragAndDrop.current) { @@ -714,7 +714,21 @@ export default function Uploader({ const didSelectCollectionMapping = (mapping: CollectionMapping) => { switch (mapping) { case "root": - uploadToSingleNewCollection(importSuggestion.rootFolderName); + uploadToSingleNewCollection( + // rootFolderName would be empty here if one edge case: + // - User drags and drops a mixture of files and folders + // - They select the "upload to multiple albums" option + // - The see the error, close the error + // - Then they select the "upload to single album" option + // + // In such a flow, we'll reach here with an empty + // rootFolderName. The proper fix for this would be + // rearrange the flow and ask them to name the album here, + // but we currently don't have support for chaining modals. + // So in the meanwhile, keep a fallback album name at hand. + importSuggestion.rootFolderName ?? + t("autogenerated_default_album_name"), + ); break; case "parent": if (importSuggestion.hasRootLevelFileWithFolder) { diff --git a/web/packages/next/locales/en-US/translation.json b/web/packages/next/locales/en-US/translation.json index 5fdb380d5b..b3debe5aa0 100644 --- a/web/packages/next/locales/en-US/translation.json +++ b/web/packages/next/locales/en-US/translation.json @@ -621,5 +621,7 @@ "PASSKEY_LOGIN_ERRORED": "An error occurred while logging in with passkey.", "TRY_AGAIN": "Try again", "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "Follow the steps from your browser to continue logging in.", - "LOGIN_WITH_PASSKEY": "Login with passkey" + "LOGIN_WITH_PASSKEY": "Login with passkey", + "autogenerated_first_album_name": "My First Album", + "autogenerated_default_album_name": "New Album" } From 25fd5d542249f387129e115c6fb033d38ac6c1cf Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Tue, 30 Apr 2024 06:42:25 +0000 Subject: [PATCH 109/240] New Crowdin translations by GitHub Action --- web/packages/next/locales/bg-BG/translation.json | 4 +++- web/packages/next/locales/de-DE/translation.json | 4 +++- web/packages/next/locales/es-ES/translation.json | 4 +++- web/packages/next/locales/fa-IR/translation.json | 4 +++- web/packages/next/locales/fi-FI/translation.json | 4 +++- web/packages/next/locales/fr-FR/translation.json | 4 +++- web/packages/next/locales/it-IT/translation.json | 4 +++- web/packages/next/locales/ko-KR/translation.json | 4 +++- web/packages/next/locales/nl-NL/translation.json | 4 +++- web/packages/next/locales/pt-BR/translation.json | 4 +++- web/packages/next/locales/pt-PT/translation.json | 4 +++- web/packages/next/locales/ru-RU/translation.json | 4 +++- web/packages/next/locales/sv-SE/translation.json | 4 +++- web/packages/next/locales/th-TH/translation.json | 4 +++- web/packages/next/locales/tr-TR/translation.json | 4 +++- web/packages/next/locales/zh-CN/translation.json | 4 +++- 16 files changed, 48 insertions(+), 16 deletions(-) diff --git a/web/packages/next/locales/bg-BG/translation.json b/web/packages/next/locales/bg-BG/translation.json index 1661e8fac0..28689ba498 100644 --- a/web/packages/next/locales/bg-BG/translation.json +++ b/web/packages/next/locales/bg-BG/translation.json @@ -621,5 +621,7 @@ "PASSKEY_LOGIN_ERRORED": "", "TRY_AGAIN": "", "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", - "LOGIN_WITH_PASSKEY": "" + "LOGIN_WITH_PASSKEY": "", + "autogenerated_first_album_name": "", + "autogenerated_default_album_name": "" } diff --git a/web/packages/next/locales/de-DE/translation.json b/web/packages/next/locales/de-DE/translation.json index de7980f3e3..a0ee15a7c6 100644 --- a/web/packages/next/locales/de-DE/translation.json +++ b/web/packages/next/locales/de-DE/translation.json @@ -621,5 +621,7 @@ "PASSKEY_LOGIN_ERRORED": "Ein Fehler trat auf beim Anmelden mit dem Passkey auf.", "TRY_AGAIN": "Erneut versuchen", "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "Folge den Schritten in deinem Browser, um mit dem Anmelden fortzufahren.", - "LOGIN_WITH_PASSKEY": "Mit Passkey anmelden" + "LOGIN_WITH_PASSKEY": "Mit Passkey anmelden", + "autogenerated_first_album_name": "", + "autogenerated_default_album_name": "" } diff --git a/web/packages/next/locales/es-ES/translation.json b/web/packages/next/locales/es-ES/translation.json index 5435514573..a01d322b74 100644 --- a/web/packages/next/locales/es-ES/translation.json +++ b/web/packages/next/locales/es-ES/translation.json @@ -621,5 +621,7 @@ "PASSKEY_LOGIN_ERRORED": "", "TRY_AGAIN": "", "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", - "LOGIN_WITH_PASSKEY": "" + "LOGIN_WITH_PASSKEY": "", + "autogenerated_first_album_name": "", + "autogenerated_default_album_name": "" } diff --git a/web/packages/next/locales/fa-IR/translation.json b/web/packages/next/locales/fa-IR/translation.json index 9dc5ccb7a8..0c3749d135 100644 --- a/web/packages/next/locales/fa-IR/translation.json +++ b/web/packages/next/locales/fa-IR/translation.json @@ -621,5 +621,7 @@ "PASSKEY_LOGIN_ERRORED": "", "TRY_AGAIN": "", "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", - "LOGIN_WITH_PASSKEY": "" + "LOGIN_WITH_PASSKEY": "", + "autogenerated_first_album_name": "", + "autogenerated_default_album_name": "" } diff --git a/web/packages/next/locales/fi-FI/translation.json b/web/packages/next/locales/fi-FI/translation.json index 2d2a56b54c..d945fcde32 100644 --- a/web/packages/next/locales/fi-FI/translation.json +++ b/web/packages/next/locales/fi-FI/translation.json @@ -621,5 +621,7 @@ "PASSKEY_LOGIN_ERRORED": "", "TRY_AGAIN": "", "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", - "LOGIN_WITH_PASSKEY": "" + "LOGIN_WITH_PASSKEY": "", + "autogenerated_first_album_name": "", + "autogenerated_default_album_name": "" } diff --git a/web/packages/next/locales/fr-FR/translation.json b/web/packages/next/locales/fr-FR/translation.json index 308728b982..f3113202fd 100644 --- a/web/packages/next/locales/fr-FR/translation.json +++ b/web/packages/next/locales/fr-FR/translation.json @@ -621,5 +621,7 @@ "PASSKEY_LOGIN_ERRORED": "Une erreur s'est produite lors de la connexion avec le code d'accès.", "TRY_AGAIN": "Réessayer", "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "Suivez les étapes de votre navigateur pour poursuivre la connexion.", - "LOGIN_WITH_PASSKEY": "Se connecter avec le code d'accès" + "LOGIN_WITH_PASSKEY": "Se connecter avec le code d'accès", + "autogenerated_first_album_name": "", + "autogenerated_default_album_name": "" } diff --git a/web/packages/next/locales/it-IT/translation.json b/web/packages/next/locales/it-IT/translation.json index b66131ad71..bf555911c3 100644 --- a/web/packages/next/locales/it-IT/translation.json +++ b/web/packages/next/locales/it-IT/translation.json @@ -621,5 +621,7 @@ "PASSKEY_LOGIN_ERRORED": "", "TRY_AGAIN": "", "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", - "LOGIN_WITH_PASSKEY": "" + "LOGIN_WITH_PASSKEY": "", + "autogenerated_first_album_name": "", + "autogenerated_default_album_name": "" } diff --git a/web/packages/next/locales/ko-KR/translation.json b/web/packages/next/locales/ko-KR/translation.json index 63b6491def..aee2c6cd5b 100644 --- a/web/packages/next/locales/ko-KR/translation.json +++ b/web/packages/next/locales/ko-KR/translation.json @@ -621,5 +621,7 @@ "PASSKEY_LOGIN_ERRORED": "", "TRY_AGAIN": "", "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", - "LOGIN_WITH_PASSKEY": "" + "LOGIN_WITH_PASSKEY": "", + "autogenerated_first_album_name": "", + "autogenerated_default_album_name": "" } diff --git a/web/packages/next/locales/nl-NL/translation.json b/web/packages/next/locales/nl-NL/translation.json index c12a38f8b3..62b846b14e 100644 --- a/web/packages/next/locales/nl-NL/translation.json +++ b/web/packages/next/locales/nl-NL/translation.json @@ -621,5 +621,7 @@ "PASSKEY_LOGIN_ERRORED": "Er is een fout opgetreden tijdens het inloggen met een passkey.", "TRY_AGAIN": "Probeer opnieuw", "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "Volg de stappen van je browser om door te gaan met inloggen.", - "LOGIN_WITH_PASSKEY": "Inloggen met passkey" + "LOGIN_WITH_PASSKEY": "Inloggen met passkey", + "autogenerated_first_album_name": "", + "autogenerated_default_album_name": "" } diff --git a/web/packages/next/locales/pt-BR/translation.json b/web/packages/next/locales/pt-BR/translation.json index 5749591d17..dfe0030c56 100644 --- a/web/packages/next/locales/pt-BR/translation.json +++ b/web/packages/next/locales/pt-BR/translation.json @@ -621,5 +621,7 @@ "PASSKEY_LOGIN_ERRORED": "Ocorreu um erro ao entrar com a chave de acesso.", "TRY_AGAIN": "Tente novamente", "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "Siga os passos do seu navegador para continuar acessando.", - "LOGIN_WITH_PASSKEY": "Entrar com a chave de acesso" + "LOGIN_WITH_PASSKEY": "Entrar com a chave de acesso", + "autogenerated_first_album_name": "", + "autogenerated_default_album_name": "" } diff --git a/web/packages/next/locales/pt-PT/translation.json b/web/packages/next/locales/pt-PT/translation.json index 20ec4d9ea9..f6980b56e3 100644 --- a/web/packages/next/locales/pt-PT/translation.json +++ b/web/packages/next/locales/pt-PT/translation.json @@ -621,5 +621,7 @@ "PASSKEY_LOGIN_ERRORED": "", "TRY_AGAIN": "", "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", - "LOGIN_WITH_PASSKEY": "" + "LOGIN_WITH_PASSKEY": "", + "autogenerated_first_album_name": "", + "autogenerated_default_album_name": "" } diff --git a/web/packages/next/locales/ru-RU/translation.json b/web/packages/next/locales/ru-RU/translation.json index 95c4f6c58b..5d036c6c8a 100644 --- a/web/packages/next/locales/ru-RU/translation.json +++ b/web/packages/next/locales/ru-RU/translation.json @@ -621,5 +621,7 @@ "PASSKEY_LOGIN_ERRORED": "При входе в систему с помощью пароля произошла ошибка.", "TRY_AGAIN": "Пробовать снова", "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "Следуйте инструкциям в вашем браузере, чтобы продолжить вход в систему.", - "LOGIN_WITH_PASSKEY": "Войдите в систему с помощью пароля" + "LOGIN_WITH_PASSKEY": "Войдите в систему с помощью пароля", + "autogenerated_first_album_name": "", + "autogenerated_default_album_name": "" } diff --git a/web/packages/next/locales/sv-SE/translation.json b/web/packages/next/locales/sv-SE/translation.json index 77462524d5..ba6ecee097 100644 --- a/web/packages/next/locales/sv-SE/translation.json +++ b/web/packages/next/locales/sv-SE/translation.json @@ -621,5 +621,7 @@ "PASSKEY_LOGIN_ERRORED": "", "TRY_AGAIN": "", "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", - "LOGIN_WITH_PASSKEY": "" + "LOGIN_WITH_PASSKEY": "", + "autogenerated_first_album_name": "", + "autogenerated_default_album_name": "" } diff --git a/web/packages/next/locales/th-TH/translation.json b/web/packages/next/locales/th-TH/translation.json index 2d2a56b54c..d945fcde32 100644 --- a/web/packages/next/locales/th-TH/translation.json +++ b/web/packages/next/locales/th-TH/translation.json @@ -621,5 +621,7 @@ "PASSKEY_LOGIN_ERRORED": "", "TRY_AGAIN": "", "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", - "LOGIN_WITH_PASSKEY": "" + "LOGIN_WITH_PASSKEY": "", + "autogenerated_first_album_name": "", + "autogenerated_default_album_name": "" } diff --git a/web/packages/next/locales/tr-TR/translation.json b/web/packages/next/locales/tr-TR/translation.json index 2d2a56b54c..d945fcde32 100644 --- a/web/packages/next/locales/tr-TR/translation.json +++ b/web/packages/next/locales/tr-TR/translation.json @@ -621,5 +621,7 @@ "PASSKEY_LOGIN_ERRORED": "", "TRY_AGAIN": "", "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", - "LOGIN_WITH_PASSKEY": "" + "LOGIN_WITH_PASSKEY": "", + "autogenerated_first_album_name": "", + "autogenerated_default_album_name": "" } diff --git a/web/packages/next/locales/zh-CN/translation.json b/web/packages/next/locales/zh-CN/translation.json index 7a76b58b60..d2345f1ae7 100644 --- a/web/packages/next/locales/zh-CN/translation.json +++ b/web/packages/next/locales/zh-CN/translation.json @@ -621,5 +621,7 @@ "PASSKEY_LOGIN_ERRORED": "使用通行密钥登录时出错。", "TRY_AGAIN": "重试", "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "按照浏览器中提示的步骤继续登录。", - "LOGIN_WITH_PASSKEY": "使用通行密钥来登录" + "LOGIN_WITH_PASSKEY": "使用通行密钥来登录", + "autogenerated_first_album_name": "", + "autogenerated_default_album_name": "" } From 92a066d32212ae8324b090a784fa59ae8a23633b Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 12:31:24 +0530 Subject: [PATCH 110/240] Revert "search-bar component added" This reverts commit 4ed194f397fd4fdf0daf0238f93b08171f3528e5. Two main functional issues was: - When trying to upload files, if I would search something without results, the dialog would just close. - As a user, I expect to be able to move out of the search bar with escape. However, pressing escape closes the whole dialog, causing the user's drag and dropped or selected files to be lost. This is not specifically a problem with the code added in this commit, however this UX will need to be fixed in some way before introducing the search bar. One medium issue: - The dialog box kept changing size when I searched. This is visually jarring. Other minor issues: - The input element did not match the look and feel for the rest of the search input elements in the app. Aligning the search bar to the left and right of the album thumbnails and using the MUI Input element (as our code uses MUI elements in other places) might've fixed it. - There were various debugging console.logs left over, once of which even seemed like a case we need to handle. - The strings were not localized. --- .../Collections/CollectionSelector/index.tsx | 38 +++---------------- 1 file changed, 5 insertions(+), 33 deletions(-) diff --git a/web/apps/photos/src/components/Collections/CollectionSelector/index.tsx b/web/apps/photos/src/components/Collections/CollectionSelector/index.tsx index 794c97606f..b9ea88639f 100644 --- a/web/apps/photos/src/components/Collections/CollectionSelector/index.tsx +++ b/web/apps/photos/src/components/Collections/CollectionSelector/index.tsx @@ -39,7 +39,6 @@ interface Props { collections: Collection[]; collectionSummaries: CollectionSummaries; } - function CollectionSelector({ attributes, collectionSummaries, @@ -47,7 +46,7 @@ function CollectionSelector({ ...props }: Props) { const appContext = useContext(AppContext); - const [searchQuery, setSearchQuery] = useState(""); + const [collectionsToShow, setCollectionsToShow] = useState< CollectionSummary[] >([]); @@ -82,7 +81,6 @@ function CollectionSelector({ return isMoveToAllowedCollection(type); } }) - .sort((a, b) => { return a.name.localeCompare(b.name); }) @@ -91,24 +89,15 @@ function CollectionSelector({ COLLECTION_SORT_ORDER.get(a.type) - COLLECTION_SORT_ORDER.get(b.type) ); - }) - .filter((collection) => - collection.name - .toLowerCase() - .includes(searchQuery.toLowerCase()), - ); + }); if (collectionsToShow.length === 0) { - if (searchQuery !== "") { - return console.log("No Albums with that name..."); - } else { - props.onClose(); - attributes.showNextModal(); - } + props.onClose(); + attributes.showNextModal(); } setCollectionsToShow(collectionsToShow); }; main(); - }, [collectionSummaries, attributes, props.open, searchQuery]); + }, [collectionSummaries, attributes, props.open]); if (!collectionsToShow?.length) { return <>; @@ -132,12 +121,6 @@ function CollectionSelector({ props.onClose(); }; - const handleSearchInputChange = (e) => { - if (e) { - setSearchQuery(e.target.value); - } else console.log("No collections to show....."); - }; - return ( - Date: Tue, 30 Apr 2024 13:01:29 +0530 Subject: [PATCH 111/240] Remove uses of path --- .../photos/src/components/Upload/Uploader.tsx | 4 +- web/packages/shared/hooks/useFileInput.tsx | 41 ++++++------------- 2 files changed, 15 insertions(+), 30 deletions(-) diff --git a/web/apps/photos/src/components/Upload/Uploader.tsx b/web/apps/photos/src/components/Upload/Uploader.tsx index fdc6ee9329..90b5f94b4e 100644 --- a/web/apps/photos/src/components/Upload/Uploader.tsx +++ b/web/apps/photos/src/components/Upload/Uploader.tsx @@ -324,8 +324,8 @@ export default function Uploader({ // Trigger an upload when any of the dependencies change. useEffect(() => { const allItemAndPaths = [ - /* TODO(MR): ElectronFile | use webkitRelativePath || name here */ - webFiles.map((f) => [f, f["path"] ?? f.name]), + // See: [Note: webkitRelativePath] + webFiles.map((f) => [f, f.webkitRelativePath ?? f.name]), desktopFiles.map((fp) => [fp, fp.path]), desktopFilePaths.map((p) => [p, p]), desktopZipItems.map((ze) => [ze, ze[1]]), diff --git a/web/packages/shared/hooks/useFileInput.tsx b/web/packages/shared/hooks/useFileInput.tsx index 4eb346d39c..158a71b44e 100644 --- a/web/packages/shared/hooks/useFileInput.tsx +++ b/web/packages/shared/hooks/useFileInput.tsx @@ -72,19 +72,27 @@ export default function useFileInput({ event, ) => { if (!!event.target && !!event.target.files) { - const files = [...event.target.files].map((file) => - toFileWithPath(file), - ); - setSelectedFiles(files); + setSelectedFiles([...event.target.files]); } }; + // [Note: webkitRelativePath] + // + // If the webkitdirectory attribute of an HTML element is set then + // the File objects that we get will have `webkitRelativePath` property + // containing the relative path to the selected directory. + // + // https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/webkitdirectory + const directoryOpts = directory + ? { directory: "", webkitdirectory: "" } + : {}; + const getInputProps = useCallback( () => ({ type: "file", multiple: true, style: { display: "none" }, - ...(directory ? { directory: "", webkitdirectory: "" } : {}), + ...directoryOpts, ref: inputRef, onChange: handleChange, ...(accept ? { accept } : {}), @@ -98,26 +106,3 @@ export default function useFileInput({ selectedFiles: selectedFiles, }; } - -// https://github.com/react-dropzone/file-selector/blob/master/src/file.ts#L88 -export function toFileWithPath(file: File, path?: string): FileWithPath { - if (typeof (file as any).path !== "string") { - // on electron, path is already set to the absolute path - const { webkitRelativePath } = file; - Object.defineProperty(file, "path", { - value: - typeof path === "string" - ? path - : typeof webkitRelativePath === "string" && // If is set, - // the File will have a {webkitRelativePath} property - // https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/webkitdirectory - webkitRelativePath.length > 0 - ? webkitRelativePath - : file.name, - writable: false, - configurable: false, - enumerable: true, - }); - } - return file; -} From f84937f8c1dc38a83e0e4b342bf932a624a17f47 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 13:26:55 +0530 Subject: [PATCH 112/240] Bye ElectronFile --- desktop/src/main/dialogs.ts | 72 ---------- desktop/src/main/ipc.ts | 19 +-- desktop/src/main/services/dialog.ts | 10 ++ desktop/src/main/services/fs.ts | 154 --------------------- desktop/src/main/services/image.ts | 2 +- desktop/src/main/services/upload.ts | 53 +------ desktop/src/main/services/watch.ts | 8 +- desktop/src/main/utils-path.ts | 8 ++ desktop/src/main/utils-temp.ts | 2 +- desktop/src/preload.ts | 14 +- web/packages/next/types/ipc.ts | 30 ++-- web/packages/shared/hooks/useFileInput.tsx | 23 --- 12 files changed, 44 insertions(+), 351 deletions(-) delete mode 100644 desktop/src/main/dialogs.ts create mode 100644 desktop/src/main/services/dialog.ts delete mode 100644 desktop/src/main/services/fs.ts create mode 100644 desktop/src/main/utils-path.ts diff --git a/desktop/src/main/dialogs.ts b/desktop/src/main/dialogs.ts deleted file mode 100644 index f119e3d133..0000000000 --- a/desktop/src/main/dialogs.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { dialog } from "electron/main"; -import fs from "node:fs/promises"; -import path from "node:path"; -import type { ElectronFile } from "../types/ipc"; -import { getElectronFile } from "./services/fs"; -import { getElectronFilesFromGoogleZip } from "./services/upload"; - -export const selectDirectory = async () => { - const result = await dialog.showOpenDialog({ - properties: ["openDirectory"], - }); - if (result.filePaths && result.filePaths.length > 0) { - return result.filePaths[0]?.split(path.sep)?.join(path.posix.sep); - } -}; - -export const showUploadFilesDialog = async () => { - const selectedFiles = await dialog.showOpenDialog({ - properties: ["openFile", "multiSelections"], - }); - const filePaths = selectedFiles.filePaths; - return await Promise.all(filePaths.map(getElectronFile)); -}; - -export const showUploadDirsDialog = async () => { - const dir = await dialog.showOpenDialog({ - properties: ["openDirectory", "multiSelections"], - }); - - let filePaths: string[] = []; - for (const dirPath of dir.filePaths) { - filePaths = [...filePaths, ...(await getDirFilePaths(dirPath))]; - } - - return await Promise.all(filePaths.map(getElectronFile)); -}; - -// https://stackoverflow.com/a/63111390 -const getDirFilePaths = async (dirPath: string) => { - if (!(await fs.stat(dirPath)).isDirectory()) { - return [dirPath]; - } - - let files: string[] = []; - const filePaths = await fs.readdir(dirPath); - - for (const filePath of filePaths) { - const absolute = path.join(dirPath, filePath); - files = [...files, ...(await getDirFilePaths(absolute))]; - } - - return files; -}; - -export const showUploadZipDialog = async () => { - const selectedFiles = await dialog.showOpenDialog({ - properties: ["openFile", "multiSelections"], - filters: [{ name: "Zip File", extensions: ["zip"] }], - }); - const filePaths = selectedFiles.filePaths; - - let files: ElectronFile[] = []; - - for (const filePath of filePaths) { - files = [...files, ...(await getElectronFilesFromGoogleZip(filePath))]; - } - - return { - zipPaths: filePaths, - files, - }; -}; diff --git a/desktop/src/main/ipc.ts b/desktop/src/main/ipc.ts index df6ab7c8ea..bb5daeabac 100644 --- a/desktop/src/main/ipc.ts +++ b/desktop/src/main/ipc.ts @@ -16,12 +16,6 @@ import type { PendingUploads, ZipItem, } from "../types/ipc"; -import { - selectDirectory, - showUploadDirsDialog, - showUploadFilesDialog, - showUploadZipDialog, -} from "./dialogs"; import { fsExists, fsIsDir, @@ -39,6 +33,7 @@ import { updateAndRestart, updateOnNextRestart, } from "./services/app-update"; +import { selectDirectory } from "./services/dialog"; import { ffmpegExec } from "./services/ffmpeg"; import { convertToJPEG, generateImageThumbnail } from "./services/image"; import { @@ -102,6 +97,8 @@ export const attachIPCHandlers = () => { // See [Note: Catching exception during .send/.on] ipcMain.on("logToDisk", (_, message) => logToDisk(message)); + ipcMain.handle("selectDirectory", () => selectDirectory()); + ipcMain.on("clearStores", () => clearStores()); ipcMain.handle("saveEncryptionKey", (_, encryptionKey) => @@ -193,16 +190,6 @@ export const attachIPCHandlers = () => { faceEmbedding(input), ); - // - File selection - - ipcMain.handle("selectDirectory", () => selectDirectory()); - - ipcMain.handle("showUploadFilesDialog", () => showUploadFilesDialog()); - - ipcMain.handle("showUploadDirsDialog", () => showUploadDirsDialog()); - - ipcMain.handle("showUploadZipDialog", () => showUploadZipDialog()); - // - Upload ipcMain.handle("listZipItems", (_, zipPath: string) => diff --git a/desktop/src/main/services/dialog.ts b/desktop/src/main/services/dialog.ts new file mode 100644 index 0000000000..e98a6a9dd6 --- /dev/null +++ b/desktop/src/main/services/dialog.ts @@ -0,0 +1,10 @@ +import { dialog } from "electron/main"; +import { posixPath } from "../utils-path"; + +export const selectDirectory = async () => { + const result = await dialog.showOpenDialog({ + properties: ["openDirectory"], + }); + const dirPath = result.filePaths[0]; + return dirPath ? posixPath(dirPath) : undefined; +}; diff --git a/desktop/src/main/services/fs.ts b/desktop/src/main/services/fs.ts deleted file mode 100644 index 609fc82d7e..0000000000 --- a/desktop/src/main/services/fs.ts +++ /dev/null @@ -1,154 +0,0 @@ -import StreamZip from "node-stream-zip"; -import { existsSync } from "node:fs"; -import fs from "node:fs/promises"; -import path from "node:path"; -import { ElectronFile } from "../../types/ipc"; -import log from "../log"; - -const FILE_STREAM_CHUNK_SIZE: number = 4 * 1024 * 1024; - -const getFileStream = async (filePath: string) => { - const file = await fs.open(filePath, "r"); - let offset = 0; - const readableStream = new ReadableStream({ - async pull(controller) { - try { - const buff = new Uint8Array(FILE_STREAM_CHUNK_SIZE); - const bytesRead = (await file.read( - buff, - 0, - FILE_STREAM_CHUNK_SIZE, - offset, - )) as unknown as number; - offset += bytesRead; - if (bytesRead === 0) { - controller.close(); - await file.close(); - } else { - controller.enqueue(buff.slice(0, bytesRead)); - } - } catch (e) { - await file.close(); - } - }, - async cancel() { - await file.close(); - }, - }); - return readableStream; -}; - -export async function getElectronFile(filePath: string): Promise { - const fileStats = await fs.stat(filePath); - return { - path: filePath.split(path.sep).join(path.posix.sep), - name: path.basename(filePath), - size: fileStats.size, - lastModified: fileStats.mtime.valueOf(), - stream: async () => { - if (!existsSync(filePath)) { - throw new Error("electronFile does not exist"); - } - return await getFileStream(filePath); - }, - blob: async () => { - if (!existsSync(filePath)) { - throw new Error("electronFile does not exist"); - } - const blob = await fs.readFile(filePath); - return new Blob([new Uint8Array(blob)]); - }, - arrayBuffer: async () => { - if (!existsSync(filePath)) { - throw new Error("electronFile does not exist"); - } - const blob = await fs.readFile(filePath); - return new Uint8Array(blob); - }, - }; -} - -export const getZipFileStream = async ( - zip: StreamZip.StreamZipAsync, - filePath: string, -) => { - const stream = await zip.stream(filePath); - const done = { - current: false, - }; - const inProgress = { - current: false, - }; - // eslint-disable-next-line no-unused-vars - let resolveObj: (value?: any) => void = null; - // eslint-disable-next-line no-unused-vars - let rejectObj: (reason?: any) => void = null; - stream.on("readable", () => { - try { - if (resolveObj) { - inProgress.current = true; - const chunk = stream.read(FILE_STREAM_CHUNK_SIZE) as Buffer; - if (chunk) { - resolveObj(new Uint8Array(chunk)); - resolveObj = null; - } - inProgress.current = false; - } - } catch (e) { - rejectObj(e); - } - }); - stream.on("end", () => { - try { - done.current = true; - if (resolveObj && !inProgress.current) { - resolveObj(null); - resolveObj = null; - } - } catch (e) { - rejectObj(e); - } - }); - stream.on("error", (e) => { - try { - done.current = true; - if (rejectObj) { - rejectObj(e); - rejectObj = null; - } - } catch (e) { - rejectObj(e); - } - }); - - const readStreamData = async () => { - return new Promise((resolve, reject) => { - const chunk = stream.read(FILE_STREAM_CHUNK_SIZE) as Buffer; - - if (chunk || done.current) { - resolve(chunk); - } else { - resolveObj = resolve; - rejectObj = reject; - } - }); - }; - - const readableStream = new ReadableStream({ - async pull(controller) { - try { - const data = await readStreamData(); - - if (data) { - controller.enqueue(data); - } else { - controller.close(); - } - } catch (e) { - log.error("Failed to pull from readableStream", e); - controller.close(); - } - }, - }); - return readableStream; -}; diff --git a/desktop/src/main/services/image.ts b/desktop/src/main/services/image.ts index c48e87c5bf..273607c4bc 100644 --- a/desktop/src/main/services/image.ts +++ b/desktop/src/main/services/image.ts @@ -1,7 +1,7 @@ /** @file Image format conversions and thumbnail generation */ import fs from "node:fs/promises"; -import path from "path"; +import path from "node:path"; import { CustomErrorMessage, type ZipItem } from "../../types/ipc"; import log from "../log"; import { execAsync, isDev } from "../utils-electron"; diff --git a/desktop/src/main/services/upload.ts b/desktop/src/main/services/upload.ts index 9b24cc0ead..a1103a748b 100644 --- a/desktop/src/main/services/upload.ts +++ b/desktop/src/main/services/upload.ts @@ -1,10 +1,9 @@ import StreamZip from "node-stream-zip"; import fs from "node:fs/promises"; +import path from "node:path"; import { existsSync } from "original-fs"; -import path from "path"; -import type { ElectronFile, PendingUploads, ZipItem } from "../../types/ipc"; +import type { PendingUploads, ZipItem } from "../../types/ipc"; import { uploadStatusStore } from "../stores/upload-status"; -import { getZipFileStream } from "./fs"; export const listZipItems = async (zipPath: string): Promise => { const zip = new StreamZip.async({ file: zipPath }); @@ -99,51 +98,3 @@ export const markUploadedZipItems = async ( }; export const clearPendingUploads = () => uploadStatusStore.clear(); - -export const getElectronFilesFromGoogleZip = async (filePath: string) => { - const zip = new StreamZip.async({ - file: filePath, - }); - const zipName = path.basename(filePath, ".zip"); - - const entries = await zip.entries(); - const files: ElectronFile[] = []; - - for (const entry of Object.values(entries)) { - const basename = path.basename(entry.name); - if (entry.isFile && basename.length > 0 && basename[0] !== ".") { - files.push(await getZipEntryAsElectronFile(zipName, zip, entry)); - } - } - - zip.close(); - - return files; -}; - -export async function getZipEntryAsElectronFile( - zipName: string, - zip: StreamZip.StreamZipAsync, - entry: StreamZip.ZipEntry, -): Promise { - return { - path: path - .join(zipName, entry.name) - .split(path.sep) - .join(path.posix.sep), - name: path.basename(entry.name), - size: entry.size, - lastModified: entry.time, - stream: async () => { - return await getZipFileStream(zip, entry.name); - }, - blob: async () => { - const buffer = await zip.entryData(entry.name); - return new Blob([new Uint8Array(buffer)]); - }, - arrayBuffer: async () => { - const buffer = await zip.entryData(entry.name); - return new Uint8Array(buffer); - }, - }; -} diff --git a/desktop/src/main/services/watch.ts b/desktop/src/main/services/watch.ts index 73a13c5455..85463ae49e 100644 --- a/desktop/src/main/services/watch.ts +++ b/desktop/src/main/services/watch.ts @@ -6,6 +6,7 @@ import { FolderWatch, type CollectionMapping } from "../../types/ipc"; import { fsIsDir } from "../fs"; import log from "../log"; import { watchStore } from "../stores/watch"; +import { posixPath } from "../utils-path"; /** * Create and return a new file system watcher. @@ -46,13 +47,6 @@ const eventData = (path: string): [string, FolderWatch] => { return [path, watch]; }; -/** - * Convert a file system {@link filePath} that uses the local system specific - * path separators into a path that uses POSIX file separators. - */ -const posixPath = (filePath: string) => - filePath.split(path.sep).join(path.posix.sep); - export const watchGet = (watcher: FSWatcher) => { const [valid, deleted] = folderWatches().reduce( ([valid, deleted], watch) => { diff --git a/desktop/src/main/utils-path.ts b/desktop/src/main/utils-path.ts new file mode 100644 index 0000000000..b5e358e03b --- /dev/null +++ b/desktop/src/main/utils-path.ts @@ -0,0 +1,8 @@ +import path from "node:path"; + +/** + * Convert a file system {@link filePath} that uses the local system specific + * path separators into a path that uses POSIX file separators. + */ +export const posixPath = (filePath: string) => + filePath.split(path.sep).join(path.posix.sep); diff --git a/desktop/src/main/utils-temp.ts b/desktop/src/main/utils-temp.ts index 3f3a6081e4..5928931f2e 100644 --- a/desktop/src/main/utils-temp.ts +++ b/desktop/src/main/utils-temp.ts @@ -2,7 +2,7 @@ import { app } from "electron/main"; import StreamZip from "node-stream-zip"; import { existsSync } from "node:fs"; import fs from "node:fs/promises"; -import path from "path"; +import path from "node:path"; import type { ZipItem } from "../types/ipc"; /** diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index 61955b5240..52fe068e47 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -63,6 +63,9 @@ const openDirectory = (dirPath: string): Promise => const openLogDirectory = (): Promise => ipcRenderer.invoke("openLogDirectory"); +const selectDirectory = (): Promise => + ipcRenderer.invoke("selectDirectory"); + const clearStores = () => ipcRenderer.send("clearStores"); const encryptionKey = (): Promise => @@ -174,9 +177,6 @@ const faceEmbedding = (input: Float32Array): Promise => // TODO: Deprecated - use dialogs on the renderer process itself -const selectDirectory = (): Promise => - ipcRenderer.invoke("selectDirectory"); - const showUploadFilesDialog = (): Promise => ipcRenderer.invoke("showUploadFilesDialog"); @@ -310,6 +310,7 @@ contextBridge.exposeInMainWorld("electron", { logToDisk, openDirectory, openLogDirectory, + selectDirectory, clearStores, encryptionKey, saveEncryptionKey, @@ -348,13 +349,6 @@ contextBridge.exposeInMainWorld("electron", { detectFaces, faceEmbedding, - // - File selection - - selectDirectory, - showUploadFilesDialog, - showUploadDirsDialog, - showUploadZipDialog, - // - Watch watch: { diff --git a/web/packages/next/types/ipc.ts b/web/packages/next/types/ipc.ts index 173b12b17c..d97a7e5643 100644 --- a/web/packages/next/types/ipc.ts +++ b/web/packages/next/types/ipc.ts @@ -3,8 +3,6 @@ // // See [Note: types.ts <-> preload.ts <-> ipc.ts] -import type { ElectronFile } from "./file"; - /** * Extra APIs provided by our Node.js layer when our code is running inside our * desktop (Electron) app. @@ -51,6 +49,18 @@ export interface Electron { */ openLogDirectory: () => Promise; + /** + * Ask the user to select a directory on their local file system, and return + * it path. + * + * We don't strictly need IPC for this, we can use a hidden element + * and trigger its click for the same behaviour (as we do for the + * `useFileInput` hook that we use for uploads). However, it's a bit + * cumbersome, and we anyways will need to IPC to get back its full path, so + * it is just convenient to expose this direct method. + */ + selectDirectory: () => Promise; + /** * Clear any stored data. * @@ -122,6 +132,8 @@ export interface Electron { */ skipAppUpdate: (version: string) => void; + // - FS + /** * A subset of file system access APIs. * @@ -332,20 +344,6 @@ export interface Electron { */ faceEmbedding: (input: Float32Array) => Promise; - // - File selection - // TODO: Deprecated - use dialogs on the renderer process itself - - selectDirectory: () => Promise; - - showUploadFilesDialog: () => Promise; - - showUploadDirsDialog: () => Promise; - - showUploadZipDialog: () => Promise<{ - zipPaths: string[]; - files: ElectronFile[]; - }>; - // - Watch /** diff --git a/web/packages/shared/hooks/useFileInput.tsx b/web/packages/shared/hooks/useFileInput.tsx index 158a71b44e..ae1dfcab0a 100644 --- a/web/packages/shared/hooks/useFileInput.tsx +++ b/web/packages/shared/hooks/useFileInput.tsx @@ -1,28 +1,5 @@ import { useCallback, useRef, useState } from "react"; -/** - * [Note: File paths when running under Electron] - * - * We have access to the absolute path of the web {@link File} object when we - * are running in the context of our desktop app. - * - * https://www.electronjs.org/docs/latest/api/file-object - * - * This is in contrast to the `webkitRelativePath` that we get when we're - * running in the browser, which is the relative path to the directory that the - * user selected (or just the name of the file if the user selected or - * drag/dropped a single one). - * - * Note that this is a deprecated approach. From Electron docs: - * - * > Warning: The path property that Electron adds to the File interface is - * > deprecated and will be removed in a future Electron release. We recommend - * > you use `webUtils.getPathForFile` instead. - */ -export interface FileWithPath extends File { - readonly path?: string; -} - interface UseFileInputParams { directory?: boolean; accept?: string; From d6aeef85d6892a3d9775cc0783c68636ca626695 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 13:33:18 +0530 Subject: [PATCH 113/240] Rearrange --- desktop/src/main.ts | 2 +- desktop/src/main/ipc.ts | 25 +++++----- desktop/src/main/log.ts | 2 +- desktop/src/main/menu.ts | 2 +- desktop/src/main/services/dialog.ts | 10 ---- desktop/src/main/services/dir.ts | 48 +++++++++++++++++++ desktop/src/main/services/ffmpeg.ts | 4 +- desktop/src/main/{ => services}/fs.ts | 0 desktop/src/main/services/image.ts | 4 +- desktop/src/main/services/ml-clip.ts | 2 +- desktop/src/main/services/watch.ts | 4 +- .../{utils-electron.ts => utils/electron.ts} | 40 +--------------- desktop/src/main/{utils.ts => utils/index.ts} | 0 .../src/main/{utils-path.ts => utils/path.ts} | 0 .../src/main/{utils-temp.ts => utils/temp.ts} | 2 +- 15 files changed, 74 insertions(+), 71 deletions(-) delete mode 100644 desktop/src/main/services/dialog.ts create mode 100644 desktop/src/main/services/dir.ts rename desktop/src/main/{ => services}/fs.ts (100%) rename desktop/src/main/{utils-electron.ts => utils/electron.ts} (53%) rename desktop/src/main/{utils.ts => utils/index.ts} (100%) rename desktop/src/main/{utils-path.ts => utils/path.ts} (100%) rename desktop/src/main/{utils-temp.ts => utils/temp.ts} (98%) diff --git a/desktop/src/main.ts b/desktop/src/main.ts index 2774ec730c..4b6db7eac9 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -26,7 +26,7 @@ import { createWatcher } from "./main/services/watch"; import { userPreferences } from "./main/stores/user-preferences"; import { migrateLegacyWatchStoreIfNeeded } from "./main/stores/watch"; import { registerStreamProtocol } from "./main/stream"; -import { isDev } from "./main/utils-electron"; +import { isDev } from "./main/utils/electron"; /** * The URL where the renderer HTML is being served from. diff --git a/desktop/src/main/ipc.ts b/desktop/src/main/ipc.ts index bb5daeabac..eb8b6cdda8 100644 --- a/desktop/src/main/ipc.ts +++ b/desktop/src/main/ipc.ts @@ -16,6 +16,19 @@ import type { PendingUploads, ZipItem, } from "../types/ipc"; +import { logToDisk } from "./log"; +import { + appVersion, + skipAppUpdate, + updateAndRestart, + updateOnNextRestart, +} from "./services/app-update"; +import { + openDirectory, + openLogDirectory, + selectDirectory, +} from "./services/dir"; +import { ffmpegExec } from "./services/ffmpeg"; import { fsExists, fsIsDir, @@ -25,16 +38,7 @@ import { fsRm, fsRmdir, fsWriteFile, -} from "./fs"; -import { logToDisk } from "./log"; -import { - appVersion, - skipAppUpdate, - updateAndRestart, - updateOnNextRestart, -} from "./services/app-update"; -import { selectDirectory } from "./services/dialog"; -import { ffmpegExec } from "./services/ffmpeg"; +} from "./services/fs"; import { convertToJPEG, generateImageThumbnail } from "./services/image"; import { clipImageEmbedding, @@ -63,7 +67,6 @@ import { watchUpdateIgnoredFiles, watchUpdateSyncedFiles, } from "./services/watch"; -import { openDirectory, openLogDirectory } from "./utils-electron"; /** * Listen for IPC events sent/invoked by the renderer process, and route them to diff --git a/desktop/src/main/log.ts b/desktop/src/main/log.ts index 22ebb5300a..7fc25a94b0 100644 --- a/desktop/src/main/log.ts +++ b/desktop/src/main/log.ts @@ -1,6 +1,6 @@ import log from "electron-log"; import util from "node:util"; -import { isDev } from "./utils-electron"; +import { isDev } from "./utils/electron"; /** * Initialize logging in the main process. diff --git a/desktop/src/main/menu.ts b/desktop/src/main/menu.ts index 12b1ee17d3..990dd40e5d 100644 --- a/desktop/src/main/menu.ts +++ b/desktop/src/main/menu.ts @@ -9,7 +9,7 @@ import { allowWindowClose } from "../main"; import { forceCheckForAppUpdates } from "./services/app-update"; import autoLauncher from "./services/auto-launcher"; import { userPreferences } from "./stores/user-preferences"; -import { isDev, openLogDirectory } from "./utils-electron"; +import { isDev, openLogDirectory } from "./utils/electron"; /** Create and return the entries in the app's main menu bar */ export const createApplicationMenu = async (mainWindow: BrowserWindow) => { diff --git a/desktop/src/main/services/dialog.ts b/desktop/src/main/services/dialog.ts deleted file mode 100644 index e98a6a9dd6..0000000000 --- a/desktop/src/main/services/dialog.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { dialog } from "electron/main"; -import { posixPath } from "../utils-path"; - -export const selectDirectory = async () => { - const result = await dialog.showOpenDialog({ - properties: ["openDirectory"], - }); - const dirPath = result.filePaths[0]; - return dirPath ? posixPath(dirPath) : undefined; -}; diff --git a/desktop/src/main/services/dir.ts b/desktop/src/main/services/dir.ts new file mode 100644 index 0000000000..a6917fe27c --- /dev/null +++ b/desktop/src/main/services/dir.ts @@ -0,0 +1,48 @@ +import { shell } from "electron/common"; +import { app, dialog } from "electron/main"; +import path from "node:path"; +import { posixPath } from "../utils/path"; + +export const selectDirectory = async () => { + const result = await dialog.showOpenDialog({ + properties: ["openDirectory"], + }); + const dirPath = result.filePaths[0]; + return dirPath ? posixPath(dirPath) : undefined; +}; + +/** + * Open the given {@link dirPath} in the system's folder viewer. + * + * For example, on macOS this'll open {@link dirPath} in Finder. + */ +export const openDirectory = async (dirPath: string) => { + const res = await shell.openPath(path.normalize(dirPath)); + // shell.openPath resolves with a string containing the error message + // corresponding to the failure if a failure occurred, otherwise "". + if (res) throw new Error(`Failed to open directory ${dirPath}: res`); +}; + +/** + * Open the app's log directory in the system's folder viewer. + * + * @see {@link openDirectory} + */ +export const openLogDirectory = () => openDirectory(logDirectoryPath()); + +/** + * Return the path where the logs for the app are saved. + * + * [Note: Electron app paths] + * + * By default, these paths are at the following locations: + * + * - macOS: `~/Library/Application Support/ente` + * - Linux: `~/.config/ente` + * - Windows: `%APPDATA%`, e.g. `C:\Users\\AppData\Local\ente` + * - Windows: C:\Users\\AppData\Local\ + * + * https://www.electronjs.org/docs/latest/api/app + * + */ +const logDirectoryPath = () => app.getPath("logs"); diff --git a/desktop/src/main/services/ffmpeg.ts b/desktop/src/main/services/ffmpeg.ts index 35977409ae..78b7a9e9a3 100644 --- a/desktop/src/main/services/ffmpeg.ts +++ b/desktop/src/main/services/ffmpeg.ts @@ -3,12 +3,12 @@ import fs from "node:fs/promises"; import type { ZipItem } from "../../types/ipc"; import log from "../log"; import { withTimeout } from "../utils"; -import { execAsync } from "../utils-electron"; +import { execAsync } from "../utils/electron"; import { deleteTempFile, makeFileForDataOrPathOrZipItem, makeTempFilePath, -} from "../utils-temp"; +} from "../utils/temp"; /* Duplicated in the web app's code (used by the WASM FFmpeg implementation). */ const ffmpegPathPlaceholder = "FFMPEG"; diff --git a/desktop/src/main/fs.ts b/desktop/src/main/services/fs.ts similarity index 100% rename from desktop/src/main/fs.ts rename to desktop/src/main/services/fs.ts diff --git a/desktop/src/main/services/image.ts b/desktop/src/main/services/image.ts index 273607c4bc..957fe81200 100644 --- a/desktop/src/main/services/image.ts +++ b/desktop/src/main/services/image.ts @@ -4,12 +4,12 @@ import fs from "node:fs/promises"; import path from "node:path"; import { CustomErrorMessage, type ZipItem } from "../../types/ipc"; import log from "../log"; -import { execAsync, isDev } from "../utils-electron"; +import { execAsync, isDev } from "../utils/electron"; import { deleteTempFile, makeFileForDataOrPathOrZipItem, makeTempFilePath, -} from "../utils-temp"; +} from "../utils/temp"; export const convertToJPEG = async (imageData: Uint8Array) => { const inputFilePath = await makeTempFilePath(); diff --git a/desktop/src/main/services/ml-clip.ts b/desktop/src/main/services/ml-clip.ts index cdd2baab76..99e512aa61 100644 --- a/desktop/src/main/services/ml-clip.ts +++ b/desktop/src/main/services/ml-clip.ts @@ -11,7 +11,7 @@ import * as ort from "onnxruntime-node"; import Tokenizer from "../../thirdparty/clip-bpe-ts/mod"; import log from "../log"; import { writeStream } from "../stream"; -import { deleteTempFile, makeTempFilePath } from "../utils-temp"; +import { deleteTempFile, makeTempFilePath } from "../utils/temp"; import { makeCachedInferenceSession } from "./ml"; const cachedCLIPImageSession = makeCachedInferenceSession( diff --git a/desktop/src/main/services/watch.ts b/desktop/src/main/services/watch.ts index 85463ae49e..5e57df3e5e 100644 --- a/desktop/src/main/services/watch.ts +++ b/desktop/src/main/services/watch.ts @@ -3,10 +3,10 @@ import { BrowserWindow } from "electron/main"; import fs from "node:fs/promises"; import path from "node:path"; import { FolderWatch, type CollectionMapping } from "../../types/ipc"; -import { fsIsDir } from "../fs"; import log from "../log"; import { watchStore } from "../stores/watch"; -import { posixPath } from "../utils-path"; +import { posixPath } from "../utils/path"; +import { fsIsDir } from "./fs"; /** * Create and return a new file system watcher. diff --git a/desktop/src/main/utils-electron.ts b/desktop/src/main/utils/electron.ts similarity index 53% rename from desktop/src/main/utils-electron.ts rename to desktop/src/main/utils/electron.ts index e8a98f1dfe..97d05ea6d0 100644 --- a/desktop/src/main/utils-electron.ts +++ b/desktop/src/main/utils/electron.ts @@ -1,10 +1,8 @@ import shellescape from "any-shell-escape"; -import { shell } from "electron"; /* TODO(MR): Why is this not in /main? */ import { app } from "electron/main"; import { exec } from "node:child_process"; -import path from "node:path"; import { promisify } from "node:util"; -import log from "./log"; +import log from "../log"; /** `true` if the app is running in development mode. */ export const isDev = !app.isPackaged; @@ -41,39 +39,3 @@ export const execAsync = (command: string | string[]) => { }; const execAsync_ = promisify(exec); - -/** - * Open the given {@link dirPath} in the system's folder viewer. - * - * For example, on macOS this'll open {@link dirPath} in Finder. - */ -export const openDirectory = async (dirPath: string) => { - const res = await shell.openPath(path.normalize(dirPath)); - // shell.openPath resolves with a string containing the error message - // corresponding to the failure if a failure occurred, otherwise "". - if (res) throw new Error(`Failed to open directory ${dirPath}: res`); -}; - -/** - * Open the app's log directory in the system's folder viewer. - * - * @see {@link openDirectory} - */ -export const openLogDirectory = () => openDirectory(logDirectoryPath()); - -/** - * Return the path where the logs for the app are saved. - * - * [Note: Electron app paths] - * - * By default, these paths are at the following locations: - * - * - macOS: `~/Library/Application Support/ente` - * - Linux: `~/.config/ente` - * - Windows: `%APPDATA%`, e.g. `C:\Users\\AppData\Local\ente` - * - Windows: C:\Users\\AppData\Local\ - * - * https://www.electronjs.org/docs/latest/api/app - * - */ -const logDirectoryPath = () => app.getPath("logs"); diff --git a/desktop/src/main/utils.ts b/desktop/src/main/utils/index.ts similarity index 100% rename from desktop/src/main/utils.ts rename to desktop/src/main/utils/index.ts diff --git a/desktop/src/main/utils-path.ts b/desktop/src/main/utils/path.ts similarity index 100% rename from desktop/src/main/utils-path.ts rename to desktop/src/main/utils/path.ts diff --git a/desktop/src/main/utils-temp.ts b/desktop/src/main/utils/temp.ts similarity index 98% rename from desktop/src/main/utils-temp.ts rename to desktop/src/main/utils/temp.ts index 5928931f2e..28aea245d8 100644 --- a/desktop/src/main/utils-temp.ts +++ b/desktop/src/main/utils/temp.ts @@ -3,7 +3,7 @@ import StreamZip from "node-stream-zip"; import { existsSync } from "node:fs"; import fs from "node:fs/promises"; import path from "node:path"; -import type { ZipItem } from "../types/ipc"; +import type { ZipItem } from "../../types/ipc"; /** * Our very own directory within the system temp directory. Go crazy, but From 6c4adb112702e0fdbf6a0e33b85ea4a4fe0a9aa1 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 13:37:17 +0530 Subject: [PATCH 114/240] Housekeeping --- desktop/src/main.ts | 6 ------ desktop/src/main/init.ts | 21 --------------------- desktop/src/main/services/dir.ts | 2 +- desktop/src/main/services/watch.ts | 2 +- desktop/src/main/utils/electron.ts | 8 ++++++++ desktop/src/main/utils/index.ts | 2 +- desktop/src/main/utils/path.ts | 8 -------- 7 files changed, 11 insertions(+), 38 deletions(-) delete mode 100644 desktop/src/main/init.ts delete mode 100644 desktop/src/main/utils/path.ts diff --git a/desktop/src/main.ts b/desktop/src/main.ts index 4b6db7eac9..9fb9706047 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -401,12 +401,6 @@ const main = () => { setDownloadPath(mainWindow.webContents); allowExternalLinks(mainWindow.webContents); - // TODO(MR): Remove or resurrect - // The commit that introduced this header override had the message - // "fix cors issue for uploads". Not sure what that means, so disabling - // it for now to see why exactly this is required. - // addAllowOriginHeader(mainWindow); - // Start loading the renderer. mainWindow.loadURL(rendererURL); diff --git a/desktop/src/main/init.ts b/desktop/src/main/init.ts deleted file mode 100644 index d0aee17f8f..0000000000 --- a/desktop/src/main/init.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { BrowserWindow } from "electron"; - -export function addAllowOriginHeader(mainWindow: BrowserWindow) { - mainWindow.webContents.session.webRequest.onHeadersReceived( - (details, callback) => { - details.responseHeaders = lowerCaseHeaders(details.responseHeaders); - details.responseHeaders["access-control-allow-origin"] = ["*"]; - callback({ - responseHeaders: details.responseHeaders, - }); - }, - ); -} - -function lowerCaseHeaders(responseHeaders: Record) { - const headers: Record = {}; - for (const key of Object.keys(responseHeaders)) { - headers[key.toLowerCase()] = responseHeaders[key]; - } - return headers; -} diff --git a/desktop/src/main/services/dir.ts b/desktop/src/main/services/dir.ts index a6917fe27c..4e2a8c65e4 100644 --- a/desktop/src/main/services/dir.ts +++ b/desktop/src/main/services/dir.ts @@ -1,7 +1,7 @@ import { shell } from "electron/common"; import { app, dialog } from "electron/main"; import path from "node:path"; -import { posixPath } from "../utils/path"; +import { posixPath } from "../utils/electron"; export const selectDirectory = async () => { const result = await dialog.showOpenDialog({ diff --git a/desktop/src/main/services/watch.ts b/desktop/src/main/services/watch.ts index 5e57df3e5e..588279b70a 100644 --- a/desktop/src/main/services/watch.ts +++ b/desktop/src/main/services/watch.ts @@ -5,7 +5,7 @@ import path from "node:path"; import { FolderWatch, type CollectionMapping } from "../../types/ipc"; import log from "../log"; import { watchStore } from "../stores/watch"; -import { posixPath } from "../utils/path"; +import { posixPath } from "../utils/electron"; import { fsIsDir } from "./fs"; /** diff --git a/desktop/src/main/utils/electron.ts b/desktop/src/main/utils/electron.ts index 97d05ea6d0..d627ec5c46 100644 --- a/desktop/src/main/utils/electron.ts +++ b/desktop/src/main/utils/electron.ts @@ -1,12 +1,20 @@ import shellescape from "any-shell-escape"; import { app } from "electron/main"; import { exec } from "node:child_process"; +import path from "node:path"; import { promisify } from "node:util"; import log from "../log"; /** `true` if the app is running in development mode. */ export const isDev = !app.isPackaged; +/** + * Convert a file system {@link filePath} that uses the local system specific + * path separators into a path that uses POSIX file separators. + */ +export const posixPath = (filePath: string) => + filePath.split(path.sep).join(path.posix.sep); + /** * Run a shell command asynchronously. * diff --git a/desktop/src/main/utils/index.ts b/desktop/src/main/utils/index.ts index 132859a436..1ae35d55d3 100644 --- a/desktop/src/main/utils/index.ts +++ b/desktop/src/main/utils/index.ts @@ -1,5 +1,5 @@ /** - * @file grab bag of utitity functions. + * @file grab bag of utility functions. * * Many of these are verbatim copies of functions from web code since there * isn't currently a common package that both of them share. diff --git a/desktop/src/main/utils/path.ts b/desktop/src/main/utils/path.ts deleted file mode 100644 index b5e358e03b..0000000000 --- a/desktop/src/main/utils/path.ts +++ /dev/null @@ -1,8 +0,0 @@ -import path from "node:path"; - -/** - * Convert a file system {@link filePath} that uses the local system specific - * path separators into a path that uses POSIX file separators. - */ -export const posixPath = (filePath: string) => - filePath.split(path.sep).join(path.posix.sep); From b52c9f558fd686ea7521f34e984d2e9e9cc20e4a Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 13:39:58 +0530 Subject: [PATCH 115/240] Remove cache size overrides Need a bit more benchmarking or real world feedback to see if this is even something that is helping us. --- desktop/src/main.ts | 27 --------------------------- web/packages/next/blob-cache.ts | 2 -- 2 files changed, 29 deletions(-) diff --git a/desktop/src/main.ts b/desktop/src/main.ts index 9fb9706047..6f8881dd6e 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -141,30 +141,6 @@ const registerPrivilegedSchemes = () => { ]); }; -/** - * [Note: Increased disk cache for the desktop app] - * - * Set the "disk-cache-size" command line flag to ask the Chromium process to - * use a larger size for the caches that it keeps on disk. This allows us to use - * the web based caching mechanisms on both the web and the desktop app, just - * ask the embedded Chromium to be a bit more generous in disk usage when - * running as the desktop app. - * - * The size we provide is in bytes. - * https://www.electronjs.org/docs/latest/api/command-line-switches#--disk-cache-sizesize - * - * Note that increasing the disk cache size does not guarantee that Chromium - * will respect in verbatim, it uses its own heuristics atop this hint. - * https://superuser.com/questions/378991/what-is-chrome-default-cache-size-limit/1577693#1577693 - * - * See also: [Note: Caching files]. - */ -const increaseDiskCache = () => - app.commandLine.appendSwitch( - "disk-cache-size", - `${5 * 1024 * 1024 * 1024}`, // 5 GB - ); - /** * Create an return the {@link BrowserWindow} that will form our app's UI. * @@ -321,8 +297,6 @@ const setupTrayItem = (mainWindow: BrowserWindow) => { * Older versions of our app used to maintain a cache dir using the main * process. This has been deprecated in favor of using a normal web cache. * - * See [Note: Increased disk cache for the desktop app] - * * Delete the old cache dir if it exists. This code was added March 2024, and * can be removed after some time once most people have upgraded to newer * versions. @@ -375,7 +349,6 @@ const main = () => { // The order of the next two calls is important setupRendererServer(); registerPrivilegedSchemes(); - increaseDiskCache(); migrateLegacyWatchStoreIfNeeded(); app.on("second-instance", () => { diff --git a/web/packages/next/blob-cache.ts b/web/packages/next/blob-cache.ts index 0e092fed61..e6c3734df2 100644 --- a/web/packages/next/blob-cache.ts +++ b/web/packages/next/blob-cache.ts @@ -50,8 +50,6 @@ export type BlobCacheNamespace = (typeof blobCacheNames)[number]; * ([the WebKit bug](https://bugs.webkit.org/show_bug.cgi?id=231706)), so it's * not trivial to use this as a full on replacement of the Web Cache in the * browser. So for now we go with this split implementation. - * - * See also: [Note: Increased disk cache for the desktop app]. */ export interface BlobCache { /** From 4feefb9b8d497fa4a9072b4eec40950ed9776ca7 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 13:41:18 +0530 Subject: [PATCH 116/240] Fix comment --- desktop/src/main/log.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/desktop/src/main/log.ts b/desktop/src/main/log.ts index 7fc25a94b0..d2421da62e 100644 --- a/desktop/src/main/log.ts +++ b/desktop/src/main/log.ts @@ -7,9 +7,9 @@ import { isDev } from "./utils/electron"; * * This will set our underlying logger up to log to a file named `ente.log`, * - * - on Linux at ~/.config/ente/logs/main.log - * - on macOS at ~/Library/Logs/ente/main.log - * - on Windows at %USERPROFILE%\AppData\Roaming\ente\logs\main.log + * - on Linux at ~/.config/ente/logs/ente.log + * - on macOS at ~/Library/Logs/ente/ente.log + * - on Windows at %USERPROFILE%\AppData\Roaming\ente\logs\ente.log * * On dev builds, it will also log to the console. */ From 8400620488f47e4826877030b3979e94bc66e233 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 13:42:41 +0530 Subject: [PATCH 117/240] Gone from desktop --- desktop/src/preload.ts | 16 ---------------- desktop/src/types/ipc.ts | 22 ---------------------- 2 files changed, 38 deletions(-) diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index 52fe068e47..ecc800db3c 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -44,7 +44,6 @@ import { contextBridge, ipcRenderer, webUtils } from "electron/renderer"; import type { AppUpdate, CollectionMapping, - ElectronFile, FolderWatch, PendingUploads, ZipItem, @@ -173,21 +172,6 @@ const detectFaces = (input: Float32Array): Promise => const faceEmbedding = (input: Float32Array): Promise => ipcRenderer.invoke("faceEmbedding", input); -// - File selection - -// TODO: Deprecated - use dialogs on the renderer process itself - -const showUploadFilesDialog = (): Promise => - ipcRenderer.invoke("showUploadFilesDialog"); - -const showUploadDirsDialog = (): Promise => - ipcRenderer.invoke("showUploadDirsDialog"); - -const showUploadZipDialog = (): Promise<{ - zipPaths: string[]; - files: ElectronFile[]; -}> => ipcRenderer.invoke("showUploadZipDialog"); - // - Watch const watchGet = (): Promise => ipcRenderer.invoke("watchGet"); diff --git a/desktop/src/types/ipc.ts b/desktop/src/types/ipc.ts index 6e47b7a3a6..c02ed17260 100644 --- a/desktop/src/types/ipc.ts +++ b/desktop/src/types/ipc.ts @@ -42,25 +42,3 @@ export interface PendingUploads { export const CustomErrorMessage = { NotAvailable: "This feature in not available on the current OS/arch", }; - -/** - * Deprecated - Use File + webUtils.getPathForFile instead - * - * Electron used to augment the standard web - * [File](https://developer.mozilla.org/en-US/docs/Web/API/File) object with an - * additional `path` property. This is now deprecated, and will be removed in a - * future release. - * https://www.electronjs.org/docs/latest/api/file-object - * - * The alternative to the `path` property is to use `webUtils.getPathForFile` - * https://www.electronjs.org/docs/latest/api/web-utils - */ -export interface ElectronFile { - name: string; - path: string; - size: number; - lastModified: number; - stream: () => Promise>; - blob: () => Promise; - arrayBuffer: () => Promise; -} From 14348351a97120c7a897639042e0060ce32e8b56 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 13:44:46 +0530 Subject: [PATCH 118/240] Fix call of undefined --- desktop/src/main/utils/temp.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/desktop/src/main/utils/temp.ts b/desktop/src/main/utils/temp.ts index 28aea245d8..09071c1573 100644 --- a/desktop/src/main/utils/temp.ts +++ b/desktop/src/main/utils/temp.ts @@ -76,15 +76,14 @@ interface FileForDataOrPathOrZipItem { */ isFileTemporary: boolean; /** - * If set, this'll be a function that can be called to actually write the - * contents of the source `Uint8Array | string | ZipItem` into the file at - * {@link path}. + * A function that can be called to actually write the contents of the + * source `Uint8Array | string | ZipItem` into the file at {@link path}. * - * It will be undefined if the source is already a path since nothing needs - * to be written in that case. In the other two cases this function will - * write the data or zip item into the file at {@link path}. + * It will do nothing in the case when the source is already a path. In the + * other two cases this function will write the data or zip item into the + * file at {@link path}. */ - writeToTemporaryFile?: () => Promise; + writeToTemporaryFile: () => Promise; } /** @@ -101,7 +100,7 @@ export const makeFileForDataOrPathOrZipItem = async ( ): Promise => { let path: string; let isFileTemporary: boolean; - let writeToTemporaryFile: () => Promise | undefined; + let writeToTemporaryFile = async () => {}; if (typeof dataOrPathOrZipItem == "string") { path = dataOrPathOrZipItem; From 333f9c58f25ec772eba6159666bdbe312454179a Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 13:56:11 +0530 Subject: [PATCH 119/240] strict 1 --- desktop/src/main.ts | 2 +- desktop/src/main/log.ts | 2 +- desktop/src/main/menu.ts | 3 +- desktop/src/main/services/dir.ts | 2 +- desktop/src/main/services/ffmpeg.ts | 4 +- desktop/src/main/services/image.ts | 2 +- desktop/src/main/services/ml-clip.ts | 7 ++- desktop/src/main/services/watch.ts | 2 +- desktop/src/main/stream.ts | 5 +- desktop/src/main/utils/common.ts | 35 ++++++++++++++ desktop/src/main/utils/electron.ts | 49 ------------------- desktop/src/main/utils/index.ts | 70 +++++++++++++++++----------- desktop/tsconfig.json | 6 +-- 13 files changed, 96 insertions(+), 93 deletions(-) create mode 100644 desktop/src/main/utils/common.ts delete mode 100644 desktop/src/main/utils/electron.ts diff --git a/desktop/src/main.ts b/desktop/src/main.ts index 6f8881dd6e..42c5ab7329 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -26,7 +26,7 @@ import { createWatcher } from "./main/services/watch"; import { userPreferences } from "./main/stores/user-preferences"; import { migrateLegacyWatchStoreIfNeeded } from "./main/stores/watch"; import { registerStreamProtocol } from "./main/stream"; -import { isDev } from "./main/utils/electron"; +import { isDev } from "./main/utils"; /** * The URL where the renderer HTML is being served from. diff --git a/desktop/src/main/log.ts b/desktop/src/main/log.ts index d2421da62e..c1902d8ebd 100644 --- a/desktop/src/main/log.ts +++ b/desktop/src/main/log.ts @@ -1,6 +1,6 @@ import log from "electron-log"; import util from "node:util"; -import { isDev } from "./utils/electron"; +import { isDev } from "./utils"; /** * Initialize logging in the main process. diff --git a/desktop/src/main/menu.ts b/desktop/src/main/menu.ts index 990dd40e5d..0693c01dc0 100644 --- a/desktop/src/main/menu.ts +++ b/desktop/src/main/menu.ts @@ -8,8 +8,9 @@ import { import { allowWindowClose } from "../main"; import { forceCheckForAppUpdates } from "./services/app-update"; import autoLauncher from "./services/auto-launcher"; +import { openLogDirectory } from "./services/dir"; import { userPreferences } from "./stores/user-preferences"; -import { isDev, openLogDirectory } from "./utils/electron"; +import { isDev } from "./utils"; /** Create and return the entries in the app's main menu bar */ export const createApplicationMenu = async (mainWindow: BrowserWindow) => { diff --git a/desktop/src/main/services/dir.ts b/desktop/src/main/services/dir.ts index 4e2a8c65e4..ef3adb013d 100644 --- a/desktop/src/main/services/dir.ts +++ b/desktop/src/main/services/dir.ts @@ -1,7 +1,7 @@ import { shell } from "electron/common"; import { app, dialog } from "electron/main"; import path from "node:path"; -import { posixPath } from "../utils/electron"; +import { posixPath } from "../utils"; export const selectDirectory = async () => { const result = await dialog.showOpenDialog({ diff --git a/desktop/src/main/services/ffmpeg.ts b/desktop/src/main/services/ffmpeg.ts index 78b7a9e9a3..dc417c5952 100644 --- a/desktop/src/main/services/ffmpeg.ts +++ b/desktop/src/main/services/ffmpeg.ts @@ -2,8 +2,8 @@ import pathToFfmpeg from "ffmpeg-static"; import fs from "node:fs/promises"; import type { ZipItem } from "../../types/ipc"; import log from "../log"; -import { withTimeout } from "../utils"; -import { execAsync } from "../utils/electron"; +import { execAsync } from "../utils"; +import { withTimeout } from "../utils/common"; import { deleteTempFile, makeFileForDataOrPathOrZipItem, diff --git a/desktop/src/main/services/image.ts b/desktop/src/main/services/image.ts index 957fe81200..d607b0ead3 100644 --- a/desktop/src/main/services/image.ts +++ b/desktop/src/main/services/image.ts @@ -4,7 +4,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { CustomErrorMessage, type ZipItem } from "../../types/ipc"; import log from "../log"; -import { execAsync, isDev } from "../utils/electron"; +import { execAsync, isDev } from "../utils"; import { deleteTempFile, makeFileForDataOrPathOrZipItem, diff --git a/desktop/src/main/services/ml-clip.ts b/desktop/src/main/services/ml-clip.ts index 99e512aa61..67c6d2db76 100644 --- a/desktop/src/main/services/ml-clip.ts +++ b/desktop/src/main/services/ml-clip.ts @@ -22,6 +22,7 @@ const cachedCLIPImageSession = makeCachedInferenceSession( export const clipImageEmbedding = async (jpegImageData: Uint8Array) => { const tempFilePath = await makeTempFilePath(); const imageStream = new Response(jpegImageData.buffer).body; + if (!imageStream) throw new Error("Missing body that we just fed data to"); await writeStream(tempFilePath, imageStream); try { return await clipImageEmbedding_(tempFilePath); @@ -134,11 +135,9 @@ const cachedCLIPTextSession = makeCachedInferenceSession( 64173509 /* 61.2 MB */, ); -let _tokenizer: Tokenizer = null; +let _tokenizer: Tokenizer | undefined; const getTokenizer = () => { - if (!_tokenizer) { - _tokenizer = new Tokenizer(); - } + if (!_tokenizer) _tokenizer = new Tokenizer(); return _tokenizer; }; diff --git a/desktop/src/main/services/watch.ts b/desktop/src/main/services/watch.ts index 588279b70a..4d7b89e460 100644 --- a/desktop/src/main/services/watch.ts +++ b/desktop/src/main/services/watch.ts @@ -5,7 +5,7 @@ import path from "node:path"; import { FolderWatch, type CollectionMapping } from "../../types/ipc"; import log from "../log"; import { watchStore } from "../stores/watch"; -import { posixPath } from "../utils/electron"; +import { posixPath } from "../utils"; import { fsIsDir } from "./fs"; /** diff --git a/desktop/src/main/stream.ts b/desktop/src/main/stream.ts index b37970cfae..be84c022f5 100644 --- a/desktop/src/main/stream.ts +++ b/desktop/src/main/stream.ts @@ -99,7 +99,10 @@ const handleReadZip = async (zipPath: string, entryName: string) => { try { const zip = new StreamZip.async({ file: zipPath }); const entry = await zip.entry(entryName); + if (!entry) return new Response("", { status: 404 }); + const stream = await zip.stream(entry); + // TODO(MR): when to call zip.close() return new Response(Readable.toWeb(new Readable(stream)), { @@ -122,7 +125,7 @@ const handleReadZip = async (zipPath: string, entryName: string) => { `Failed to read entry ${entryName} from zip file at ${zipPath}`, e, ); - return new Response(`Failed to read stream: ${e.message}`, { + return new Response(`Failed to read stream: ${String(e)}`, { status: 500, }); } diff --git a/desktop/src/main/utils/common.ts b/desktop/src/main/utils/common.ts new file mode 100644 index 0000000000..100a8ad2d2 --- /dev/null +++ b/desktop/src/main/utils/common.ts @@ -0,0 +1,35 @@ +/** + * @file grab bag of utility functions. + * + * These are verbatim copies of functions from web code since there isn't + * currently a common package that both of them share. + */ + +/** + * Wait for {@link ms} milliseconds + * + * This function is a promisified `setTimeout`. It returns a promise that + * resolves after {@link ms} milliseconds. + */ +export const wait = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)); + +/** + * Await the given {@link promise} for {@link timeoutMS} milliseconds. If it + * does not resolve within {@link timeoutMS}, then reject with a timeout error. + */ +export const withTimeout = async (promise: Promise, ms: number) => { + let timeoutId: ReturnType; + const rejectOnTimeout = new Promise((_, reject) => { + timeoutId = setTimeout( + () => reject(new Error("Operation timed out")), + ms, + ); + }); + const promiseAndCancelTimeout = async () => { + const result = await promise; + clearTimeout(timeoutId); + return result; + }; + return Promise.race([promiseAndCancelTimeout(), rejectOnTimeout]); +}; diff --git a/desktop/src/main/utils/electron.ts b/desktop/src/main/utils/electron.ts deleted file mode 100644 index d627ec5c46..0000000000 --- a/desktop/src/main/utils/electron.ts +++ /dev/null @@ -1,49 +0,0 @@ -import shellescape from "any-shell-escape"; -import { app } from "electron/main"; -import { exec } from "node:child_process"; -import path from "node:path"; -import { promisify } from "node:util"; -import log from "../log"; - -/** `true` if the app is running in development mode. */ -export const isDev = !app.isPackaged; - -/** - * Convert a file system {@link filePath} that uses the local system specific - * path separators into a path that uses POSIX file separators. - */ -export const posixPath = (filePath: string) => - filePath.split(path.sep).join(path.posix.sep); - -/** - * Run a shell command asynchronously. - * - * This is a convenience promisified version of child_process.exec. It runs the - * command asynchronously and returns its stdout and stderr if there were no - * errors. - * - * If the command is passed as a string, then it will be executed verbatim. - * - * If the command is passed as an array, then the first argument will be treated - * as the executable and the remaining (optional) items as the command line - * parameters. This function will shellescape and join the array to form the - * command that finally gets executed. - * - * > Note: This is not a 1-1 replacement of child_process.exec - if you're - * > trying to run a trivial shell command, say something that produces a lot of - * > output, this might not be the best option and it might be better to use the - * > underlying functions. - */ -export const execAsync = (command: string | string[]) => { - const escapedCommand = Array.isArray(command) - ? shellescape(command) - : command; - const startTime = Date.now(); - const result = execAsync_(escapedCommand); - log.debug( - () => `${escapedCommand} (${Math.round(Date.now() - startTime)} ms)`, - ); - return result; -}; - -const execAsync_ = promisify(exec); diff --git a/desktop/src/main/utils/index.ts b/desktop/src/main/utils/index.ts index 1ae35d55d3..d627ec5c46 100644 --- a/desktop/src/main/utils/index.ts +++ b/desktop/src/main/utils/index.ts @@ -1,35 +1,49 @@ -/** - * @file grab bag of utility functions. - * - * Many of these are verbatim copies of functions from web code since there - * isn't currently a common package that both of them share. - */ +import shellescape from "any-shell-escape"; +import { app } from "electron/main"; +import { exec } from "node:child_process"; +import path from "node:path"; +import { promisify } from "node:util"; +import log from "../log"; + +/** `true` if the app is running in development mode. */ +export const isDev = !app.isPackaged; /** - * Wait for {@link ms} milliseconds - * - * This function is a promisified `setTimeout`. It returns a promise that - * resolves after {@link ms} milliseconds. + * Convert a file system {@link filePath} that uses the local system specific + * path separators into a path that uses POSIX file separators. */ -export const wait = (ms: number) => - new Promise((resolve) => setTimeout(resolve, ms)); +export const posixPath = (filePath: string) => + filePath.split(path.sep).join(path.posix.sep); /** - * Await the given {@link promise} for {@link timeoutMS} milliseconds. If it - * does not resolve within {@link timeoutMS}, then reject with a timeout error. + * Run a shell command asynchronously. + * + * This is a convenience promisified version of child_process.exec. It runs the + * command asynchronously and returns its stdout and stderr if there were no + * errors. + * + * If the command is passed as a string, then it will be executed verbatim. + * + * If the command is passed as an array, then the first argument will be treated + * as the executable and the remaining (optional) items as the command line + * parameters. This function will shellescape and join the array to form the + * command that finally gets executed. + * + * > Note: This is not a 1-1 replacement of child_process.exec - if you're + * > trying to run a trivial shell command, say something that produces a lot of + * > output, this might not be the best option and it might be better to use the + * > underlying functions. */ -export const withTimeout = async (promise: Promise, ms: number) => { - let timeoutId: ReturnType; - const rejectOnTimeout = new Promise((_, reject) => { - timeoutId = setTimeout( - () => reject(new Error("Operation timed out")), - ms, - ); - }); - const promiseAndCancelTimeout = async () => { - const result = await promise; - clearTimeout(timeoutId); - return result; - }; - return Promise.race([promiseAndCancelTimeout(), rejectOnTimeout]); +export const execAsync = (command: string | string[]) => { + const escapedCommand = Array.isArray(command) + ? shellescape(command) + : command; + const startTime = Date.now(); + const result = execAsync_(escapedCommand); + log.debug( + () => `${escapedCommand} (${Math.round(Date.now() - startTime)} ms)`, + ); + return result; }; + +const execAsync_ = promisify(exec); diff --git a/desktop/tsconfig.json b/desktop/tsconfig.json index 700ea3fa00..16946bf3fe 100644 --- a/desktop/tsconfig.json +++ b/desktop/tsconfig.json @@ -50,12 +50,12 @@ "outDir": "app", /* Temporary overrides to get things to compile with the older config */ - "strict": false, - "noImplicitAny": true + // "strict": false, + "noImplicitAny": true, /* Below is the state we want */ /* Enable these one by one */ - // "strict": true, + "strict": true, /* Require the `type` modifier when importing types */ // "verbatimModuleSyntax": true From 72b9113d3045c74740d8310b84cf412ab28ca482 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 13:58:15 +0530 Subject: [PATCH 120/240] ensure --- desktop/src/main/services/ffmpeg.ts | 4 ++-- desktop/src/main/services/fs.ts | 1 + desktop/src/main/services/ml-clip.ts | 4 ++-- desktop/src/main/utils/common.ts | 9 +++++++++ web/packages/utils/ensure.ts | 5 +++-- 5 files changed, 17 insertions(+), 6 deletions(-) diff --git a/desktop/src/main/services/ffmpeg.ts b/desktop/src/main/services/ffmpeg.ts index dc417c5952..850b70d445 100644 --- a/desktop/src/main/services/ffmpeg.ts +++ b/desktop/src/main/services/ffmpeg.ts @@ -3,7 +3,7 @@ import fs from "node:fs/promises"; import type { ZipItem } from "../../types/ipc"; import log from "../log"; import { execAsync } from "../utils"; -import { withTimeout } from "../utils/common"; +import { ensure, withTimeout } from "../utils/common"; import { deleteTempFile, makeFileForDataOrPathOrZipItem, @@ -110,5 +110,5 @@ const ffmpegBinaryPath = () => { // This substitution of app.asar by app.asar.unpacked is suggested by the // ffmpeg-static library author themselves: // https://github.com/eugeneware/ffmpeg-static/issues/16 - return pathToFfmpeg.replace("app.asar", "app.asar.unpacked"); + return ensure(pathToFfmpeg).replace("app.asar", "app.asar.unpacked"); }; diff --git a/desktop/src/main/services/fs.ts b/desktop/src/main/services/fs.ts index 2428d3a80c..4570a4a33a 100644 --- a/desktop/src/main/services/fs.ts +++ b/desktop/src/main/services/fs.ts @@ -1,6 +1,7 @@ /** * @file file system related functions exposed over the context bridge. */ + import { existsSync } from "node:fs"; import fs from "node:fs/promises"; diff --git a/desktop/src/main/services/ml-clip.ts b/desktop/src/main/services/ml-clip.ts index 67c6d2db76..149f634b55 100644 --- a/desktop/src/main/services/ml-clip.ts +++ b/desktop/src/main/services/ml-clip.ts @@ -11,6 +11,7 @@ import * as ort from "onnxruntime-node"; import Tokenizer from "../../thirdparty/clip-bpe-ts/mod"; import log from "../log"; import { writeStream } from "../stream"; +import { ensure } from "../utils/common"; import { deleteTempFile, makeTempFilePath } from "../utils/temp"; import { makeCachedInferenceSession } from "./ml"; @@ -22,8 +23,7 @@ const cachedCLIPImageSession = makeCachedInferenceSession( export const clipImageEmbedding = async (jpegImageData: Uint8Array) => { const tempFilePath = await makeTempFilePath(); const imageStream = new Response(jpegImageData.buffer).body; - if (!imageStream) throw new Error("Missing body that we just fed data to"); - await writeStream(tempFilePath, imageStream); + await writeStream(tempFilePath, ensure(imageStream)); try { return await clipImageEmbedding_(tempFilePath); } finally { diff --git a/desktop/src/main/utils/common.ts b/desktop/src/main/utils/common.ts index 100a8ad2d2..1f5016e617 100644 --- a/desktop/src/main/utils/common.ts +++ b/desktop/src/main/utils/common.ts @@ -5,6 +5,15 @@ * currently a common package that both of them share. */ +/** + * Throw an exception if the given value is `null` or `undefined`. + */ +export const ensure = (v: T | null | undefined): T => { + if (v === null) throw new Error("Required value was null"); + if (v === undefined) throw new Error("Required value was not found"); + return v; +}; + /** * Wait for {@link ms} milliseconds * diff --git a/web/packages/utils/ensure.ts b/web/packages/utils/ensure.ts index 761cedc992..93706bfb61 100644 --- a/web/packages/utils/ensure.ts +++ b/web/packages/utils/ensure.ts @@ -1,7 +1,8 @@ /** - * Throw an exception if the given value is undefined. + * Throw an exception if the given value is `null` or `undefined`. */ -export const ensure = (v: T | undefined): T => { +export const ensure = (v: T | null | undefined): T => { + if (v === null) throw new Error("Required value was null"); if (v === undefined) throw new Error("Required value was not found"); return v; }; From bee2cd533ce0fece2d221fa9cbdacb55634124f4 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 14:01:28 +0530 Subject: [PATCH 121/240] strict 2 --- desktop/src/main/services/ml.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/desktop/src/main/services/ml.ts b/desktop/src/main/services/ml.ts index 8292596a22..6b38bc74dc 100644 --- a/desktop/src/main/services/ml.ts +++ b/desktop/src/main/services/ml.ts @@ -34,6 +34,7 @@ import { writeStream } from "../stream"; * actively trigger a download until the returned function is called. * * @param modelName The name of the model to download. + * * @param modelByteSize The size in bytes that we expect the model to have. If * the size of the downloaded model does not match the expected size, then we * will redownload it. @@ -99,13 +100,15 @@ const downloadModel = async (saveLocation: string, name: string) => { // `mkdir -p` the directory where we want to save the model. const saveDir = path.dirname(saveLocation); await fs.mkdir(saveDir, { recursive: true }); - // Download + // Download. log.info(`Downloading ML model from ${name}`); const url = `https://models.ente.io/${name}`; const res = await net.fetch(url); if (!res.ok) throw new Error(`Failed to fetch ${url}: HTTP ${res.status}`); - // Save - await writeStream(saveLocation, res.body); + const body = res.body; + if (!body) throw new Error(`Received an null response for ${url}`); + // Save. + await writeStream(saveLocation, body); log.info(`Downloaded CLIP model ${name}`); }; @@ -114,9 +117,9 @@ const downloadModel = async (saveLocation: string, name: string) => { */ const createInferenceSession = async (modelPath: string) => { return await ort.InferenceSession.create(modelPath, { - // Restrict the number of threads to 1 + // Restrict the number of threads to 1. intraOpNumThreads: 1, - // Be more conservative with RAM usage + // Be more conservative with RAM usage. enableCpuMemArena: false, }); }; From 0c312c0ea15d1cfc60f68b93008c619fdc32ba02 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 14:10:59 +0530 Subject: [PATCH 122/240] strict 3 --- desktop/src/main/services/upload.ts | 10 +++++++--- desktop/src/types/ipc.ts | 2 +- web/apps/photos/src/components/Upload/Uploader.tsx | 6 +----- web/packages/next/types/ipc.ts | 4 +++- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/desktop/src/main/services/upload.ts b/desktop/src/main/services/upload.ts index a1103a748b..c3efbb9f40 100644 --- a/desktop/src/main/services/upload.ts +++ b/desktop/src/main/services/upload.ts @@ -35,6 +35,10 @@ export const pathOrZipItemSize = async ( const [zipPath, entryName] = pathOrZipItem; const zip = new StreamZip.async({ file: zipPath }); const entry = await zip.entry(entryName); + if (!entry) + throw new Error( + `An entry with name ${entryName} does not exist in the zip file at ${zipPath}`, + ); const size = entry.size; zip.close(); return size; @@ -60,7 +64,7 @@ export const pendingUploads = async (): Promise => { // file, but the dedup logic will kick in at that point so no harm will come // off it. if (allZipItems === undefined) { - const allZipPaths = uploadStatusStore.get("filePaths"); + const allZipPaths = uploadStatusStore.get("filePaths") ?? []; const zipPaths = allZipPaths.filter((f) => existsSync(f)); zipItems = []; for (const zip of zipPaths) @@ -83,7 +87,7 @@ export const setPendingUploads = async (pendingUploads: PendingUploads) => export const markUploadedFiles = async (paths: string[]) => { const existing = uploadStatusStore.get("filePaths"); - const updated = existing.filter((p) => !paths.includes(p)); + const updated = existing?.filter((p) => !paths.includes(p)); uploadStatusStore.set("filePaths", updated); }; @@ -91,7 +95,7 @@ export const markUploadedZipItems = async ( items: [zipPath: string, entryName: string][], ) => { const existing = uploadStatusStore.get("zipItems"); - const updated = existing.filter( + const updated = existing?.filter( (z) => !items.some((e) => z[0] == e[0] && z[1] == e[1]), ); uploadStatusStore.set("zipItems", updated); diff --git a/desktop/src/types/ipc.ts b/desktop/src/types/ipc.ts index c02ed17260..c8c7ef6b4a 100644 --- a/desktop/src/types/ipc.ts +++ b/desktop/src/types/ipc.ts @@ -28,7 +28,7 @@ export interface FolderWatchSyncedFile { export type ZipItem = [zipPath: string, entryName: string]; export interface PendingUploads { - collectionName: string; + collectionName?: string; filePaths: string[]; zipItems: ZipItem[]; } diff --git a/web/apps/photos/src/components/Upload/Uploader.tsx b/web/apps/photos/src/components/Upload/Uploader.tsx index 90b5f94b4e..0d310d7f90 100644 --- a/web/apps/photos/src/components/Upload/Uploader.tsx +++ b/web/apps/photos/src/components/Upload/Uploader.tsx @@ -930,9 +930,5 @@ export const setPendingUploads = async ( } } - await electron.setPendingUploads({ - collectionName, - filePaths, - zipItems: zipItems, - }); + await electron.setPendingUploads({ collectionName, filePaths, zipItems }); }; diff --git a/web/packages/next/types/ipc.ts b/web/packages/next/types/ipc.ts index d97a7e5643..895dfcf701 100644 --- a/web/packages/next/types/ipc.ts +++ b/web/packages/next/types/ipc.ts @@ -650,8 +650,10 @@ export interface PendingUploads { * This is name of the collection (when uploading to a singular collection) * or the root collection (when uploading to separate * albums) to which we * these uploads are meant to go to. See {@link CollectionMapping}. + * + * It will not be set if we're just uploading standalone files. */ - collectionName: string; + collectionName?: string; /** * Paths of regular files that need to be uploaded. */ From 612d8682b5151b95ab6e345691598f7bceca5773 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 14:19:25 +0530 Subject: [PATCH 123/240] strict --- desktop/src/main/services/watch.ts | 19 +++++++++---------- desktop/src/main/stream.ts | 7 ++++--- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/desktop/src/main/services/watch.ts b/desktop/src/main/services/watch.ts index 4d7b89e460..e8e3803902 100644 --- a/desktop/src/main/services/watch.ts +++ b/desktop/src/main/services/watch.ts @@ -47,16 +47,15 @@ const eventData = (path: string): [string, FolderWatch] => { return [path, watch]; }; -export const watchGet = (watcher: FSWatcher) => { - const [valid, deleted] = folderWatches().reduce( - ([valid, deleted], watch) => { - (fsIsDir(watch.folderPath) ? valid : deleted).push(watch); - return [valid, deleted]; - }, - [[], []], - ); - if (deleted.length) { - for (const watch of deleted) watchRemove(watcher, watch.folderPath); +export const watchGet = async (watcher: FSWatcher): Promise => { + const valid: FolderWatch[] = []; + const deletedPaths: string[] = []; + for (const watch of folderWatches()) { + if (await fsIsDir(watch.folderPath)) valid.push(watch); + else deletedPaths.push(watch.folderPath); + } + if (deletedPaths.length) { + await Promise.all(deletedPaths.map((p) => watchRemove(watcher, p))); setFolderWatches(valid); } return valid; diff --git a/desktop/src/main/stream.ts b/desktop/src/main/stream.ts index be84c022f5..26021fdf1f 100644 --- a/desktop/src/main/stream.ts +++ b/desktop/src/main/stream.ts @@ -8,6 +8,7 @@ import fs from "node:fs/promises"; import { Readable } from "node:stream"; import { pathToFileURL } from "node:url"; import log from "./log"; +import { ensure } from "./utils/common"; /** * Register a protocol handler that we use for streaming large files between the @@ -89,7 +90,7 @@ const handleRead = async (path: string) => { return res; } catch (e) { log.error(`Failed to read stream at ${path}`, e); - return new Response(`Failed to read stream: ${e.message}`, { + return new Response(`Failed to read stream: ${String(e)}`, { status: 500, }); } @@ -133,11 +134,11 @@ const handleReadZip = async (zipPath: string, entryName: string) => { const handleWrite = async (path: string, request: Request) => { try { - await writeStream(path, request.body); + await writeStream(path, ensure(request.body)); return new Response("", { status: 200 }); } catch (e) { log.error(`Failed to write stream to ${path}`, e); - return new Response(`Failed to write stream: ${e.message}`, { + return new Response(`Failed to write stream: ${String(e)}`, { status: 500, }); } From 24fc486721081d4a47d15fe4291fa482df7b64a4 Mon Sep 17 00:00:00 2001 From: vishnukvmd Date: Tue, 30 Apr 2024 14:29:12 +0530 Subject: [PATCH 124/240] Pull out the code to download a file to gallery --- mobile/lib/ui/viewer/file/file_app_bar.dart | 93 +----------------- mobile/lib/utils/file_download_util.dart | 100 ++++++++++++++++++++ 2 files changed, 102 insertions(+), 91 deletions(-) diff --git a/mobile/lib/ui/viewer/file/file_app_bar.dart b/mobile/lib/ui/viewer/file/file_app_bar.dart index e029aeb898..f0339f368f 100644 --- a/mobile/lib/ui/viewer/file/file_app_bar.dart +++ b/mobile/lib/ui/viewer/file/file_app_bar.dart @@ -4,30 +4,23 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:media_extension/media_extension.dart'; -import 'package:path/path.dart' as file_path; -import 'package:photo_manager/photo_manager.dart'; -import 'package:photos/core/event_bus.dart'; -import 'package:photos/db/files_db.dart'; -import 'package:photos/events/local_photos_updated_event.dart'; import "package:photos/generated/l10n.dart"; import "package:photos/l10n/l10n.dart"; import "package:photos/models/file/extensions/file_props.dart"; import 'package:photos/models/file/file.dart'; import 'package:photos/models/file/file_type.dart'; import 'package:photos/models/file/trash_file.dart'; -import 'package:photos/models/ignored_file.dart'; import "package:photos/models/metadata/common_keys.dart"; import 'package:photos/models/selected_files.dart'; import "package:photos/service_locator.dart"; import 'package:photos/services/collections_service.dart'; import 'package:photos/services/hidden_service.dart'; -import 'package:photos/services/ignored_files_service.dart'; -import 'package:photos/services/local_sync_service.dart'; import 'package:photos/ui/collections/collection_action_sheet.dart'; import 'package:photos/ui/viewer/file/custom_app_bar.dart'; import "package:photos/ui/viewer/file_details/favorite_widget.dart"; import "package:photos/ui/viewer/file_details/upload_icon_widget.dart"; import 'package:photos/utils/dialog_util.dart'; +import "package:photos/utils/file_download_util.dart"; import 'package:photos/utils/file_util.dart'; import "package:photos/utils/magic_util.dart"; import 'package:photos/utils/toast_util.dart'; @@ -330,98 +323,16 @@ class FileAppBarState extends State { ); await dialog.show(); try { - final FileType type = file.fileType; - final bool downloadLivePhotoOnDroid = - type == FileType.livePhoto && Platform.isAndroid; - AssetEntity? savedAsset; - final File? fileToSave = await getFile(file); - //Disabling notifications for assets changing to insert the file into - //files db before triggering a sync. - await PhotoManager.stopChangeNotify(); - if (type == FileType.image) { - savedAsset = await PhotoManager.editor - .saveImageWithPath(fileToSave!.path, title: file.title!); - } else if (type == FileType.video) { - savedAsset = await PhotoManager.editor - .saveVideo(fileToSave!, title: file.title!); - } else if (type == FileType.livePhoto) { - final File? liveVideoFile = - await getFileFromServer(file, liveVideo: true); - if (liveVideoFile == null) { - throw AssertionError("Live video can not be null"); - } - if (downloadLivePhotoOnDroid) { - await _saveLivePhotoOnDroid(fileToSave!, liveVideoFile, file); - } else { - savedAsset = await PhotoManager.editor.darwin.saveLivePhoto( - imageFile: fileToSave!, - videoFile: liveVideoFile, - title: file.title!, - ); - } - } - - if (savedAsset != null) { - file.localID = savedAsset.id; - await FilesDB.instance.insert(file); - Bus.instance.fire( - LocalPhotosUpdatedEvent( - [file], - source: "download", - ), - ); - } else if (!downloadLivePhotoOnDroid && savedAsset == null) { - _logger.severe('Failed to save assert of type $type'); - } + await downloadToGallery(file); showToast(context, S.of(context).fileSavedToGallery); await dialog.hide(); } catch (e) { _logger.warning("Failed to save file", e); await dialog.hide(); await showGenericErrorDialog(context: context, error: e); - } finally { - await PhotoManager.startChangeNotify(); - LocalSyncService.instance.checkAndSync().ignore(); } } - Future _saveLivePhotoOnDroid( - File image, - File video, - EnteFile enteFile, - ) async { - debugPrint("Downloading LivePhoto on Droid"); - AssetEntity? savedAsset = await (PhotoManager.editor - .saveImageWithPath(image.path, title: enteFile.title!)); - if (savedAsset == null) { - throw Exception("Failed to save image of live photo"); - } - IgnoredFile ignoreVideoFile = IgnoredFile( - savedAsset.id, - savedAsset.title ?? '', - savedAsset.relativePath ?? 'remoteDownload', - "remoteDownload", - ); - await IgnoredFilesService.instance.cacheAndInsert([ignoreVideoFile]); - final videoTitle = file_path.basenameWithoutExtension(enteFile.title!) + - file_path.extension(video.path); - savedAsset = (await (PhotoManager.editor.saveVideo( - video, - title: videoTitle, - ))); - if (savedAsset == null) { - throw Exception("Failed to save video of live photo"); - } - - ignoreVideoFile = IgnoredFile( - savedAsset.id, - savedAsset.title ?? videoTitle, - savedAsset.relativePath ?? 'remoteDownload', - "remoteDownload", - ); - await IgnoredFilesService.instance.cacheAndInsert([ignoreVideoFile]); - } - Future _setAs(EnteFile file) async { final dialog = createProgressDialog(context, S.of(context).pleaseWait); await dialog.show(); diff --git a/mobile/lib/utils/file_download_util.dart b/mobile/lib/utils/file_download_util.dart index f99a435276..b0a1d20d56 100644 --- a/mobile/lib/utils/file_download_util.dart +++ b/mobile/lib/utils/file_download_util.dart @@ -4,14 +4,23 @@ import "package:computer/computer.dart"; import 'package:dio/dio.dart'; import "package:flutter/foundation.dart"; import 'package:logging/logging.dart'; +import 'package:path/path.dart' as file_path; +import "package:photo_manager/photo_manager.dart"; import 'package:photos/core/configuration.dart'; +import "package:photos/core/event_bus.dart"; import 'package:photos/core/network/network.dart'; +import "package:photos/db/files_db.dart"; +import "package:photos/events/local_photos_updated_event.dart"; import 'package:photos/models/file/file.dart'; import "package:photos/models/file/file_type.dart"; +import "package:photos/models/ignored_file.dart"; import 'package:photos/services/collections_service.dart'; +import "package:photos/services/ignored_files_service.dart"; +import "package:photos/services/local_sync_service.dart"; import 'package:photos/utils/crypto_util.dart'; import "package:photos/utils/data_util.dart"; import "package:photos/utils/fake_progress.dart"; +import "package:photos/utils/file_util.dart"; final _logger = Logger("file_download_util"); @@ -115,6 +124,97 @@ Future getFileKeyUsingBgWorker(EnteFile file) async { ); } +Future downloadToGallery(EnteFile file) async { + try { + final FileType type = file.fileType; + final bool downloadLivePhotoOnDroid = + type == FileType.livePhoto && Platform.isAndroid; + AssetEntity? savedAsset; + final File? fileToSave = await getFile(file); + //Disabling notifications for assets changing to insert the file into + //files db before triggering a sync. + await PhotoManager.stopChangeNotify(); + if (type == FileType.image) { + savedAsset = await PhotoManager.editor + .saveImageWithPath(fileToSave!.path, title: file.title!); + } else if (type == FileType.video) { + savedAsset = + await PhotoManager.editor.saveVideo(fileToSave!, title: file.title!); + } else if (type == FileType.livePhoto) { + final File? liveVideoFile = + await getFileFromServer(file, liveVideo: true); + if (liveVideoFile == null) { + throw AssertionError("Live video can not be null"); + } + if (downloadLivePhotoOnDroid) { + await _saveLivePhotoOnDroid(fileToSave!, liveVideoFile, file); + } else { + savedAsset = await PhotoManager.editor.darwin.saveLivePhoto( + imageFile: fileToSave!, + videoFile: liveVideoFile, + title: file.title!, + ); + } + } + + if (savedAsset != null) { + file.localID = savedAsset.id; + await FilesDB.instance.insert(file); + Bus.instance.fire( + LocalPhotosUpdatedEvent( + [file], + source: "download", + ), + ); + } else if (!downloadLivePhotoOnDroid && savedAsset == null) { + _logger.severe('Failed to save assert of type $type'); + } + } catch (e) { + _logger.warning("Failed to save file", e); + rethrow; + } finally { + await PhotoManager.startChangeNotify(); + LocalSyncService.instance.checkAndSync().ignore(); + } +} + +Future _saveLivePhotoOnDroid( + File image, + File video, + EnteFile enteFile, +) async { + debugPrint("Downloading LivePhoto on Droid"); + AssetEntity? savedAsset = await (PhotoManager.editor + .saveImageWithPath(image.path, title: enteFile.title!)); + if (savedAsset == null) { + throw Exception("Failed to save image of live photo"); + } + IgnoredFile ignoreVideoFile = IgnoredFile( + savedAsset.id, + savedAsset.title ?? '', + savedAsset.relativePath ?? 'remoteDownload', + "remoteDownload", + ); + await IgnoredFilesService.instance.cacheAndInsert([ignoreVideoFile]); + final videoTitle = file_path.basenameWithoutExtension(enteFile.title!) + + file_path.extension(video.path); + savedAsset = (await (PhotoManager.editor.saveVideo( + video, + title: videoTitle, + ))); + if (savedAsset == null) { + throw Exception("Failed to save video of live photo"); + } + + ignoreVideoFile = IgnoredFile( + savedAsset.id, + savedAsset.title ?? videoTitle, + savedAsset.relativePath ?? 'remoteDownload', + "remoteDownload", + ); + await IgnoredFilesService.instance.cacheAndInsert([ignoreVideoFile]); +} + Uint8List _decryptFileKey(Map args) { final encryptedKey = CryptoUtil.base642bin(args["encryptedKey"]); final nonce = CryptoUtil.base642bin(args["keyDecryptionNonce"]); From 5681f1496765c46a484e72899ca90b21c6d6a984 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 14:31:06 +0530 Subject: [PATCH 125/240] Clarify that entry names are guaranteed to be posixy --- web/packages/next/types/ipc.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/web/packages/next/types/ipc.ts b/web/packages/next/types/ipc.ts index 895dfcf701..d5007ea800 100644 --- a/web/packages/next/types/ipc.ts +++ b/web/packages/next/types/ipc.ts @@ -632,6 +632,19 @@ export interface FolderWatchSyncedFile { * The name of the entry is not just the file name, but rather is the full path * of the file within the zip. That is, each entry name uniquely identifies a * particular file within the given zip. + * + * When `entryName` is a path within a nested directory, it is guaranteed to use + * the POSIX path separator ("/") since that is the path separator required by + * the ZIP format itself + * + * > 4.4.17.1 The name of the file, with optional relative path. + * > + * > The path stored MUST NOT contain a drive or device letter, or a leading + * > slash. All slashes MUST be forward slashes '/' as opposed to backwards + * > slashes '\' for compatibility with Amiga and UNIX file systems etc. If + * > input came from standard input, there is no file name field. + * > + * > https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT */ export type ZipItem = [zipPath: string, entryName: string]; From ae057da331e938248c49c9eb00b06cb5fc8eae4d Mon Sep 17 00:00:00 2001 From: vishnukvmd Date: Tue, 30 Apr 2024 14:37:15 +0530 Subject: [PATCH 126/240] Remove redundant code --- .../file_selection_actions_widget.dart | 57 +++++++++---------- 1 file changed, 26 insertions(+), 31 deletions(-) diff --git a/mobile/lib/ui/viewer/actions/file_selection_actions_widget.dart b/mobile/lib/ui/viewer/actions/file_selection_actions_widget.dart index e2e29e021e..9de4c27e6a 100644 --- a/mobile/lib/ui/viewer/actions/file_selection_actions_widget.dart +++ b/mobile/lib/ui/viewer/actions/file_selection_actions_widget.dart @@ -379,41 +379,36 @@ class _FileSelectionActionsWidgetState ), ); - if (items.isNotEmpty) { - final scrollController = ScrollController(); - // h4ck: https://github.com/flutter/flutter/issues/57920#issuecomment-893970066 - return MediaQuery( - data: MediaQuery.of(context).removePadding(removeBottom: true), - child: SafeArea( - child: Scrollbar( - radius: const Radius.circular(1), - thickness: 2, - controller: scrollController, - thumbVisibility: true, - child: SingleChildScrollView( - physics: const BouncingScrollPhysics( - decelerationRate: ScrollDecelerationRate.fast, - ), - scrollDirection: Axis.horizontal, - child: Container( - padding: const EdgeInsets.only(bottom: 24), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(width: 4), - ...items, - const SizedBox(width: 4), - ], - ), + final scrollController = ScrollController(); + // h4ck: https://github.com/flutter/flutter/issues/57920#issuecomment-893970066 + return MediaQuery( + data: MediaQuery.of(context).removePadding(removeBottom: true), + child: SafeArea( + child: Scrollbar( + radius: const Radius.circular(1), + thickness: 2, + controller: scrollController, + thumbVisibility: true, + child: SingleChildScrollView( + physics: const BouncingScrollPhysics( + decelerationRate: ScrollDecelerationRate.fast, + ), + scrollDirection: Axis.horizontal, + child: Container( + padding: const EdgeInsets.only(bottom: 24), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(width: 4), + ...items, + const SizedBox(width: 4), + ], ), ), ), ), - ); - } else { - // TODO: Return "Select All" here - return const SizedBox.shrink(); - } + ), + ); } Future _moveFiles() async { From ebed315cb084dc602db64666df85ce49b45e17f2 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Tue, 30 Apr 2024 13:05:19 +0530 Subject: [PATCH 127/240] [mob] Add popup item widget --- mobile/lib/ui/common/popup_item.dart | 38 ++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 mobile/lib/ui/common/popup_item.dart diff --git a/mobile/lib/ui/common/popup_item.dart b/mobile/lib/ui/common/popup_item.dart new file mode 100644 index 0000000000..5f32104af0 --- /dev/null +++ b/mobile/lib/ui/common/popup_item.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +class EntePopupMenuItem extends PopupMenuItem { + final String label; + final IconData? icon; + final Widget? iconWidget; + + EntePopupMenuItem( + this.label, { + required T value, + this.icon, + this.iconWidget, + Key? key, + }) : assert( + icon != null || iconWidget != null, + 'Either icon or iconWidget must be provided.', + ), + assert( + !(icon != null && iconWidget != null), + 'Only one of icon or iconWidget can be provided.', + ), + super( + value: value, + key: key, + child: Row( + children: [ + if (iconWidget != null) + iconWidget + else if (icon != null) + Icon(icon), + const Padding( + padding: EdgeInsets.all(8), + ), + Text(label), + ], + ), // Initially empty, will be populated in build + ); +} From 268c7d8a9901b4c406370de85234228144dc2beb Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Tue, 30 Apr 2024 14:42:21 +0530 Subject: [PATCH 128/240] [mob] Use EntePopupMenuItem in gallery_app_bar --- .../gallery/gallery_app_bar_widget.dart | 331 +++++------------- 1 file changed, 93 insertions(+), 238 deletions(-) diff --git a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart index 1f9fb0bbb9..83a55975f0 100644 --- a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart +++ b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart @@ -24,6 +24,7 @@ import 'package:photos/services/collections_service.dart'; import 'package:photos/services/sync_service.dart'; import 'package:photos/services/update_service.dart'; import 'package:photos/ui/actions/collection/collection_sharing_actions.dart'; +import "package:photos/ui/common/popup_item.dart"; import 'package:photos/ui/components/action_sheet_widget.dart'; import 'package:photos/ui/components/buttons/button_widget.dart'; import 'package:photos/ui/components/models/button_type.dart'; @@ -319,263 +320,117 @@ class _GalleryAppBarWidgetState extends State { ), ); } - final List> items = []; - if (galleryType.canRename()) { - items.add( - PopupMenuItem( + final List> items = []; + items.addAll([ + if (galleryType.canRename()) + EntePopupMenuItem( + isQuickLink + ? S.of(context).convertToAlbum + : S.of(context).renameAlbum, value: AlbumPopupAction.rename, - child: Row( - children: [ - Icon(isQuickLink ? Icons.photo_album_outlined : Icons.edit), - const Padding( - padding: EdgeInsets.all(8), - ), - Text( - isQuickLink - ? S.of(context).convertToAlbum - : S.of(context).renameAlbum, - ), - ], - ), + icon: isQuickLink ? Icons.photo_album_outlined : Icons.edit, ), - ); - } - if (galleryType.canSetCover()) { - items.add( - PopupMenuItem( + if (galleryType.canSetCover()) + EntePopupMenuItem( + S.of(context).setCover, value: AlbumPopupAction.setCover, - child: Row( - children: [ - const Icon(Icons.image_outlined), - const Padding( - padding: EdgeInsets.all(8), - ), - Text(S.of(context).setCover), - ], - ), + icon: Icons.image_outlined, ), - ); - } - if (galleryType.showMap()) { - items.add( - PopupMenuItem( + if (galleryType.showMap()) + EntePopupMenuItem( + S.of(context).map, value: AlbumPopupAction.map, - child: Row( - children: [ - const Icon(Icons.map_outlined), - const Padding( - padding: EdgeInsets.all(8), - ), - Text(S.of(context).map), - ], - ), + icon: Icons.map_outlined, ), - ); - } - - if (galleryType.canSort()) { - items.add( - PopupMenuItem( + if (galleryType.canSort()) + EntePopupMenuItem( + S.of(context).sortAlbumsBy, value: AlbumPopupAction.sort, - child: Row( - children: [ - const Icon(Icons.sort_outlined), - const Padding( - padding: EdgeInsets.all(8), - ), - Text( - S.of(context).sortAlbumsBy, - ), - ], - ), + icon: Icons.sort_outlined, ), - ); - } - - if (galleryType == GalleryType.uncategorized) { - items.add( - PopupMenuItem( + if (galleryType == GalleryType.uncategorized) + EntePopupMenuItem( + S.of(context).cleanUncategorized, value: AlbumPopupAction.cleanUncategorized, - child: Row( - children: [ - const Icon(Icons.crop_original_outlined), - const Padding( - padding: EdgeInsets.all(8), - ), - Text(S.of(context).cleanUncategorized), - ], - ), + icon: Icons.crop_original_outlined, ), - ); - } - if (galleryType.canPin()) { - items.add( - PopupMenuItem( + if (galleryType.canPin()) + EntePopupMenuItem( + widget.collection!.isPinned + ? S.of(context).unpinAlbum + : S.of(context).pinAlbum, value: AlbumPopupAction.pinAlbum, - child: Row( - children: [ - widget.collection!.isPinned - ? const Icon(CupertinoIcons.pin_slash) - : Transform.rotate( - angle: 45 * math.pi / 180, // rotate by 45 degrees - child: const Icon(CupertinoIcons.pin), - ), - const Padding( - padding: EdgeInsets.all(8), - ), - Text( - widget.collection!.isPinned - ? S.of(context).unpinAlbum - : S.of(context).pinAlbum, - ), - ], - ), + iconWidget: widget.collection!.isPinned + ? const Icon(CupertinoIcons.pin_slash) + : Transform.rotate( + angle: 45 * math.pi / 180, // rotate by 45 degrees + child: const Icon(CupertinoIcons.pin), + ), ), - ); - } + ]); final bool isArchived = widget.collection?.isArchived() ?? false; final bool isHidden = widget.collection?.isHidden() ?? false; - // Do not show archive option for favorite collection. If collection is - // already archived, allow user to unarchive that collection. - if (isArchived || (galleryType.canArchive() && !isHidden)) { - items.add( - PopupMenuItem( - value: AlbumPopupAction.ownedArchive, - child: Row( - children: [ - Icon(isArchived ? Icons.unarchive : Icons.archive_outlined), - const Padding( - padding: EdgeInsets.all(8), - ), - Text( - isArchived - ? S.of(context).unarchiveAlbum - : S.of(context).archiveAlbum, - ), - ], - ), - ), - ); - } - if (!isArchived && galleryType.canHide()) { - items.add( - PopupMenuItem( - value: AlbumPopupAction.ownedHide, - child: Row( - children: [ - Icon( - isHidden - ? Icons.visibility_outlined - : Icons.visibility_off_outlined, - ), - const Padding( - padding: EdgeInsets.all(8), - ), - Text( - isHidden ? S.of(context).unhide : S.of(context).hide, - ), - ], - ), - ), - ); - } - if (widget.collection != null && isInternalUser) { - items.add( - PopupMenuItem( - value: AlbumPopupAction.playOnTv, - child: Row( - children: [ - const Icon(Icons.tv_outlined), - const Padding( - padding: EdgeInsets.all(8), - ), - Text(context.l10n.playOnTv), - ], - ), - ), - ); - } - if (galleryType.canDelete()) { - items.add( - PopupMenuItem( - value: isQuickLink - ? AlbumPopupAction.removeLink - : AlbumPopupAction.delete, - child: Row( - children: [ - Icon( - isQuickLink - ? Icons.remove_circle_outline - : Icons.delete_outline, - ), - const Padding( - padding: EdgeInsets.all(8), - ), - Text( - isQuickLink - ? S.of(context).removeLink - : S.of(context).deleteAlbum, - ), - ], + items.addAll( + [ + // Do not show archive option for favorite collection. If collection is + // already archived, allow user to unarchive that collection. + if (isArchived || (galleryType.canArchive() && !isHidden)) + EntePopupMenuItem( + value: AlbumPopupAction.ownedArchive, + isArchived + ? S.of(context).unarchiveAlbum + : S.of(context).archiveAlbum, + icon: isArchived ? Icons.unarchive : Icons.archive_outlined, ), - ), - ); - } - - if (galleryType == GalleryType.sharedCollection) { - final bool hasShareeArchived = widget.collection!.hasShareeArchived(); - items.add( - PopupMenuItem( - value: AlbumPopupAction.sharedArchive, - child: Row( - children: [ - Icon( - hasShareeArchived ? Icons.unarchive : Icons.archive_outlined, - ), - const Padding( - padding: EdgeInsets.all(8), - ), - Text( - hasShareeArchived - ? S.of(context).unarchiveAlbum - : S.of(context).archiveAlbum, - ), - ], + if (!isArchived && galleryType.canHide()) + EntePopupMenuItem( + value: AlbumPopupAction.ownedHide, + isHidden ? S.of(context).unhide : S.of(context).hide, + icon: isHidden + ? Icons.visibility_outlined + : Icons.visibility_off_outlined, ), - ), - ); - items.add( - PopupMenuItem( - value: AlbumPopupAction.leave, - child: Row( - children: [ - const Icon(Icons.logout), - const Padding( - padding: EdgeInsets.all(8), - ), - Text(S.of(context).leaveAlbum), - ], + if (widget.collection != null && isInternalUser) + EntePopupMenuItem( + value: AlbumPopupAction.playOnTv, + context.l10n.playOnTv, + icon: Icons.tv_outlined, ), - ), - ); - } - if (galleryType == GalleryType.localFolder) { - items.add( - PopupMenuItem( - value: AlbumPopupAction.freeUpSpace, - child: Row( - children: [ - const Icon(Icons.delete_sweep_outlined), - const Padding( - padding: EdgeInsets.all(8), - ), - Text(S.of(context).freeUpDeviceSpace), - ], + if (galleryType.canDelete()) + EntePopupMenuItem( + isQuickLink ? S.of(context).removeLink : S.of(context).deleteAlbum, + value: isQuickLink + ? AlbumPopupAction.removeLink + : AlbumPopupAction.delete, + icon: isQuickLink + ? Icons.remove_circle_outline + : Icons.delete_outline, ), - ), - ); - } + if (galleryType == GalleryType.sharedCollection) + EntePopupMenuItem( + widget.collection!.hasShareeArchived() + ? S.of(context).unarchiveAlbum + : S.of(context).archiveAlbum, + value: AlbumPopupAction.sharedArchive, + icon: widget.collection!.hasShareeArchived() + ? Icons.unarchive + : Icons.archive_outlined, + ), + if (galleryType == GalleryType.sharedCollection) + EntePopupMenuItem( + S.of(context).leaveAlbum, + value: AlbumPopupAction.leave, + icon: Icons.logout, + ), + if (galleryType == GalleryType.localFolder) + EntePopupMenuItem( + S.of(context).freeUpDeviceSpace, + value: AlbumPopupAction.freeUpSpace, + icon: Icons.delete_sweep_outlined, + ), + ], + ); if (items.isNotEmpty) { actions.add( PopupMenuButton( From 9cc730e6a9e1a6bdb5e6dee77e0cde939e2e1705 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 14:56:13 +0530 Subject: [PATCH 129/240] more posix --- web/apps/photos/src/components/Upload/Uploader.tsx | 4 +++- web/packages/next/types/ipc.ts | 2 +- web/packages/shared/hooks/useFileInput.tsx | 4 ++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/web/apps/photos/src/components/Upload/Uploader.tsx b/web/apps/photos/src/components/Upload/Uploader.tsx index 0d310d7f90..cfd674e3f8 100644 --- a/web/apps/photos/src/components/Upload/Uploader.tsx +++ b/web/apps/photos/src/components/Upload/Uploader.tsx @@ -324,10 +324,12 @@ export default function Uploader({ // Trigger an upload when any of the dependencies change. useEffect(() => { const allItemAndPaths = [ - // See: [Note: webkitRelativePath] + // See: [Note: webkitRelativePath]. In particular, they use POSIX + // separators. webFiles.map((f) => [f, f.webkitRelativePath ?? f.name]), desktopFiles.map((fp) => [fp, fp.path]), desktopFilePaths.map((p) => [p, p]), + // ze[1], the entry name, uses POSIX separators. desktopZipItems.map((ze) => [ze, ze[1]]), ].flat() as [UploadItem, string][]; diff --git a/web/packages/next/types/ipc.ts b/web/packages/next/types/ipc.ts index d5007ea800..c851062410 100644 --- a/web/packages/next/types/ipc.ts +++ b/web/packages/next/types/ipc.ts @@ -641,7 +641,7 @@ export interface FolderWatchSyncedFile { * > * > The path stored MUST NOT contain a drive or device letter, or a leading * > slash. All slashes MUST be forward slashes '/' as opposed to backwards - * > slashes '\' for compatibility with Amiga and UNIX file systems etc. If + * > slashes '\' for compatibility with Amiga and UNIX file systems etc. If * > input came from standard input, there is no file name field. * > * > https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT diff --git a/web/packages/shared/hooks/useFileInput.tsx b/web/packages/shared/hooks/useFileInput.tsx index ae1dfcab0a..71f027cefe 100644 --- a/web/packages/shared/hooks/useFileInput.tsx +++ b/web/packages/shared/hooks/useFileInput.tsx @@ -60,6 +60,10 @@ export default function useFileInput({ // containing the relative path to the selected directory. // // https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/webkitdirectory + // + // These paths use the POSIX path separator ("/"). + // https://stackoverflow.com/questions/62806233/when-using-webkitrelativepath-is-the-path-separator-operating-system-specific + // const directoryOpts = directory ? { directory: "", webkitdirectory: "" } : {}; From 824e73f150b0b02e419e737f499fa41abc859c9a Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 15:30:57 +0530 Subject: [PATCH 130/240] strict --- desktop/src/main/stream.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/desktop/src/main/stream.ts b/desktop/src/main/stream.ts index 26021fdf1f..7b773baed3 100644 --- a/desktop/src/main/stream.ts +++ b/desktop/src/main/stream.ts @@ -102,11 +102,23 @@ const handleReadZip = async (zipPath: string, entryName: string) => { const entry = await zip.entry(entryName); if (!entry) return new Response("", { status: 404 }); + // This returns an "old style" NodeJS.ReadableStream. const stream = await zip.stream(entry); + // Convert it into a new style NodeJS.Readable. + const nodeReadable = new Readable().wrap(stream); + // Then convert it into a Web stream. + const webReadableStreamAny = Readable.toWeb(nodeReadable); + // However, we get a ReadableStream now. This doesn't go into the + // `BodyInit` expected by the Response constructor, which wants a + // ReadableStream. Force a cast. + const webReadableStream = + webReadableStreamAny as ReadableStream; - // TODO(MR): when to call zip.close() + // Close the zip handle when the underlying stream closes. + // TODO(MR): Verify + stream.on("end", () => zip.close()); - return new Response(Readable.toWeb(new Readable(stream)), { + return new Response(webReadableStream, { headers: { // We don't know the exact type, but it doesn't really matter, // just set it to a generic binary content-type so that the From 2f3a2421f796c481e6bfe9f260963c09f26f5585 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 15:44:16 +0530 Subject: [PATCH 131/240] Strict --- desktop/src/main/services/ml-clip.ts | 70 +++++++++++------------ desktop/src/main/services/ml-face.ts | 3 +- desktop/src/main/stores/upload-status.ts | 8 +-- desktop/src/thirdparty/clip-bpe-ts/mod.ts | 3 + desktop/src/types/ipc.ts | 2 +- desktop/tsconfig.json | 31 +++++----- 6 files changed, 58 insertions(+), 59 deletions(-) diff --git a/desktop/src/main/services/ml-clip.ts b/desktop/src/main/services/ml-clip.ts index 149f634b55..451cdcb09b 100644 --- a/desktop/src/main/services/ml-clip.ts +++ b/desktop/src/main/services/ml-clip.ts @@ -45,7 +45,7 @@ const clipImageEmbedding_ = async (jpegFilePath: string) => { `onnx/clip image embedding took ${Date.now() - t1} ms (prep: ${t2 - t1} ms, inference: ${Date.now() - t2} ms)`, ); /* Need these model specific casts to type the result */ - const imageEmbedding = results["output"].data as Float32Array; + const imageEmbedding = ensure(results["output"]).data as Float32Array; return normalizeEmbedding(imageEmbedding); }; @@ -56,19 +56,19 @@ const getRGBData = async (jpegFilePath: string) => { formatAsRGBA: false, }); - const nx: number = rawImageData.width; - const ny: number = rawImageData.height; - const inputImage: Uint8Array = rawImageData.data; + const nx = rawImageData.width; + const ny = rawImageData.height; + const inputImage = rawImageData.data; - const nx2: number = 224; - const ny2: number = 224; - const totalSize: number = 3 * nx2 * ny2; + const nx2 = 224; + const ny2 = 224; + const totalSize = 3 * nx2 * ny2; const result: number[] = Array(totalSize).fill(0); - const scale: number = Math.max(nx, ny) / 224; + const scale = Math.max(nx, ny) / 224; - const nx3: number = Math.round(nx / scale); - const ny3: number = Math.round(ny / scale); + const nx3 = Math.round(nx / scale); + const ny3 = Math.round(ny / scale); const mean: number[] = [0.48145466, 0.4578275, 0.40821073]; const std: number[] = [0.26862954, 0.26130258, 0.27577711]; @@ -77,40 +77,40 @@ const getRGBData = async (jpegFilePath: string) => { for (let x = 0; x < nx3; x++) { for (let c = 0; c < 3; c++) { // Linear interpolation - const sx: number = (x + 0.5) * scale - 0.5; - const sy: number = (y + 0.5) * scale - 0.5; + const sx = (x + 0.5) * scale - 0.5; + const sy = (y + 0.5) * scale - 0.5; - const x0: number = Math.max(0, Math.floor(sx)); - const y0: number = Math.max(0, Math.floor(sy)); + const x0 = Math.max(0, Math.floor(sx)); + const y0 = Math.max(0, Math.floor(sy)); - const x1: number = Math.min(x0 + 1, nx - 1); - const y1: number = Math.min(y0 + 1, ny - 1); + const x1 = Math.min(x0 + 1, nx - 1); + const y1 = Math.min(y0 + 1, ny - 1); - const dx: number = sx - x0; - const dy: number = sy - y0; + const dx = sx - x0; + const dy = sy - y0; - const j00: number = 3 * (y0 * nx + x0) + c; - const j01: number = 3 * (y0 * nx + x1) + c; - const j10: number = 3 * (y1 * nx + x0) + c; - const j11: number = 3 * (y1 * nx + x1) + c; + const j00 = 3 * (y0 * nx + x0) + c; + const j01 = 3 * (y0 * nx + x1) + c; + const j10 = 3 * (y1 * nx + x0) + c; + const j11 = 3 * (y1 * nx + x1) + c; - const v00: number = inputImage[j00]; - const v01: number = inputImage[j01]; - const v10: number = inputImage[j10]; - const v11: number = inputImage[j11]; + const v00 = inputImage[j00] ?? 0; + const v01 = inputImage[j01] ?? 0; + const v10 = inputImage[j10] ?? 0; + const v11 = inputImage[j11] ?? 0; - const v0: number = v00 * (1 - dx) + v01 * dx; - const v1: number = v10 * (1 - dx) + v11 * dx; + const v0 = v00 * (1 - dx) + v01 * dx; + const v1 = v10 * (1 - dx) + v11 * dx; - const v: number = v0 * (1 - dy) + v1 * dy; + const v = v0 * (1 - dy) + v1 * dy; - const v2: number = Math.min(Math.max(Math.round(v), 0), 255); + const v2 = Math.min(Math.max(Math.round(v), 0), 255); // createTensorWithDataList is dumb compared to reshape and // hence has to be given with one channel after another - const i: number = y * nx3 + x + (c % 3) * 224 * 224; + const i = y * nx3 + x + (c % 3) * 224 * 224; - result[i] = (v2 / 255 - mean[c]) / std[c]; + result[i] = (v2 / 255 - (mean[c] ?? 0)) / (std[c] ?? 1); } } } @@ -121,11 +121,11 @@ const getRGBData = async (jpegFilePath: string) => { const normalizeEmbedding = (embedding: Float32Array) => { let normalization = 0; for (let index = 0; index < embedding.length; index++) { - normalization += embedding[index] * embedding[index]; + normalization += ensure(embedding[index]) * ensure(embedding[index]); } const sqrtNormalization = Math.sqrt(normalization); for (let index = 0; index < embedding.length; index++) { - embedding[index] = embedding[index] / sqrtNormalization; + embedding[index] = ensure(embedding[index]) / sqrtNormalization; } return embedding; }; @@ -168,6 +168,6 @@ export const clipTextEmbeddingIfAvailable = async (text: string) => { () => `onnx/clip text embedding took ${Date.now() - t1} ms (prep: ${t2 - t1} ms, inference: ${Date.now() - t2} ms)`, ); - const textEmbedding = results["output"].data as Float32Array; + const textEmbedding = ensure(results["output"]).data as Float32Array; return normalizeEmbedding(textEmbedding); }; diff --git a/desktop/src/main/services/ml-face.ts b/desktop/src/main/services/ml-face.ts index 2309d193cd..e4e43a4b0e 100644 --- a/desktop/src/main/services/ml-face.ts +++ b/desktop/src/main/services/ml-face.ts @@ -8,6 +8,7 @@ */ import * as ort from "onnxruntime-node"; import log from "../log"; +import { ensure } from "../utils/common"; import { makeCachedInferenceSession } from "./ml"; const cachedFaceDetectionSession = makeCachedInferenceSession( @@ -23,7 +24,7 @@ export const detectFaces = async (input: Float32Array) => { }; const results = await session.run(feeds); log.debug(() => `onnx/yolo face detection took ${Date.now() - t} ms`); - return results["output"].data; + return ensure(results["output"]).data; }; const cachedFaceEmbeddingSession = makeCachedInferenceSession( diff --git a/desktop/src/main/stores/upload-status.ts b/desktop/src/main/stores/upload-status.ts index 472f38a7f9..f098e9fc5c 100644 --- a/desktop/src/main/stores/upload-status.ts +++ b/desktop/src/main/stores/upload-status.ts @@ -6,24 +6,24 @@ export interface UploadStatusStore { * * Not all pending uploads will have an associated collection. */ - collectionName?: string; + collectionName: string | undefined; /** * Paths to regular files that are pending upload. * * This should generally be present, albeit empty, but it is marked optional * in sympathy with its siblings. */ - filePaths?: string[]; + filePaths: string[] | undefined; /** * Each item is the path to a zip file and the name of an entry within it. * * This is marked optional since legacy stores will not have it. */ - zipItems?: [zipPath: string, entryName: string][]; + zipItems: [zipPath: string, entryName: string][] | undefined; /** * @deprecated Legacy paths to zip files, now subsumed into zipItems. */ - zipPaths?: string[]; + zipPaths: string[] | undefined; } const uploadStatusSchema: Schema = { diff --git a/desktop/src/thirdparty/clip-bpe-ts/mod.ts b/desktop/src/thirdparty/clip-bpe-ts/mod.ts index 6cdf246f75..b59b762e7c 100644 --- a/desktop/src/thirdparty/clip-bpe-ts/mod.ts +++ b/desktop/src/thirdparty/clip-bpe-ts/mod.ts @@ -410,6 +410,7 @@ export default class { newWord.push(first + second); i += 2; } else { + // @ts-expect-error "Array indexing can return undefined but not modifying thirdparty code" newWord.push(word[i]); i += 1; } @@ -434,6 +435,7 @@ export default class { .map((b) => this.byteEncoder[b.charCodeAt(0) as number]) .join(""); bpeTokens.push( + // @ts-expect-error "Array indexing can return undefined but not modifying thirdparty code" ...this.bpe(token) .split(" ") .map((bpeToken: string) => this.encoder[bpeToken]), @@ -458,6 +460,7 @@ export default class { .join(""); text = [...text] .map((c) => this.byteDecoder[c]) + // @ts-expect-error "Array indexing can return undefined but not modifying thirdparty code" .map((v) => String.fromCharCode(v)) .join("") .replace(/<\/w>/g, " "); diff --git a/desktop/src/types/ipc.ts b/desktop/src/types/ipc.ts index c8c7ef6b4a..f4985bfc71 100644 --- a/desktop/src/types/ipc.ts +++ b/desktop/src/types/ipc.ts @@ -28,7 +28,7 @@ export interface FolderWatchSyncedFile { export type ZipItem = [zipPath: string, entryName: string]; export interface PendingUploads { - collectionName?: string; + collectionName: string | undefined; filePaths: string[]; zipItems: ZipItem[]; } diff --git a/desktop/tsconfig.json b/desktop/tsconfig.json index 16946bf3fe..654158a50d 100644 --- a/desktop/tsconfig.json +++ b/desktop/tsconfig.json @@ -41,33 +41,28 @@ "target": "es2022", "module": "node16", + /* Emit the generated JS into `app/` */ + "outDir": "app", + /* Enable various workarounds to play better with CJS libraries */ "esModuleInterop": true, /* Speed things up by not type checking `node_modules` */ "skipLibCheck": true, - /* Emit the generated JS into `app/` */ - "outDir": "app", - - /* Temporary overrides to get things to compile with the older config */ - // "strict": false, - "noImplicitAny": true, - - /* Below is the state we want */ - /* Enable these one by one */ - "strict": true, - /* Require the `type` modifier when importing types */ - // "verbatimModuleSyntax": true + /* We want this, but it causes "ESM syntax is not allowed in a CommonJS + module when 'verbatimModuleSyntax' is enabled" currently */ + /* "verbatimModuleSyntax": true, */ + "strict": true, /* Stricter than strict */ - // "noImplicitReturns": true, - // "noUnusedParameters": true, - // "noUnusedLocals": true, - // "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noUnusedParameters": true, + "noUnusedLocals": true, + "noFallthroughCasesInSwitch": true, /* e.g. makes array indexing returns undefined */ - // "noUncheckedIndexedAccess": true, - // "exactOptionalPropertyTypes": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true }, /* Transpile all `.ts` files in `src/` */ "include": ["src/**/*.ts"] From 51ffaa4a904551412bd2a82063c722246fe89d80 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 15:59:10 +0530 Subject: [PATCH 132/240] Preempt --- desktop/docs/dependencies.md | 3 +++ desktop/package.json | 1 + desktop/src/main/stream.ts | 1 + desktop/tsconfig.json | 50 +++++++----------------------------- desktop/yarn.lock | 5 ++++ 5 files changed, 19 insertions(+), 41 deletions(-) diff --git a/desktop/docs/dependencies.md b/desktop/docs/dependencies.md index 5c6b222b08..6052357033 100644 --- a/desktop/docs/dependencies.md +++ b/desktop/docs/dependencies.md @@ -90,6 +90,9 @@ Some extra ones specific to the code here are: Unix commands in our `package.json` scripts. This allows us to use the same commands (like `ln`) across different platforms like Linux and Windows. +- [@tsconfig/recommended](https://github.com/tsconfig/bases) gives us a base + tsconfig for the Node.js version that our current Electron version uses. + ## Functionality ### Format conversion diff --git a/desktop/package.json b/desktop/package.json index 69d54f75be..509ee85838 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -34,6 +34,7 @@ "onnxruntime-node": "^1.17" }, "devDependencies": { + "@tsconfig/node20": "^20.1.4", "@types/auto-launch": "^5.0", "@types/ffmpeg-static": "^3.0", "@typescript-eslint/eslint-plugin": "^7", diff --git a/desktop/src/main/stream.ts b/desktop/src/main/stream.ts index 7b773baed3..15715c5500 100644 --- a/desktop/src/main/stream.ts +++ b/desktop/src/main/stream.ts @@ -6,6 +6,7 @@ import StreamZip from "node-stream-zip"; import { createWriteStream, existsSync } from "node:fs"; import fs from "node:fs/promises"; import { Readable } from "node:stream"; +import { ReadableStream } from "node:stream/web"; import { pathToFileURL } from "node:url"; import log from "./log"; import { ensure } from "./utils/common"; diff --git a/desktop/tsconfig.json b/desktop/tsconfig.json index 654158a50d..7806cd93a7 100644 --- a/desktop/tsconfig.json +++ b/desktop/tsconfig.json @@ -3,52 +3,20 @@ into JavaScript that'll then be loaded and run by the main (node) process of our Electron app. */ + /* + * Recommended target, lib and other settings for code running in the + * version of Node.js bundled with Electron. + * + * Currently, with Electron 30, this is Node.js 20.11.1. + * https://www.electronjs.org/blog/electron-30-0 + */ + "extends": "@tsconfig/node20/tsconfig.json", + /* TSConfig docs: https://aka.ms/tsconfig.json */ - "compilerOptions": { - /* Recommended target, lib and other settings for code running in the - version of Node.js bundled with Electron. - - Currently, with Electron 29, this is Node.js 20.9 - https://www.electronjs.org/blog/electron-29-0 - - Note that we cannot do - - "extends": "@tsconfig/node20/tsconfig.json", - - because that sets "lib": ["es2023"]. However (and I don't fully - understand what's going on here), that breaks our compilation since - tsc can then not find type definitions of things like ReadableStream. - - Adding "dom" to "lib" (e.g. `"lib": ["es2023", "dom"]`) fixes the - issue, but that doesn't sound correct - the main Electron process - isn't running in a browser context. - - It is possible that we're using some of the types incorrectly. For - now, we just omit the "lib" definition and rely on the defaults for - the "target" we've chosen. This is also what the current - electron-forge starter does: - - yarn create electron-app electron-forge-starter -- --template=webpack-typescript - - Enhancement: Can revisit this later. - - Refs: - - https://github.com/electron/electron/issues/27092 - - https://github.com/electron/electron/issues/16146 - */ - - "target": "es2022", - "module": "node16", - /* Emit the generated JS into `app/` */ "outDir": "app", - /* Enable various workarounds to play better with CJS libraries */ - "esModuleInterop": true, - /* Speed things up by not type checking `node_modules` */ - "skipLibCheck": true, - /* Require the `type` modifier when importing types */ /* We want this, but it causes "ESM syntax is not allowed in a CommonJS module when 'verbatimModuleSyntax' is enabled" currently */ diff --git a/desktop/yarn.lock b/desktop/yarn.lock index a5b86f1eb3..bf9057d472 100644 --- a/desktop/yarn.lock +++ b/desktop/yarn.lock @@ -246,6 +246,11 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== +"@tsconfig/node20@^20.1.4": + version "20.1.4" + resolved "https://registry.yarnpkg.com/@tsconfig/node20/-/node20-20.1.4.tgz#3457d42eddf12d3bde3976186ab0cd22b85df928" + integrity sha512-sqgsT69YFeLWf5NtJ4Xq/xAF8p4ZQHlmGW74Nu2tD4+g5fAsposc4ZfaaPixVu4y01BEiDCWLRDCvDM5JOsRxg== + "@types/auto-launch@^5.0": version "5.0.5" resolved "https://registry.yarnpkg.com/@types/auto-launch/-/auto-launch-5.0.5.tgz#439ed36aaaea501e2e2cfbddd8a20c366c34863b" From 9b996ff353d34e8bde539993abfb8d54511a827f Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 16:03:52 +0530 Subject: [PATCH 133/240] Lint+ --- desktop/.eslintrc.js | 2 +- desktop/src/thirdparty/clip-bpe-ts/mod.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/desktop/.eslintrc.js b/desktop/.eslintrc.js index 977071a270..d9c13a4423 100644 --- a/desktop/.eslintrc.js +++ b/desktop/.eslintrc.js @@ -4,7 +4,7 @@ module.exports = { "eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", /* What we really want eventually */ - // "plugin:@typescript-eslint/strict-type-checked", + "plugin:@typescript-eslint/strict-type-checked", // "plugin:@typescript-eslint/stylistic-type-checked", ], plugins: ["@typescript-eslint"], diff --git a/desktop/src/thirdparty/clip-bpe-ts/mod.ts b/desktop/src/thirdparty/clip-bpe-ts/mod.ts index b59b762e7c..4d00eef0e4 100644 --- a/desktop/src/thirdparty/clip-bpe-ts/mod.ts +++ b/desktop/src/thirdparty/clip-bpe-ts/mod.ts @@ -1,3 +1,5 @@ +/* eslint-disable */ + import * as htmlEntities from "html-entities"; import bpeVocabData from "./bpe_simple_vocab_16e6"; // import ftfy from "https://deno.land/x/ftfy_pyodide@v0.1.1/mod.js"; From 872245cf0e4743fb2f1658e98d00707c669d53ca Mon Sep 17 00:00:00 2001 From: vishnukvmd Date: Tue, 30 Apr 2024 16:04:32 +0530 Subject: [PATCH 134/240] Update package for icons --- mobile/pubspec.lock | 4 ++-- mobile/pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 0610e45888..4dd40c9a0c 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -342,10 +342,10 @@ packages: dependency: "direct main" description: name: cupertino_icons - sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "1.0.8" dart_style: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 9c96bc7621..3cd8b6e668 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -39,7 +39,7 @@ dependencies: connectivity_plus: ^6.0.2 cross_file: ^0.3.3 crypto: ^3.0.2 - cupertino_icons: ^1.0.0 + cupertino_icons: ^1.0.8 defer_pointer: ^0.0.2 device_info_plus: ^9.0.3 dio: ^4.0.6 From 6f338867e4ae9f3bcf6c3efd6d4f3952771fcb9c Mon Sep 17 00:00:00 2001 From: vishnukvmd Date: Tue, 30 Apr 2024 16:04:50 +0530 Subject: [PATCH 135/240] Add log --- mobile/lib/db/files_db.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/mobile/lib/db/files_db.dart b/mobile/lib/db/files_db.dart index fce6500869..7022100b73 100644 --- a/mobile/lib/db/files_db.dart +++ b/mobile/lib/db/files_db.dart @@ -455,6 +455,7 @@ class FilesDB { } Future insert(EnteFile file) async { + _logger.info("Inserting $file"); final db = await instance.database; return db.insert( filesTable, From db47f8eaf5b819cf50773ecf5cac5ecb0657d243 Mon Sep 17 00:00:00 2001 From: vishnukvmd Date: Tue, 30 Apr 2024 16:05:00 +0530 Subject: [PATCH 136/240] Add copy --- mobile/lib/generated/intl/messages_en.dart | 2 ++ mobile/lib/generated/l10n.dart | 10 ++++++++++ mobile/lib/l10n/intl_en.arb | 1 + 3 files changed, 13 insertions(+) diff --git a/mobile/lib/generated/intl/messages_en.dart b/mobile/lib/generated/intl/messages_en.dart index eef309aa5d..51e895c9eb 100644 --- a/mobile/lib/generated/intl/messages_en.dart +++ b/mobile/lib/generated/intl/messages_en.dart @@ -721,6 +721,8 @@ class MessageLookup extends MessageLookupByLibrary { "filesBackedUpFromDevice": m22, "filesBackedUpInAlbum": m23, "filesDeleted": MessageLookupByLibrary.simpleMessage("Files deleted"), + "filesSavedToGallery": + MessageLookupByLibrary.simpleMessage("Files saved to gallery"), "flip": MessageLookupByLibrary.simpleMessage("Flip"), "forYourMemories": MessageLookupByLibrary.simpleMessage("for your memories"), diff --git a/mobile/lib/generated/l10n.dart b/mobile/lib/generated/l10n.dart index 3fa9c2209a..68f9fff40a 100644 --- a/mobile/lib/generated/l10n.dart +++ b/mobile/lib/generated/l10n.dart @@ -5945,6 +5945,16 @@ class S { ); } + /// `Files saved to gallery` + String get filesSavedToGallery { + return Intl.message( + 'Files saved to gallery', + name: 'filesSavedToGallery', + desc: '', + args: [], + ); + } + /// `Failed to save file to gallery` String get fileFailedToSaveToGallery { return Intl.message( diff --git a/mobile/lib/l10n/intl_en.arb b/mobile/lib/l10n/intl_en.arb index 7115c69508..97691a938f 100644 --- a/mobile/lib/l10n/intl_en.arb +++ b/mobile/lib/l10n/intl_en.arb @@ -835,6 +835,7 @@ "close": "Close", "setAs": "Set as", "fileSavedToGallery": "File saved to gallery", + "filesSavedToGallery": "Files saved to gallery", "fileFailedToSaveToGallery": "Failed to save file to gallery", "download": "Download", "pressAndHoldToPlayVideo": "Press and hold to play video", From d7bef6cd4d8cc50554675d59137b9945548f212d Mon Sep 17 00:00:00 2001 From: vishnukvmd Date: Tue, 30 Apr 2024 16:05:06 +0530 Subject: [PATCH 137/240] Log filetype --- mobile/lib/models/file/file.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/lib/models/file/file.dart b/mobile/lib/models/file/file.dart index 2aa5a4558d..d96a81e1ce 100644 --- a/mobile/lib/models/file/file.dart +++ b/mobile/lib/models/file/file.dart @@ -308,7 +308,7 @@ class EnteFile { @override String toString() { return '''File(generatedID: $generatedID, localID: $localID, title: $title, - uploadedFileId: $uploadedFileID, modificationTime: $modificationTime, + type: $fileType, uploadedFileId: $uploadedFileID, modificationTime: $modificationTime, ownerID: $ownerID, collectionID: $collectionID, updationTime: $updationTime)'''; } From cdddbc4602eeba6993767bbb692a3829e7a5d42b Mon Sep 17 00:00:00 2001 From: vishnukvmd Date: Tue, 30 Apr 2024 16:05:44 +0530 Subject: [PATCH 138/240] Increase severity of error log --- mobile/lib/utils/file_download_util.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/lib/utils/file_download_util.dart b/mobile/lib/utils/file_download_util.dart index b0a1d20d56..a8847e3fdb 100644 --- a/mobile/lib/utils/file_download_util.dart +++ b/mobile/lib/utils/file_download_util.dart @@ -170,7 +170,7 @@ Future downloadToGallery(EnteFile file) async { _logger.severe('Failed to save assert of type $type'); } } catch (e) { - _logger.warning("Failed to save file", e); + _logger.severe("Failed to save file", e); rethrow; } finally { await PhotoManager.startChangeNotify(); From fe5e6c18e8542d1ab7acec756071c619269fdf7f Mon Sep 17 00:00:00 2001 From: vishnukvmd Date: Tue, 30 Apr 2024 16:06:03 +0530 Subject: [PATCH 139/240] Provide option to download multiple items --- .../file_selection_actions_widget.dart | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/mobile/lib/ui/viewer/actions/file_selection_actions_widget.dart b/mobile/lib/ui/viewer/actions/file_selection_actions_widget.dart index 9de4c27e6a..a630e3354a 100644 --- a/mobile/lib/ui/viewer/actions/file_selection_actions_widget.dart +++ b/mobile/lib/ui/viewer/actions/file_selection_actions_widget.dart @@ -3,6 +3,7 @@ import "dart:async"; import 'package:fast_base58/fast_base58.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import "package:logging/logging.dart"; import "package:modal_bottom_sheet/modal_bottom_sheet.dart"; import 'package:photos/core/configuration.dart'; import "package:photos/generated/l10n.dart"; @@ -30,6 +31,8 @@ import 'package:photos/ui/sharing/manage_links_widget.dart'; import "package:photos/ui/tools/collage/collage_creator_page.dart"; import "package:photos/ui/viewer/location/update_location_data_widget.dart"; import 'package:photos/utils/delete_file_util.dart'; +import "package:photos/utils/dialog_util.dart"; +import "package:photos/utils/file_download_util.dart"; import 'package:photos/utils/magic_util.dart'; import 'package:photos/utils/navigation_util.dart'; import "package:photos/utils/share_util.dart"; @@ -56,6 +59,7 @@ class FileSelectionActionsWidget extends StatefulWidget { class _FileSelectionActionsWidgetState extends State { + static final _logger = Logger("FileSelectionActionsWidget"); late int currentUserID; late FilesSplit split; late CollectionActions collectionActions; @@ -115,6 +119,8 @@ class _FileSelectionActionsWidgetState !widget.selectedFiles.files.any( (element) => element.fileType == FileType.video, ); + final showDownloadOption = + widget.selectedFiles.files.any((element) => element.localID == null); //To animate adding and removing of [SelectedActionButton], add all items //and set [shouldShow] to false for items that should not be shown and true @@ -367,6 +373,16 @@ class _FileSelectionActionsWidgetState ); } + if (showDownloadOption) { + items.add( + SelectionActionButton( + labelText: S.of(context).download, + icon: Icons.cloud_download_outlined, + onTap: () => _download(widget.selectedFiles.files.toList()), + ), + ); + } + items.add( SelectionActionButton( labelText: S.of(context).share, @@ -642,4 +658,29 @@ class _FileSelectionActionsWidgetState widget.selectedFiles.clearAll(); } } + + Future _download(List files) async { + final dialog = createProgressDialog( + context, + S.of(context).downloading, + isDismissible: true, + ); + await dialog.show(); + try { + final futures = []; + for (final file in files) { + if (file.localID == null) { + futures.add(downloadToGallery(file)); + } + } + await Future.wait(futures); + await dialog.hide(); + widget.selectedFiles.clearAll(); + showToast(context, S.of(context).filesSavedToGallery); + } catch (e) { + _logger.warning("Failed to save files", e); + await dialog.hide(); + await showGenericErrorDialog(context: context, error: e); + } + } } From a9671481d8d06139a5118b0ee1f48dfe6ba6b7a4 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 16:10:56 +0530 Subject: [PATCH 140/240] Allow numbers to be used in template literals --- desktop/.eslintrc.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/desktop/.eslintrc.js b/desktop/.eslintrc.js index d9c13a4423..8074990af4 100644 --- a/desktop/.eslintrc.js +++ b/desktop/.eslintrc.js @@ -1,5 +1,6 @@ /* eslint-env node */ module.exports = { + root: true, extends: [ "eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", @@ -12,10 +13,17 @@ module.exports = { parserOptions: { project: true, }, - root: true, ignorePatterns: [".eslintrc.js", "app", "out", "dist"], env: { es2022: true, node: true, }, + rules: { + "@typescript-eslint/restrict-template-expressions": [ + "error", + { + allowNumber: true, + }, + ], + }, }; From 755ee4a0c24ccd8447820b28d58753e3c0d85673 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 16:13:16 +0530 Subject: [PATCH 141/240] hopefully --- desktop/.eslintrc.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/desktop/.eslintrc.js b/desktop/.eslintrc.js index 8074990af4..541ba33a9c 100644 --- a/desktop/.eslintrc.js +++ b/desktop/.eslintrc.js @@ -25,5 +25,10 @@ module.exports = { allowNumber: true, }, ], + /* Temporary (RIP) */ + "@typescript-eslint/no-unsafe-return": "off", + "@typescript-eslint/no-confusing-void-expression": "off", + "@typescript-eslint/no-misused-promises": "off", + "@typescript-eslint/no-floating-promises": "off", }, }; From 99e72a119f4f1c49d960ae0f8cbdd6d5f98c99a7 Mon Sep 17 00:00:00 2001 From: vishnukvmd Date: Tue, 30 Apr 2024 16:13:48 +0530 Subject: [PATCH 142/240] Update download icon --- mobile/lib/ui/viewer/file/file_app_bar.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/lib/ui/viewer/file/file_app_bar.dart b/mobile/lib/ui/viewer/file/file_app_bar.dart index f0339f368f..2918924dbc 100644 --- a/mobile/lib/ui/viewer/file/file_app_bar.dart +++ b/mobile/lib/ui/viewer/file/file_app_bar.dart @@ -158,7 +158,7 @@ class FileAppBarState extends State { Icon( Platform.isAndroid ? Icons.download - : CupertinoIcons.cloud_download, + : Icons.cloud_download_outlined, color: Theme.of(context).iconTheme.color, ), const Padding( From 994ca4b6a3781ca28524aec4128b2c984b9fe2e4 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 16:25:35 +0530 Subject: [PATCH 143/240] That's why cache fails --- desktop/src/main.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/desktop/src/main.ts b/desktop/src/main.ts index 42c5ab7329..4cd25881f0 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -174,7 +174,7 @@ const createMainWindow = async () => { if (isDev) window.webContents.openDevTools(); window.webContents.on("render-process-gone", (_, details) => { - log.error(`render-process-gone: ${details}`); + log.error(`render-process-gone: ${details.reason}`); window.webContents.reload(); }); @@ -302,17 +302,19 @@ const setupTrayItem = (mainWindow: BrowserWindow) => { * versions. */ const deleteLegacyDiskCacheDirIfExists = async () => { - // The existing code was passing "cache" as a parameter to getPath. This is - // incorrect if we go by the types - "cache" is not a valid value for the - // parameter to `app.getPath`. + // The existing code was passing "cache" as a parameter to getPath. // - // It might be an issue in the types, since at runtime it seems to work. For - // example, on macOS I get `~/Library/Caches`. + // However, "cache" is not a valid parameter to getPath. It works! (for + // example, on macOS I get `~/Library/Caches`), but it is intentionally not + // documented as part of the public API: + // + // - docs: remove "cache" from app.getPath + // https://github.com/electron/electron/pull/33509 // // Irrespective, we replicate the original behaviour so that we get back the - // same path that the old got was getting. + // same path that the old code was getting. // - // @ts-expect-error + // @ts-expect-error "cache" works but is not part of the public API. const cacheDir = path.join(app.getPath("cache"), "ente"); if (existsSync(cacheDir)) { log.info(`Removing legacy disk cache from ${cacheDir}`); From 9771db6ade1a6230ffe3290dfa49bf9260edae5c Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 16:28:48 +0530 Subject: [PATCH 144/240] Use the built in transformer --- desktop/src/main/stream.ts | 39 ++++++-------------------------------- 1 file changed, 6 insertions(+), 33 deletions(-) diff --git a/desktop/src/main/stream.ts b/desktop/src/main/stream.ts index 15715c5500..b97900659f 100644 --- a/desktop/src/main/stream.ts +++ b/desktop/src/main/stream.ts @@ -163,39 +163,12 @@ const handleWrite = async (path: string, request: Request) => { * The returned promise resolves when the write completes. * * @param filePath The local filesystem path where the file should be written. - * @param readableStream A [web - * ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) + * + * @param readableStream A web + * [ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream). */ export const writeStream = (filePath: string, readableStream: ReadableStream) => - writeNodeStream(filePath, convertWebReadableStreamToNode(readableStream)); - -/** - * Convert a Web ReadableStream into a Node.js ReadableStream - * - * This can be used to, for example, write a ReadableStream obtained via - * `net.fetch` into a file using the Node.js `fs` APIs - */ -const convertWebReadableStreamToNode = (readableStream: ReadableStream) => { - const reader = readableStream.getReader(); - const rs = new Readable(); - - rs._read = async () => { - try { - const result = await reader.read(); - - if (!result.done) { - rs.push(Buffer.from(result.value)); - } else { - rs.push(null); - return; - } - } catch (e) { - rs.emit("error", e); - } - }; - - return rs; -}; + writeNodeStream(filePath, Readable.fromWeb(readableStream)); const writeNodeStream = async (filePath: string, fileStream: Readable) => { const writeable = createWriteStream(filePath); @@ -208,11 +181,11 @@ const writeNodeStream = async (filePath: string, fileStream: Readable) => { await new Promise((resolve, reject) => { writeable.on("finish", resolve); - writeable.on("error", async (e: unknown) => { + writeable.on("error", async (err: Error) => { if (existsSync(filePath)) { await fs.unlink(filePath); } - reject(e); + reject(err); }); }); }; From 01c77c39495a06e8c8c29349aad9717dc1afb8d3 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 16:29:24 +0530 Subject: [PATCH 145/240] unk --- desktop/src/main/log.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/desktop/src/main/log.ts b/desktop/src/main/log.ts index c1902d8ebd..c4d2f3cbbe 100644 --- a/desktop/src/main/log.ts +++ b/desktop/src/main/log.ts @@ -65,7 +65,7 @@ const logError_ = (message: string) => { if (isDev) console.error(`[error] ${message}`); }; -const logInfo = (...params: any[]) => { +const logInfo = (...params: unknown[]) => { const message = params .map((p) => (typeof p == "string" ? p : util.inspect(p))) .join(" "); @@ -73,7 +73,7 @@ const logInfo = (...params: any[]) => { if (isDev) console.log(`[info] ${message}`); }; -const logDebug = (param: () => any) => { +const logDebug = (param: () => unknown) => { if (isDev) { const p = param(); console.log(`[debug] ${typeof p == "string" ? p : util.inspect(p)}`); From 9e279da6b3f9a530af8f56fc7e85ebc16c67bf00 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 16:30:19 +0530 Subject: [PATCH 146/240] annotations --- desktop/src/main/ipc.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/desktop/src/main/ipc.ts b/desktop/src/main/ipc.ts index eb8b6cdda8..66cfddabd4 100644 --- a/desktop/src/main/ipc.ts +++ b/desktop/src/main/ipc.ts @@ -93,18 +93,20 @@ export const attachIPCHandlers = () => { ipcMain.handle("appVersion", () => appVersion()); - ipcMain.handle("openDirectory", (_, dirPath) => openDirectory(dirPath)); + ipcMain.handle("openDirectory", (_, dirPath: string) => + openDirectory(dirPath), + ); ipcMain.handle("openLogDirectory", () => openLogDirectory()); // See [Note: Catching exception during .send/.on] - ipcMain.on("logToDisk", (_, message) => logToDisk(message)); + ipcMain.on("logToDisk", (_, message: string) => logToDisk(message)); ipcMain.handle("selectDirectory", () => selectDirectory()); ipcMain.on("clearStores", () => clearStores()); - ipcMain.handle("saveEncryptionKey", (_, encryptionKey) => + ipcMain.handle("saveEncryptionKey", (_, encryptionKey: string) => saveEncryptionKey(encryptionKey), ); @@ -114,21 +116,23 @@ export const attachIPCHandlers = () => { ipcMain.on("updateAndRestart", () => updateAndRestart()); - ipcMain.on("updateOnNextRestart", (_, version) => + ipcMain.on("updateOnNextRestart", (_, version: string) => updateOnNextRestart(version), ); - ipcMain.on("skipAppUpdate", (_, version) => skipAppUpdate(version)); + ipcMain.on("skipAppUpdate", (_, version: string) => skipAppUpdate(version)); // - FS - ipcMain.handle("fsExists", (_, path) => fsExists(path)); + ipcMain.handle("fsExists", (_, path: string) => fsExists(path)); ipcMain.handle("fsRename", (_, oldPath: string, newPath: string) => fsRename(oldPath, newPath), ); - ipcMain.handle("fsMkdirIfNeeded", (_, dirPath) => fsMkdirIfNeeded(dirPath)); + ipcMain.handle("fsMkdirIfNeeded", (_, dirPath: string) => + fsMkdirIfNeeded(dirPath), + ); ipcMain.handle("fsRmdir", (_, path: string) => fsRmdir(path)); From 7fb912c9dfd55109c7669d3d90dceb8fb2fb966d Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 16:34:48 +0530 Subject: [PATCH 147/240] ensure --- desktop/src/main/utils/temp.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/desktop/src/main/utils/temp.ts b/desktop/src/main/utils/temp.ts index 09071c1573..b336a902f1 100644 --- a/desktop/src/main/utils/temp.ts +++ b/desktop/src/main/utils/temp.ts @@ -4,6 +4,7 @@ import { existsSync } from "node:fs"; import fs from "node:fs/promises"; import path from "node:path"; import type { ZipItem } from "../../types/ipc"; +import { ensure } from "./common"; /** * Our very own directory within the system temp directory. Go crazy, but @@ -17,13 +18,10 @@ const enteTempDirPath = async () => { /** Generate a random string suitable for being used as a file name prefix */ const randomPrefix = () => { - const alphabet = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + const ch = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + const randomChar = () => ensure(ch[Math.floor(Math.random() * ch.length)]); - let result = ""; - for (let i = 0; i < 10; i++) - result += alphabet[Math.floor(Math.random() * alphabet.length)]; - return result; + return Array(10).fill("").map(randomChar).join(""); }; /** From 1076471d51fd27dc07e60e5072d32d37abe3ecc5 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 16:42:52 +0530 Subject: [PATCH 148/240] Turn one off --- desktop/src/main/stores/watch.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/desktop/src/main/stores/watch.ts b/desktop/src/main/stores/watch.ts index 7ee383038a..6006c28ac6 100644 --- a/desktop/src/main/stores/watch.ts +++ b/desktop/src/main/stores/watch.ts @@ -54,8 +54,11 @@ export const watchStore = new Store({ */ export const migrateLegacyWatchStoreIfNeeded = () => { let needsUpdate = false; - const watches = watchStore.get("mappings")?.map((watch) => { + const watches = watchStore.get("mappings").map((watch) => { let collectionMapping = watch.collectionMapping; + // The required type defines the latest schema, but before migration + // this'll be undefined, so tell ESLint to calm down. + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!collectionMapping) { collectionMapping = watch.uploadStrategy == 1 ? "parent" : "root"; needsUpdate = true; From 46d67f0c49dd77a17a63824e89562998a8f05fb5 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 16:49:56 +0530 Subject: [PATCH 149/240] Disentagle map from modifications --- desktop/src/main/services/watch.ts | 4 ++-- desktop/src/main/stores/watch.ts | 11 ++++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/desktop/src/main/services/watch.ts b/desktop/src/main/services/watch.ts index e8e3803902..ca550a787b 100644 --- a/desktop/src/main/services/watch.ts +++ b/desktop/src/main/services/watch.ts @@ -73,7 +73,7 @@ export const watchAdd = async ( ) => { const watches = folderWatches(); - if (!fsIsDir(folderPath)) + if (!(await fsIsDir(folderPath))) throw new Error( `Attempting to add a folder watch for a folder path ${folderPath} that is not an existing directory`, ); @@ -97,7 +97,7 @@ export const watchAdd = async ( return watches; }; -export const watchRemove = async (watcher: FSWatcher, folderPath: string) => { +export const watchRemove = (watcher: FSWatcher, folderPath: string) => { const watches = folderWatches(); const filtered = watches.filter((watch) => watch.folderPath != folderPath); if (watches.length == filtered.length) diff --git a/desktop/src/main/stores/watch.ts b/desktop/src/main/stores/watch.ts index 6006c28ac6..59032c9acd 100644 --- a/desktop/src/main/stores/watch.ts +++ b/desktop/src/main/stores/watch.ts @@ -3,7 +3,7 @@ import { type FolderWatch } from "../../types/ipc"; import log from "../log"; interface WatchStore { - mappings: FolderWatchWithLegacyFields[]; + mappings?: FolderWatchWithLegacyFields[]; } type FolderWatchWithLegacyFields = FolderWatch & { @@ -54,7 +54,8 @@ export const watchStore = new Store({ */ export const migrateLegacyWatchStoreIfNeeded = () => { let needsUpdate = false; - const watches = watchStore.get("mappings").map((watch) => { + const updatedWatches = []; + for (const watch of watchStore.get("mappings") ?? []) { let collectionMapping = watch.collectionMapping; // The required type defines the latest schema, but before migration // this'll be undefined, so tell ESLint to calm down. @@ -67,10 +68,10 @@ export const migrateLegacyWatchStoreIfNeeded = () => { delete watch.rootFolderName; needsUpdate = true; } - return { ...watch, collectionMapping }; - }); + updatedWatches.push({ ...watch, collectionMapping }); + } if (needsUpdate) { - watchStore.set("mappings", watches); + watchStore.set("mappings", updatedWatches); log.info("Migrated legacy watch store data to new schema"); } }; From 9cce8b379ca468641d876d384e588ece0b41aed3 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 16:51:19 +0530 Subject: [PATCH 150/240] Remove unnecessary asyncs --- desktop/src/main/services/upload.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/desktop/src/main/services/upload.ts b/desktop/src/main/services/upload.ts index c3efbb9f40..cf96399190 100644 --- a/desktop/src/main/services/upload.ts +++ b/desktop/src/main/services/upload.ts @@ -82,16 +82,16 @@ export const pendingUploads = async (): Promise => { }; }; -export const setPendingUploads = async (pendingUploads: PendingUploads) => +export const setPendingUploads = (pendingUploads: PendingUploads) => uploadStatusStore.set(pendingUploads); -export const markUploadedFiles = async (paths: string[]) => { +export const markUploadedFiles = (paths: string[]) => { const existing = uploadStatusStore.get("filePaths"); const updated = existing?.filter((p) => !paths.includes(p)); uploadStatusStore.set("filePaths", updated); }; -export const markUploadedZipItems = async ( +export const markUploadedZipItems = ( items: [zipPath: string, entryName: string][], ) => { const existing = uploadStatusStore.get("zipItems"); From f4660baeb8eff59a6feb8336d07e96656e937a06 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 16:58:58 +0530 Subject: [PATCH 151/240] Remove unnecessary awaits --- desktop/src/main/services/store.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/desktop/src/main/services/store.ts b/desktop/src/main/services/store.ts index 9ec65c8c38..1884efbc5d 100644 --- a/desktop/src/main/services/store.ts +++ b/desktop/src/main/services/store.ts @@ -14,15 +14,15 @@ export const clearStores = () => { watchStore.clear(); }; -export const saveEncryptionKey = async (encryptionKey: string) => { - const encryptedKey: Buffer = await safeStorage.encryptString(encryptionKey); +export const saveEncryptionKey = (encryptionKey: string) => { + const encryptedKey = safeStorage.encryptString(encryptionKey); const b64EncryptedKey = Buffer.from(encryptedKey).toString("base64"); safeStorageStore.set("encryptionKey", b64EncryptedKey); }; -export const encryptionKey = async (): Promise => { +export const encryptionKey = (): string | undefined => { const b64EncryptedKey = safeStorageStore.get("encryptionKey"); if (!b64EncryptedKey) return undefined; const keyBuffer = Buffer.from(b64EncryptedKey, "base64"); - return await safeStorage.decryptString(keyBuffer); + return safeStorage.decryptString(keyBuffer); }; From 20459afc7bcd400f2d15168fc1800cf0d709f78b Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Tue, 30 Apr 2024 17:33:25 +0530 Subject: [PATCH 152/240] [cast] Add logs --- web/apps/cast/src/pages/index.tsx | 26 +++++----- web/apps/cast/src/pages/slideshow.tsx | 73 +++++++++++++++------------ 2 files changed, 55 insertions(+), 44 deletions(-) diff --git a/web/apps/cast/src/pages/index.tsx b/web/apps/cast/src/pages/index.tsx index 12c859e9c8..fa4d9e4859 100644 --- a/web/apps/cast/src/pages/index.tsx +++ b/web/apps/cast/src/pages/index.tsx @@ -41,6 +41,19 @@ export default function PairingMode() { init(); }, []); + const init = async () => { + try { + const data = generateSecureData(6); + setDigits(convertDataToDecimalString(data).split("")); + const keypair = await generateKeyPair(); + setPublicKeyB64(await toB64(keypair.publicKey)); + setPrivateKeyB64(await toB64(keypair.privateKey)); + } catch (e) { + log.error("failed to generate keypair", e); + throw e; + } + }; + useEffect(() => { if (!cast) { return; @@ -102,19 +115,6 @@ export default function PairingMode() { } }; - const init = async () => { - try { - const data = generateSecureData(6); - setDigits(convertDataToDecimalString(data).split("")); - const keypair = await generateKeyPair(); - setPublicKeyB64(await toB64(keypair.publicKey)); - setPrivateKeyB64(await toB64(keypair.privateKey)); - } catch (e) { - log.error("failed to generate keypair", e); - throw e; - } - }; - const generateKeyPair = async () => { await _sodium.ready; diff --git a/web/apps/cast/src/pages/slideshow.tsx b/web/apps/cast/src/pages/slideshow.tsx index 99b2209dec..b860523ab9 100644 --- a/web/apps/cast/src/pages/slideshow.tsx +++ b/web/apps/cast/src/pages/slideshow.tsx @@ -30,6 +30,7 @@ export default function Slideshow() { const syncCastFiles = async (token: string) => { try { + console.log("syncCastFiles"); const castToken = window.localStorage.getItem("castToken"); const requestedCollectionKey = window.localStorage.getItem("collectionKey"); @@ -50,6 +51,7 @@ export default function Slideshow() { } } catch (e) { log.error("error during sync", e); + // go back to preview page router.push("/"); } }; @@ -100,45 +102,54 @@ export default function Slideshow() { }, [collectionFiles]); const showNextSlide = async () => { - const currentIndex = collectionFiles.findIndex( - (file) => file.id === currentFileId, - ); + try { + const currentIndex = collectionFiles.findIndex( + (file) => file.id === currentFileId, + ); - const nextIndex = (currentIndex + 1) % collectionFiles.length; - const nextNextIndex = (nextIndex + 1) % collectionFiles.length; + const nextIndex = (currentIndex + 1) % collectionFiles.length; + const nextNextIndex = (nextIndex + 1) % collectionFiles.length; - const nextFile = collectionFiles[nextIndex]; - const nextNextFile = collectionFiles[nextNextIndex]; + const nextFile = collectionFiles[nextIndex]; + const nextNextFile = collectionFiles[nextNextIndex]; - let nextURL = renderableFileURLCache.get(nextFile.id); - let nextNextURL = renderableFileURLCache.get(nextNextFile.id); + let nextURL = renderableFileURLCache.get(nextFile.id); + let nextNextURL = renderableFileURLCache.get(nextNextFile.id); - if (!nextURL) { - try { - const blob = await getPreviewableImage(nextFile, castToken); - const url = URL.createObjectURL(blob); - renderableFileURLCache.set(nextFile.id, url); - nextURL = url; - } catch (e) { - return; + if (!nextURL) { + try { + const blob = await getPreviewableImage(nextFile, castToken); + const url = URL.createObjectURL(blob); + renderableFileURLCache.set(nextFile.id, url); + nextURL = url; + } catch (e) { + console.log("error in nextUrl", e); + return; + } } - } - if (!nextNextURL) { - try { - const blob = await getPreviewableImage(nextNextFile, castToken); - const url = URL.createObjectURL(blob); - renderableFileURLCache.set(nextNextFile.id, url); - nextNextURL = url; - } catch (e) { - return; + if (!nextNextURL) { + try { + const blob = await getPreviewableImage( + nextNextFile, + castToken, + ); + const url = URL.createObjectURL(blob); + renderableFileURLCache.set(nextNextFile.id, url); + nextNextURL = url; + } catch (e) { + console.log("error in nextNextURL", e); + return; + } } - } - setLoading(false); - setCurrentFileId(nextFile.id); - setCurrentFileURL(nextURL); - setNextFileURL(nextNextURL); + setLoading(false); + setCurrentFileId(nextFile.id); + setCurrentFileURL(nextURL); + setNextFileURL(nextNextURL); + } catch (e) { + console.log("error in showNextSlide", e); + } }; if (loading) return ; From d308d334f8c9d53abb7fa6cbdb33e9cc3d77eb8c Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 17:01:24 +0530 Subject: [PATCH 153/240] tt --- desktop/src/main.ts | 6 +++--- desktop/src/main/services/auto-launcher.ts | 2 +- desktop/src/main/services/ml-clip.ts | 4 ++-- desktop/src/main/services/ml-face.ts | 4 +++- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/desktop/src/main.ts b/desktop/src/main.ts index 4cd25881f0..b0c51fb937 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -146,7 +146,7 @@ const registerPrivilegedSchemes = () => { * * This window will show the HTML served from {@link rendererURL}. */ -const createMainWindow = async () => { +const createMainWindow = () => { // Create the main window. This'll show our web content. const window = new BrowserWindow({ webPreferences: { @@ -160,7 +160,7 @@ const createMainWindow = async () => { show: false, }); - const wasAutoLaunched = await autoLauncher.wasAutoLaunched(); + const wasAutoLaunched = autoLauncher.wasAutoLaunched(); if (wasAutoLaunched) { // Don't automatically show the app's window if we were auto-launched. // On macOS, also hide the dock icon on macOS. @@ -367,7 +367,7 @@ const main = () => { // Note that some Electron APIs can only be used after this event occurs. app.on("ready", async () => { // Create window and prepare for the renderer. - mainWindow = await createMainWindow(); + mainWindow = createMainWindow(); attachIPCHandlers(); attachFSWatchIPCHandlers(createWatcher(mainWindow)); registerStreamProtocol(); diff --git a/desktop/src/main/services/auto-launcher.ts b/desktop/src/main/services/auto-launcher.ts index c704f73999..4e97a02257 100644 --- a/desktop/src/main/services/auto-launcher.ts +++ b/desktop/src/main/services/auto-launcher.ts @@ -38,7 +38,7 @@ class AutoLauncher { } } - async wasAutoLaunched() { + wasAutoLaunched() { if (this.autoLaunch) { return app.commandLine.hasSwitch("hidden"); } else { diff --git a/desktop/src/main/services/ml-clip.ts b/desktop/src/main/services/ml-clip.ts index 451cdcb09b..ddbfb0881d 100644 --- a/desktop/src/main/services/ml-clip.ts +++ b/desktop/src/main/services/ml-clip.ts @@ -49,7 +49,7 @@ const clipImageEmbedding_ = async (jpegFilePath: string) => { return normalizeEmbedding(imageEmbedding); }; -const getRGBData = async (jpegFilePath: string) => { +const getRGBData = async (jpegFilePath: string): Promise => { const jpegData = await fs.readFile(jpegFilePath); const rawImageData = jpeg.decode(jpegData, { useTArray: true, @@ -64,7 +64,7 @@ const getRGBData = async (jpegFilePath: string) => { const ny2 = 224; const totalSize = 3 * nx2 * ny2; - const result: number[] = Array(totalSize).fill(0); + const result = Array(totalSize).fill(0); const scale = Math.max(nx, ny) / 224; const nx3 = Math.round(nx / scale); diff --git a/desktop/src/main/services/ml-face.ts b/desktop/src/main/services/ml-face.ts index e4e43a4b0e..e72f043e06 100644 --- a/desktop/src/main/services/ml-face.ts +++ b/desktop/src/main/services/ml-face.ts @@ -47,5 +47,7 @@ export const faceEmbedding = async (input: Float32Array) => { const results = await session.run(feeds); log.debug(() => `onnx/yolo face embedding took ${Date.now() - t} ms`); /* Need these model specific casts to extract and type the result */ - return (results.embeddings as unknown as any)["cpuData"] as Float32Array; + return (results.embeddings as unknown as Record)[ + "cpuData" + ] as Float32Array; }; From 82316ff290f41db23ba380b8c3d67f72a8aa9d74 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 17:43:30 +0530 Subject: [PATCH 154/240] Unawaited promises --- desktop/.eslintrc.js | 2 +- desktop/src/main/menu.ts | 2 +- desktop/src/main/services/upload.ts | 4 ++-- desktop/src/main/utils/temp.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/desktop/.eslintrc.js b/desktop/.eslintrc.js index 541ba33a9c..daf2c2838e 100644 --- a/desktop/.eslintrc.js +++ b/desktop/.eslintrc.js @@ -29,6 +29,6 @@ module.exports = { "@typescript-eslint/no-unsafe-return": "off", "@typescript-eslint/no-confusing-void-expression": "off", "@typescript-eslint/no-misused-promises": "off", - "@typescript-eslint/no-floating-promises": "off", + // "@typescript-eslint/no-floating-promises": "off", }, }; diff --git a/desktop/src/main/menu.ts b/desktop/src/main/menu.ts index 0693c01dc0..5dd2b335ef 100644 --- a/desktop/src/main/menu.ts +++ b/desktop/src/main/menu.ts @@ -35,7 +35,7 @@ export const createApplicationMenu = async (mainWindow: BrowserWindow) => { ); const toggleAutoLaunch = () => { - autoLauncher.toggleAutoLaunch(); + void autoLauncher.toggleAutoLaunch(); isAutoLaunchEnabled = !isAutoLaunchEnabled; }; diff --git a/desktop/src/main/services/upload.ts b/desktop/src/main/services/upload.ts index cf96399190..96835bfb00 100644 --- a/desktop/src/main/services/upload.ts +++ b/desktop/src/main/services/upload.ts @@ -20,7 +20,7 @@ export const listZipItems = async (zipPath: string): Promise => { } } - zip.close(); + await zip.close(); return entryNames.map((entryName) => [zipPath, entryName]); }; @@ -40,7 +40,7 @@ export const pathOrZipItemSize = async ( `An entry with name ${entryName} does not exist in the zip file at ${zipPath}`, ); const size = entry.size; - zip.close(); + await zip.close(); return size; } }; diff --git a/desktop/src/main/utils/temp.ts b/desktop/src/main/utils/temp.ts index b336a902f1..582f0a2b3d 100644 --- a/desktop/src/main/utils/temp.ts +++ b/desktop/src/main/utils/temp.ts @@ -114,7 +114,7 @@ export const makeFileForDataOrPathOrZipItem = async ( const [zipPath, entryName] = dataOrPathOrZipItem; const zip = new StreamZip.async({ file: zipPath }); await zip.extract(entryName, path); - zip.close(); + await zip.close(); }; } } From ecfb7d944dd468770e140b5f6a71d8305ac20acd Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Tue, 30 Apr 2024 17:45:35 +0530 Subject: [PATCH 155/240] [web][cast] Add slide logs --- web/apps/cast/src/pages/slideshow.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/web/apps/cast/src/pages/slideshow.tsx b/web/apps/cast/src/pages/slideshow.tsx index b860523ab9..11b34baf75 100644 --- a/web/apps/cast/src/pages/slideshow.tsx +++ b/web/apps/cast/src/pages/slideshow.tsx @@ -103,13 +103,26 @@ export default function Slideshow() { const showNextSlide = async () => { try { + console.log("showNextSlide"); const currentIndex = collectionFiles.findIndex( (file) => file.id === currentFileId, ); + console.log( + "showNextSlide-index", + currentIndex, + collectionFiles.length, + ); + const nextIndex = (currentIndex + 1) % collectionFiles.length; const nextNextIndex = (nextIndex + 1) % collectionFiles.length; + console.log( + "showNextSlide-nextIndex and nextNextIndex", + nextIndex, + nextNextIndex, + ); + const nextFile = collectionFiles[nextIndex]; const nextNextFile = collectionFiles[nextNextIndex]; From 13d5a9f71af2a5f996f3ef7a9561c400b35f67d5 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Tue, 30 Apr 2024 18:09:35 +0530 Subject: [PATCH 156/240] [web][cast] Add logs --- web/apps/cast/src/components/PhotoAuditorium.tsx | 4 +++- web/apps/cast/src/pages/slideshow.tsx | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/web/apps/cast/src/components/PhotoAuditorium.tsx b/web/apps/cast/src/components/PhotoAuditorium.tsx index 6aa2c3990b..c77c9e6cad 100644 --- a/web/apps/cast/src/components/PhotoAuditorium.tsx +++ b/web/apps/cast/src/components/PhotoAuditorium.tsx @@ -11,14 +11,16 @@ export const PhotoAuditorium: React.FC = ({ showNextSlide, }) => { useEffect(() => { + console.log("showing slide"); const timeoutId = window.setTimeout(() => { + console.log("showing next slide timer"); showNextSlide(); }, 10000); return () => { if (timeoutId) clearTimeout(timeoutId); }; - }, [showNextSlide]); + }, []); return (
Date: Tue, 30 Apr 2024 18:20:01 +0530 Subject: [PATCH 157/240] [web][cast] More logs --- web/apps/cast/src/pages/index.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/web/apps/cast/src/pages/index.tsx b/web/apps/cast/src/pages/index.tsx index fa4d9e4859..7ad310fe12 100644 --- a/web/apps/cast/src/pages/index.tsx +++ b/web/apps/cast/src/pages/index.tsx @@ -174,12 +174,18 @@ export default function PairingMode() { const router = useRouter(); useEffect(() => { + console.log("useEffect for pairing called"); if (digits.length < 1 || !publicKeyB64 || !privateKeyB64) return; const interval = setInterval(async () => { + console.log("polling for cast data"); const data = await pollForCastData(); - if (!data) return; + if (!data) { + console.log("no data"); + return; + } storeCastData(data); + console.log("pushing slideshow"); await router.push("/slideshow"); }, 1000); From bda52267967427e7fb73c7614d16a59811293af7 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 18:32:34 +0530 Subject: [PATCH 158/240] More unawaited --- desktop/src/main.ts | 12 ++++++------ desktop/src/main/services/app-update.ts | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/desktop/src/main.ts b/desktop/src/main.ts index b0c51fb937..f5ad891588 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -202,8 +202,8 @@ const createMainWindow = () => { app.dock.hide(); }); - window.on("show", () => { - if (process.platform == "darwin") app.dock.show(); + window.on("show", async () => { + if (process.platform == "darwin") await app.dock.show(); }); // Let ipcRenderer know when mainWindow is in the foreground so that it can @@ -257,7 +257,7 @@ export const allowExternalLinks = (webContents: WebContents) => { // Returning `action` "deny" accomplishes this. webContents.setWindowOpenHandler(({ url }) => { if (!url.startsWith(rendererURL)) { - shell.openExternal(url); + void shell.openExternal(url); return { action: "deny" }; } else { return { action: "allow" }; @@ -377,7 +377,7 @@ const main = () => { allowExternalLinks(mainWindow.webContents); // Start loading the renderer. - mainWindow.loadURL(rendererURL); + void mainWindow.loadURL(rendererURL); // Continue on with the rest of the startup sequence. Menu.setApplicationMenu(await createApplicationMenu(mainWindow)); @@ -385,8 +385,8 @@ const main = () => { if (!isDev) setupAutoUpdater(mainWindow); try { - deleteLegacyDiskCacheDirIfExists(); - deleteLegacyKeysStoreIfExists(); + await deleteLegacyDiskCacheDirIfExists(); + await deleteLegacyKeysStoreIfExists(); } catch (e) { // Log but otherwise ignore errors during non-critical startup // actions. diff --git a/desktop/src/main/services/app-update.ts b/desktop/src/main/services/app-update.ts index e20d42fb70..a9e3109342 100644 --- a/desktop/src/main/services/app-update.ts +++ b/desktop/src/main/services/app-update.ts @@ -13,7 +13,7 @@ export const setupAutoUpdater = (mainWindow: BrowserWindow) => { const oneDay = 1 * 24 * 60 * 60 * 1000; setInterval(() => checkForUpdatesAndNotify(mainWindow), oneDay); - checkForUpdatesAndNotify(mainWindow); + void checkForUpdatesAndNotify(mainWindow); }; /** @@ -22,7 +22,7 @@ export const setupAutoUpdater = (mainWindow: BrowserWindow) => { export const forceCheckForAppUpdates = (mainWindow: BrowserWindow) => { userPreferences.delete("skipAppVersion"); userPreferences.delete("muteUpdateNotificationVersion"); - checkForUpdatesAndNotify(mainWindow); + void checkForUpdatesAndNotify(mainWindow); }; const checkForUpdatesAndNotify = async (mainWindow: BrowserWindow) => { @@ -56,7 +56,7 @@ const checkForUpdatesAndNotify = async (mainWindow: BrowserWindow) => { mainWindow.webContents.send("appUpdateAvailable", update); log.debug(() => "Attempting auto update"); - autoUpdater.downloadUpdate(); + await autoUpdater.downloadUpdate(); let timeoutId: ReturnType; const fiveMinutes = 5 * 60 * 1000; From 9a281725652ebf96120e71d0426ec938d6deb613 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 18:37:50 +0530 Subject: [PATCH 159/240] iife wrapper --- desktop/.eslintrc.js | 2 +- desktop/src/main.ts | 52 +++++++++++++++++++++++--------------------- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/desktop/.eslintrc.js b/desktop/.eslintrc.js index daf2c2838e..8c1867fc79 100644 --- a/desktop/.eslintrc.js +++ b/desktop/.eslintrc.js @@ -28,7 +28,7 @@ module.exports = { /* Temporary (RIP) */ "@typescript-eslint/no-unsafe-return": "off", "@typescript-eslint/no-confusing-void-expression": "off", - "@typescript-eslint/no-misused-promises": "off", + // "@typescript-eslint/no-misused-promises": "off", // "@typescript-eslint/no-floating-promises": "off", }, }; diff --git a/desktop/src/main.ts b/desktop/src/main.ts index f5ad891588..c849c755f7 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -202,8 +202,8 @@ const createMainWindow = () => { app.dock.hide(); }); - window.on("show", async () => { - if (process.platform == "darwin") await app.dock.show(); + window.on("show", () => { + if (process.platform == "darwin") void app.dock.show(); }); // Let ipcRenderer know when mainWindow is in the foreground so that it can @@ -365,33 +365,35 @@ const main = () => { // Emitted once, when Electron has finished initializing. // // Note that some Electron APIs can only be used after this event occurs. - app.on("ready", async () => { - // Create window and prepare for the renderer. - mainWindow = createMainWindow(); - attachIPCHandlers(); - attachFSWatchIPCHandlers(createWatcher(mainWindow)); - registerStreamProtocol(); + app.on("ready", () => { + void (async () => { + // Create window and prepare for the renderer. + mainWindow = createMainWindow(); + attachIPCHandlers(); + attachFSWatchIPCHandlers(createWatcher(mainWindow)); + registerStreamProtocol(); - // Configure the renderer's environment. - setDownloadPath(mainWindow.webContents); - allowExternalLinks(mainWindow.webContents); + // Configure the renderer's environment. + setDownloadPath(mainWindow.webContents); + allowExternalLinks(mainWindow.webContents); - // Start loading the renderer. - void mainWindow.loadURL(rendererURL); + // Start loading the renderer. + void mainWindow.loadURL(rendererURL); - // Continue on with the rest of the startup sequence. - Menu.setApplicationMenu(await createApplicationMenu(mainWindow)); - setupTrayItem(mainWindow); - if (!isDev) setupAutoUpdater(mainWindow); + // Continue on with the rest of the startup sequence. + Menu.setApplicationMenu(await createApplicationMenu(mainWindow)); + setupTrayItem(mainWindow); + if (!isDev) setupAutoUpdater(mainWindow); - try { - await deleteLegacyDiskCacheDirIfExists(); - await deleteLegacyKeysStoreIfExists(); - } catch (e) { - // Log but otherwise ignore errors during non-critical startup - // actions. - log.error("Ignoring startup error", e); - } + try { + await deleteLegacyDiskCacheDirIfExists(); + await deleteLegacyKeysStoreIfExists(); + } catch (e) { + // Log but otherwise ignore errors during non-critical startup + // actions. + log.error("Ignoring startup error", e); + } + })(); }); // This is a macOS only event. Show our window when the user activates the From 7b16fa9f38cc4dcfe64d3c88769d2d3058c57498 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 18:39:18 +0530 Subject: [PATCH 160/240] void --- desktop/src/main/menu.ts | 12 +++++++----- desktop/src/main/services/app-update.ts | 2 +- desktop/src/main/stream.ts | 10 +++++----- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/desktop/src/main/menu.ts b/desktop/src/main/menu.ts index 5dd2b335ef..1019c7a8fb 100644 --- a/desktop/src/main/menu.ts +++ b/desktop/src/main/menu.ts @@ -30,7 +30,7 @@ export const createApplicationMenu = async (mainWindow: BrowserWindow) => { const handleCheckForUpdates = () => forceCheckForAppUpdates(mainWindow); const handleViewChangelog = () => - shell.openExternal( + void shell.openExternal( "https://github.com/ente-io/ente/blob/main/desktop/CHANGELOG.md", ); @@ -46,13 +46,15 @@ export const createApplicationMenu = async (mainWindow: BrowserWindow) => { shouldHideDockIcon = !shouldHideDockIcon; }; - const handleHelp = () => shell.openExternal("https://help.ente.io/photos/"); + const handleHelp = () => + void shell.openExternal("https://help.ente.io/photos/"); - const handleSupport = () => shell.openExternal("mailto:support@ente.io"); + const handleSupport = () => + void shell.openExternal("mailto:support@ente.io"); - const handleBlog = () => shell.openExternal("https://ente.io/blog/"); + const handleBlog = () => void shell.openExternal("https://ente.io/blog/"); - const handleViewLogs = openLogDirectory; + const handleViewLogs = () => void openLogDirectory(); return Menu.buildFromTemplate([ { diff --git a/desktop/src/main/services/app-update.ts b/desktop/src/main/services/app-update.ts index a9e3109342..bc4bd38d6d 100644 --- a/desktop/src/main/services/app-update.ts +++ b/desktop/src/main/services/app-update.ts @@ -12,7 +12,7 @@ export const setupAutoUpdater = (mainWindow: BrowserWindow) => { autoUpdater.autoDownload = false; const oneDay = 1 * 24 * 60 * 60 * 1000; - setInterval(() => checkForUpdatesAndNotify(mainWindow), oneDay); + setInterval(() => void checkForUpdatesAndNotify(mainWindow), oneDay); void checkForUpdatesAndNotify(mainWindow); }; diff --git a/desktop/src/main/stream.ts b/desktop/src/main/stream.ts index b97900659f..3e27de12b4 100644 --- a/desktop/src/main/stream.ts +++ b/desktop/src/main/stream.ts @@ -117,7 +117,7 @@ const handleReadZip = async (zipPath: string, entryName: string) => { // Close the zip handle when the underlying stream closes. // TODO(MR): Verify - stream.on("end", () => zip.close()); + stream.on("end", () => void zip.close()); return new Response(webReadableStream, { headers: { @@ -173,17 +173,17 @@ export const writeStream = (filePath: string, readableStream: ReadableStream) => const writeNodeStream = async (filePath: string, fileStream: Readable) => { const writeable = createWriteStream(filePath); - fileStream.on("error", (error) => { - writeable.destroy(error); // Close the writable stream with an error + fileStream.on("error", (err) => { + writeable.destroy(err); // Close the writable stream with an error }); fileStream.pipe(writeable); await new Promise((resolve, reject) => { writeable.on("finish", resolve); - writeable.on("error", async (err: Error) => { + writeable.on("error", (err) => { if (existsSync(filePath)) { - await fs.unlink(filePath); + void fs.unlink(filePath); } reject(err); }); From 7e2ee61a97dc612d88a89b4e993d5500b9de6de9 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 18:46:13 +0530 Subject: [PATCH 161/240] void expressions are fine --- desktop/.eslintrc.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/desktop/.eslintrc.js b/desktop/.eslintrc.js index 8c1867fc79..33c458b14c 100644 --- a/desktop/.eslintrc.js +++ b/desktop/.eslintrc.js @@ -4,8 +4,8 @@ module.exports = { extends: [ "eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", - /* What we really want eventually */ "plugin:@typescript-eslint/strict-type-checked", + /* What we really want eventually */ // "plugin:@typescript-eslint/stylistic-type-checked", ], plugins: ["@typescript-eslint"], @@ -19,16 +19,21 @@ module.exports = { node: true, }, rules: { + /* Allow numbers to be used in template literals */ "@typescript-eslint/restrict-template-expressions": [ "error", { allowNumber: true, }, ], + /* Allow void expressions as the entire body of an arrow function */ + "@typescript-eslint/no-confusing-void-expression": [ + "error", + { + ignoreArrowShorthand: true, + }, + ], /* Temporary (RIP) */ "@typescript-eslint/no-unsafe-return": "off", - "@typescript-eslint/no-confusing-void-expression": "off", - // "@typescript-eslint/no-misused-promises": "off", - // "@typescript-eslint/no-floating-promises": "off", }, }; From 76c98bdf326865e66c411415b7a78ddb465f6ac6 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 18:50:15 +0530 Subject: [PATCH 162/240] handle unsafe returns --- desktop/.eslintrc.js | 2 - desktop/src/main/services/ml-clip.ts | 2 +- desktop/src/preload.ts | 86 +++++++++++----------------- 3 files changed, 36 insertions(+), 54 deletions(-) diff --git a/desktop/.eslintrc.js b/desktop/.eslintrc.js index 33c458b14c..ed16db5264 100644 --- a/desktop/.eslintrc.js +++ b/desktop/.eslintrc.js @@ -33,7 +33,5 @@ module.exports = { ignoreArrowShorthand: true, }, ], - /* Temporary (RIP) */ - "@typescript-eslint/no-unsafe-return": "off", }, }; diff --git a/desktop/src/main/services/ml-clip.ts b/desktop/src/main/services/ml-clip.ts index ddbfb0881d..c9378edf29 100644 --- a/desktop/src/main/services/ml-clip.ts +++ b/desktop/src/main/services/ml-clip.ts @@ -64,7 +64,7 @@ const getRGBData = async (jpegFilePath: string): Promise => { const ny2 = 224; const totalSize = 3 * nx2 * ny2; - const result = Array(totalSize).fill(0); + const result = Array(totalSize).fill(0); const scale = Math.max(nx, ny) / 224; const nx3 = Math.round(nx / scale); diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index ecc800db3c..2b5eb8fcc3 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -51,26 +51,23 @@ import type { // - General -const appVersion = (): Promise => ipcRenderer.invoke("appVersion"); +const appVersion = () => ipcRenderer.invoke("appVersion"); const logToDisk = (message: string): void => ipcRenderer.send("logToDisk", message); -const openDirectory = (dirPath: string): Promise => +const openDirectory = (dirPath: string) => ipcRenderer.invoke("openDirectory", dirPath); -const openLogDirectory = (): Promise => - ipcRenderer.invoke("openLogDirectory"); +const openLogDirectory = () => ipcRenderer.invoke("openLogDirectory"); -const selectDirectory = (): Promise => - ipcRenderer.invoke("selectDirectory"); +const selectDirectory = () => ipcRenderer.invoke("selectDirectory"); const clearStores = () => ipcRenderer.send("clearStores"); -const encryptionKey = (): Promise => - ipcRenderer.invoke("encryptionKey"); +const encryptionKey = () => ipcRenderer.invoke("encryptionKey"); -const saveEncryptionKey = (encryptionKey: string): Promise => +const saveEncryptionKey = (encryptionKey: string) => ipcRenderer.invoke("saveEncryptionKey", encryptionKey); const onMainWindowFocus = (cb?: () => void) => { @@ -102,39 +99,36 @@ const skipAppUpdate = (version: string) => { // - FS -const fsExists = (path: string): Promise => - ipcRenderer.invoke("fsExists", path); +const fsExists = (path: string) => ipcRenderer.invoke("fsExists", path); -const fsMkdirIfNeeded = (dirPath: string): Promise => +const fsMkdirIfNeeded = (dirPath: string) => ipcRenderer.invoke("fsMkdirIfNeeded", dirPath); -const fsRename = (oldPath: string, newPath: string): Promise => +const fsRename = (oldPath: string, newPath: string) => ipcRenderer.invoke("fsRename", oldPath, newPath); -const fsRmdir = (path: string): Promise => - ipcRenderer.invoke("fsRmdir", path); +const fsRmdir = (path: string) => ipcRenderer.invoke("fsRmdir", path); -const fsRm = (path: string): Promise => ipcRenderer.invoke("fsRm", path); +const fsRm = (path: string) => ipcRenderer.invoke("fsRm", path); -const fsReadTextFile = (path: string): Promise => +const fsReadTextFile = (path: string) => ipcRenderer.invoke("fsReadTextFile", path); -const fsWriteFile = (path: string, contents: string): Promise => +const fsWriteFile = (path: string, contents: string) => ipcRenderer.invoke("fsWriteFile", path, contents); -const fsIsDir = (dirPath: string): Promise => - ipcRenderer.invoke("fsIsDir", dirPath); +const fsIsDir = (dirPath: string) => ipcRenderer.invoke("fsIsDir", dirPath); // - Conversion -const convertToJPEG = (imageData: Uint8Array): Promise => +const convertToJPEG = (imageData: Uint8Array) => ipcRenderer.invoke("convertToJPEG", imageData); const generateImageThumbnail = ( dataOrPathOrZipItem: Uint8Array | string | ZipItem, maxDimension: number, maxSize: number, -): Promise => +) => ipcRenderer.invoke( "generateImageThumbnail", dataOrPathOrZipItem, @@ -147,7 +141,7 @@ const ffmpegExec = ( dataOrPathOrZipItem: Uint8Array | string | ZipItem, outputFileExtension: string, timeoutMS: number, -): Promise => +) => ipcRenderer.invoke( "ffmpegExec", command, @@ -158,44 +152,37 @@ const ffmpegExec = ( // - ML -const clipImageEmbedding = (jpegImageData: Uint8Array): Promise => +const clipImageEmbedding = (jpegImageData: Uint8Array) => ipcRenderer.invoke("clipImageEmbedding", jpegImageData); -const clipTextEmbeddingIfAvailable = ( - text: string, -): Promise => +const clipTextEmbeddingIfAvailable = (text: string) => ipcRenderer.invoke("clipTextEmbeddingIfAvailable", text); -const detectFaces = (input: Float32Array): Promise => +const detectFaces = (input: Float32Array) => ipcRenderer.invoke("detectFaces", input); -const faceEmbedding = (input: Float32Array): Promise => +const faceEmbedding = (input: Float32Array) => ipcRenderer.invoke("faceEmbedding", input); // - Watch -const watchGet = (): Promise => ipcRenderer.invoke("watchGet"); +const watchGet = () => ipcRenderer.invoke("watchGet"); -const watchAdd = ( - folderPath: string, - collectionMapping: CollectionMapping, -): Promise => +const watchAdd = (folderPath: string, collectionMapping: CollectionMapping) => ipcRenderer.invoke("watchAdd", folderPath, collectionMapping); -const watchRemove = (folderPath: string): Promise => +const watchRemove = (folderPath: string) => ipcRenderer.invoke("watchRemove", folderPath); const watchUpdateSyncedFiles = ( syncedFiles: FolderWatch["syncedFiles"], folderPath: string, -): Promise => - ipcRenderer.invoke("watchUpdateSyncedFiles", syncedFiles, folderPath); +) => ipcRenderer.invoke("watchUpdateSyncedFiles", syncedFiles, folderPath); const watchUpdateIgnoredFiles = ( ignoredFiles: FolderWatch["ignoredFiles"], folderPath: string, -): Promise => - ipcRenderer.invoke("watchUpdateIgnoredFiles", ignoredFiles, folderPath); +) => ipcRenderer.invoke("watchUpdateIgnoredFiles", ignoredFiles, folderPath); const watchOnAddFile = (f: (path: string, watch: FolderWatch) => void) => { ipcRenderer.removeAllListeners("watchAddFile"); @@ -218,34 +205,31 @@ const watchOnRemoveDir = (f: (path: string, watch: FolderWatch) => void) => { ); }; -const watchFindFiles = (folderPath: string): Promise => +const watchFindFiles = (folderPath: string) => ipcRenderer.invoke("watchFindFiles", folderPath); // - Upload const pathForFile = (file: File) => webUtils.getPathForFile(file); -const listZipItems = (zipPath: string): Promise => +const listZipItems = (zipPath: string) => ipcRenderer.invoke("listZipItems", zipPath); -const pathOrZipItemSize = (pathOrZipItem: string | ZipItem): Promise => +const pathOrZipItemSize = (pathOrZipItem: string | ZipItem) => ipcRenderer.invoke("pathOrZipItemSize", pathOrZipItem); -const pendingUploads = (): Promise => - ipcRenderer.invoke("pendingUploads"); +const pendingUploads = () => ipcRenderer.invoke("pendingUploads"); -const setPendingUploads = (pendingUploads: PendingUploads): Promise => +const setPendingUploads = (pendingUploads: PendingUploads) => ipcRenderer.invoke("setPendingUploads", pendingUploads); -const markUploadedFiles = (paths: PendingUploads["filePaths"]): Promise => +const markUploadedFiles = (paths: PendingUploads["filePaths"]) => ipcRenderer.invoke("markUploadedFiles", paths); -const markUploadedZipItems = ( - items: PendingUploads["zipItems"], -): Promise => ipcRenderer.invoke("markUploadedZipItems", items); +const markUploadedZipItems = (items: PendingUploads["zipItems"]) => + ipcRenderer.invoke("markUploadedZipItems", items); -const clearPendingUploads = (): Promise => - ipcRenderer.invoke("clearPendingUploads"); +const clearPendingUploads = () => ipcRenderer.invoke("clearPendingUploads"); /** * These objects exposed here will become available to the JS code in our From 50a14470202512df6d72eea2104a19da7f69fa0f Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 18:53:17 +0530 Subject: [PATCH 163/240] Stylistic --- desktop/.eslintrc.js | 3 +-- desktop/src/main/services/ml-clip.ts | 13 ++++++------- desktop/src/main/services/ml-face.ts | 7 +++---- desktop/src/main/services/upload.ts | 2 +- desktop/src/main/utils/temp.ts | 4 +++- 5 files changed, 14 insertions(+), 15 deletions(-) diff --git a/desktop/.eslintrc.js b/desktop/.eslintrc.js index ed16db5264..44d03ef0c1 100644 --- a/desktop/.eslintrc.js +++ b/desktop/.eslintrc.js @@ -5,8 +5,7 @@ module.exports = { "eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/strict-type-checked", - /* What we really want eventually */ - // "plugin:@typescript-eslint/stylistic-type-checked", + "plugin:@typescript-eslint/stylistic-type-checked", ], plugins: ["@typescript-eslint"], parser: "@typescript-eslint/parser", diff --git a/desktop/src/main/services/ml-clip.ts b/desktop/src/main/services/ml-clip.ts index c9378edf29..e3dd99204a 100644 --- a/desktop/src/main/services/ml-clip.ts +++ b/desktop/src/main/services/ml-clip.ts @@ -45,7 +45,7 @@ const clipImageEmbedding_ = async (jpegFilePath: string) => { `onnx/clip image embedding took ${Date.now() - t1} ms (prep: ${t2 - t1} ms, inference: ${Date.now() - t2} ms)`, ); /* Need these model specific casts to type the result */ - const imageEmbedding = ensure(results["output"]).data as Float32Array; + const imageEmbedding = ensure(results.output).data as Float32Array; return normalizeEmbedding(imageEmbedding); }; @@ -120,13 +120,12 @@ const getRGBData = async (jpegFilePath: string): Promise => { const normalizeEmbedding = (embedding: Float32Array) => { let normalization = 0; - for (let index = 0; index < embedding.length; index++) { - normalization += ensure(embedding[index]) * ensure(embedding[index]); - } + for (const v of embedding) normalization += v * v; + const sqrtNormalization = Math.sqrt(normalization); - for (let index = 0; index < embedding.length; index++) { + for (let index = 0; index < embedding.length; index++) embedding[index] = ensure(embedding[index]) / sqrtNormalization; - } + return embedding; }; @@ -168,6 +167,6 @@ export const clipTextEmbeddingIfAvailable = async (text: string) => { () => `onnx/clip text embedding took ${Date.now() - t1} ms (prep: ${t2 - t1} ms, inference: ${Date.now() - t2} ms)`, ); - const textEmbedding = ensure(results["output"]).data as Float32Array; + const textEmbedding = ensure(results.output).data as Float32Array; return normalizeEmbedding(textEmbedding); }; diff --git a/desktop/src/main/services/ml-face.ts b/desktop/src/main/services/ml-face.ts index e72f043e06..9765252555 100644 --- a/desktop/src/main/services/ml-face.ts +++ b/desktop/src/main/services/ml-face.ts @@ -24,7 +24,7 @@ export const detectFaces = async (input: Float32Array) => { }; const results = await session.run(feeds); log.debug(() => `onnx/yolo face detection took ${Date.now() - t} ms`); - return ensure(results["output"]).data; + return ensure(results.output).data; }; const cachedFaceEmbeddingSession = makeCachedInferenceSession( @@ -47,7 +47,6 @@ export const faceEmbedding = async (input: Float32Array) => { const results = await session.run(feeds); log.debug(() => `onnx/yolo face embedding took ${Date.now() - t} ms`); /* Need these model specific casts to extract and type the result */ - return (results.embeddings as unknown as Record)[ - "cpuData" - ] as Float32Array; + return (results.embeddings as unknown as Record) + .cpuData as Float32Array; }; diff --git a/desktop/src/main/services/upload.ts b/desktop/src/main/services/upload.ts index 96835bfb00..7871b56fda 100644 --- a/desktop/src/main/services/upload.ts +++ b/desktop/src/main/services/upload.ts @@ -14,7 +14,7 @@ export const listZipItems = async (zipPath: string): Promise => { for (const entry of Object.values(entries)) { const basename = path.basename(entry.name); // Ignore "hidden" files (files whose names begins with a dot). - if (entry.isFile && basename.length > 0 && basename[0] != ".") { + if (entry.isFile && basename.startsWith(".")) { // `entry.name` is the path within the zip. entryNames.push(entry.name); } diff --git a/desktop/src/main/utils/temp.ts b/desktop/src/main/utils/temp.ts index 582f0a2b3d..11f7a5d845 100644 --- a/desktop/src/main/utils/temp.ts +++ b/desktop/src/main/utils/temp.ts @@ -98,7 +98,9 @@ export const makeFileForDataOrPathOrZipItem = async ( ): Promise => { let path: string; let isFileTemporary: boolean; - let writeToTemporaryFile = async () => {}; + let writeToTemporaryFile = async () => { + /* no-op */ + }; if (typeof dataOrPathOrZipItem == "string") { path = dataOrPathOrZipItem; From 1eff04fe92bd670708aaa7b70ada9e81458718d6 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 18:57:19 +0530 Subject: [PATCH 164/240] Enable lints --- .github/workflows/desktop-lint.yml | 30 ++++++++++++++++++++++++++++++ desktop/package.json | 4 ++-- 2 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/desktop-lint.yml diff --git a/.github/workflows/desktop-lint.yml b/.github/workflows/desktop-lint.yml new file mode 100644 index 0000000000..0b8263f3d3 --- /dev/null +++ b/.github/workflows/desktop-lint.yml @@ -0,0 +1,30 @@ +name: "Lint (desktop)" + +on: + # Run on every push to a branch other than main that changes desktop/ + push: + branches-ignore: [main, "deploy/**"] + paths: + - "desktop/**" + - ".github/workflows/desktop-lint.yml" + +jobs: + lint: + runs-on: ubuntu-latest + defaults: + run: + working-directory: desktop + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup node and enable yarn caching + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: "yarn" + cache-dependency-path: "desktop/yarn.lock" + + - run: yarn install + + - run: yarn lint diff --git a/desktop/package.json b/desktop/package.json index 509ee85838..d4c571cad7 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -15,8 +15,8 @@ "dev-main": "tsc && electron app/main.js", "dev-renderer": "cd ../web && yarn install && yarn dev:photos", "postinstall": "electron-builder install-app-deps", - "lint": "yarn prettier --check . && eslint --ext .ts src", - "lint-fix": "yarn prettier --write . && eslint --fix --ext .ts src" + "lint": "yarn prettier --check . && eslint --ext .ts src && yarn tsc", + "lint-fix": "yarn prettier --write . && eslint --fix --ext .ts src && yarn tsc" }, "dependencies": { "any-shell-escape": "^0.1", From 348b78467478ad8cf2cfbdea86495248295da4e3 Mon Sep 17 00:00:00 2001 From: vishnukvmd Date: Tue, 30 Apr 2024 19:52:30 +0530 Subject: [PATCH 165/240] Remove Isar DB for Embeddings --- mobile/lib/db/embeddings_db.dart | 79 -- mobile/lib/db/embeddings_sqlite_db.dart | 16 + mobile/lib/models/embedding.dart | 10 - mobile/lib/models/embedding.g.dart | 1059 ----------------- .../semantic_search_service.dart | 1 + 5 files changed, 17 insertions(+), 1148 deletions(-) delete mode 100644 mobile/lib/db/embeddings_db.dart delete mode 100644 mobile/lib/models/embedding.g.dart diff --git a/mobile/lib/db/embeddings_db.dart b/mobile/lib/db/embeddings_db.dart deleted file mode 100644 index a339d4d0d2..0000000000 --- a/mobile/lib/db/embeddings_db.dart +++ /dev/null @@ -1,79 +0,0 @@ -import "dart:io"; - -import "package:isar/isar.dart"; -import 'package:path_provider/path_provider.dart'; -import "package:photos/core/event_bus.dart"; -import "package:photos/events/embedding_updated_event.dart"; -import "package:photos/models/embedding.dart"; - -class EmbeddingsDB { - late final Isar _isar; - - EmbeddingsDB._privateConstructor(); - - static final EmbeddingsDB instance = EmbeddingsDB._privateConstructor(); - - Future init() async { - final dir = await getApplicationDocumentsDirectory(); - _isar = await Isar.open( - [EmbeddingSchema], - directory: dir.path, - ); - await _clearDeprecatedStore(dir); - } - - Future clearTable() async { - await _isar.writeTxn(() => _isar.clear()); - } - - Future> getAll(Model model) async { - return _isar.embeddings.filter().modelEqualTo(model).findAll(); - } - - Future put(Embedding embedding) { - return _isar.writeTxn(() async { - await _isar.embeddings.putByIndex(Embedding.index, embedding); - Bus.instance.fire(EmbeddingUpdatedEvent()); - }); - } - - Future putMany(List embeddings) { - return _isar.writeTxn(() async { - await _isar.embeddings.putAllByIndex(Embedding.index, embeddings); - Bus.instance.fire(EmbeddingUpdatedEvent()); - }); - } - - Future> getUnsyncedEmbeddings() async { - return await _isar.embeddings.filter().updationTimeEqualTo(null).findAll(); - } - - Future deleteEmbeddings(List fileIDs) async { - await _isar.writeTxn(() async { - final embeddings = []; - for (final fileID in fileIDs) { - embeddings.addAll( - await _isar.embeddings.filter().fileIDEqualTo(fileID).findAll(), - ); - } - await _isar.embeddings.deleteAll(embeddings.map((e) => e.id).toList()); - Bus.instance.fire(EmbeddingUpdatedEvent()); - }); - } - - Future deleteAllForModel(Model model) async { - await _isar.writeTxn(() async { - final embeddings = - await _isar.embeddings.filter().modelEqualTo(model).findAll(); - await _isar.embeddings.deleteAll(embeddings.map((e) => e.id).toList()); - Bus.instance.fire(EmbeddingUpdatedEvent()); - }); - } - - Future _clearDeprecatedStore(Directory dir) async { - final deprecatedStore = Directory(dir.path + "/object-box-store"); - if (await deprecatedStore.exists()) { - await deprecatedStore.delete(recursive: true); - } - } -} diff --git a/mobile/lib/db/embeddings_sqlite_db.dart b/mobile/lib/db/embeddings_sqlite_db.dart index 1a101d4ebf..2c77af2811 100644 --- a/mobile/lib/db/embeddings_sqlite_db.dart +++ b/mobile/lib/db/embeddings_sqlite_db.dart @@ -27,6 +27,11 @@ class EmbeddingsDB { return _dbFuture!; } + Future init() async { + final dir = await getApplicationDocumentsDirectory(); + await _clearDeprecatedStores(dir); + } + Future _initDatabase() async { final Directory documentsDirectory = await getApplicationDocumentsDirectory(); @@ -126,4 +131,15 @@ class EmbeddingsDB { embedding.updationTime, ]; } + + Future _clearDeprecatedStores(Directory dir) async { + final deprecatedStore = Directory(dir.path + "/object-box-store"); + if (await deprecatedStore.exists()) { + await deprecatedStore.delete(recursive: true); + } + final deprecatedDB = File(dir.path + "/default.isar"); + if (await deprecatedDB.exists()) { + await deprecatedDB.delete(); + } + } } diff --git a/mobile/lib/models/embedding.dart b/mobile/lib/models/embedding.dart index 1f78687b91..c8f742caa9 100644 --- a/mobile/lib/models/embedding.dart +++ b/mobile/lib/models/embedding.dart @@ -1,17 +1,7 @@ import "dart:convert"; -import "package:isar/isar.dart"; - -part 'embedding.g.dart'; - -@collection class Embedding { - static const index = 'unique_file_model_embedding'; - - Id id = Isar.autoIncrement; final int fileID; - @enumerated - @Index(name: index, composite: [CompositeIndex('fileID')], unique: true, replace: true) final Model model; final List embedding; int? updationTime; diff --git a/mobile/lib/models/embedding.g.dart b/mobile/lib/models/embedding.g.dart deleted file mode 100644 index ca041a0d0a..0000000000 --- a/mobile/lib/models/embedding.g.dart +++ /dev/null @@ -1,1059 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'embedding.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetEmbeddingCollection on Isar { - IsarCollection get embeddings => this.collection(); -} - -const EmbeddingSchema = CollectionSchema( - name: r'Embedding', - id: -8064100183150254587, - properties: { - r'embedding': PropertySchema( - id: 0, - name: r'embedding', - type: IsarType.doubleList, - ), - r'fileID': PropertySchema( - id: 1, - name: r'fileID', - type: IsarType.long, - ), - r'model': PropertySchema( - id: 2, - name: r'model', - type: IsarType.byte, - enumMap: _EmbeddingmodelEnumValueMap, - ), - r'updationTime': PropertySchema( - id: 3, - name: r'updationTime', - type: IsarType.long, - ) - }, - estimateSize: _embeddingEstimateSize, - serialize: _embeddingSerialize, - deserialize: _embeddingDeserialize, - deserializeProp: _embeddingDeserializeProp, - idName: r'id', - indexes: { - r'unique_file_model_embedding': IndexSchema( - id: 6248303800853228628, - name: r'unique_file_model_embedding', - unique: true, - replace: true, - properties: [ - IndexPropertySchema( - name: r'model', - type: IndexType.value, - caseSensitive: false, - ), - IndexPropertySchema( - name: r'fileID', - type: IndexType.value, - caseSensitive: false, - ) - ], - ) - }, - links: {}, - embeddedSchemas: {}, - getId: _embeddingGetId, - getLinks: _embeddingGetLinks, - attach: _embeddingAttach, - version: '3.1.0+1', -); - -int _embeddingEstimateSize( - Embedding object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.embedding.length * 8; - return bytesCount; -} - -void _embeddingSerialize( - Embedding object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeDoubleList(offsets[0], object.embedding); - writer.writeLong(offsets[1], object.fileID); - writer.writeByte(offsets[2], object.model.index); - writer.writeLong(offsets[3], object.updationTime); -} - -Embedding _embeddingDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = Embedding( - embedding: reader.readDoubleList(offsets[0]) ?? [], - fileID: reader.readLong(offsets[1]), - model: _EmbeddingmodelValueEnumMap[reader.readByteOrNull(offsets[2])] ?? - Model.onnxClip, - updationTime: reader.readLongOrNull(offsets[3]), - ); - object.id = id; - return object; -} - -P _embeddingDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readDoubleList(offset) ?? []) as P; - case 1: - return (reader.readLong(offset)) as P; - case 2: - return (_EmbeddingmodelValueEnumMap[reader.readByteOrNull(offset)] ?? - Model.onnxClip) as P; - case 3: - return (reader.readLongOrNull(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -const _EmbeddingmodelEnumValueMap = { - 'onnxClip': 0, - 'ggmlClip': 1, -}; -const _EmbeddingmodelValueEnumMap = { - 0: Model.onnxClip, - 1: Model.ggmlClip, -}; - -Id _embeddingGetId(Embedding object) { - return object.id; -} - -List> _embeddingGetLinks(Embedding object) { - return []; -} - -void _embeddingAttach(IsarCollection col, Id id, Embedding object) { - object.id = id; -} - -extension EmbeddingByIndex on IsarCollection { - Future getByModelFileID(Model model, int fileID) { - return getByIndex(r'unique_file_model_embedding', [model, fileID]); - } - - Embedding? getByModelFileIDSync(Model model, int fileID) { - return getByIndexSync(r'unique_file_model_embedding', [model, fileID]); - } - - Future deleteByModelFileID(Model model, int fileID) { - return deleteByIndex(r'unique_file_model_embedding', [model, fileID]); - } - - bool deleteByModelFileIDSync(Model model, int fileID) { - return deleteByIndexSync(r'unique_file_model_embedding', [model, fileID]); - } - - Future> getAllByModelFileID( - List modelValues, List fileIDValues) { - final len = modelValues.length; - assert(fileIDValues.length == len, - 'All index values must have the same length'); - final values = >[]; - for (var i = 0; i < len; i++) { - values.add([modelValues[i], fileIDValues[i]]); - } - - return getAllByIndex(r'unique_file_model_embedding', values); - } - - List getAllByModelFileIDSync( - List modelValues, List fileIDValues) { - final len = modelValues.length; - assert(fileIDValues.length == len, - 'All index values must have the same length'); - final values = >[]; - for (var i = 0; i < len; i++) { - values.add([modelValues[i], fileIDValues[i]]); - } - - return getAllByIndexSync(r'unique_file_model_embedding', values); - } - - Future deleteAllByModelFileID( - List modelValues, List fileIDValues) { - final len = modelValues.length; - assert(fileIDValues.length == len, - 'All index values must have the same length'); - final values = >[]; - for (var i = 0; i < len; i++) { - values.add([modelValues[i], fileIDValues[i]]); - } - - return deleteAllByIndex(r'unique_file_model_embedding', values); - } - - int deleteAllByModelFileIDSync( - List modelValues, List fileIDValues) { - final len = modelValues.length; - assert(fileIDValues.length == len, - 'All index values must have the same length'); - final values = >[]; - for (var i = 0; i < len; i++) { - values.add([modelValues[i], fileIDValues[i]]); - } - - return deleteAllByIndexSync(r'unique_file_model_embedding', values); - } - - Future putByModelFileID(Embedding object) { - return putByIndex(r'unique_file_model_embedding', object); - } - - Id putByModelFileIDSync(Embedding object, {bool saveLinks = true}) { - return putByIndexSync(r'unique_file_model_embedding', object, - saveLinks: saveLinks); - } - - Future> putAllByModelFileID(List objects) { - return putAllByIndex(r'unique_file_model_embedding', objects); - } - - List putAllByModelFileIDSync(List objects, - {bool saveLinks = true}) { - return putAllByIndexSync(r'unique_file_model_embedding', objects, - saveLinks: saveLinks); - } -} - -extension EmbeddingQueryWhereSort - on QueryBuilder { - QueryBuilder anyId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } - - QueryBuilder anyModelFileID() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - const IndexWhereClause.any(indexName: r'unique_file_model_embedding'), - ); - }); - } -} - -extension EmbeddingQueryWhere - on QueryBuilder { - QueryBuilder idEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between( - lower: id, - upper: id, - )); - }); - } - - QueryBuilder idNotEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ); - } - }); - } - - QueryBuilder idGreaterThan(Id id, - {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: include), - ); - }); - } - - QueryBuilder idLessThan(Id id, - {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: include), - ); - }); - } - - QueryBuilder idBetween( - Id lowerId, - Id upperId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between( - lower: lowerId, - includeLower: includeLower, - upper: upperId, - includeUpper: includeUpper, - )); - }); - } - - QueryBuilder modelEqualToAnyFileID( - Model model) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IndexWhereClause.equalTo( - indexName: r'unique_file_model_embedding', - value: [model], - )); - }); - } - - QueryBuilder - modelNotEqualToAnyFileID(Model model) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause(IndexWhereClause.between( - indexName: r'unique_file_model_embedding', - lower: [], - upper: [model], - includeUpper: false, - )) - .addWhereClause(IndexWhereClause.between( - indexName: r'unique_file_model_embedding', - lower: [model], - includeLower: false, - upper: [], - )); - } else { - return query - .addWhereClause(IndexWhereClause.between( - indexName: r'unique_file_model_embedding', - lower: [model], - includeLower: false, - upper: [], - )) - .addWhereClause(IndexWhereClause.between( - indexName: r'unique_file_model_embedding', - lower: [], - upper: [model], - includeUpper: false, - )); - } - }); - } - - QueryBuilder - modelGreaterThanAnyFileID( - Model model, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IndexWhereClause.between( - indexName: r'unique_file_model_embedding', - lower: [model], - includeLower: include, - upper: [], - )); - }); - } - - QueryBuilder modelLessThanAnyFileID( - Model model, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IndexWhereClause.between( - indexName: r'unique_file_model_embedding', - lower: [], - upper: [model], - includeUpper: include, - )); - }); - } - - QueryBuilder modelBetweenAnyFileID( - Model lowerModel, - Model upperModel, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IndexWhereClause.between( - indexName: r'unique_file_model_embedding', - lower: [lowerModel], - includeLower: includeLower, - upper: [upperModel], - includeUpper: includeUpper, - )); - }); - } - - QueryBuilder modelFileIDEqualTo( - Model model, int fileID) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IndexWhereClause.equalTo( - indexName: r'unique_file_model_embedding', - value: [model, fileID], - )); - }); - } - - QueryBuilder - modelEqualToFileIDNotEqualTo(Model model, int fileID) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause(IndexWhereClause.between( - indexName: r'unique_file_model_embedding', - lower: [model], - upper: [model, fileID], - includeUpper: false, - )) - .addWhereClause(IndexWhereClause.between( - indexName: r'unique_file_model_embedding', - lower: [model, fileID], - includeLower: false, - upper: [model], - )); - } else { - return query - .addWhereClause(IndexWhereClause.between( - indexName: r'unique_file_model_embedding', - lower: [model, fileID], - includeLower: false, - upper: [model], - )) - .addWhereClause(IndexWhereClause.between( - indexName: r'unique_file_model_embedding', - lower: [model], - upper: [model, fileID], - includeUpper: false, - )); - } - }); - } - - QueryBuilder - modelEqualToFileIDGreaterThan( - Model model, - int fileID, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IndexWhereClause.between( - indexName: r'unique_file_model_embedding', - lower: [model, fileID], - includeLower: include, - upper: [model], - )); - }); - } - - QueryBuilder - modelEqualToFileIDLessThan( - Model model, - int fileID, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IndexWhereClause.between( - indexName: r'unique_file_model_embedding', - lower: [model], - upper: [model, fileID], - includeUpper: include, - )); - }); - } - - QueryBuilder - modelEqualToFileIDBetween( - Model model, - int lowerFileID, - int upperFileID, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IndexWhereClause.between( - indexName: r'unique_file_model_embedding', - lower: [model, lowerFileID], - includeLower: includeLower, - upper: [model, upperFileID], - includeUpper: includeUpper, - )); - }); - } -} - -extension EmbeddingQueryFilter - on QueryBuilder { - QueryBuilder - embeddingElementEqualTo( - double value, { - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'embedding', - value: value, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - embeddingElementGreaterThan( - double value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'embedding', - value: value, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - embeddingElementLessThan( - double value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'embedding', - value: value, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - embeddingElementBetween( - double lower, - double upper, { - bool includeLower = true, - bool includeUpper = true, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'embedding', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - embeddingLengthEqualTo(int length) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'embedding', - length, - true, - length, - true, - ); - }); - } - - QueryBuilder embeddingIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'embedding', - 0, - true, - 0, - true, - ); - }); - } - - QueryBuilder - embeddingIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'embedding', - 0, - false, - 999999, - true, - ); - }); - } - - QueryBuilder - embeddingLengthLessThan( - int length, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'embedding', - 0, - true, - length, - include, - ); - }); - } - - QueryBuilder - embeddingLengthGreaterThan( - int length, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'embedding', - length, - include, - 999999, - true, - ); - }); - } - - QueryBuilder - embeddingLengthBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'embedding', - lower, - includeLower, - upper, - includeUpper, - ); - }); - } - - QueryBuilder fileIDEqualTo( - int value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'fileID', - value: value, - )); - }); - } - - QueryBuilder fileIDGreaterThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'fileID', - value: value, - )); - }); - } - - QueryBuilder fileIDLessThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'fileID', - value: value, - )); - }); - } - - QueryBuilder fileIDBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'fileID', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - )); - }); - } - - QueryBuilder idEqualTo( - Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'id', - value: value, - )); - }); - } - - QueryBuilder idGreaterThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - )); - }); - } - - QueryBuilder idLessThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - )); - }); - } - - QueryBuilder idBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - )); - }); - } - - QueryBuilder modelEqualTo( - Model value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'model', - value: value, - )); - }); - } - - QueryBuilder modelGreaterThan( - Model value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'model', - value: value, - )); - }); - } - - QueryBuilder modelLessThan( - Model value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'model', - value: value, - )); - }); - } - - QueryBuilder modelBetween( - Model lower, - Model upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'model', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - )); - }); - } - - QueryBuilder - updationTimeIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNull( - property: r'updationTime', - )); - }); - } - - QueryBuilder - updationTimeIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNotNull( - property: r'updationTime', - )); - }); - } - - QueryBuilder updationTimeEqualTo( - int? value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'updationTime', - value: value, - )); - }); - } - - QueryBuilder - updationTimeGreaterThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'updationTime', - value: value, - )); - }); - } - - QueryBuilder - updationTimeLessThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'updationTime', - value: value, - )); - }); - } - - QueryBuilder updationTimeBetween( - int? lower, - int? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'updationTime', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - )); - }); - } -} - -extension EmbeddingQueryObject - on QueryBuilder {} - -extension EmbeddingQueryLinks - on QueryBuilder {} - -extension EmbeddingQuerySortBy on QueryBuilder { - QueryBuilder sortByFileID() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileID', Sort.asc); - }); - } - - QueryBuilder sortByFileIDDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileID', Sort.desc); - }); - } - - QueryBuilder sortByModel() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'model', Sort.asc); - }); - } - - QueryBuilder sortByModelDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'model', Sort.desc); - }); - } - - QueryBuilder sortByUpdationTime() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updationTime', Sort.asc); - }); - } - - QueryBuilder sortByUpdationTimeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updationTime', Sort.desc); - }); - } -} - -extension EmbeddingQuerySortThenBy - on QueryBuilder { - QueryBuilder thenByFileID() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileID', Sort.asc); - }); - } - - QueryBuilder thenByFileIDDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileID', Sort.desc); - }); - } - - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByModel() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'model', Sort.asc); - }); - } - - QueryBuilder thenByModelDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'model', Sort.desc); - }); - } - - QueryBuilder thenByUpdationTime() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updationTime', Sort.asc); - }); - } - - QueryBuilder thenByUpdationTimeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updationTime', Sort.desc); - }); - } -} - -extension EmbeddingQueryWhereDistinct - on QueryBuilder { - QueryBuilder distinctByEmbedding() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'embedding'); - }); - } - - QueryBuilder distinctByFileID() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'fileID'); - }); - } - - QueryBuilder distinctByModel() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'model'); - }); - } - - QueryBuilder distinctByUpdationTime() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'updationTime'); - }); - } -} - -extension EmbeddingQueryProperty - on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder, QQueryOperations> embeddingProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'embedding'); - }); - } - - QueryBuilder fileIDProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'fileID'); - }); - } - - QueryBuilder modelProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'model'); - }); - } - - QueryBuilder updationTimeProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'updationTime'); - }); - } -} diff --git a/mobile/lib/services/machine_learning/semantic_search/semantic_search_service.dart b/mobile/lib/services/machine_learning/semantic_search/semantic_search_service.dart index 0587ff5223..eb5bfeb912 100644 --- a/mobile/lib/services/machine_learning/semantic_search/semantic_search_service.dart +++ b/mobile/lib/services/machine_learning/semantic_search/semantic_search_service.dart @@ -73,6 +73,7 @@ class SemanticSearchService { ? ONNX(shouldDownloadOverMobileData) : GGML(shouldDownloadOverMobileData); await EmbeddingStore.instance.init(); + await EmbeddingsDB.instance.init(); await _loadEmbeddings(); Bus.instance.on().listen((event) { _embeddingLoaderDebouncer.run(() async { From 9d6059a1762fcab38893f72b20b21e8db8d6ea25 Mon Sep 17 00:00:00 2001 From: vishnukvmd Date: Tue, 30 Apr 2024 20:06:40 +0530 Subject: [PATCH 166/240] v0.8.86 --- mobile/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 0ad736a1ae..ea01ba0868 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -12,7 +12,7 @@ description: ente photos application # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.8.85+605 +version: 0.8.86+606 publish_to: none environment: From 54e8d64b9e0ad4ac6d80bd925e012da9686a0ecf Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 22:46:04 +0530 Subject: [PATCH 167/240] Fix the loading of utils/index Naming it index doesn't cause isDev to be loaded. --- desktop/src/main.ts | 2 +- desktop/src/main/log.ts | 2 +- desktop/src/main/menu.ts | 2 +- desktop/src/main/services/dir.ts | 2 +- desktop/src/main/services/ffmpeg.ts | 2 +- desktop/src/main/services/image.ts | 2 +- desktop/src/main/services/watch.ts | 2 +- desktop/src/main/utils/{index.ts => electron.ts} | 0 8 files changed, 7 insertions(+), 7 deletions(-) rename desktop/src/main/utils/{index.ts => electron.ts} (100%) diff --git a/desktop/src/main.ts b/desktop/src/main.ts index c849c755f7..8beb45881b 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -26,7 +26,7 @@ import { createWatcher } from "./main/services/watch"; import { userPreferences } from "./main/stores/user-preferences"; import { migrateLegacyWatchStoreIfNeeded } from "./main/stores/watch"; import { registerStreamProtocol } from "./main/stream"; -import { isDev } from "./main/utils"; +import { isDev } from "./main/utils/electron"; /** * The URL where the renderer HTML is being served from. diff --git a/desktop/src/main/log.ts b/desktop/src/main/log.ts index c4d2f3cbbe..cf1404a90a 100644 --- a/desktop/src/main/log.ts +++ b/desktop/src/main/log.ts @@ -1,6 +1,6 @@ import log from "electron-log"; import util from "node:util"; -import { isDev } from "./utils"; +import { isDev } from "./utils/electron"; /** * Initialize logging in the main process. diff --git a/desktop/src/main/menu.ts b/desktop/src/main/menu.ts index 1019c7a8fb..024a226f11 100644 --- a/desktop/src/main/menu.ts +++ b/desktop/src/main/menu.ts @@ -10,7 +10,7 @@ import { forceCheckForAppUpdates } from "./services/app-update"; import autoLauncher from "./services/auto-launcher"; import { openLogDirectory } from "./services/dir"; import { userPreferences } from "./stores/user-preferences"; -import { isDev } from "./utils"; +import { isDev } from "./utils/electron"; /** Create and return the entries in the app's main menu bar */ export const createApplicationMenu = async (mainWindow: BrowserWindow) => { diff --git a/desktop/src/main/services/dir.ts b/desktop/src/main/services/dir.ts index ef3adb013d..4e2a8c65e4 100644 --- a/desktop/src/main/services/dir.ts +++ b/desktop/src/main/services/dir.ts @@ -1,7 +1,7 @@ import { shell } from "electron/common"; import { app, dialog } from "electron/main"; import path from "node:path"; -import { posixPath } from "../utils"; +import { posixPath } from "../utils/electron"; export const selectDirectory = async () => { const result = await dialog.showOpenDialog({ diff --git a/desktop/src/main/services/ffmpeg.ts b/desktop/src/main/services/ffmpeg.ts index 850b70d445..0a5c4eed2c 100644 --- a/desktop/src/main/services/ffmpeg.ts +++ b/desktop/src/main/services/ffmpeg.ts @@ -2,8 +2,8 @@ import pathToFfmpeg from "ffmpeg-static"; import fs from "node:fs/promises"; import type { ZipItem } from "../../types/ipc"; import log from "../log"; -import { execAsync } from "../utils"; import { ensure, withTimeout } from "../utils/common"; +import { execAsync } from "../utils/electron"; import { deleteTempFile, makeFileForDataOrPathOrZipItem, diff --git a/desktop/src/main/services/image.ts b/desktop/src/main/services/image.ts index d607b0ead3..957fe81200 100644 --- a/desktop/src/main/services/image.ts +++ b/desktop/src/main/services/image.ts @@ -4,7 +4,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { CustomErrorMessage, type ZipItem } from "../../types/ipc"; import log from "../log"; -import { execAsync, isDev } from "../utils"; +import { execAsync, isDev } from "../utils/electron"; import { deleteTempFile, makeFileForDataOrPathOrZipItem, diff --git a/desktop/src/main/services/watch.ts b/desktop/src/main/services/watch.ts index ca550a787b..a56c0cf6c3 100644 --- a/desktop/src/main/services/watch.ts +++ b/desktop/src/main/services/watch.ts @@ -5,7 +5,7 @@ import path from "node:path"; import { FolderWatch, type CollectionMapping } from "../../types/ipc"; import log from "../log"; import { watchStore } from "../stores/watch"; -import { posixPath } from "../utils"; +import { posixPath } from "../utils/electron"; import { fsIsDir } from "./fs"; /** diff --git a/desktop/src/main/utils/index.ts b/desktop/src/main/utils/electron.ts similarity index 100% rename from desktop/src/main/utils/index.ts rename to desktop/src/main/utils/electron.ts From a0d44b58e218f5a83adbd9e9138a4d4054f4e391 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 30 Apr 2024 22:48:10 +0530 Subject: [PATCH 168/240] Fix load Using .on("ready" was not causing the window to start loading the renderer unless createWindow was made async. --- desktop/src/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src/main.ts b/desktop/src/main.ts index 8beb45881b..eb1114cc4c 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -365,7 +365,7 @@ const main = () => { // Emitted once, when Electron has finished initializing. // // Note that some Electron APIs can only be used after this event occurs. - app.on("ready", () => { + void app.whenReady().then(() => { void (async () => { // Create window and prepare for the renderer. mainWindow = createMainWindow(); From ac9f5095d1c584303d3331fa797b34ea5050cf8b Mon Sep 17 00:00:00 2001 From: Prateek Sunal Date: Wed, 1 May 2024 00:57:54 +0530 Subject: [PATCH 169/240] fix: rpm deps and webview package --- auth/linux/packaging/rpm/make_config.yaml | 2 +- auth/pubspec.lock | 6 +++--- auth/pubspec.yaml | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/auth/linux/packaging/rpm/make_config.yaml b/auth/linux/packaging/rpm/make_config.yaml index 5d5f3aab53..e82dd63bfb 100644 --- a/auth/linux/packaging/rpm/make_config.yaml +++ b/auth/linux/packaging/rpm/make_config.yaml @@ -11,7 +11,7 @@ display_name: Auth requires: - libsqlite3x - - webkit2gtk-4.0 + - webkit2gtk4.0 - libsodium - libsecret - libappindicator diff --git a/auth/pubspec.lock b/auth/pubspec.lock index 2d61b77c39..7724160420 100644 --- a/auth/pubspec.lock +++ b/auth/pubspec.lock @@ -293,9 +293,9 @@ packages: dependency: "direct main" description: path: "packages/desktop_webview_window" - ref: HEAD - resolved-ref: "8cbbf9cd6efcfee5e0f420a36f7f8e7e64b667a1" - url: "https://github.com/MixinNetwork/flutter-plugins" + ref: fix-webkit-version + resolved-ref: fe2223e4edfecdbb3a97bb9e3ced73db4ae9d979 + url: "https://github.com/ente-io/flutter-desktopwebview-fork" source: git version: "0.2.4" device_info_plus: diff --git a/auth/pubspec.yaml b/auth/pubspec.yaml index 2ef543aa69..cdc39383d3 100644 --- a/auth/pubspec.yaml +++ b/auth/pubspec.yaml @@ -20,7 +20,8 @@ dependencies: convert: ^3.1.1 desktop_webview_window: git: - url: https://github.com/MixinNetwork/flutter-plugins + url: https://github.com/ente-io/flutter-desktopwebview-fork + ref: fix-webkit-version path: packages/desktop_webview_window device_info_plus: ^9.1.1 dio: ^5.4.0 From d0d5ead80be86fc52770887e711e89a9efca7fdc Mon Sep 17 00:00:00 2001 From: Prateek Sunal Date: Wed, 1 May 2024 01:07:13 +0530 Subject: [PATCH 170/240] fix: install patchelf for dynamic linking --- .github/workflows/auth-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/auth-release.yml b/.github/workflows/auth-release.yml index cc3e598e36..707bae895f 100644 --- a/.github/workflows/auth-release.yml +++ b/.github/workflows/auth-release.yml @@ -85,7 +85,7 @@ jobs: - name: Install dependencies for desktop build run: | sudo apt-get update -y - sudo apt-get install -y libsecret-1-dev libsodium-dev libwebkit2gtk-4.0-dev libfuse2 ninja-build libgtk-3-dev dpkg-dev pkg-config rpm libsqlite3-dev locate appindicator3-0.1 libappindicator3-dev libffi-dev libtiff5 + sudo apt-get install -y libsecret-1-dev libsodium-dev libwebkit2gtk-4.0-dev libfuse2 ninja-build libgtk-3-dev dpkg-dev pkg-config rpm patchelf libsqlite3-dev locate appindicator3-0.1 libappindicator3-dev libffi-dev libtiff5 sudo updatedb --localpaths='/usr/lib/x86_64-linux-gnu' - name: Install appimagetool From b39b470b959095f0c7180326daadfa9bd77ba182 Mon Sep 17 00:00:00 2001 From: Prateek Sunal Date: Wed, 1 May 2024 01:48:32 +0530 Subject: [PATCH 171/240] chore: bump version --- auth/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth/pubspec.yaml b/auth/pubspec.yaml index cdc39383d3..388f76869f 100644 --- a/auth/pubspec.yaml +++ b/auth/pubspec.yaml @@ -1,6 +1,6 @@ name: ente_auth description: ente two-factor authenticator -version: 2.0.55+255 +version: 2.0.56+256 publish_to: none environment: From 4e8f2e65f032aeb211e6022d6506d9205a5101d4 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 1 May 2024 08:15:43 +0530 Subject: [PATCH 172/240] Handle undefined better --- desktop/src/main/menu.ts | 2 +- desktop/src/main/services/app-update.ts | 7 ++- desktop/src/main/services/store.ts | 2 +- desktop/src/main/services/upload.ts | 61 ++++++++++++++++++--- desktop/src/main/stores/safe-storage.ts | 2 +- desktop/src/main/stores/upload-status.ts | 8 +-- desktop/src/main/stores/user-preferences.ts | 2 +- 7 files changed, 66 insertions(+), 18 deletions(-) diff --git a/desktop/src/main/menu.ts b/desktop/src/main/menu.ts index 024a226f11..b6fa7acfe8 100644 --- a/desktop/src/main/menu.ts +++ b/desktop/src/main/menu.ts @@ -19,7 +19,7 @@ export const createApplicationMenu = async (mainWindow: BrowserWindow) => { // Whenever the menu is redrawn the current value of these variables is used // to set the checked state for the various settings checkboxes. let isAutoLaunchEnabled = await autoLauncher.isEnabled(); - let shouldHideDockIcon = userPreferences.get("hideDockIcon"); + let shouldHideDockIcon = !!userPreferences.get("hideDockIcon"); const macOSOnly = (options: MenuItemConstructorOptions[]) => process.platform == "darwin" ? options : []; diff --git a/desktop/src/main/services/app-update.ts b/desktop/src/main/services/app-update.ts index bc4bd38d6d..8d66cb8c3b 100644 --- a/desktop/src/main/services/app-update.ts +++ b/desktop/src/main/services/app-update.ts @@ -36,18 +36,21 @@ const checkForUpdatesAndNotify = async (mainWindow: BrowserWindow) => { log.debug(() => `Update check found version ${version}`); + if (!version) + throw new Error("Unexpected empty version obtained from auto-updater"); + if (compareVersions(version, app.getVersion()) <= 0) { log.debug(() => "Skipping update, already at latest version"); return; } - if (version === userPreferences.get("skipAppVersion")) { + if (version == userPreferences.get("skipAppVersion")) { log.info(`User chose to skip version ${version}`); return; } const mutedVersion = userPreferences.get("muteUpdateNotificationVersion"); - if (version === mutedVersion) { + if (version == mutedVersion) { log.info(`User has muted update notifications for version ${version}`); return; } diff --git a/desktop/src/main/services/store.ts b/desktop/src/main/services/store.ts index 1884efbc5d..20cc91ea4d 100644 --- a/desktop/src/main/services/store.ts +++ b/desktop/src/main/services/store.ts @@ -9,8 +9,8 @@ import { watchStore } from "../stores/watch"; * This is useful to reset state when the user logs out. */ export const clearStores = () => { - uploadStatusStore.clear(); safeStorageStore.clear(); + uploadStatusStore.clear(); watchStore.clear(); }; diff --git a/desktop/src/main/services/upload.ts b/desktop/src/main/services/upload.ts index 7871b56fda..795ce48aff 100644 --- a/desktop/src/main/services/upload.ts +++ b/desktop/src/main/services/upload.ts @@ -46,7 +46,7 @@ export const pathOrZipItemSize = async ( }; export const pendingUploads = async (): Promise => { - const collectionName = uploadStatusStore.get("collectionName"); + const collectionName = uploadStatusStore.get("collectionName") ?? undefined; const allFilePaths = uploadStatusStore.get("filePaths") ?? []; const filePaths = allFilePaths.filter((f) => existsSync(f)); @@ -62,7 +62,7 @@ export const pendingUploads = async (): Promise => { // // This potentially can be cause us to try reuploading an already uploaded // file, but the dedup logic will kick in at that point so no harm will come - // off it. + // of it. if (allZipItems === undefined) { const allZipPaths = uploadStatusStore.get("filePaths") ?? []; const zipPaths = allZipPaths.filter((f) => existsSync(f)); @@ -82,20 +82,65 @@ export const pendingUploads = async (): Promise => { }; }; -export const setPendingUploads = (pendingUploads: PendingUploads) => - uploadStatusStore.set(pendingUploads); +/** + * [Note: Missing values in electron-store] + * + * Suppose we were to create a store like this: + * + * const store = new Store({ + * schema: { + * foo: { type: "string" }, + * bars: { type: "array", items: { type: "string" } }, + * }, + * }); + * + * If we fetch `store.get("foo")` or `store.get("bars")`, we get `undefined`. + * But if we try to set these back to `undefined`, say `store.set("foo", + * someUndefValue)`, we get asked to + * + * TypeError: Use `delete()` to clear values + * + * This happens even if we do bulk object updates, e.g. with a JS object that + * has undefined keys: + * + * > TypeError: Setting a value of type `undefined` for key `collectionName` is + * > not allowed as it's not supported by JSON + * + * So what should the TypeScript type for "foo" be? + * + * If it is were to not include the possibility of `undefined`, then the type + * would lie because `store.get("foo")` can indeed be `undefined. But if we were + * to include the possibility of `undefined`, then trying to `store.set("foo", + * someUndefValue)` will throw. + * + * The approach we take is to rely on false-y values (empty strings and empty + * arrays) to indicate missing values, and then converting those to `undefined` + * when reading from the store, and converting `undefined` to the corresponding + * false-y value when writing. + */ +export const setPendingUploads = ({ + collectionName, + filePaths, + zipItems, +}: PendingUploads) => { + uploadStatusStore.set({ + collectionName: collectionName ?? "", + filePaths: filePaths, + zipItems: zipItems, + }); +}; export const markUploadedFiles = (paths: string[]) => { - const existing = uploadStatusStore.get("filePaths"); - const updated = existing?.filter((p) => !paths.includes(p)); + const existing = uploadStatusStore.get("filePaths") ?? []; + const updated = existing.filter((p) => !paths.includes(p)); uploadStatusStore.set("filePaths", updated); }; export const markUploadedZipItems = ( items: [zipPath: string, entryName: string][], ) => { - const existing = uploadStatusStore.get("zipItems"); - const updated = existing?.filter( + const existing = uploadStatusStore.get("zipItems") ?? []; + const updated = existing.filter( (z) => !items.some((e) => z[0] == e[0] && z[1] == e[1]), ); uploadStatusStore.set("zipItems", updated); diff --git a/desktop/src/main/stores/safe-storage.ts b/desktop/src/main/stores/safe-storage.ts index 1e1369db89..040af1f3ed 100644 --- a/desktop/src/main/stores/safe-storage.ts +++ b/desktop/src/main/stores/safe-storage.ts @@ -1,7 +1,7 @@ import Store, { Schema } from "electron-store"; interface SafeStorageStore { - encryptionKey: string; + encryptionKey?: string; } const safeStorageSchema: Schema = { diff --git a/desktop/src/main/stores/upload-status.ts b/desktop/src/main/stores/upload-status.ts index f098e9fc5c..472f38a7f9 100644 --- a/desktop/src/main/stores/upload-status.ts +++ b/desktop/src/main/stores/upload-status.ts @@ -6,24 +6,24 @@ export interface UploadStatusStore { * * Not all pending uploads will have an associated collection. */ - collectionName: string | undefined; + collectionName?: string; /** * Paths to regular files that are pending upload. * * This should generally be present, albeit empty, but it is marked optional * in sympathy with its siblings. */ - filePaths: string[] | undefined; + filePaths?: string[]; /** * Each item is the path to a zip file and the name of an entry within it. * * This is marked optional since legacy stores will not have it. */ - zipItems: [zipPath: string, entryName: string][] | undefined; + zipItems?: [zipPath: string, entryName: string][]; /** * @deprecated Legacy paths to zip files, now subsumed into zipItems. */ - zipPaths: string[] | undefined; + zipPaths?: string[]; } const uploadStatusSchema: Schema = { diff --git a/desktop/src/main/stores/user-preferences.ts b/desktop/src/main/stores/user-preferences.ts index b4a02bc5be..f3b1929892 100644 --- a/desktop/src/main/stores/user-preferences.ts +++ b/desktop/src/main/stores/user-preferences.ts @@ -1,7 +1,7 @@ import Store, { Schema } from "electron-store"; interface UserPreferences { - hideDockIcon: boolean; + hideDockIcon?: boolean; skipAppVersion?: string; muteUpdateNotificationVersion?: string; } From 6c716ad8925ba77a882c0daef9186db573200d2a Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 1 May 2024 08:30:35 +0530 Subject: [PATCH 173/240] Tell prettier not to print the names of the files it processes --- desktop/package.json | 4 ++-- web/package.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/desktop/package.json b/desktop/package.json index d4c571cad7..8cbe1b7553 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -15,8 +15,8 @@ "dev-main": "tsc && electron app/main.js", "dev-renderer": "cd ../web && yarn install && yarn dev:photos", "postinstall": "electron-builder install-app-deps", - "lint": "yarn prettier --check . && eslint --ext .ts src && yarn tsc", - "lint-fix": "yarn prettier --write . && eslint --fix --ext .ts src && yarn tsc" + "lint": "yarn prettier --check --log-level warn . && eslint --ext .ts src && yarn tsc", + "lint-fix": "yarn prettier --write --log-level warn . && eslint --fix --ext .ts src && yarn tsc" }, "dependencies": { "any-shell-escape": "^0.1", diff --git a/web/package.json b/web/package.json index 2d5919eb1a..647ee3ba3a 100644 --- a/web/package.json +++ b/web/package.json @@ -27,8 +27,8 @@ "dev:payments": "yarn workspace payments dev", "dev:photos": "yarn workspace photos next dev", "dev:staff": "yarn workspace staff dev", - "lint": "yarn prettier --check . && yarn workspaces run eslint --report-unused-disable-directives .", - "lint-fix": "yarn prettier --write . && yarn workspaces run eslint --fix .", + "lint": "yarn prettier --check --log-level warn . && yarn workspaces run eslint --report-unused-disable-directives .", + "lint-fix": "yarn prettier --write --log-level warn . && yarn workspaces run eslint --fix .", "preview": "yarn preview:photos", "preview:accounts": "yarn build:accounts && python3 -m http.server -d apps/accounts/out 3001", "preview:auth": "yarn build:auth && python3 -m http.server -d apps/auth/out 3000", From 9cbf69f9b3871b5529b84669834e4dfcc2b30193 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 1 May 2024 08:46:43 +0530 Subject: [PATCH 174/240] upgrade-interactive --- desktop/yarn.lock | 197 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 138 insertions(+), 59 deletions(-) diff --git a/desktop/yarn.lock b/desktop/yarn.lock index bf9057d472..ddfe37183e 100644 --- a/desktop/yarn.lock +++ b/desktop/yarn.lock @@ -181,6 +181,13 @@ wrap-ansi "^8.1.0" wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" +"@isaacs/fs-minipass@^4.0.0": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz#2d59ae3ab4b38fb4270bfa23d30f8e2e86c7fe32" + integrity sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w== + dependencies: + minipass "^7.0.4" + "@malept/cross-spawn-promise@^1.1.0": version "1.1.1" resolved "https://registry.yarnpkg.com/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz#504af200af6b98e198bce768bc1730c6936ae01d" @@ -357,15 +364,15 @@ "@types/node" "*" "@typescript-eslint/eslint-plugin@^7": - version "7.6.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.6.0.tgz#1f5df5cda490a0bcb6fbdd3382e19f1241024242" - integrity sha512-gKmTNwZnblUdnTIJu3e9kmeRRzV2j1a/LUO27KNNAnIC5zjy1aSvXSRp4rVNlmAoHlQ7HzX42NbKpcSr4jF80A== + version "7.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.8.0.tgz#c78e309fe967cb4de05b85cdc876fb95f8e01b6f" + integrity sha512-gFTT+ezJmkwutUPmB0skOj3GZJtlEGnlssems4AjkVweUPGj7jRwwqg0Hhg7++kPGJqKtTYx+R05Ftww372aIg== dependencies: "@eslint-community/regexpp" "^4.10.0" - "@typescript-eslint/scope-manager" "7.6.0" - "@typescript-eslint/type-utils" "7.6.0" - "@typescript-eslint/utils" "7.6.0" - "@typescript-eslint/visitor-keys" "7.6.0" + "@typescript-eslint/scope-manager" "7.8.0" + "@typescript-eslint/type-utils" "7.8.0" + "@typescript-eslint/utils" "7.8.0" + "@typescript-eslint/visitor-keys" "7.8.0" debug "^4.3.4" graphemer "^1.4.0" ignore "^5.3.1" @@ -374,46 +381,46 @@ ts-api-utils "^1.3.0" "@typescript-eslint/parser@^7": - version "7.6.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.6.0.tgz#0aca5de3045d68b36e88903d15addaf13d040a95" - integrity sha512-usPMPHcwX3ZoPWnBnhhorc14NJw9J4HpSXQX4urF2TPKG0au0XhJoZyX62fmvdHONUkmyUe74Hzm1//XA+BoYg== + version "7.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.8.0.tgz#1e1db30c8ab832caffee5f37e677dbcb9357ddc8" + integrity sha512-KgKQly1pv0l4ltcftP59uQZCi4HUYswCLbTqVZEJu7uLX8CTLyswqMLqLN+2QFz4jCptqWVV4SB7vdxcH2+0kQ== dependencies: - "@typescript-eslint/scope-manager" "7.6.0" - "@typescript-eslint/types" "7.6.0" - "@typescript-eslint/typescript-estree" "7.6.0" - "@typescript-eslint/visitor-keys" "7.6.0" + "@typescript-eslint/scope-manager" "7.8.0" + "@typescript-eslint/types" "7.8.0" + "@typescript-eslint/typescript-estree" "7.8.0" + "@typescript-eslint/visitor-keys" "7.8.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@7.6.0": - version "7.6.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.6.0.tgz#1e9972f654210bd7500b31feadb61a233f5b5e9d" - integrity sha512-ngttyfExA5PsHSx0rdFgnADMYQi+Zkeiv4/ZxGYUWd0nLs63Ha0ksmp8VMxAIC0wtCFxMos7Lt3PszJssG/E6w== +"@typescript-eslint/scope-manager@7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.8.0.tgz#bb19096d11ec6b87fb6640d921df19b813e02047" + integrity sha512-viEmZ1LmwsGcnr85gIq+FCYI7nO90DVbE37/ll51hjv9aG+YZMb4WDE2fyWpUR4O/UrhGRpYXK/XajcGTk2B8g== dependencies: - "@typescript-eslint/types" "7.6.0" - "@typescript-eslint/visitor-keys" "7.6.0" + "@typescript-eslint/types" "7.8.0" + "@typescript-eslint/visitor-keys" "7.8.0" -"@typescript-eslint/type-utils@7.6.0": - version "7.6.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.6.0.tgz#644f75075f379827d25fe0713e252ccd4e4a428c" - integrity sha512-NxAfqAPNLG6LTmy7uZgpK8KcuiS2NZD/HlThPXQRGwz6u7MDBWRVliEEl1Gj6U7++kVJTpehkhZzCJLMK66Scw== +"@typescript-eslint/type-utils@7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.8.0.tgz#9de166f182a6e4d1c5da76e94880e91831e3e26f" + integrity sha512-H70R3AefQDQpz9mGv13Uhi121FNMh+WEaRqcXTX09YEDky21km4dV1ZXJIp8QjXc4ZaVkXVdohvWDzbnbHDS+A== dependencies: - "@typescript-eslint/typescript-estree" "7.6.0" - "@typescript-eslint/utils" "7.6.0" + "@typescript-eslint/typescript-estree" "7.8.0" + "@typescript-eslint/utils" "7.8.0" debug "^4.3.4" ts-api-utils "^1.3.0" -"@typescript-eslint/types@7.6.0": - version "7.6.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.6.0.tgz#53dba7c30c87e5f10a731054266dd905f1fbae38" - integrity sha512-h02rYQn8J+MureCvHVVzhl69/GAfQGPQZmOMjG1KfCl7o3HtMSlPaPUAPu6lLctXI5ySRGIYk94clD/AUMCUgQ== +"@typescript-eslint/types@7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.8.0.tgz#1fd2577b3ad883b769546e2d1ef379f929a7091d" + integrity sha512-wf0peJ+ZGlcH+2ZS23aJbOv+ztjeeP8uQ9GgwMJGVLx/Nj9CJt17GWgWWoSmoRVKAX2X+7fzEnAjxdvK2gqCLw== -"@typescript-eslint/typescript-estree@7.6.0": - version "7.6.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.6.0.tgz#112a3775563799fd3f011890ac8322f80830ac17" - integrity sha512-+7Y/GP9VuYibecrCQWSKgl3GvUM5cILRttpWtnAu8GNL9j11e4tbuGZmZjJ8ejnKYyBRb2ddGQ3rEFCq3QjMJw== +"@typescript-eslint/typescript-estree@7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.8.0.tgz#b028a9226860b66e623c1ee55cc2464b95d2987c" + integrity sha512-5pfUCOwK5yjPaJQNy44prjCwtr981dO8Qo9J9PwYXZ0MosgAbfEMB008dJ5sNo3+/BN6ytBPuSvXUg9SAqB0dg== dependencies: - "@typescript-eslint/types" "7.6.0" - "@typescript-eslint/visitor-keys" "7.6.0" + "@typescript-eslint/types" "7.8.0" + "@typescript-eslint/visitor-keys" "7.8.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" @@ -421,25 +428,25 @@ semver "^7.6.0" ts-api-utils "^1.3.0" -"@typescript-eslint/utils@7.6.0": - version "7.6.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.6.0.tgz#e400d782280b6f724c8a1204269d984c79202282" - integrity sha512-x54gaSsRRI+Nwz59TXpCsr6harB98qjXYzsRxGqvA5Ue3kQH+FxS7FYU81g/omn22ML2pZJkisy6Q+ElK8pBCA== +"@typescript-eslint/utils@7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.8.0.tgz#57a79f9c0c0740ead2f622e444cfaeeb9fd047cd" + integrity sha512-L0yFqOCflVqXxiZyXrDr80lnahQfSOfc9ELAAZ75sqicqp2i36kEZZGuUymHNFoYOqxRT05up760b4iGsl02nQ== dependencies: "@eslint-community/eslint-utils" "^4.4.0" "@types/json-schema" "^7.0.15" "@types/semver" "^7.5.8" - "@typescript-eslint/scope-manager" "7.6.0" - "@typescript-eslint/types" "7.6.0" - "@typescript-eslint/typescript-estree" "7.6.0" + "@typescript-eslint/scope-manager" "7.8.0" + "@typescript-eslint/types" "7.8.0" + "@typescript-eslint/typescript-estree" "7.8.0" semver "^7.6.0" -"@typescript-eslint/visitor-keys@7.6.0": - version "7.6.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.6.0.tgz#d1ce13145844379021e1f9bd102c1d78946f4e76" - integrity sha512-4eLB7t+LlNUmXzfOu1VAIAdkjbu5xNSerURS9X/S5TUKWFRpXRQZbmtPqgKmYx8bj3J0irtQXSiWAOY82v+cgw== +"@typescript-eslint/visitor-keys@7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.8.0.tgz#7285aab991da8bee411a42edbd5db760d22fdd91" + integrity sha512-q4/gibTNBQNA0lGyYQCmWRS5D15n8rXh4QjK3KV+MBPlTYHpfBUT3D3PaPR/HeNiI9W6R7FvlkcGhNyAoP+caA== dependencies: - "@typescript-eslint/types" "7.6.0" + "@typescript-eslint/types" "7.8.0" eslint-visitor-keys "^3.4.3" "@ungap/structured-clone@^1.2.0": @@ -829,6 +836,11 @@ chownr@^2.0.0: resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== +chownr@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-3.0.0.tgz#9855e64ecd240a9cc4267ce8a4aa5d24a1da15e4" + integrity sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g== + chromium-pickle-js@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz#04a106672c18b085ab774d983dfa3ea138f22205" @@ -1637,6 +1649,17 @@ glob@^10.3.10: minipass "^5.0.0 || ^6.0.2 || ^7.0.0" path-scurry "^1.10.1" +glob@^10.3.7: + version "10.3.12" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.12.tgz#3a65c363c2e9998d220338e88a5f6ac97302960b" + integrity sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg== + dependencies: + foreground-child "^3.1.0" + jackspeak "^2.3.6" + minimatch "^9.0.1" + minipass "^7.0.4" + path-scurry "^1.10.2" + glob@^7.0.0, glob@^7.1.3, glob@^7.1.6: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" @@ -1954,7 +1977,7 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== -jackspeak@^2.3.5: +jackspeak@^2.3.5, jackspeak@^2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.3.6.tgz#647ecc472238aee4b06ac0e461acc21a8c505ca8" integrity sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ== @@ -2126,6 +2149,11 @@ lowercase-keys@^2.0.0: resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== +lru-cache@^10.2.0: + version "10.2.2" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.2.tgz#48206bc114c1252940c41b25b41af5b545aca878" + integrity sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ== + lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" @@ -2240,7 +2268,7 @@ minipass@^5.0.0: resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== -"minipass@^5.0.0 || ^6.0.2 || ^7.0.0": +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.0.4: version "7.0.4" resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c" integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ== @@ -2253,6 +2281,14 @@ minizlib@^2.1.1: minipass "^3.0.0" yallist "^4.0.0" +minizlib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-3.0.1.tgz#46d5329d1eb3c83924eff1d3b858ca0a31581012" + integrity sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg== + dependencies: + minipass "^7.0.4" + rimraf "^5.0.5" + mkdirp@^0.5.1: version "0.5.6" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" @@ -2265,6 +2301,11 @@ mkdirp@^1.0.3: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +mkdirp@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50" + integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg== + ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" @@ -2329,17 +2370,18 @@ onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" -onnxruntime-common@1.17.0: - version "1.17.0" - resolved "https://registry.yarnpkg.com/onnxruntime-common/-/onnxruntime-common-1.17.0.tgz#b2534ce021b1c1b19182bec39aaea8d547d2013e" - integrity sha512-Vq1remJbCPITjDMJ04DA7AklUTnbYUp4vbnm6iL7ukSt+7VErH0NGYfekRSTjxxurEtX7w41PFfnQlE6msjPJw== +onnxruntime-common@1.17.3: + version "1.17.3" + resolved "https://registry.yarnpkg.com/onnxruntime-common/-/onnxruntime-common-1.17.3.tgz#aadc456477873a540ee3d611ae9cd4f3de7c43e5" + integrity sha512-IkbaDelNVX8cBfHFgsNADRIq2TlXMFWW+nG55mwWvQT4i0NZb32Jf35Pf6h9yjrnK78RjcnlNYaI37w394ovMw== onnxruntime-node@^1.17: - version "1.17.0" - resolved "https://registry.yarnpkg.com/onnxruntime-node/-/onnxruntime-node-1.17.0.tgz#38af0ba527cb44c1afb639bdcb4e549edba029a1" - integrity sha512-pRxdqSP3a6wtiFVkVX1V3/gsEMwBRUA9D2oYmcN3cjF+j+ILS+SIY2L7KxdWapsG6z64i5rUn8ijFZdIvbojBg== + version "1.17.3" + resolved "https://registry.yarnpkg.com/onnxruntime-node/-/onnxruntime-node-1.17.3.tgz#53b8b7ef68bf3834bba9d7be592e4c2d718d2018" + integrity sha512-NtbN1pfApTSEjVq46LrJ396aPP2Gjhy+oYZi5Bu1leDXAEvVap/BQ8CZELiLs7z0UnXy3xjJW23HiB4P3//FIw== dependencies: - onnxruntime-common "1.17.0" + onnxruntime-common "1.17.3" + tar "^7.0.1" optionator@^0.9.3: version "0.9.3" @@ -2453,6 +2495,14 @@ path-scurry@^1.10.1: lru-cache "^9.1.1 || ^10.0.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" +path-scurry@^1.10.2: + version "1.10.2" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.10.2.tgz#8f6357eb1239d5fa1da8b9f70e9c080675458ba7" + integrity sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA== + dependencies: + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + path-type@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" @@ -2660,6 +2710,13 @@ rimraf@^3.0.2: dependencies: glob "^7.1.3" +rimraf@^5.0.5: + version "5.0.5" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-5.0.5.tgz#9be65d2d6e683447d2e9013da2bf451139a61ccf" + integrity sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A== + dependencies: + glob "^10.3.7" + roarr@^2.15.3: version "2.15.4" resolved "https://registry.yarnpkg.com/roarr/-/roarr-2.15.4.tgz#f5fe795b7b838ccfe35dc608e0282b9eba2e7afd" @@ -2982,6 +3039,18 @@ tar@^6.1.12: mkdirp "^1.0.3" yallist "^4.0.0" +tar@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-7.0.1.tgz#8f6ccebcd91b69e9767a6fc4892799e8b0e606d5" + integrity sha512-IjMhdQMZFpKsHEQT3woZVxBtCQY+0wk3CVxdRkGXEgyGa0dNS/ehPvOMr2nmfC7x5Zj2N+l6yZUpmICjLGS35w== + dependencies: + "@isaacs/fs-minipass" "^4.0.0" + chownr "^3.0.0" + minipass "^5.0.0" + minizlib "^3.0.1" + mkdirp "^3.0.1" + yallist "^5.0.0" + temp-file@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/temp-file/-/temp-file-3.4.0.tgz#766ea28911c683996c248ef1a20eea04d51652c7" @@ -3083,7 +3152,12 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== -typescript@^5, typescript@^5.3.3: +typescript@^5: + version "5.4.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" + integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== + +typescript@^5.3.3: version "5.4.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.3.tgz#5c6fedd4c87bee01cd7a528a30145521f8e0feff" integrity sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg== @@ -3192,6 +3266,11 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== +yallist@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-5.0.0.tgz#00e2de443639ed0d78fd87de0d27469fbcffb533" + integrity sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw== + yargs-parser@^21.1.1: version "21.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" From e18731e625c57ba77a3f2b5320d290f0e822618c Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 1 May 2024 08:51:47 +0530 Subject: [PATCH 175/240] yarn upgrade-interactive typescript and lints --- web/yarn.lock | 157 ++++++++++++++++++++++++++------------------------ 1 file changed, 82 insertions(+), 75 deletions(-) diff --git a/web/yarn.lock b/web/yarn.lock index 6886647d7a..af3a5f2102 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -528,7 +528,7 @@ dependencies: eslint-visitor-keys "^3.3.0" -"@eslint-community/regexpp@^4.5.1", "@eslint-community/regexpp@^4.6.1": +"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.6.1": version "4.10.0" resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63" integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== @@ -1018,7 +1018,7 @@ "@types/react" "*" hoist-non-react-statics "^3.3.0" -"@types/json-schema@^7.0.12": +"@types/json-schema@^7.0.15": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== @@ -1134,10 +1134,10 @@ resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.8.tgz#ce5ace04cfeabe7ef87c0091e50752e36707deff" integrity sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A== -"@types/semver@^7.5.0": - version "7.5.7" - resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.7.tgz#326f5fdda70d13580777bcaa1bc6fa772a5aef0e" - integrity sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg== +"@types/semver@^7.5.8": + version "7.5.8" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" + integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ== "@types/uuid@^9.0.2": version "9.0.8" @@ -1150,21 +1150,21 @@ integrity sha512-Tuk4q7q0DnpzyJDI4aMeghGuFu2iS1QAdKpabn8JfbtfGmVDUgvZv1I7mEjP61Bvnp3ljKCC8BE6YYSTNxmvRQ== "@typescript-eslint/eslint-plugin@^7": - version "7.0.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.2.tgz#c13a34057be425167cc4a765158c46fdf2fd981d" - integrity sha512-/XtVZJtbaphtdrWjr+CJclaCVGPtOdBpFEnvtNf/jRV0IiEemRrL0qABex/nEt8isYcnFacm3nPHYQwL+Wb7qg== + version "7.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.8.0.tgz#c78e309fe967cb4de05b85cdc876fb95f8e01b6f" + integrity sha512-gFTT+ezJmkwutUPmB0skOj3GZJtlEGnlssems4AjkVweUPGj7jRwwqg0Hhg7++kPGJqKtTYx+R05Ftww372aIg== dependencies: - "@eslint-community/regexpp" "^4.5.1" - "@typescript-eslint/scope-manager" "7.0.2" - "@typescript-eslint/type-utils" "7.0.2" - "@typescript-eslint/utils" "7.0.2" - "@typescript-eslint/visitor-keys" "7.0.2" + "@eslint-community/regexpp" "^4.10.0" + "@typescript-eslint/scope-manager" "7.8.0" + "@typescript-eslint/type-utils" "7.8.0" + "@typescript-eslint/utils" "7.8.0" + "@typescript-eslint/visitor-keys" "7.8.0" debug "^4.3.4" graphemer "^1.4.0" - ignore "^5.2.4" + ignore "^5.3.1" natural-compare "^1.4.0" - semver "^7.5.4" - ts-api-utils "^1.0.1" + semver "^7.6.0" + ts-api-utils "^1.3.0" "@typescript-eslint/parser@^5.4.2 || ^6.0.0": version "6.21.0" @@ -1178,14 +1178,14 @@ debug "^4.3.4" "@typescript-eslint/parser@^7": - version "7.0.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.0.2.tgz#95c31233d343db1ca1df8df7811b5b87ca7b1a68" - integrity sha512-GdwfDglCxSmU+QTS9vhz2Sop46ebNCXpPPvsByK7hu0rFGRHL+AusKQJ7SoN+LbLh6APFpQwHKmDSwN35Z700Q== + version "7.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.8.0.tgz#1e1db30c8ab832caffee5f37e677dbcb9357ddc8" + integrity sha512-KgKQly1pv0l4ltcftP59uQZCi4HUYswCLbTqVZEJu7uLX8CTLyswqMLqLN+2QFz4jCptqWVV4SB7vdxcH2+0kQ== dependencies: - "@typescript-eslint/scope-manager" "7.0.2" - "@typescript-eslint/types" "7.0.2" - "@typescript-eslint/typescript-estree" "7.0.2" - "@typescript-eslint/visitor-keys" "7.0.2" + "@typescript-eslint/scope-manager" "7.8.0" + "@typescript-eslint/types" "7.8.0" + "@typescript-eslint/typescript-estree" "7.8.0" + "@typescript-eslint/visitor-keys" "7.8.0" debug "^4.3.4" "@typescript-eslint/scope-manager@6.21.0": @@ -1196,33 +1196,33 @@ "@typescript-eslint/types" "6.21.0" "@typescript-eslint/visitor-keys" "6.21.0" -"@typescript-eslint/scope-manager@7.0.2": - version "7.0.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.0.2.tgz#6ec4cc03752758ddd1fdaae6fbd0ed9a2ca4fe63" - integrity sha512-l6sa2jF3h+qgN2qUMjVR3uCNGjWw4ahGfzIYsCtFrQJCjhbrDPdiihYT8FnnqFwsWX+20hK592yX9I2rxKTP4g== +"@typescript-eslint/scope-manager@7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.8.0.tgz#bb19096d11ec6b87fb6640d921df19b813e02047" + integrity sha512-viEmZ1LmwsGcnr85gIq+FCYI7nO90DVbE37/ll51hjv9aG+YZMb4WDE2fyWpUR4O/UrhGRpYXK/XajcGTk2B8g== dependencies: - "@typescript-eslint/types" "7.0.2" - "@typescript-eslint/visitor-keys" "7.0.2" + "@typescript-eslint/types" "7.8.0" + "@typescript-eslint/visitor-keys" "7.8.0" -"@typescript-eslint/type-utils@7.0.2": - version "7.0.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.0.2.tgz#a7fc0adff0c202562721357e7478207d380a757b" - integrity sha512-IKKDcFsKAYlk8Rs4wiFfEwJTQlHcdn8CLwLaxwd6zb8HNiMcQIFX9sWax2k4Cjj7l7mGS5N1zl7RCHOVwHq2VQ== +"@typescript-eslint/type-utils@7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.8.0.tgz#9de166f182a6e4d1c5da76e94880e91831e3e26f" + integrity sha512-H70R3AefQDQpz9mGv13Uhi121FNMh+WEaRqcXTX09YEDky21km4dV1ZXJIp8QjXc4ZaVkXVdohvWDzbnbHDS+A== dependencies: - "@typescript-eslint/typescript-estree" "7.0.2" - "@typescript-eslint/utils" "7.0.2" + "@typescript-eslint/typescript-estree" "7.8.0" + "@typescript-eslint/utils" "7.8.0" debug "^4.3.4" - ts-api-utils "^1.0.1" + ts-api-utils "^1.3.0" "@typescript-eslint/types@6.21.0": version "6.21.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.21.0.tgz#205724c5123a8fef7ecd195075fa6e85bac3436d" integrity sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg== -"@typescript-eslint/types@7.0.2": - version "7.0.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.0.2.tgz#b6edd108648028194eb213887d8d43ab5750351c" - integrity sha512-ZzcCQHj4JaXFjdOql6adYV4B/oFOFjPOC9XYwCaZFRvqN8Llfvv4gSxrkQkd2u4Ci62i2c6W6gkDwQJDaRc4nA== +"@typescript-eslint/types@7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.8.0.tgz#1fd2577b3ad883b769546e2d1ef379f929a7091d" + integrity sha512-wf0peJ+ZGlcH+2ZS23aJbOv+ztjeeP8uQ9GgwMJGVLx/Nj9CJt17GWgWWoSmoRVKAX2X+7fzEnAjxdvK2gqCLw== "@typescript-eslint/typescript-estree@6.21.0": version "6.21.0" @@ -1238,32 +1238,32 @@ semver "^7.5.4" ts-api-utils "^1.0.1" -"@typescript-eslint/typescript-estree@7.0.2": - version "7.0.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.2.tgz#3c6dc8a3b9799f4ef7eca0d224ded01974e4cb39" - integrity sha512-3AMc8khTcELFWcKcPc0xiLviEvvfzATpdPj/DXuOGIdQIIFybf4DMT1vKRbuAEOFMwhWt7NFLXRkbjsvKZQyvw== +"@typescript-eslint/typescript-estree@7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.8.0.tgz#b028a9226860b66e623c1ee55cc2464b95d2987c" + integrity sha512-5pfUCOwK5yjPaJQNy44prjCwtr981dO8Qo9J9PwYXZ0MosgAbfEMB008dJ5sNo3+/BN6ytBPuSvXUg9SAqB0dg== dependencies: - "@typescript-eslint/types" "7.0.2" - "@typescript-eslint/visitor-keys" "7.0.2" + "@typescript-eslint/types" "7.8.0" + "@typescript-eslint/visitor-keys" "7.8.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" - minimatch "9.0.3" - semver "^7.5.4" - ts-api-utils "^1.0.1" + minimatch "^9.0.4" + semver "^7.6.0" + ts-api-utils "^1.3.0" -"@typescript-eslint/utils@7.0.2": - version "7.0.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.0.2.tgz#8756123054cd934c8ba7db6a6cffbc654b10b5c4" - integrity sha512-PZPIONBIB/X684bhT1XlrkjNZJIEevwkKDsdwfiu1WeqBxYEEdIgVDgm8/bbKHVu+6YOpeRqcfImTdImx/4Bsw== +"@typescript-eslint/utils@7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.8.0.tgz#57a79f9c0c0740ead2f622e444cfaeeb9fd047cd" + integrity sha512-L0yFqOCflVqXxiZyXrDr80lnahQfSOfc9ELAAZ75sqicqp2i36kEZZGuUymHNFoYOqxRT05up760b4iGsl02nQ== dependencies: "@eslint-community/eslint-utils" "^4.4.0" - "@types/json-schema" "^7.0.12" - "@types/semver" "^7.5.0" - "@typescript-eslint/scope-manager" "7.0.2" - "@typescript-eslint/types" "7.0.2" - "@typescript-eslint/typescript-estree" "7.0.2" - semver "^7.5.4" + "@types/json-schema" "^7.0.15" + "@types/semver" "^7.5.8" + "@typescript-eslint/scope-manager" "7.8.0" + "@typescript-eslint/types" "7.8.0" + "@typescript-eslint/typescript-estree" "7.8.0" + semver "^7.6.0" "@typescript-eslint/visitor-keys@6.21.0": version "6.21.0" @@ -1273,13 +1273,13 @@ "@typescript-eslint/types" "6.21.0" eslint-visitor-keys "^3.4.1" -"@typescript-eslint/visitor-keys@7.0.2": - version "7.0.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.2.tgz#2899b716053ad7094962beb895d11396fc12afc7" - integrity sha512-8Y+YiBmqPighbm5xA2k4wKTxRzx9EkBu7Rlw+WHqMvRJ3RPz/BMBO9b2ru0LUNmXg120PHUXD5+SWFy2R8DqlQ== +"@typescript-eslint/visitor-keys@7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.8.0.tgz#7285aab991da8bee411a42edbd5db760d22fdd91" + integrity sha512-q4/gibTNBQNA0lGyYQCmWRS5D15n8rXh4QjK3KV+MBPlTYHpfBUT3D3PaPR/HeNiI9W6R7FvlkcGhNyAoP+caA== dependencies: - "@typescript-eslint/types" "7.0.2" - eslint-visitor-keys "^3.4.1" + "@typescript-eslint/types" "7.8.0" + eslint-visitor-keys "^3.4.3" "@ungap/structured-clone@^1.2.0": version "1.2.0" @@ -2893,7 +2893,7 @@ ieee754@^1.2.1: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== -ignore@^5.2.0, ignore@^5.2.4: +ignore@^5.2.0, ignore@^5.2.4, ignore@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== @@ -3449,6 +3449,13 @@ minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" +minimatch@^9.0.4: + version "9.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.4.tgz#8e49c731d1749cbec05050ee5145147b32496a51" + integrity sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw== + dependencies: + brace-expansion "^2.0.1" + minimist@^1.2.0, minimist@^1.2.6: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" @@ -4173,7 +4180,7 @@ semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.5.4: +semver@^7.5.4, semver@^7.6.0: version "7.6.0" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== @@ -4565,10 +4572,10 @@ truncate-utf8-bytes@^1.0.0: dependencies: utf8-byte-length "^1.0.1" -ts-api-utils@^1.0.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.2.1.tgz#f716c7e027494629485b21c0df6180f4d08f5e8b" - integrity sha512-RIYA36cJn2WiH9Hy77hdF9r7oEwxAtB/TS9/S4Qd90Ap4z5FSiin5zEiTL44OII1Y3IIlEvxwxFUVgrHSZ/UpA== +ts-api-utils@^1.0.1, ts-api-utils@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1" + integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ== tsconfig-paths@^3.15.0: version "3.15.0" @@ -4659,9 +4666,9 @@ typed-array-length@^1.0.6: possible-typed-array-names "^1.0.0" typescript@^5: - version "5.3.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" - integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== + version "5.4.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" + integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== unbox-primitive@^1.0.2: version "1.0.2" From 42b214a4ba45b490253317be8b0799c5a68c3276 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 1 May 2024 09:05:12 +0530 Subject: [PATCH 176/240] Remove duplicate state and reset store on successful completion Clears out the collection name --- .../src/services/upload/uploadManager.ts | 37 +++---------------- 1 file changed, 6 insertions(+), 31 deletions(-) diff --git a/web/apps/photos/src/services/upload/uploadManager.ts b/web/apps/photos/src/services/upload/uploadManager.ts index 99fe6ced39..3d53adbeaf 100644 --- a/web/apps/photos/src/services/upload/uploadManager.ts +++ b/web/apps/photos/src/services/upload/uploadManager.ts @@ -321,7 +321,6 @@ class UploadManager { >(maxConcurrentUploads); private parsedMetadataJSONMap: Map; private itemsToBeUploaded: ClusteredUploadItem[]; - private remainingItems: ClusteredUploadItem[] = []; private failedItems: ClusteredUploadItem[]; private existingFiles: EnteFile[]; private setFiles: SetFiles; @@ -360,7 +359,6 @@ class UploadManager { private resetState() { this.itemsToBeUploaded = []; - this.remainingItems = []; this.failedItems = []; this.parsedMetadataJSONMap = new Map(); @@ -440,17 +438,13 @@ class UploadManager { await this.uploadMediaItems(clusteredMediaItems); } } catch (e) { - if (e.message === CustomError.UPLOAD_CANCELLED) { - if (isElectron()) { - this.remainingItems = []; - await cancelRemainingUploads(); - } - } else { - log.error("Uploading failed", e); + if (e.message != CustomError.UPLOAD_CANCELLED) { + log.error("Upload failed", e); throw e; } } finally { this.uiService.setUploadStage(UPLOAD_STAGES.FINISH); + void globalThis.electron?.clearPendingUploads(); for (let i = 0; i < maxConcurrentUploads; i++) { this.cryptoWorkers[i]?.terminate(); } @@ -503,15 +497,8 @@ class UploadManager { private async uploadMediaItems(mediaItems: ClusteredUploadItem[]) { this.itemsToBeUploaded = [...this.itemsToBeUploaded, ...mediaItems]; - - if (isElectron()) { - this.remainingItems = [...this.remainingItems, ...mediaItems]; - } - this.uiService.reset(mediaItems.length); - await UploadService.setFileCount(mediaItems.length); - this.uiService.setUploadStage(UPLOAD_STAGES.UPLOADING); const uploadProcesses = []; @@ -584,8 +571,10 @@ class UploadManager { `Uploaded ${uploadableItem.fileName} with result ${uploadResult}`, ); try { + const electron = globalThis.electron; + if (electron) await markUploaded(electron, uploadableItem); + let decryptedFile: EnteFile; - await this.removeFromPendingUploads(uploadableItem); switch (uploadResult) { case UPLOAD_RESULT.FAILED: case UPLOAD_RESULT.BLOCKED: @@ -688,18 +677,6 @@ class UploadManager { this.setFiles((files) => sortFiles([...files, decryptedFile])); } - private async removeFromPendingUploads( - clusteredUploadItem: ClusteredUploadItem, - ) { - const electron = globalThis.electron; - if (electron) { - this.remainingItems = this.remainingItems.filter( - (f) => f.localID != clusteredUploadItem.localID, - ); - await markUploaded(electron, clusteredUploadItem); - } - } - public shouldAllowNewUpload = () => { return !this.uploadInProgress || watcher.isUploadRunning(); }; @@ -847,8 +824,6 @@ const markUploaded = async (electron: Electron, item: ClusteredUploadItem) => { } }; -const cancelRemainingUploads = () => ensureElectron().clearPendingUploads(); - /** * Go through the given files, combining any sibling image + video assets into a * single live photo when appropriate. From abee517f8c989739f6a718d64564a9366b14a48a Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 1 May 2024 09:13:20 +0530 Subject: [PATCH 177/240] Fix remote build by recreating yarn.lock --- desktop/yarn.lock | 151 +++++++++++++++++----------------------------- 1 file changed, 56 insertions(+), 95 deletions(-) diff --git a/desktop/yarn.lock b/desktop/yarn.lock index ddfe37183e..436ae84382 100644 --- a/desktop/yarn.lock +++ b/desktop/yarn.lock @@ -7,11 +7,6 @@ resolved "https://registry.yarnpkg.com/7zip-bin/-/7zip-bin-5.2.0.tgz#7a03314684dd6572b7dfa89e68ce31d60286854d" integrity sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A== -"@aashutoshrathi/word-wrap@^1.2.3": - version "1.2.6" - resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" - integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== - "@babel/code-frame@^7.0.0": version "7.24.2" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.2.tgz#718b4b19841809a58b29b68cde80bc5e1aa6d9ae" @@ -20,25 +15,25 @@ "@babel/highlight" "^7.24.2" picocolors "^1.0.0" -"@babel/helper-validator-identifier@^7.22.20": - version "7.22.20" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" - integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== +"@babel/helper-validator-identifier@^7.24.5": + version "7.24.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz#918b1a7fa23056603506370089bd990d8720db62" + integrity sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA== "@babel/highlight@^7.24.2": - version "7.24.2" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.2.tgz#3f539503efc83d3c59080a10e6634306e0370d26" - integrity sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA== + version "7.24.5" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.5.tgz#bc0613f98e1dd0720e99b2a9ee3760194a704b6e" + integrity sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw== dependencies: - "@babel/helper-validator-identifier" "^7.22.20" + "@babel/helper-validator-identifier" "^7.24.5" chalk "^2.4.2" js-tokens "^4.0.0" picocolors "^1.0.0" "@babel/runtime@^7.21.0": - version "7.24.0" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.0.tgz#584c450063ffda59697021430cb47101b085951e" - integrity sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw== + version "7.24.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.5.tgz#230946857c053a36ccc66e1dd03b17dd0c4ed02c" + integrity sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g== dependencies: regenerator-runtime "^0.14.0" @@ -165,9 +160,9 @@ integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== "@humanwhocodes/object-schema@^2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz#d9fae00a2d5cb40f92cfe64b47ad749fbc38f917" - integrity sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw== + version "2.0.3" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" + integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== "@isaacs/cliui@^8.0.2": version "8.0.2" @@ -499,14 +494,14 @@ ajv@^6.10.0, ajv@^6.12.0, ajv@^6.12.4: uri-js "^4.2.2" ajv@^8.0.0, ajv@^8.6.3: - version "8.12.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" - integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== + version "8.13.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.13.0.tgz#a3939eaec9fb80d217ddf0c3376948c023f28c91" + integrity sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA== dependencies: - fast-deep-equal "^3.1.1" + fast-deep-equal "^3.1.3" json-schema-traverse "^1.0.0" require-from-string "^2.0.2" - uri-js "^4.2.2" + uri-js "^4.4.1" ansi-regex@^5.0.1: version "5.0.1" @@ -1043,7 +1038,7 @@ define-data-property@^1.0.1: es-errors "^1.3.0" gopd "^1.0.1" -define-properties@^1.1.3: +define-properties@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== @@ -1150,9 +1145,9 @@ eastasianwidth@^0.2.0: integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== ejs@^3.1.8: - version "3.1.9" - resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.9.tgz#03c9e8777fe12686a9effcef22303ca3d8eeb361" - integrity sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ== + version "3.1.10" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b" + integrity sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA== dependencies: jake "^10.8.5" @@ -1232,9 +1227,9 @@ electron-updater@^6.1: tiny-typed-emitter "^2.1.0" electron@^29: - version "29.3.0" - resolved "https://registry.yarnpkg.com/electron/-/electron-29.3.0.tgz#8e65cb08e9c0952c66d3196e1b5c811c43b8c5b0" - integrity sha512-ZxFKm0/v48GSoBuO3DdnMlCYXefEUKUHLMsKxyXY4nZGgzbBKpF/X8haZa2paNj23CLfsCKBOtfc2vsEQiOOsA== + version "29.3.1" + resolved "https://registry.yarnpkg.com/electron/-/electron-29.3.1.tgz#87c82b2cd2c326f78f036499377a5448bea5d4bb" + integrity sha512-auge1/6RVqgUd6TgIq88wKdUCJi2cjESi3jy7d+6X4JzvBGprKBqMJ8JSSFpu/Px1YJrFUKAxfy6SC+TQf1uLw== dependencies: "@electron/get" "^2.0.0" "@types/node" "^20.9.0" @@ -1512,17 +1507,18 @@ find-up@^5.0.0: path-exists "^4.0.0" flat-cache@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" - integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== + version "3.2.0" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" + integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== dependencies: - flatted "^3.1.0" + flatted "^3.2.9" + keyv "^4.5.3" rimraf "^3.0.2" -flatted@^3.1.0: - version "3.2.6" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.6.tgz#022e9218c637f9f3fc9c35ab9c9193f05add60b2" - integrity sha512-0sQoMh9s0BYsm+12Huy/rkKxVu4R1+r96YX5cG44rHV0pQ6iC3Q+mkoMFaGWObMFYQxCVT+ssG1ksneA2MI9KQ== +flatted@^3.2.9: + version "3.3.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" + integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== foreground-child@^3.1.0: version "3.1.1" @@ -1638,18 +1634,7 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" -glob@^10.3.10: - version "10.3.10" - resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.10.tgz#0351ebb809fd187fe421ab96af83d3a70715df4b" - integrity sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g== - dependencies: - foreground-child "^3.1.0" - jackspeak "^2.3.5" - minimatch "^9.0.1" - minipass "^5.0.0 || ^6.0.2 || ^7.0.0" - path-scurry "^1.10.1" - -glob@^10.3.7: +glob@^10.3.10, glob@^10.3.7: version "10.3.12" resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.12.tgz#3a65c363c2e9998d220338e88a5f6ac97302960b" integrity sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg== @@ -1692,11 +1677,12 @@ globals@^13.19.0: type-fest "^0.20.2" globalthis@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf" - integrity sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA== + version "1.0.4" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" + integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== dependencies: - define-properties "^1.1.3" + define-properties "^1.2.1" + gopd "^1.0.1" globby@^11.1.0: version "11.1.0" @@ -1977,7 +1963,7 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== -jackspeak@^2.3.5, jackspeak@^2.3.6: +jackspeak@^2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.3.6.tgz#647ecc472238aee4b06ac0e461acc21a8c505ca8" integrity sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ== @@ -2077,7 +2063,7 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" -keyv@^4.0.0: +keyv@^4.0.0, keyv@^4.5.3: version "4.5.4" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== @@ -2161,11 +2147,6 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -"lru-cache@^9.1.1 || ^10.0.0": - version "10.2.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3" - integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q== - matcher@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/matcher/-/matcher-3.0.0.tgz#bd9060f4c5b70aa8041ccc6f80368760994f30ca" @@ -2237,14 +2218,7 @@ minimatch@^5.0.1, minimatch@^5.1.1: dependencies: brace-expansion "^2.0.1" -minimatch@^9.0.1: - version "9.0.3" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" - integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== - dependencies: - brace-expansion "^2.0.1" - -minimatch@^9.0.4: +minimatch@^9.0.1, minimatch@^9.0.4: version "9.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.4.tgz#8e49c731d1749cbec05050ee5145147b32496a51" integrity sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw== @@ -2384,16 +2358,16 @@ onnxruntime-node@^1.17: tar "^7.0.1" optionator@^0.9.3: - version "0.9.3" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" - integrity sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg== + version "0.9.4" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" + integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== dependencies: - "@aashutoshrathi/word-wrap" "^1.2.3" deep-is "^0.1.3" fast-levenshtein "^2.0.6" levn "^0.4.1" prelude-ls "^1.2.1" type-check "^0.4.0" + word-wrap "^1.2.5" p-cancelable@^2.0.0: version "2.1.1" @@ -2487,14 +2461,6 @@ path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-scurry@^1.10.1: - version "1.10.1" - resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.10.1.tgz#9ba6bf5aa8500fe9fd67df4f0d9483b2b0bfc698" - integrity sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ== - dependencies: - lru-cache "^9.1.1 || ^10.0.0" - minipass "^5.0.0 || ^6.0.2 || ^7.0.0" - path-scurry@^1.10.2: version "1.10.2" resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.10.2.tgz#8f6357eb1239d5fa1da8b9f70e9c080675458ba7" @@ -3105,12 +3071,7 @@ ts-api-utils@^1.3.0: resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1" integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ== -tslib@^2.1.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" - integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== - -tslib@^2.6.2: +tslib@^2.1.0, tslib@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== @@ -3152,16 +3113,11 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== -typescript@^5: +typescript@^5, typescript@^5.3.3: version "5.4.5" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== -typescript@^5.3.3: - version "5.4.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.3.tgz#5c6fedd4c87bee01cd7a528a30145521f8e0feff" - integrity sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg== - undici-types@~5.26.4: version "5.26.5" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" @@ -3182,7 +3138,7 @@ untildify@^3.0.2: resolved "https://registry.yarnpkg.com/untildify/-/untildify-3.0.3.tgz#1e7b42b140bcfd922b22e70ca1265bfe3634c7c9" integrity sha512-iSk/J8efr8uPT/Z4eSUywnqyrQU7DSdMfdqK4iWEaUVVmcP5JcnpRqmVMwcwcnmI1ATFNgC5V90u09tBynNFKA== -uri-js@^4.2.2: +uri-js@^4.2.2, uri-js@^4.4.1: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== @@ -3228,6 +3184,11 @@ winreg@1.2.4: resolved "https://registry.yarnpkg.com/winreg/-/winreg-1.2.4.tgz#ba065629b7a925130e15779108cf540990e98d1b" integrity sha512-IHpzORub7kYlb8A43Iig3reOvlcBJGX9gZ0WycHhghHtA65X0LYnMRuJs+aH1abVnMJztQkvQNlltnbPi5aGIA== +word-wrap@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== + "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" From 7056e04c020e3eea853e092618d5da3b7fd24370 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 1 May 2024 09:13:41 +0530 Subject: [PATCH 178/240] Fix lint for web Copy overrides from desktop --- web/packages/build-config/eslintrc-base.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/web/packages/build-config/eslintrc-base.js b/web/packages/build-config/eslintrc-base.js index b302be36d4..3e65638c1b 100644 --- a/web/packages/build-config/eslintrc-base.js +++ b/web/packages/build-config/eslintrc-base.js @@ -10,4 +10,20 @@ module.exports = { parserOptions: { project: true }, parser: "@typescript-eslint/parser", ignorePatterns: [".eslintrc.js"], + rules: { + /* Allow numbers to be used in template literals */ + "@typescript-eslint/restrict-template-expressions": [ + "error", + { + allowNumber: true, + }, + ], + /* Allow void expressions as the entire body of an arrow function */ + "@typescript-eslint/no-confusing-void-expression": [ + "error", + { + ignoreArrowShorthand: true, + }, + ], + }, }; From 735213b474d68e8561e23c03d2dfefd1c9810d32 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 1 May 2024 09:15:09 +0530 Subject: [PATCH 179/240] linter linter on the wall who's the fairest of them all --- web/apps/staff/src/App.tsx | 2 +- web/packages/next/log.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/apps/staff/src/App.tsx b/web/apps/staff/src/App.tsx index f8984fecbd..01d79b18cc 100644 --- a/web/apps/staff/src/App.tsx +++ b/web/apps/staff/src/App.tsx @@ -9,7 +9,7 @@ export const App: React.FC = () => { .then((userDetails) => { console.log("Fetched user details", userDetails); }) - .catch((e) => { + .catch((e: unknown) => { console.error("Failed to fetch user details", e); }); }; diff --git a/web/packages/next/log.ts b/web/packages/next/log.ts index a04520ed3d..f9ef7e5493 100644 --- a/web/packages/next/log.ts +++ b/web/packages/next/log.ts @@ -17,7 +17,7 @@ export const logToDisk = (message: string) => { }; const workerLogToDisk = (message: string) => { - workerBridge.logToDisk(message).catch((e) => { + workerBridge.logToDisk(message).catch((e: unknown) => { console.error( "Failed to log a message from worker", e, From eb1d8a8210a2420a772fdba8f419575b82959fc2 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 1 May 2024 09:26:19 +0530 Subject: [PATCH 180/240] Add a workaround for broken yarn classic dependency resolution ...that causes `yarn install` to fail. Ref: - https://github.com/isaacs/jackspeak/issues/5 - https://github.com/yargs/cliui/issues/159 --- desktop/package.json | 3 ++ desktop/yarn.lock | 73 +++++--------------------------------------- 2 files changed, 11 insertions(+), 65 deletions(-) diff --git a/desktop/package.json b/desktop/package.json index 8cbe1b7553..5ec8b45be3 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -18,6 +18,9 @@ "lint": "yarn prettier --check --log-level warn . && eslint --ext .ts src && yarn tsc", "lint-fix": "yarn prettier --write --log-level warn . && eslint --fix --ext .ts src && yarn tsc" }, + "resolutions": { + "jackspeak": "2.1.1" + }, "dependencies": { "any-shell-escape": "^0.1", "auto-launch": "^5.0", diff --git a/desktop/yarn.lock b/desktop/yarn.lock index 436ae84382..2210d47450 100644 --- a/desktop/yarn.lock +++ b/desktop/yarn.lock @@ -164,18 +164,6 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== -"@isaacs/cliui@^8.0.2": - version "8.0.2" - resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" - integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== - dependencies: - string-width "^5.1.2" - string-width-cjs "npm:string-width@^4.2.0" - strip-ansi "^7.0.1" - strip-ansi-cjs "npm:strip-ansi@^6.0.1" - wrap-ansi "^8.1.0" - wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" - "@isaacs/fs-minipass@^4.0.0": version "4.0.1" resolved "https://registry.yarnpkg.com/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz#2d59ae3ab4b38fb4270bfa23d30f8e2e86c7fe32" @@ -508,11 +496,6 @@ ansi-regex@^5.0.1: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== -ansi-regex@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" - integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== - ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" @@ -527,11 +510,6 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" -ansi-styles@^6.1.0: - version "6.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" - integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== - any-shell-escape@^0.1: version "0.1.1" resolved "https://registry.yarnpkg.com/any-shell-escape/-/any-shell-escape-0.1.1.tgz#d55ab972244c71a9a5e1ab0879f30bf110806959" @@ -1139,11 +1117,6 @@ dotenv@^9.0.2: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-9.0.2.tgz#dacc20160935a37dea6364aa1bef819fb9b6ab05" integrity sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg== -eastasianwidth@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" - integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== - ejs@^3.1.8: version "3.1.10" resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b" @@ -1240,11 +1213,6 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== -emoji-regex@^9.2.2: - version "9.2.2" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" - integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== - end-of-stream@^1.1.0: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" @@ -1963,12 +1931,12 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== -jackspeak@^2.3.6: - version "2.3.6" - resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.3.6.tgz#647ecc472238aee4b06ac0e461acc21a8c505ca8" - integrity sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ== +jackspeak@2.1.1, jackspeak@^2.3.6: + version "2.1.1" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.1.1.tgz#2a42db4cfbb7e55433c28b6f75d8b796af9669cd" + integrity sha512-juf9stUEwUaILepraGOWIJTLwg48bUnBmRqd2ln2Os1sW987zeoj/hzhbvRB95oMuS2ZTpjULmdwHNX4rzZIZw== dependencies: - "@isaacs/cliui" "^8.0.2" + cliui "^8.0.1" optionalDependencies: "@pkgjs/parseargs" "^0.11.0" @@ -2908,7 +2876,7 @@ stat-mode@^1.0.0: resolved "https://registry.yarnpkg.com/stat-mode/-/stat-mode-1.0.0.tgz#68b55cb61ea639ff57136f36b216a291800d1465" integrity sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg== -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -2917,15 +2885,6 @@ stat-mode@^1.0.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -string-width@^5.0.1, string-width@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" - integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== - dependencies: - eastasianwidth "^0.2.0" - emoji-regex "^9.2.2" - strip-ansi "^7.0.1" - string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" @@ -2933,20 +2892,13 @@ string_decoder@^1.1.1: dependencies: safe-buffer "~5.2.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: ansi-regex "^5.0.1" -strip-ansi@^7.0.1: - version "7.1.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" - integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== - dependencies: - ansi-regex "^6.0.1" - strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" @@ -3189,7 +3141,7 @@ word-wrap@^1.2.5: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -3198,15 +3150,6 @@ word-wrap@^1.2.5: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" - integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== - dependencies: - ansi-styles "^6.1.0" - string-width "^5.0.1" - strip-ansi "^7.0.1" - wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" From baf943550f72b019d7349e2817dc2c2373077e54 Mon Sep 17 00:00:00 2001 From: Vishnu Mohandas Date: Wed, 1 May 2024 10:01:41 +0530 Subject: [PATCH 181/240] Don't sent internal build for review --- .github/workflows/mobile-internal-release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/mobile-internal-release.yml b/.github/workflows/mobile-internal-release.yml index 9779a5d7aa..4ee7367424 100644 --- a/.github/workflows/mobile-internal-release.yml +++ b/.github/workflows/mobile-internal-release.yml @@ -54,3 +54,4 @@ jobs: packageName: io.ente.photos releaseFiles: mobile/build/app/outputs/bundle/playstoreRelease/app-playstore-release.aab track: internal + changesNotSentForReview: true From bd2969daffb2b3079789ce9d3cf08fdc26c5e999 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 1 May 2024 10:03:03 +0530 Subject: [PATCH 182/240] Fix inverted condition --- desktop/src/main/services/upload.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src/main/services/upload.ts b/desktop/src/main/services/upload.ts index 795ce48aff..f7d0436c0b 100644 --- a/desktop/src/main/services/upload.ts +++ b/desktop/src/main/services/upload.ts @@ -14,7 +14,7 @@ export const listZipItems = async (zipPath: string): Promise => { for (const entry of Object.values(entries)) { const basename = path.basename(entry.name); // Ignore "hidden" files (files whose names begins with a dot). - if (entry.isFile && basename.startsWith(".")) { + if (entry.isFile && !basename.startsWith(".")) { // `entry.name` is the path within the zip. entryNames.push(entry.name); } From 74f93efe1691a86a5f77283dfba8f041981f642e Mon Sep 17 00:00:00 2001 From: Vishnu Mohandas Date: Wed, 1 May 2024 10:03:26 +0530 Subject: [PATCH 183/240] Don't send changes for review in internal build --- .github/workflows/mobile-internal-release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/mobile-internal-release.yml b/.github/workflows/mobile-internal-release.yml index 9779a5d7aa..4ee7367424 100644 --- a/.github/workflows/mobile-internal-release.yml +++ b/.github/workflows/mobile-internal-release.yml @@ -54,3 +54,4 @@ jobs: packageName: io.ente.photos releaseFiles: mobile/build/app/outputs/bundle/playstoreRelease/app-playstore-release.aab track: internal + changesNotSentForReview: true From 38969d6f45b2558d9517b4c180756d90c59b2f5e Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 1 May 2024 10:17:51 +0530 Subject: [PATCH 184/240] Fix zip reader --- desktop/src/main/stream.ts | 3 ++- web/apps/photos/src/utils/native-stream.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/desktop/src/main/stream.ts b/desktop/src/main/stream.ts index 3e27de12b4..30aebaa9da 100644 --- a/desktop/src/main/stream.ts +++ b/desktop/src/main/stream.ts @@ -48,7 +48,8 @@ export const registerStreamProtocol = () => { const { host, pathname, hash } = new URL(url); // Convert e.g. "%20" to spaces. const path = decodeURIComponent(pathname); - const hashPath = decodeURIComponent(hash); + // `hash` begins with a "#", slice that off. + const hashPath = decodeURIComponent(hash.slice(1)); switch (host) { case "read": return handleRead(path); diff --git a/web/apps/photos/src/utils/native-stream.ts b/web/apps/photos/src/utils/native-stream.ts index 8ada6070cd..8d93becfe0 100644 --- a/web/apps/photos/src/utils/native-stream.ts +++ b/web/apps/photos/src/utils/native-stream.ts @@ -42,7 +42,7 @@ export const readStream = async ( url = new URL(`stream://read${pathOrZipItem}`); } else { const [zipPath, entryName] = pathOrZipItem; - url = new URL(`stream://read${zipPath}`); + url = new URL(`stream://read-zip${zipPath}`); url.hash = entryName; } From 15b013ea36c45833f6d1d8efd833a43eeb14f9bd Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 1 May 2024 10:19:21 +0530 Subject: [PATCH 185/240] Verified with a console log --- desktop/src/main/stream.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/desktop/src/main/stream.ts b/desktop/src/main/stream.ts index 30aebaa9da..74f6d428c3 100644 --- a/desktop/src/main/stream.ts +++ b/desktop/src/main/stream.ts @@ -117,7 +117,6 @@ const handleReadZip = async (zipPath: string, entryName: string) => { webReadableStreamAny as ReadableStream; // Close the zip handle when the underlying stream closes. - // TODO(MR): Verify stream.on("end", () => void zip.close()); return new Response(webReadableStream, { From 55c603d34504113dea40c23e00f5506ca862f603 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 1 May 2024 10:37:58 +0530 Subject: [PATCH 186/240] Tweak logs --- web/apps/photos/src/components/PhotoFrame.tsx | 2 +- web/apps/photos/src/utils/file/index.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/web/apps/photos/src/components/PhotoFrame.tsx b/web/apps/photos/src/components/PhotoFrame.tsx index f7db350daa..89f1ce887e 100644 --- a/web/apps/photos/src/components/PhotoFrame.tsx +++ b/web/apps/photos/src/components/PhotoFrame.tsx @@ -308,7 +308,7 @@ const PhotoFrame = ({ item: EnteFile, ) => { log.info( - `[${item.id}] getSlideData called for thumbnail: ${!!item.msrc} sourceLoaded: ${item.isSourceLoaded} fetching:${fetching[item.id]}`, + `[${item.id}] getSlideData called for thumbnail: ${!!item.msrc} sourceLoaded: ${!!item.isSourceLoaded} fetching: ${!!fetching[item.id]}`, ); if (!item.msrc) { diff --git a/web/apps/photos/src/utils/file/index.ts b/web/apps/photos/src/utils/file/index.ts index abbc8b0fa3..2b98ba3188 100644 --- a/web/apps/photos/src/utils/file/index.ts +++ b/web/apps/photos/src/utils/file/index.ts @@ -301,7 +301,8 @@ export const getRenderableImage = async (fileName: string, imageBlob: Blob) => { const tempFile = new File([imageBlob], fileName); const fileTypeInfo = await detectFileTypeInfo(tempFile); log.debug( - () => `Need renderable image for ${JSON.stringify(fileTypeInfo)}`, + () => + `Need renderable image for ${JSON.stringify({ fileName, ...fileTypeInfo })}`, ); const { extension } = fileTypeInfo; From 81feeef79252de2d24113979edc258e655523c17 Mon Sep 17 00:00:00 2001 From: vishnukvmd Date: Wed, 1 May 2024 10:41:15 +0530 Subject: [PATCH 187/240] Remove dependency on Isar --- mobile/ios/Podfile.lock | 6 --- mobile/ios/Runner.xcodeproj/project.pbxproj | 2 - mobile/pubspec.lock | 48 --------------------- mobile/pubspec.yaml | 3 -- mobile/scripts/build_isar.sh | 17 -------- 5 files changed, 76 deletions(-) delete mode 100755 mobile/scripts/build_isar.sh diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index 88bc70bf78..7315149574 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -108,8 +108,6 @@ PODS: - FlutterMacOS - integration_test (0.0.1): - Flutter - - isar_flutter_libs (1.0.0): - - Flutter - libwebp (1.3.2): - libwebp/demux (= 1.3.2) - libwebp/mux (= 1.3.2) @@ -246,7 +244,6 @@ DEPENDENCIES: - image_editor_common (from `.symlinks/plugins/image_editor_common/ios`) - in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/darwin`) - integration_test (from `.symlinks/plugins/integration_test/ios`) - - isar_flutter_libs (from `.symlinks/plugins/isar_flutter_libs/ios`) - local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`) - local_auth_ios (from `.symlinks/plugins/local_auth_ios/ios`) - media_extension (from `.symlinks/plugins/media_extension/ios`) @@ -341,8 +338,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/in_app_purchase_storekit/darwin" integration_test: :path: ".symlinks/plugins/integration_test/ios" - isar_flutter_libs: - :path: ".symlinks/plugins/isar_flutter_libs/ios" local_auth_darwin: :path: ".symlinks/plugins/local_auth_darwin/darwin" local_auth_ios: @@ -427,7 +422,6 @@ SPEC CHECKSUMS: image_editor_common: d6f6644ae4a6de80481e89fe6d0a8c49e30b4b43 in_app_purchase_storekit: 0e4b3c2e43ba1e1281f4f46dd71b0593ce529892 integration_test: 13825b8a9334a850581300559b8839134b124670 - isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073 libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009 local_auth_darwin: c7e464000a6a89e952235699e32b329457608d98 local_auth_ios: 5046a18c018dd973247a0564496c8898dbb5adf9 diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 89c4926296..c88f9da380 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -308,7 +308,6 @@ "${BUILT_PRODUCTS_DIR}/image_editor_common/image_editor_common.framework", "${BUILT_PRODUCTS_DIR}/in_app_purchase_storekit/in_app_purchase_storekit.framework", "${BUILT_PRODUCTS_DIR}/integration_test/integration_test.framework", - "${BUILT_PRODUCTS_DIR}/isar_flutter_libs/isar_flutter_libs.framework", "${BUILT_PRODUCTS_DIR}/libwebp/libwebp.framework", "${BUILT_PRODUCTS_DIR}/local_auth_darwin/local_auth_darwin.framework", "${BUILT_PRODUCTS_DIR}/local_auth_ios/local_auth_ios.framework", @@ -390,7 +389,6 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_editor_common.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/in_app_purchase_storekit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/integration_test.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/isar_flutter_libs.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libwebp.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/local_auth_darwin.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/local_auth_ios.framework", diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 19553bdcb2..ae74068eb8 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -363,14 +363,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" - dartx: - dependency: transitive - description: - name: dartx - sha256: "8b25435617027257d43e6508b5fe061012880ddfdaa75a71d607c3de2a13d244" - url: "https://pub.dev" - source: hosted - version: "1.2.0" dbus: dependency: transitive description: @@ -1116,30 +1108,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" - isar: - dependency: "direct main" - description: - name: isar - sha256: "99165dadb2cf2329d3140198363a7e7bff9bbd441871898a87e26914d25cf1ea" - url: "https://pub.dev" - source: hosted - version: "3.1.0+1" - isar_flutter_libs: - dependency: "direct main" - description: - name: isar_flutter_libs - sha256: bc6768cc4b9c61aabff77152e7f33b4b17d2fc93134f7af1c3dd51500fe8d5e8 - url: "https://pub.dev" - source: hosted - version: "3.1.0+1" - isar_generator: - dependency: "direct dev" - description: - name: isar_generator - sha256: "76c121e1295a30423604f2f819bc255bc79f852f3bc8743a24017df6068ad133" - url: "https://pub.dev" - source: hosted - version: "3.1.0+1" js: dependency: transitive description: @@ -2220,14 +2188,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.9" - time: - dependency: transitive - description: - name: time - sha256: ad8e018a6c9db36cb917a031853a1aae49467a93e0d464683e029537d848c221 - url: "https://pub.dev" - source: hosted - version: "2.1.4" timezone: dependency: transitive description: @@ -2597,14 +2557,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" - xxh3: - dependency: transitive - description: - name: xxh3 - sha256: a92b30944a9aeb4e3d4f3c3d4ddb3c7816ca73475cd603682c4f8149690f56d7 - url: "https://pub.dev" - source: hosted - version: "1.0.1" yaml: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index ea01ba0868..a839ce6748 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -99,8 +99,6 @@ dependencies: image_editor: ^1.3.0 in_app_purchase: ^3.0.7 intl: ^0.18.0 - isar: ^3.1.0+1 - isar_flutter_libs: ^3.1.0+1 json_annotation: ^4.8.0 latlong2: ^0.9.0 like_button: ^2.0.5 @@ -196,7 +194,6 @@ dev_dependencies: freezed: ^2.5.2 integration_test: sdk: flutter - isar_generator: ^3.1.0+1 json_serializable: ^6.6.1 test: ^1.22.0 diff --git a/mobile/scripts/build_isar.sh b/mobile/scripts/build_isar.sh deleted file mode 100755 index 1bb1d38f6c..0000000000 --- a/mobile/scripts/build_isar.sh +++ /dev/null @@ -1,17 +0,0 @@ -# TODO: add `rustup@1.25.2` to `srclibs` -# TODO: verify if `gcc-multilib` or `libc-dev` is needed -$$rustup$$/rustup-init.sh -y -source $HOME/.cargo/env -cd thirdparty/isar/ -bash tool/build_android.sh x86 -bash tool/build_android.sh x64 -bash tool/build_android.sh armv7 -bash tool/build_android.sh arm64 -mv libisar_android_arm64.so libisar.so -mv libisar.so $PUB_CACHE/hosted/pub.dev/isar_flutter_libs-*/android/src/main/jniLibs/arm64-v8a/ -mv libisar_android_armv7.so libisar.so -mv libisar.so $PUB_CACHE/hosted/pub.dev/isar_flutter_libs-*/android/src/main/jniLibs/armeabi-v7a/ -mv libisar_android_x64.so libisar.so -mv libisar.so $PUB_CACHE/hosted/pub.dev/isar_flutter_libs-*/android/src/main/jniLibs/x86_64/ -mv libisar_android_x86.so libisar.so -mv libisar.so $PUB_CACHE/hosted/pub.dev/isar_flutter_libs-*/android/src/main/jniLibs/x86/ From de92a9dd649ccc341c85ab51143f1dcd92613f25 Mon Sep 17 00:00:00 2001 From: vishnukvmd Date: Wed, 1 May 2024 10:41:36 +0530 Subject: [PATCH 188/240] v0.8.87 --- mobile/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index a839ce6748..cfef39e565 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -12,7 +12,7 @@ description: ente photos application # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.8.86+606 +version: 0.8.87+607 publish_to: none environment: From ff5a167f48af1395954a4526d478cde570c5eff8 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 1 May 2024 11:33:42 +0530 Subject: [PATCH 189/240] Might not be JSONStringify-able --- web/apps/photos/src/services/exif.ts | 43 ++++++++-------------------- 1 file changed, 12 insertions(+), 31 deletions(-) diff --git a/web/apps/photos/src/services/exif.ts b/web/apps/photos/src/services/exif.ts index 584d79f880..073a695f75 100644 --- a/web/apps/photos/src/services/exif.ts +++ b/web/apps/photos/src/services/exif.ts @@ -167,14 +167,7 @@ function parseExifData(exifData: RawEXIFData): ParsedEXIFData { parsedExif.imageWidth = ImageWidth; parsedExif.imageHeight = ImageHeight; } else { - log.error( - `Image dimension parsing failed - ImageWidth or ImageHeight is not a number ${JSON.stringify( - { - ImageWidth, - ImageHeight, - }, - )}`, - ); + log.warn("EXIF: Ignoring non-numeric ImageWidth or ImageHeight"); } } else if (ExifImageWidth && ExifImageHeight) { if ( @@ -184,13 +177,8 @@ function parseExifData(exifData: RawEXIFData): ParsedEXIFData { parsedExif.imageWidth = ExifImageWidth; parsedExif.imageHeight = ExifImageHeight; } else { - log.error( - `Image dimension parsing failed - ExifImageWidth or ExifImageHeight is not a number ${JSON.stringify( - { - ExifImageWidth, - ExifImageHeight, - }, - )}`, + log.warn( + "EXIF: Ignoring non-numeric ExifImageWidth or ExifImageHeight", ); } } else if (PixelXDimension && PixelYDimension) { @@ -201,13 +189,8 @@ function parseExifData(exifData: RawEXIFData): ParsedEXIFData { parsedExif.imageWidth = PixelXDimension; parsedExif.imageHeight = PixelYDimension; } else { - log.error( - `Image dimension parsing failed - PixelXDimension or PixelYDimension is not a number ${JSON.stringify( - { - PixelXDimension, - PixelYDimension, - }, - )}`, + log.warn( + "EXIF: Ignoring non-numeric PixelXDimension or PixelYDimension", ); } } @@ -302,15 +285,13 @@ export function parseEXIFLocation( ); return { latitude, longitude }; } catch (e) { - log.error( - `Failed to parseEXIFLocation ${JSON.stringify({ - gpsLatitude, - gpsLatitudeRef, - gpsLongitude, - gpsLongitudeRef, - })}`, - e, - ); + const p = { + gpsLatitude, + gpsLatitudeRef, + gpsLongitude, + gpsLongitudeRef, + }; + log.error(`Failed to parse EXIF location ${JSON.stringify(p)}`, e); return { ...NULL_LOCATION }; } } From 6086d436358404ef68acf6e9bcf74e6fc29e6918 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 1 May 2024 12:29:11 +0530 Subject: [PATCH 190/240] Don't log a potentially huge list --- web/apps/photos/src/components/Upload/Uploader.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/apps/photos/src/components/Upload/Uploader.tsx b/web/apps/photos/src/components/Upload/Uploader.tsx index cfd674e3f8..e56d7583e0 100644 --- a/web/apps/photos/src/components/Upload/Uploader.tsx +++ b/web/apps/photos/src/components/Upload/Uploader.tsx @@ -261,7 +261,9 @@ export default function Uploader({ const { collectionName, filePaths, zipItems } = pending; - log.info("Resuming pending upload", pending); + log.info( + `Resuming pending of upload of ${filePaths.length + zipItems.length} items${collectionName ? " to collection " + collectionName : ""}`, + ); isPendingDesktopUpload.current = true; pendingDesktopUploadCollectionName.current = collectionName; setDesktopFilePaths(filePaths); From f91dddda93cf1e82bfa4706a63405dc5be46a009 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 1 May 2024 13:35:42 +0530 Subject: [PATCH 191/240] Fix paths --- web/apps/photos/src/services/export/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/apps/photos/src/services/export/index.ts b/web/apps/photos/src/services/export/index.ts index 82dfdbf8bf..b387d4fd3c 100644 --- a/web/apps/photos/src/services/export/index.ts +++ b/web/apps/photos/src/services/export/index.ts @@ -1409,6 +1409,6 @@ const moveToTrash = async ( metadataFileName, fs.exists, ); - await fs.rename(filePath, metadataTrashFilePath); + await fs.rename(metadataFilePath, metadataTrashFilePath); } }; From f2a764aac2ff897470df09c29e316af30bbc22aa Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 1 May 2024 13:42:09 +0530 Subject: [PATCH 192/240] Fix export trash movement --- web/apps/photos/src/services/export/index.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/web/apps/photos/src/services/export/index.ts b/web/apps/photos/src/services/export/index.ts index b387d4fd3c..c46e1d8fc7 100644 --- a/web/apps/photos/src/services/export/index.ts +++ b/web/apps/photos/src/services/export/index.ts @@ -1398,17 +1398,19 @@ const moveToTrash = async ( if (await fs.exists(filePath)) { await fs.mkdirIfNeeded(trashDir); - const trashFilePath = await safeFileName(trashDir, fileName, fs.exists); + const trashFileName = await safeFileName(trashDir, fileName, fs.exists); + const trashFilePath = `${trashDir}/${trashFileName}`; await fs.rename(filePath, trashFilePath); } if (await fs.exists(metadataFilePath)) { await fs.mkdirIfNeeded(metadataTrashDir); - const metadataTrashFilePath = await safeFileName( + const metadataTrashFileName = await safeFileName( metadataTrashDir, metadataFileName, fs.exists, ); + const metadataTrashFilePath = `${metadataTrashDir}/${metadataTrashFileName}`; await fs.rename(metadataFilePath, metadataTrashFilePath); } }; From 5ffc2f20fd28e2fe269404f0656890468a9e0b36 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 1 May 2024 13:57:33 +0530 Subject: [PATCH 193/240] Replicate previous behaviour --- web/apps/photos/src/services/export/index.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/web/apps/photos/src/services/export/index.ts b/web/apps/photos/src/services/export/index.ts index c46e1d8fc7..b02e05a428 100644 --- a/web/apps/photos/src/services/export/index.ts +++ b/web/apps/photos/src/services/export/index.ts @@ -547,6 +547,9 @@ class ExportService { isCanceled: CancellationStatus, ) { const fs = ensureElectron().fs; + const rmdirIfExists = async (dirPath: string) => { + if (await fs.exists(dirPath)) await fs.rmdir(dirPath); + }; try { const exportRecord = await this.getExportRecord(exportFolder); const collectionIDPathMap = @@ -581,11 +584,11 @@ class ExportService { ); try { // delete the collection metadata folder - await fs.rmdir( + await rmdirIfExists( getMetadataFolderExportPath(collectionExportPath), ); // delete the collection folder - await fs.rmdir(collectionExportPath); + await rmdirIfExists(collectionExportPath); } catch (e) { await this.addCollectionExportedRecord( exportFolder, From 52909f6f2118d497871fe455adadff9f08463e36 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 1 May 2024 14:01:05 +0530 Subject: [PATCH 194/240] The handler adds its own error message prefix Error occurred in handler for 'generateImageThumbnail': Error: This feature in not available on the current OS/arch --- web/apps/photos/src/services/upload/uploadService.ts | 2 +- web/apps/photos/src/utils/file/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/apps/photos/src/services/upload/uploadService.ts b/web/apps/photos/src/services/upload/uploadService.ts index 7d33038842..52f495785a 100644 --- a/web/apps/photos/src/services/upload/uploadService.ts +++ b/web/apps/photos/src/services/upload/uploadService.ts @@ -1021,7 +1021,7 @@ const withThumbnail = async ( fileTypeInfo, ); } catch (e) { - if (e.message == CustomErrorMessage.NotAvailable) { + if (e.message.endsWith(CustomErrorMessage.NotAvailable)) { moduleState.isNativeImageThumbnailGenerationNotAvailable = true; } else { log.error("Native thumbnail generation failed", e); diff --git a/web/apps/photos/src/utils/file/index.ts b/web/apps/photos/src/utils/file/index.ts index 2b98ba3188..212b2efd31 100644 --- a/web/apps/photos/src/utils/file/index.ts +++ b/web/apps/photos/src/utils/file/index.ts @@ -319,7 +319,7 @@ export const getRenderableImage = async (fileName: string, imageBlob: Blob) => { try { return await nativeConvertToJPEG(imageBlob); } catch (e) { - if (e.message == CustomErrorMessage.NotAvailable) { + if (e.message.endsWith(CustomErrorMessage.NotAvailable)) { moduleState.isNativeJPEGConversionNotAvailable = true; } else { log.error("Native conversion to JPEG failed", e); From cb73bc143dde713d4bd4b15925fc0f57810deb45 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 1 May 2024 14:35:03 +0530 Subject: [PATCH 195/240] Research --- desktop/src/main/utils/electron.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/desktop/src/main/utils/electron.ts b/desktop/src/main/utils/electron.ts index d627ec5c46..43471cd1ee 100644 --- a/desktop/src/main/utils/electron.ts +++ b/desktop/src/main/utils/electron.ts @@ -11,6 +11,19 @@ export const isDev = !app.isPackaged; /** * Convert a file system {@link filePath} that uses the local system specific * path separators into a path that uses POSIX file separators. + * + * For all paths that we persist or pass over the IPC boundary, we always use + * POSIX paths, even on Windows. + * + * Windows recognizes both forward and backslashes. This also works with drive + * names. c:\foo\bar and c:/foo/bar are both valid. + * + * > Almost all paths passed to Windows APIs are normalized. During + * > normalization, Windows performs the following steps: ... All forward + * > slashes (/) are converted into the standard Windows separator, the back + * > slash (\). + * > + * > https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats */ export const posixPath = (filePath: string) => filePath.split(path.sep).join(path.posix.sep); From 36984012a8883f1cbdddf6a59663b1014cf278a2 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 1 May 2024 15:00:18 +0530 Subject: [PATCH 196/240] They're all optionals now --- desktop/src/main/stores/upload-status.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/desktop/src/main/stores/upload-status.ts b/desktop/src/main/stores/upload-status.ts index 472f38a7f9..8cb2410df6 100644 --- a/desktop/src/main/stores/upload-status.ts +++ b/desktop/src/main/stores/upload-status.ts @@ -9,15 +9,10 @@ export interface UploadStatusStore { collectionName?: string; /** * Paths to regular files that are pending upload. - * - * This should generally be present, albeit empty, but it is marked optional - * in sympathy with its siblings. */ filePaths?: string[]; /** * Each item is the path to a zip file and the name of an entry within it. - * - * This is marked optional since legacy stores will not have it. */ zipItems?: [zipPath: string, entryName: string][]; /** From 4c28e83dbb4bebe2985f03bb9a92fd09970c2dcc Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 1 May 2024 15:07:58 +0530 Subject: [PATCH 197/240] Optimize --- desktop/src/main/services/watch.ts | 4 ++-- desktop/src/main/utils/electron.ts | 10 ++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/desktop/src/main/services/watch.ts b/desktop/src/main/services/watch.ts index a56c0cf6c3..975d8a7c35 100644 --- a/desktop/src/main/services/watch.ts +++ b/desktop/src/main/services/watch.ts @@ -35,8 +35,8 @@ export const createWatcher = (mainWindow: BrowserWindow) => { return watcher; }; -const eventData = (path: string): [string, FolderWatch] => { - path = posixPath(path); +const eventData = (platformPath: string): [string, FolderWatch] => { + const path = posixPath(platformPath); const watch = folderWatches().find((watch) => path.startsWith(watch.folderPath + "/"), diff --git a/desktop/src/main/utils/electron.ts b/desktop/src/main/utils/electron.ts index 43471cd1ee..93e8565ef2 100644 --- a/desktop/src/main/utils/electron.ts +++ b/desktop/src/main/utils/electron.ts @@ -9,8 +9,8 @@ import log from "../log"; export const isDev = !app.isPackaged; /** - * Convert a file system {@link filePath} that uses the local system specific - * path separators into a path that uses POSIX file separators. + * Convert a file system {@link platformPath} that uses the local system + * specific path separators into a path that uses POSIX file separators. * * For all paths that we persist or pass over the IPC boundary, we always use * POSIX paths, even on Windows. @@ -25,8 +25,10 @@ export const isDev = !app.isPackaged; * > * > https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats */ -export const posixPath = (filePath: string) => - filePath.split(path.sep).join(path.posix.sep); +export const posixPath = (platformPath: string) => + path.sep == path.posix.sep + ? platformPath + : platformPath.split(path.sep).join(path.posix.sep); /** * Run a shell command asynchronously. From 5f131693f4a4482a5477441cb1cd9ccfa9b90288 Mon Sep 17 00:00:00 2001 From: vishnukvmd Date: Wed, 1 May 2024 15:12:48 +0530 Subject: [PATCH 198/240] Use Int instead of String to represent an enum --- mobile/lib/db/embeddings_sqlite_db.dart | 30 +++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/mobile/lib/db/embeddings_sqlite_db.dart b/mobile/lib/db/embeddings_sqlite_db.dart index 2c77af2811..a993c3275c 100644 --- a/mobile/lib/db/embeddings_sqlite_db.dart +++ b/mobile/lib/db/embeddings_sqlite_db.dart @@ -42,7 +42,7 @@ class EmbeddingsDB { 1, (tx) async { await tx.execute( - 'CREATE TABLE $tableName ($columnFileID INTEGER NOT NULL, $columnModel TEXT NOT NULL, $columnEmbedding BLOB NOT NULL, $columnUpdationTime INTEGER, UNIQUE ($columnFileID, $columnModel))', + 'CREATE TABLE $tableName ($columnFileID INTEGER NOT NULL, $columnModel INTEGER NOT NULL, $columnEmbedding BLOB NOT NULL, $columnUpdationTime INTEGER, UNIQUE ($columnFileID, $columnModel))', ); }, ), @@ -102,7 +102,7 @@ class EmbeddingsDB { final db = await _database; await db.execute( 'DELETE FROM $tableName WHERE $columnModel = ?', - [serialize(model)], + [modelToInt(model)!], ); Bus.instance.fire(EmbeddingUpdatedEvent()); } @@ -117,7 +117,7 @@ class EmbeddingsDB { Embedding _getEmbeddingFromRow(Map row) { final fileID = row[columnFileID]; - final model = deserialize(row[columnModel]); + final model = intToModel(row[columnModel])!; final bytes = row[columnEmbedding] as Uint8List; final list = Float32List.view(bytes.buffer); return Embedding(fileID: fileID, model: model, embedding: list); @@ -126,7 +126,7 @@ class EmbeddingsDB { List _getRowFromEmbedding(Embedding embedding) { return [ embedding.fileID, - serialize(embedding.model), + modelToInt(embedding.model)!, Float32List.fromList(embedding.embedding).buffer.asUint8List(), embedding.updationTime, ]; @@ -142,4 +142,26 @@ class EmbeddingsDB { await deprecatedDB.delete(); } } + + int? modelToInt(Model model) { + switch (model) { + case Model.onnxClip: + return 1; + case Model.ggmlClip: + return 2; + default: + return null; + } + } + + Model? intToModel(int model) { + switch (model) { + case 1: + return Model.onnxClip; + case 2: + return Model.ggmlClip; + default: + return null; + } + } } From 10f2c3db6f84e013ce2cef80c6184d9a24d8ba74 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 1 May 2024 15:12:50 +0530 Subject: [PATCH 199/240] Mention why we're normalizing --- desktop/src/main/services/dir.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/desktop/src/main/services/dir.ts b/desktop/src/main/services/dir.ts index 4e2a8c65e4..d375648f6f 100644 --- a/desktop/src/main/services/dir.ts +++ b/desktop/src/main/services/dir.ts @@ -17,8 +17,11 @@ export const selectDirectory = async () => { * For example, on macOS this'll open {@link dirPath} in Finder. */ export const openDirectory = async (dirPath: string) => { + // We need to use `path.normalize` because `shell.openPath; does not support + // POSIX path, it needs to be a platform specific path: + // https://github.com/electron/electron/issues/28831#issuecomment-826370589 const res = await shell.openPath(path.normalize(dirPath)); - // shell.openPath resolves with a string containing the error message + // `shell.openPath` resolves with a string containing the error message // corresponding to the failure if a failure occurred, otherwise "". if (res) throw new Error(`Failed to open directory ${dirPath}: res`); }; From 1cd9fb1021b611a5bd7fbd0aa618e1ceb01ba212 Mon Sep 17 00:00:00 2001 From: vishnukvmd Date: Wed, 1 May 2024 15:12:57 +0530 Subject: [PATCH 200/240] v0.8.88 --- mobile/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index cfef39e565..2adf293210 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -12,7 +12,7 @@ description: ente photos application # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.8.87+607 +version: 0.8.88+608 publish_to: none environment: From bc32c89d7772347ac68592fd04cff2cbdcff887c Mon Sep 17 00:00:00 2001 From: vishnukvmd Date: Wed, 1 May 2024 15:14:44 +0530 Subject: [PATCH 201/240] Rename DB --- mobile/lib/core/configuration.dart | 2 +- mobile/lib/db/{embeddings_sqlite_db.dart => embeddings_db.dart} | 0 .../machine_learning/semantic_search/embedding_store.dart | 2 +- .../semantic_search/semantic_search_service.dart | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename mobile/lib/db/{embeddings_sqlite_db.dart => embeddings_db.dart} (100%) diff --git a/mobile/lib/core/configuration.dart b/mobile/lib/core/configuration.dart index 8b7ecbad53..cde766b1e0 100644 --- a/mobile/lib/core/configuration.dart +++ b/mobile/lib/core/configuration.dart @@ -11,7 +11,7 @@ import 'package:photos/core/constants.dart'; import 'package:photos/core/error-reporting/super_logging.dart'; import 'package:photos/core/event_bus.dart'; import 'package:photos/db/collections_db.dart'; -import "package:photos/db/embeddings_sqlite_db.dart"; +import "package:photos/db/embeddings_db.dart"; import 'package:photos/db/files_db.dart'; import 'package:photos/db/memories_db.dart'; import 'package:photos/db/trash_db.dart'; diff --git a/mobile/lib/db/embeddings_sqlite_db.dart b/mobile/lib/db/embeddings_db.dart similarity index 100% rename from mobile/lib/db/embeddings_sqlite_db.dart rename to mobile/lib/db/embeddings_db.dart diff --git a/mobile/lib/services/machine_learning/semantic_search/embedding_store.dart b/mobile/lib/services/machine_learning/semantic_search/embedding_store.dart index b3795db4e9..420b8c97f7 100644 --- a/mobile/lib/services/machine_learning/semantic_search/embedding_store.dart +++ b/mobile/lib/services/machine_learning/semantic_search/embedding_store.dart @@ -5,7 +5,7 @@ import "dart:typed_data"; import "package:computer/computer.dart"; import "package:logging/logging.dart"; import "package:photos/core/network/network.dart"; -import "package:photos/db/embeddings_sqlite_db.dart"; +import "package:photos/db/embeddings_db.dart"; import "package:photos/db/files_db.dart"; import "package:photos/models/embedding.dart"; import "package:photos/models/file/file.dart"; diff --git a/mobile/lib/services/machine_learning/semantic_search/semantic_search_service.dart b/mobile/lib/services/machine_learning/semantic_search/semantic_search_service.dart index eb5bfeb912..337ca913ff 100644 --- a/mobile/lib/services/machine_learning/semantic_search/semantic_search_service.dart +++ b/mobile/lib/services/machine_learning/semantic_search/semantic_search_service.dart @@ -7,7 +7,7 @@ import "package:logging/logging.dart"; import "package:photos/core/cache/lru_map.dart"; import "package:photos/core/configuration.dart"; import "package:photos/core/event_bus.dart"; -import "package:photos/db/embeddings_sqlite_db.dart"; +import "package:photos/db/embeddings_db.dart"; import "package:photos/db/files_db.dart"; import "package:photos/events/diff_sync_complete_event.dart"; import 'package:photos/events/embedding_updated_event.dart'; From 9dd865ff6e69647062ad3efeba3c8d3ebb5d9276 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 1 May 2024 15:25:46 +0530 Subject: [PATCH 202/240] Comment --- .../photos/src/components/Upload/Uploader.tsx | 20 ++++++++++++++----- web/packages/next/types/ipc.ts | 2 ++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/web/apps/photos/src/components/Upload/Uploader.tsx b/web/apps/photos/src/components/Upload/Uploader.tsx index e56d7583e0..53ad1fb32f 100644 --- a/web/apps/photos/src/components/Upload/Uploader.tsx +++ b/web/apps/photos/src/components/Upload/Uploader.tsx @@ -325,13 +325,26 @@ export default function Uploader({ // Trigger an upload when any of the dependencies change. useEffect(() => { + // Re the paths: + // + // - These are not necessarily the full paths. In particular, when + // running on the browser they'll be the relative paths (at best) or + // just the file-name otherwise. + // + // - All the paths use POSIX separators. See inline comments. const allItemAndPaths = [ // See: [Note: webkitRelativePath]. In particular, they use POSIX // separators. webFiles.map((f) => [f, f.webkitRelativePath ?? f.name]), + // The paths we get from the desktop app all eventually come either + // from electron.selectDirectory or electron.pathForFile, both of + // which return POSIX paths. desktopFiles.map((fp) => [fp, fp.path]), desktopFilePaths.map((p) => [p, p]), - // ze[1], the entry name, uses POSIX separators. + // The first path, that of the zip file itself, is POSIX like the + // other paths we get over the IPC boundary. And the second path, + // ze[1], the entry name, uses POSIX separators because that is what + // the ZIP format uses. desktopZipItems.map((ze) => [ze, ze[1]]), ].flat() as [UploadItem, string][]; @@ -794,10 +807,7 @@ async function waitAndRun( await task(); } -const desktopFilesAndZipItems = async ( - electron: Electron, - files: File[], -): Promise<{ fileAndPaths: FileAndPath[]; zipItems: ZipItem[] }> => { +const desktopFilesAndZipItems = async (electron: Electron, files: File[]) => { const fileAndPaths: FileAndPath[] = []; let zipItems: ZipItem[] = []; diff --git a/web/packages/next/types/ipc.ts b/web/packages/next/types/ipc.ts index c851062410..fb72bcf5ca 100644 --- a/web/packages/next/types/ipc.ts +++ b/web/packages/next/types/ipc.ts @@ -53,6 +53,8 @@ export interface Electron { * Ask the user to select a directory on their local file system, and return * it path. * + * The returned path is guaranteed to use POSIX separators ('/'). + * * We don't strictly need IPC for this, we can use a hidden element * and trigger its click for the same behaviour (as we do for the * `useFileInput` hook that we use for uploads). However, it's a bit From 0a93ba67a1fc6c1b2627c1d2e47dcd68239021ff Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 1 May 2024 17:00:44 +0530 Subject: [PATCH 203/240] Fix warnings Refs: https://stackoverflow.com/questions/69730364/what-is-the-purpose-of-shouldforwardprop-option-in-styled --- .../photos/src/components/PhotoList/index.tsx | 19 ++++++++++--------- .../Search/SearchBar/styledComponents.tsx | 4 +++- .../shared/components/Navbar/base.tsx | 5 ++++- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/web/apps/photos/src/components/PhotoList/index.tsx b/web/apps/photos/src/components/PhotoList/index.tsx index 91f712df1b..4803995d4f 100644 --- a/web/apps/photos/src/components/PhotoList/index.tsx +++ b/web/apps/photos/src/components/PhotoList/index.tsx @@ -111,14 +111,13 @@ function getShrinkRatio(width: number, columns: number) { ); } -const ListContainer = styled(Box)<{ - columns: number; - shrinkRatio: number; - groups?: number[]; +const ListContainer = styled(Box, { + shouldForwardProp: (propName) => propName != "gridTemplateColumns", +})<{ + gridTemplateColumns: string; }>` display: grid; - grid-template-columns: ${({ columns, shrinkRatio, groups }) => - getTemplateColumns(columns, shrinkRatio, groups)}; + grid-template-columns: ${(props) => props.gridTemplateColumns}; grid-column-gap: ${GAP_BTW_TILES}px; width: 100%; color: #fff; @@ -235,9 +234,11 @@ const PhotoListRow = React.memo( return ( {renderListItem(timeStampList[index], isScrolling)} diff --git a/web/apps/photos/src/components/Search/SearchBar/styledComponents.tsx b/web/apps/photos/src/components/Search/SearchBar/styledComponents.tsx index 41d4a0971e..d33c7c9490 100644 --- a/web/apps/photos/src/components/Search/SearchBar/styledComponents.tsx +++ b/web/apps/photos/src/components/Search/SearchBar/styledComponents.tsx @@ -23,7 +23,9 @@ export const SearchMobileBox = styled(FluidContainer)` } `; -export const SearchInputWrapper = styled(CenteredFlex)<{ isOpen: boolean }>` +export const SearchInputWrapper = styled(CenteredFlex, { + shouldForwardProp: (propName) => propName != "isOpen", +})<{ isOpen: boolean }>` background: ${({ theme }) => theme.colors.background.base}; max-width: 484px; margin: auto; diff --git a/web/packages/shared/components/Navbar/base.tsx b/web/packages/shared/components/Navbar/base.tsx index 101506cfd0..403dc808ca 100644 --- a/web/packages/shared/components/Navbar/base.tsx +++ b/web/packages/shared/components/Navbar/base.tsx @@ -1,6 +1,9 @@ import { styled } from "@mui/material"; import { FlexWrapper } from "../../components/Container"; -const NavbarBase = styled(FlexWrapper)<{ isMobile: boolean }>` + +const NavbarBase = styled(FlexWrapper, { + shouldForwardProp: (propName) => propName != "isMobile", +})<{ isMobile: boolean }>` min-height: 64px; position: sticky; top: 0; From b967d4bbea3349b86cfe6a070d443e62b57d4cf4 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 1 May 2024 17:39:28 +0530 Subject: [PATCH 204/240] URL encode better e.g. fixes the reading of a file with a hash in the name --- desktop/src/main/stream.ts | 3 +++ web/apps/photos/src/utils/native-stream.ts | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/desktop/src/main/stream.ts b/desktop/src/main/stream.ts index 74f6d428c3..f1000c6bde 100644 --- a/desktop/src/main/stream.ts +++ b/desktop/src/main/stream.ts @@ -50,6 +50,9 @@ export const registerStreamProtocol = () => { const path = decodeURIComponent(pathname); // `hash` begins with a "#", slice that off. const hashPath = decodeURIComponent(hash.slice(1)); + log.debug( + () => `[stream] ${host} ${path}${hashPath ? "::" + hashPath : ""}`, + ); switch (host) { case "read": return handleRead(path); diff --git a/web/apps/photos/src/utils/native-stream.ts b/web/apps/photos/src/utils/native-stream.ts index 8d93becfe0..a910d3b228 100644 --- a/web/apps/photos/src/utils/native-stream.ts +++ b/web/apps/photos/src/utils/native-stream.ts @@ -39,10 +39,12 @@ export const readStream = async ( ): Promise<{ response: Response; size: number; lastModifiedMs: number }> => { let url: URL; if (typeof pathOrZipItem == "string") { - url = new URL(`stream://read${pathOrZipItem}`); + url = new URL("stream://read"); + url.pathname = pathOrZipItem } else { const [zipPath, entryName] = pathOrZipItem; - url = new URL(`stream://read-zip${zipPath}`); + url = new URL("stream://read-zip"); + url.pathname = zipPath; url.hash = entryName; } From c988884ab1b567f71b75300b81ea2aaf1f0e1891 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 1 May 2024 17:56:46 +0530 Subject: [PATCH 205/240] Setting pathname had no effect --- web/apps/photos/src/utils/native-stream.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/web/apps/photos/src/utils/native-stream.ts b/web/apps/photos/src/utils/native-stream.ts index a910d3b228..b20cb9290a 100644 --- a/web/apps/photos/src/utils/native-stream.ts +++ b/web/apps/photos/src/utils/native-stream.ts @@ -39,12 +39,10 @@ export const readStream = async ( ): Promise<{ response: Response; size: number; lastModifiedMs: number }> => { let url: URL; if (typeof pathOrZipItem == "string") { - url = new URL("stream://read"); - url.pathname = pathOrZipItem + url = new URL(`stream://read${encodeURIComponent(pathOrZipItem)}`); } else { const [zipPath, entryName] = pathOrZipItem; - url = new URL("stream://read-zip"); - url.pathname = zipPath; + url = new URL(`stream://read-zip${encodeURIComponent(zipPath)}`); url.hash = entryName; } From de4aa3a6cac19b90829ae31fb67734ccc2d0a0cb Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 1 May 2024 18:15:52 +0530 Subject: [PATCH 206/240] URL encode --- desktop/src/main/stream.ts | 14 +++++--------- web/apps/photos/src/utils/native-stream.ts | 7 ++++--- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/desktop/src/main/stream.ts b/desktop/src/main/stream.ts index f1000c6bde..06c6e5fe93 100644 --- a/desktop/src/main/stream.ts +++ b/desktop/src/main/stream.ts @@ -45,19 +45,15 @@ export const registerStreamProtocol = () => { // stream://write/path/to/file#/path/to/another/file // host[pathname----] [pathname-2---------] // - const { host, pathname, hash } = new URL(url); - // Convert e.g. "%20" to spaces. - const path = decodeURIComponent(pathname); - // `hash` begins with a "#", slice that off. - const hashPath = decodeURIComponent(hash.slice(1)); - log.debug( - () => `[stream] ${host} ${path}${hashPath ? "::" + hashPath : ""}`, - ); + const { host, searchParams } = new URL(url); + const path = ensure(searchParams.get("path")); + const path2 = searchParams.get("path2") ?? undefined; + log.debug(() => `[stream] ${host} ${path}${path2 ? "::" + path2 : ""}`); switch (host) { case "read": return handleRead(path); case "read-zip": - return handleReadZip(path, hashPath); + return handleReadZip(path, ensure(path2)); case "write": return handleWrite(path, request); default: diff --git a/web/apps/photos/src/utils/native-stream.ts b/web/apps/photos/src/utils/native-stream.ts index b20cb9290a..73c3522ede 100644 --- a/web/apps/photos/src/utils/native-stream.ts +++ b/web/apps/photos/src/utils/native-stream.ts @@ -39,11 +39,12 @@ export const readStream = async ( ): Promise<{ response: Response; size: number; lastModifiedMs: number }> => { let url: URL; if (typeof pathOrZipItem == "string") { - url = new URL(`stream://read${encodeURIComponent(pathOrZipItem)}`); + const params = new URLSearchParams({ path: pathOrZipItem }); + url = new URL(`stream://read?${params.toString()}`); } else { const [zipPath, entryName] = pathOrZipItem; - url = new URL(`stream://read-zip${encodeURIComponent(zipPath)}`); - url.hash = entryName; + const params = new URLSearchParams({ path: zipPath, path2: entryName }); + url = new URL(`stream://read-zip?${params.toString()}`); } const req = new Request(url, { method: "GET" }); From 4eb51061cb63f5d1443e6d8612d515b76ddbb161 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 1 May 2024 18:21:45 +0530 Subject: [PATCH 207/240] For real --- desktop/src/main/stream.ts | 21 +++++++-------------- web/apps/photos/src/utils/native-stream.ts | 7 +++++-- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/desktop/src/main/stream.ts b/desktop/src/main/stream.ts index 06c6e5fe93..bae13aa121 100644 --- a/desktop/src/main/stream.ts +++ b/desktop/src/main/stream.ts @@ -37,25 +37,18 @@ export const registerStreamProtocol = () => { protocol.handle("stream", async (request: Request) => { const url = request.url; // The request URL contains the command to run as the host, and the - // pathname of the file as the path. An additional path can be specified - // as the URL hash. - // - // For example, - // - // stream://write/path/to/file#/path/to/another/file - // host[pathname----] [pathname-2---------] - // + // pathname of the file(s) as the search params. const { host, searchParams } = new URL(url); - const path = ensure(searchParams.get("path")); - const path2 = searchParams.get("path2") ?? undefined; - log.debug(() => `[stream] ${host} ${path}${path2 ? "::" + path2 : ""}`); switch (host) { case "read": - return handleRead(path); + return handleRead(ensure(searchParams.get("path"))); case "read-zip": - return handleReadZip(path, ensure(path2)); + return handleReadZip( + ensure(searchParams.get("zipPath")), + ensure(searchParams.get("entryName")), + ); case "write": - return handleWrite(path, request); + return handleWrite(ensure(searchParams.get("path")), request); default: return new Response("", { status: 404 }); } diff --git a/web/apps/photos/src/utils/native-stream.ts b/web/apps/photos/src/utils/native-stream.ts index 73c3522ede..941c5a9888 100644 --- a/web/apps/photos/src/utils/native-stream.ts +++ b/web/apps/photos/src/utils/native-stream.ts @@ -43,7 +43,7 @@ export const readStream = async ( url = new URL(`stream://read?${params.toString()}`); } else { const [zipPath, entryName] = pathOrZipItem; - const params = new URLSearchParams({ path: zipPath, path2: entryName }); + const params = new URLSearchParams({ zipPath, entryName }); url = new URL(`stream://read-zip?${params.toString()}`); } @@ -90,6 +90,9 @@ export const writeStream = async ( path: string, stream: ReadableStream, ) => { + const params = new URLSearchParams({ path }); + const url = new URL(`stream://write?${params.toString()}`); + // TODO(MR): This doesn't currently work. // // Not sure what I'm doing wrong here; I've opened an issue upstream @@ -120,7 +123,7 @@ export const writeStream = async ( }); */ - const req = new Request(`stream://write${path}`, { + const req = new Request(url, { method: "POST", body: await new Response(stream).blob(), }); From 932f26684de0c779938f7addc901ddc13e5a984a Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 1 May 2024 18:48:25 +0530 Subject: [PATCH 208/240] Electron logout --- desktop/src/main/ipc.ts | 3 +++ desktop/src/main/services/watch.ts | 4 ++++ desktop/src/preload.ts | 12 ++++++++++-- web/packages/accounts/services/user.ts | 16 ++++++++++++---- web/packages/next/types/ipc.ts | 11 +++++++++++ 5 files changed, 40 insertions(+), 6 deletions(-) diff --git a/desktop/src/main/ipc.ts b/desktop/src/main/ipc.ts index 66cfddabd4..f59969202b 100644 --- a/desktop/src/main/ipc.ts +++ b/desktop/src/main/ipc.ts @@ -64,6 +64,7 @@ import { watchFindFiles, watchGet, watchRemove, + watchReset, watchUpdateIgnoredFiles, watchUpdateSyncedFiles, } from "./services/watch"; @@ -263,4 +264,6 @@ export const attachFSWatchIPCHandlers = (watcher: FSWatcher) => { ipcMain.handle("watchFindFiles", (_, folderPath: string) => watchFindFiles(folderPath), ); + + ipcMain.handle("watchReset", () => watchReset(watcher)); }; diff --git a/desktop/src/main/services/watch.ts b/desktop/src/main/services/watch.ts index 975d8a7c35..e115239d34 100644 --- a/desktop/src/main/services/watch.ts +++ b/desktop/src/main/services/watch.ts @@ -150,3 +150,7 @@ export const watchFindFiles = async (dirPath: string) => { } return paths; }; + +export const watchReset = async (watcher: FSWatcher) => { + await watcher.close(); +}; diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index 2b5eb8fcc3..2acd8fbffa 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -208,6 +208,13 @@ const watchOnRemoveDir = (f: (path: string, watch: FolderWatch) => void) => { const watchFindFiles = (folderPath: string) => ipcRenderer.invoke("watchFindFiles", folderPath); +const watchReset = () => { + ipcRenderer.removeAllListeners("watchAddFile"); + ipcRenderer.removeAllListeners("watchRemoveFile"); + ipcRenderer.removeAllListeners("watchRemoveDir"); + return ipcRenderer.invoke("watchReset"); +}; + // - Upload const pathForFile = (file: File) => webUtils.getPathForFile(file); @@ -323,12 +330,13 @@ contextBridge.exposeInMainWorld("electron", { get: watchGet, add: watchAdd, remove: watchRemove, + updateSyncedFiles: watchUpdateSyncedFiles, + updateIgnoredFiles: watchUpdateIgnoredFiles, onAddFile: watchOnAddFile, onRemoveFile: watchOnRemoveFile, onRemoveDir: watchOnRemoveDir, findFiles: watchFindFiles, - updateSyncedFiles: watchUpdateSyncedFiles, - updateIgnoredFiles: watchUpdateIgnoredFiles, + reset: watchReset, }, // - Upload diff --git a/web/packages/accounts/services/user.ts b/web/packages/accounts/services/user.ts index fb0e1c9290..8f6d6609a1 100644 --- a/web/packages/accounts/services/user.ts +++ b/web/packages/accounts/services/user.ts @@ -40,10 +40,18 @@ export const logoutUser = async () => { } catch (e) { log.error("Ignoring error when clearing files", e); } - try { - globalThis.electron?.clearStores(); - } catch (e) { - log.error("Ignoring error when clearing electron stores", e); + const electron = globalThis.electron; + if (electron) { + try { + await electron.watch.reset(); + } catch (e) { + log.error("Ignoring error when resetting native folder watches", e); + } + try { + await electron.clearStores(); + } catch (e) { + log.error("Ignoring error when clearing native stores", e); + } } try { eventBus.emit(Events.LOGOUT); diff --git a/web/packages/next/types/ipc.ts b/web/packages/next/types/ipc.ts index fb72bcf5ca..4b05838fa1 100644 --- a/web/packages/next/types/ipc.ts +++ b/web/packages/next/types/ipc.ts @@ -462,6 +462,17 @@ export interface Electron { * The returned paths are guaranteed to use POSIX separators ('/'). */ findFiles: (folderPath: string) => Promise; + + /** + * Stop watching all existing folder watches and remove any callbacks. + * + * This function is meant to be called when the user logs out. It stops + * all existing folder watches and forgets about any "on*" callback + * functions that have been registered. + * + * The persisted state itself gets cleared via {@link clearStores}. + */ + reset: () => Promise; }; // - Upload From 90b5054fcb6c68b22bd4ab10e2f089b2d923d8ed Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 1 May 2024 19:02:43 +0530 Subject: [PATCH 209/240] chokidar seemed to have gotten stuck after a close Not sure if something else was off, but after a close new watches in the same session (after logging in) stopped reacting. --- desktop/src/main/services/watch.ts | 4 ++-- desktop/src/preload.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/desktop/src/main/services/watch.ts b/desktop/src/main/services/watch.ts index e115239d34..de66dcca1c 100644 --- a/desktop/src/main/services/watch.ts +++ b/desktop/src/main/services/watch.ts @@ -151,6 +151,6 @@ export const watchFindFiles = async (dirPath: string) => { return paths; }; -export const watchReset = async (watcher: FSWatcher) => { - await watcher.close(); +export const watchReset = (watcher: FSWatcher) => { + watcher.unwatch(folderWatches().map((watch) => watch.folderPath)); }; diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index 2acd8fbffa..589b17fab2 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -208,11 +208,11 @@ const watchOnRemoveDir = (f: (path: string, watch: FolderWatch) => void) => { const watchFindFiles = (folderPath: string) => ipcRenderer.invoke("watchFindFiles", folderPath); -const watchReset = () => { +const watchReset = async () => { ipcRenderer.removeAllListeners("watchAddFile"); ipcRenderer.removeAllListeners("watchRemoveFile"); ipcRenderer.removeAllListeners("watchRemoveDir"); - return ipcRenderer.invoke("watchReset"); + await ipcRenderer.invoke("watchReset"); }; // - Upload From 11d6cdd7c114468871715844aa46b98f7512d5c5 Mon Sep 17 00:00:00 2001 From: vishnukvmd Date: Wed, 1 May 2024 19:36:09 +0530 Subject: [PATCH 210/240] Rename variables --- mobile/lib/db/embeddings_db.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/mobile/lib/db/embeddings_db.dart b/mobile/lib/db/embeddings_db.dart index a993c3275c..0eb1d3f6d7 100644 --- a/mobile/lib/db/embeddings_db.dart +++ b/mobile/lib/db/embeddings_db.dart @@ -133,13 +133,13 @@ class EmbeddingsDB { } Future _clearDeprecatedStores(Directory dir) async { - final deprecatedStore = Directory(dir.path + "/object-box-store"); - if (await deprecatedStore.exists()) { - await deprecatedStore.delete(recursive: true); + final deprecatedObjectBox = Directory(dir.path + "/object-box-store"); + if (await deprecatedObjectBox.exists()) { + await deprecatedObjectBox.delete(recursive: true); } - final deprecatedDB = File(dir.path + "/default.isar"); - if (await deprecatedDB.exists()) { - await deprecatedDB.delete(); + final deprecatedIsar = File(dir.path + "/default.isar"); + if (await deprecatedIsar.exists()) { + await deprecatedIsar.delete(); } } From 2690b874ee5763bbadd97363edf9d026e5c71620 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 1 May 2024 19:49:09 +0530 Subject: [PATCH 211/240] Make the migration a no-op --- web/apps/photos/src/utils/storage/mlIDbStorage.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/web/apps/photos/src/utils/storage/mlIDbStorage.ts b/web/apps/photos/src/utils/storage/mlIDbStorage.ts index 40e6dad662..766c3ac9a9 100644 --- a/web/apps/photos/src/utils/storage/mlIDbStorage.ts +++ b/web/apps/photos/src/utils/storage/mlIDbStorage.ts @@ -144,7 +144,13 @@ class MLIDbStorage { .objectStore("configs") .add(DEFAULT_ML_SEARCH_CONFIG, ML_SEARCH_CONFIG_NAME); } + /* + This'll go in version 5. Note that version 4 was never released, + but it was in main for a while, so we'll just skip it to avoid + breaking the upgrade path for people who ran the mainline. + */ if (oldVersion < 4) { + /* try { await tx .objectStore("configs") @@ -163,8 +169,8 @@ class MLIDbStorage { // the shipped implementation should have a more // deterministic migration. } + */ } - log.info( `ML DB upgraded from version ${oldVersion} to version ${newVersion}`, ); From 0226a99fa3fdebb52d8a449bfc4175de167b7a6b Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 1 May 2024 20:09:36 +0530 Subject: [PATCH 212/240] Disable enabling ML search --- .../src/components/ml/MLSearchSettings.tsx | 46 ++++++++++++------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/web/apps/photos/src/components/ml/MLSearchSettings.tsx b/web/apps/photos/src/components/ml/MLSearchSettings.tsx index 583b79529c..9b50c2d6ae 100644 --- a/web/apps/photos/src/components/ml/MLSearchSettings.tsx +++ b/web/apps/photos/src/components/ml/MLSearchSettings.tsx @@ -22,7 +22,7 @@ import { getFaceSearchEnabledStatus, updateFaceSearchEnabledStatus, } from "services/userService"; -import { openLink } from "utils/common"; +import { isInternalUser } from "utils/user"; export const MLSearchSettings = ({ open, onClose, onRootClose }) => { const { @@ -255,8 +255,8 @@ function EnableFaceSearch({ open, onClose, enableFaceSearch, onRootClose }) { } function EnableMLSearch({ onClose, enableMlSearch, onRootClose }) { - const showDetails = () => - openLink("https://ente.io/blog/desktop-ml-beta", true); + // const showDetails = () => + // openLink("https://ente.io/blog/desktop-ml-beta", true); return ( @@ -269,25 +269,37 @@ function EnableMLSearch({ onClose, enableMlSearch, onRootClose }) { {" "} - + {/* */} +

+ We're putting finishing touches, coming back soon! +

+

+ + Existing indexed faces will continue to show. + +

- - - + {/* + - + > + {t("ML_MORE_DETAILS")} + + */} + + )} ); From 5ba2e35af6e71d5f4e4823fc5e03655f48494e46 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 1 May 2024 20:13:08 +0530 Subject: [PATCH 213/240] Force disable it for non internal users --- .../photos/src/utils/machineLearning/config.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/web/apps/photos/src/utils/machineLearning/config.ts b/web/apps/photos/src/utils/machineLearning/config.ts index 4d2030ca3e..30a65b8f1d 100644 --- a/web/apps/photos/src/utils/machineLearning/config.ts +++ b/web/apps/photos/src/utils/machineLearning/config.ts @@ -10,6 +10,7 @@ import mlIDbStorage, { ML_SYNC_CONFIG_NAME, ML_SYNC_JOB_CONFIG_NAME, } from "utils/storage/mlIDbStorage"; +import { isInternalUser } from "utils/user"; export async function getMLSyncJobConfig() { return mlIDbStorage.getConfig( @@ -23,10 +24,15 @@ export async function getMLSyncConfig() { } export async function getMLSearchConfig() { - return mlIDbStorage.getConfig( - ML_SEARCH_CONFIG_NAME, - DEFAULT_ML_SEARCH_CONFIG, - ); + if (isInternalUser()) { + return mlIDbStorage.getConfig( + ML_SEARCH_CONFIG_NAME, + DEFAULT_ML_SEARCH_CONFIG, + ); + } + // Force disabled for everyone else while we finalize it to avoid redundant + // reindexing for users. + return DEFAULT_ML_SEARCH_CONFIG; } export async function updateMLSyncJobConfig(newConfig: JobConfig) { From 30f22e333abc8cf84578aa1aa12dc2b515db3eff Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 1 May 2024 21:04:41 +0530 Subject: [PATCH 214/240] Pass file when we have it --- .../src/services/upload/uploadManager.ts | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/web/apps/photos/src/services/upload/uploadManager.ts b/web/apps/photos/src/services/upload/uploadManager.ts index 3d53adbeaf..38fd7037be 100644 --- a/web/apps/photos/src/services/upload/uploadManager.ts +++ b/web/apps/photos/src/services/upload/uploadManager.ts @@ -609,11 +609,25 @@ class UploadManager { ].includes(uploadResult) ) { try { + let file: File | undefined; + const uploadItem = + uploadableItem.uploadItem ?? + uploadableItem.livePhotoAssets.image; + if (uploadItem) { + if (uploadItem instanceof File) { + file = uploadItem; + } else if ( + typeof uploadItem == "string" || + Array.isArray(uploadItem) + ) { + // path from desktop, no file object + } else { + file = uploadItem.file; + } + } eventBus.emit(Events.FILE_UPLOADED, { enteFile: decryptedFile, - localFile: - uploadableItem.uploadItem ?? - uploadableItem.livePhotoAssets.image, + localFile: file, }); } catch (e) { log.warn("Ignoring error in fileUploaded handlers", e); From cd5c1e35fa32c5c3474de66b512246a1eafb9764 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 1 May 2024 21:05:29 +0530 Subject: [PATCH 215/240] Disable live clip, rely on the thumbnailed version --- web/apps/photos/src/pages/gallery/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/apps/photos/src/pages/gallery/index.tsx b/web/apps/photos/src/pages/gallery/index.tsx index 70b48c3cc6..20d95ce00f 100644 --- a/web/apps/photos/src/pages/gallery/index.tsx +++ b/web/apps/photos/src/pages/gallery/index.tsx @@ -370,7 +370,7 @@ export default function Gallery() { syncWithRemote(false, true); }, SYNC_INTERVAL_IN_MICROSECONDS); if (electron) { - void clipService.setupOnFileUploadListener(); + // void clipService.setupOnFileUploadListener(); electron.onMainWindowFocus(() => syncWithRemote(false, true)); } }; From 8327c2b8816ff6763d03e03cc4235c929f38f38c Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 2 May 2024 10:23:31 +0530 Subject: [PATCH 216/240] Remove unused ElectronFile --- .../photos/src/components/Upload/Uploader.tsx | 3 +- web/apps/photos/src/services/upload/types.ts | 12 ++++++- web/packages/next/types/file.ts | 36 ------------------- 3 files changed, 12 insertions(+), 39 deletions(-) delete mode 100644 web/packages/next/types/file.ts diff --git a/web/apps/photos/src/components/Upload/Uploader.tsx b/web/apps/photos/src/components/Upload/Uploader.tsx index 53ad1fb32f..7174306556 100644 --- a/web/apps/photos/src/components/Upload/Uploader.tsx +++ b/web/apps/photos/src/components/Upload/Uploader.tsx @@ -1,6 +1,5 @@ import { basename } from "@/next/file"; import log from "@/next/log"; -import { type FileAndPath } from "@/next/types/file"; import type { CollectionMapping, Electron, ZipItem } from "@/next/types/ipc"; import { CustomError } from "@ente/shared/error"; import { isPromise } from "@ente/shared/utils"; @@ -20,7 +19,7 @@ import { getPublicCollectionUploaderName, savePublicCollectionUploaderName, } from "services/publicCollectionService"; -import type { UploadItem } from "services/upload/types"; +import type { FileAndPath, UploadItem } from "services/upload/types"; import type { InProgressUpload, SegregatedFinishedUploads, diff --git a/web/apps/photos/src/services/upload/types.ts b/web/apps/photos/src/services/upload/types.ts index 05ad332d4a..25e2ab408a 100644 --- a/web/apps/photos/src/services/upload/types.ts +++ b/web/apps/photos/src/services/upload/types.ts @@ -1,4 +1,3 @@ -import type { FileAndPath } from "@/next/types/file"; import type { ZipItem } from "@/next/types/ipc"; /** @@ -30,6 +29,17 @@ import type { ZipItem } from "@/next/types/ipc"; */ export type UploadItem = File | FileAndPath | string | ZipItem; +/** + * When we are running in the context of our desktop app, we have access to the + * absolute path of {@link File} objects. This convenience type clubs these two + * bits of information, saving us the need to query the path again and again + * using the {@link getPathForFile} method of {@link Electron}. + */ +export interface FileAndPath { + file: File; + path: string; +} + /** * The of cases of {@link UploadItem} that apply when we're running in the * context of our desktop app. diff --git a/web/packages/next/types/file.ts b/web/packages/next/types/file.ts deleted file mode 100644 index 6dd1032cdb..0000000000 --- a/web/packages/next/types/file.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * ElectronFile is a custom interface that is used to represent - * any file on disk as a File-like object in the Electron desktop app. - * - * This was added to support the auto-resuming of failed uploads - * which needed absolute paths to the files which the - * normal File interface does not provide. - */ -export interface ElectronFile { - name: string; - path: string; - size: number; - lastModified: number; - stream: () => Promise>; - blob: () => Promise; - arrayBuffer: () => Promise; -} - -/** - * When we are running in the context of our desktop app, we have access to the - * absolute path of {@link File} objects. This convenience type clubs these two - * bits of information, saving us the need to query the path again and again - * using the {@link getPathForFile} method of {@link Electron}. - */ -export interface FileAndPath { - file: File; - path: string; -} - -export interface EventQueueItem { - type: "upload" | "trash"; - folderPath: string; - collectionName?: string; - paths?: string[]; - files?: ElectronFile[]; -} From 68721b8168b7292d32a7319d21f67e620aa8e3aa Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 2 May 2024 11:10:26 +0530 Subject: [PATCH 217/240] Pick from the correct table --- web/apps/photos/src/services/embeddingService.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/web/apps/photos/src/services/embeddingService.ts b/web/apps/photos/src/services/embeddingService.ts index a4309e314c..36af848424 100644 --- a/web/apps/photos/src/services/embeddingService.ts +++ b/web/apps/photos/src/services/embeddingService.ts @@ -86,7 +86,11 @@ export const syncEmbeddings = async () => { allLocalFiles.forEach((file) => { fileIdToKeyMap.set(file.id, file.key); }); - await cleanupDeletedEmbeddings(allLocalFiles, allEmbeddings); + await cleanupDeletedEmbeddings( + allLocalFiles, + allEmbeddings, + EMBEDDINGS_TABLE, + ); log.info(`Syncing embeddings localCount: ${allEmbeddings.length}`); for (const model of models) { let modelLastSinceTime = await getModelEmbeddingSyncTime(model); @@ -168,7 +172,11 @@ export const syncFileEmbeddings = async () => { allLocalFiles.forEach((file) => { fileIdToKeyMap.set(file.id, file.key); }); - await cleanupDeletedEmbeddings(allLocalFiles, allEmbeddings); + await cleanupDeletedEmbeddings( + allLocalFiles, + allEmbeddings, + FILE_EMBEDING_TABLE, + ); log.info(`Syncing embeddings localCount: ${allEmbeddings.length}`); for (const model of models) { let modelLastSinceTime = await getModelEmbeddingSyncTime(model); @@ -289,6 +297,7 @@ export const putEmbedding = async ( export const cleanupDeletedEmbeddings = async ( allLocalFiles: EnteFile[], allLocalEmbeddings: Embedding[] | FileML[], + tableName: string, ) => { const activeFileIds = new Set(); allLocalFiles.forEach((file) => { @@ -302,6 +311,6 @@ export const cleanupDeletedEmbeddings = async ( log.info( `cleanupDeletedEmbeddings embeddingsCount: ${allLocalEmbeddings.length} remainingEmbeddingsCount: ${remainingEmbeddings.length}`, ); - await localForage.setItem(EMBEDDINGS_TABLE, remainingEmbeddings); + await localForage.setItem(tableName, remainingEmbeddings); } }; From fa182b951dbb81e8fd0b8485d4b8aa4aecf16a74 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 2 May 2024 12:52:05 +0530 Subject: [PATCH 218/240] [desktop] Resurrect build Untested --- .github/workflows/desktop-release.yml | 72 +++++++++++++++++++++++++ desktop/.github/workflows/build.yml | 55 ------------------- desktop/README.md | 6 --- desktop/docs/release.md | 78 ++++++++++----------------- 4 files changed, 99 insertions(+), 112 deletions(-) create mode 100644 .github/workflows/desktop-release.yml delete mode 100644 desktop/.github/workflows/build.yml diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml new file mode 100644 index 0000000000..44c63e5b23 --- /dev/null +++ b/.github/workflows/desktop-release.yml @@ -0,0 +1,72 @@ +name: "Release (photos desktop)" + +# This will create a new draft release with public artifacts. +# +# Note that a release will only get created if there is an associated tag +# (GitHub releases need a corresponding tag). + +on: + workflow_dispatch: # Allow manually running the action + push: + # Run when a tag matching the pattern "photosd-v*"" is pushed + # See: [Note: Testing release workflows that are triggered by tags] + tags: + - "photosd-v*" + +jobs: + release: + runs-on: ${{ matrix.os }} + + defaults: + run: + working-directory: desktop + + strategy: + matrix: + os: [macos-latest, ubuntu-latest, windows-latest] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install dependencies + run: yarn install + + - name: Prepare for app notarization + if: startsWith(matrix.os, 'macos') + # Import Apple API key for app notarization on macOS + run: | + mkdir -p ~/private_keys/ + echo '${{ secrets.APPLE_API_KEY }}' > ~/private_keys/AuthKey_${{ secrets.APPLE_API_KEY_ID }}.p8 + + - name: Install libarchive-tools for pacman build + if: startsWith(matrix.os, 'ubuntu') + # See: + # https://github.com/electron-userland/electron-builder/issues/4181 + run: sudo apt-get install libarchive-tools + + - name: Build + uses: ente-io/action-electron-builder@v1.0.0 + with: + # GitHub token, automatically provided to the action + # (No need to define this secret in the repo settings) + github_token: ${{ secrets.GITHUB_TOKEN }} + + # If the commit is tagged with a version (e.g. "v1.0.0"), + # release the app after building + release: ${{ startsWith(github.ref, 'refs/tags/v') }} + + mac_certs: ${{ secrets.MAC_CERTS }} + mac_certs_password: ${{ secrets.MAC_CERTS_PASSWORD }} + env: + # macOS notarization API key details + API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} + API_KEY_ISSUER_ID: ${{ secrets.APPLE_API_KEY_ISSUER_ID }} + USE_HARD_LINKS: false diff --git a/desktop/.github/workflows/build.yml b/desktop/.github/workflows/build.yml deleted file mode 100644 index acd744c056..0000000000 --- a/desktop/.github/workflows/build.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: Build/release - -on: - push: - tags: - - v* - -jobs: - release: - runs-on: ${{ matrix.os }} - - strategy: - matrix: - os: [macos-latest, ubuntu-latest, windows-latest] - - steps: - - name: Check out Git repository - uses: actions/checkout@v3 - with: - submodules: recursive - - - name: Install Node.js, NPM and Yarn - uses: actions/setup-node@v3 - with: - node-version: 20 - - - name: Prepare for app notarization - if: startsWith(matrix.os, 'macos') - # Import Apple API key for app notarization on macOS - run: | - mkdir -p ~/private_keys/ - echo '${{ secrets.api_key }}' > ~/private_keys/AuthKey_${{ secrets.api_key_id }}.p8 - - - name: Install libarchive-tools for pacman build # Related https://github.com/electron-userland/electron-builder/issues/4181 - if: startsWith(matrix.os, 'ubuntu') - run: sudo apt-get install libarchive-tools - - - name: Ente Electron Builder Action - uses: ente-io/action-electron-builder@v1.0.0 - with: - # GitHub token, automatically provided to the action - # (No need to define this secret in the repo settings) - github_token: ${{ secrets.github_token }} - - # If the commit is tagged with a version (e.g. "v1.0.0"), - # release the app after building - release: ${{ startsWith(github.ref, 'refs/tags/v') }} - - mac_certs: ${{ secrets.mac_certs }} - mac_certs_password: ${{ secrets.mac_certs_password }} - env: - # macOS notarization API key - API_KEY_ID: ${{ secrets.api_key_id }} - API_KEY_ISSUER_ID: ${{ secrets.api_key_issuer_id}} - USE_HARD_LINKS: false diff --git a/desktop/README.md b/desktop/README.md index 05149f5d0c..39b7663fab 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -10,12 +10,6 @@ To know more about Ente, see [our main README](../README.md) or visit ## Building from source -> [!CAUTION] -> -> We're improving the security of the desktop app further by migrating to -> Electron's sandboxing and contextIsolation. These updates are still WIP and -> meanwhile the instructions below might not fully work on the main branch. - Fetch submodules ```sh diff --git a/desktop/docs/release.md b/desktop/docs/release.md index 7254e26fc1..0a5c2970f5 100644 --- a/desktop/docs/release.md +++ b/desktop/docs/release.md @@ -1,43 +1,33 @@ ## Releases -> [!NOTE] -> -> TODO(MR): This document needs to be audited and changed as we do the first -> release from this new monorepo. - The Github Action that builds the desktop binaries is triggered by pushing a tag -matching the pattern `photos-desktop-v1.2.3`. This value should match the -version in `package.json`. +matching the pattern `photosd-v1.2.3`. This value should match the version in +`package.json`. -So the process for doing a release would be. +To make a new release -1. Create a new branch (can be named anything). On this branch, include your - changes. +1. Create a new branch (can be named anything). On this branch, change the + `version` in `package.json` to `1.x.x` and finalize `CHANGELOG.md`. -2. Mention the changes in `CHANGELOG.md`. - -3. Changing the `version` in `package.json` to `1.x.x`. - -4. Commit and push to remote +2. Commit, tag and push to remote. Note that the tag should have a `photosd-` + prefix: ```sh - git add package.json && git commit -m 'Release v1.x.x' - git tag v1.x.x - git push && git push --tags + git add CHANGELOG.md package.json + git commit -m 'Release v1.x.x' + git tag photosd-v1.x.x + git push origin photosd-v1.x.x ``` -This by itself will already trigger a new release. The GitHub action will create -a new draft release that can then be used as descibed below. + This will trigger the GitHub action that will create a new draft release. -To wrap up, we also need to merge back these changes into main. So for that, +3. To wrap up, increase the version number in `package.json` the next release + train. That is, suppose we just released `v4.0.1`. Then we'll change the + version number in main to `v4.0.2-beta.0`. Each pre-release will modify the + `beta.0` part. Finally, at the time of the next release, this'll become + `v4.0.2`. -5. Open a PR for the branch that we're working on (where the above tag was - pushed from) to get it merged into main. - -6. In this PR, also increase the version number for the next release train. That - is, supposed we just released `v4.0.1`. Then we'll change the version number - in main to `v4.0.2-next.0`. Each pre-release will modify the `next.0` part. - Finally, at the time of the next release, this'll become `v4.0.2`. +4. Open a PR for the branch to get it merged into main. The GitHub Action runs on Windows, Linux and macOS. It produces the artifacts defined in the `build` value in `package.json`. @@ -49,26 +39,14 @@ defined in the `build` value in `package.json`. Additionally, the GitHub action notarizes the macOS DMG. For this it needs credentials provided via GitHub secrets. -During the build the Sentry webpack plugin checks to see if SENTRY_AUTH_TOKEN is -defined. If so, it uploads the sourcemaps for the renderer process to Sentry -(For our GitHub action, the SENTRY_AUTH_TOKEN is defined as a GitHub secret). +To rollout the build, we need to publish the draft release. This needs to be +done in the old photos-desktop repository since that the Electron Updater +mechanism doesn't work well with monorepos. So we need to create a new tag with +changelog updates on +[photos-desktop](https://github.com/ente-io/photos-desktop/), use that to create +a new release, copying over all the artifacts. -The sourcemaps for the main (node) process are currently not sent to Sentry -(this works fine in practice since the node process files are not minified, we -only run `tsc`). - -Once the build is done, a draft release with all these artifacts attached is -created. The build is idempotent, so if something goes wrong and we need to -re-run the GitHub action, just delete the draft release (if it got created) and -start a new run by pushing a new tag (if some code changes are required). - -If no code changes are required, say the build failed for some transient network -or sentry issue, we can even be re-run by the build by going to Github Action -age and rerun from there. This will re-trigger for the same tag. - -If everything goes well, we'll have a release on GitHub, and the corresponding -source maps for the renderer process uploaded to Sentry. There isn't anything -else to do: +Thereafter, everything is automated: - The website automatically redirects to the latest release on GitHub when people try to download. @@ -76,7 +54,7 @@ else to do: - The file formats with support auto update (Windows `exe`, the Linux AppImage and the macOS DMG) also check the latest GitHub release automatically to download and apply the update (the rest of the formats don't support auto - updates). + updates yet). - We're not putting the desktop app in other stores currently. It is available as a `brew cask`, but we only had to open a PR to add the initial formula, @@ -87,6 +65,4 @@ else to do: We can also publish the draft releases by checking the "pre-release" option. Such releases don't cause any of the channels (our website, or the desktop app auto updater, or brew) to be notified, instead these are useful for giving links -to pre-release builds to customers. Generally, in the version number for these -we'll add a label to the version, e.g. the "beta.x" in `1.x.x-beta.x`. This -should be done both in `package.json`, and what we tag the commit with. +to pre-release builds to customers. From aed781b0ffe8b54cc857f232c4b01ebe31190c11 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 2 May 2024 19:05:52 +0530 Subject: [PATCH 219/240] Use same credentials as the auth app While we won't actually be using the monorepo for releases, get the action to a known state: - MAC_OS_CERTIFICATE and MAC_OS_CERTIFICATE_PASSWORD is the same GitHub secret that the auth app already uses - Need to add APPLE_API_KEY, APPLE_API_KEY_ID, APPLE_API_KEY_ISSUER_ID. --- .github/workflows/desktop-release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml index 44c63e5b23..60f012b653 100644 --- a/.github/workflows/desktop-release.yml +++ b/.github/workflows/desktop-release.yml @@ -63,8 +63,8 @@ jobs: # release the app after building release: ${{ startsWith(github.ref, 'refs/tags/v') }} - mac_certs: ${{ secrets.MAC_CERTS }} - mac_certs_password: ${{ secrets.MAC_CERTS_PASSWORD }} + mac_certs: ${{ secrets.MAC_OS_CERTIFICATE }} + mac_certs_password: ${{ secrets.MAC_OS_CERTIFICATE_PASSWORD }} env: # macOS notarization API key details API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} From 3b3d24e9e05bc3ddccf55e64559b45dd9c843cb4 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 2 May 2024 19:34:58 +0530 Subject: [PATCH 220/240] It'll need to live in the releases repo, reword accordingly --- .../.github}/workflows/desktop-release.yml | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) rename {.github => desktop/.github}/workflows/desktop-release.yml (61%) diff --git a/.github/workflows/desktop-release.yml b/desktop/.github/workflows/desktop-release.yml similarity index 61% rename from .github/workflows/desktop-release.yml rename to desktop/.github/workflows/desktop-release.yml index 60f012b653..cb895fd4de 100644 --- a/.github/workflows/desktop-release.yml +++ b/desktop/.github/workflows/desktop-release.yml @@ -1,17 +1,26 @@ -name: "Release (photos desktop)" +name: "Release" # This will create a new draft release with public artifacts. # # Note that a release will only get created if there is an associated tag # (GitHub releases need a corresponding tag). +# +# The canonical source for this action is in the repository where we keep the +# source code for the Ente Photos desktop app: https://github.com/ente-io/ente +# +# However, it actually lives and runs in the repository that we use for making +# releases: https://github.com/ente-io/photos-desktop +# +# We need two repositories because Electron updater currently doesn't work well +# with monorepos. For more details, see `docs/release.md`. on: - workflow_dispatch: # Allow manually running the action push: - # Run when a tag matching the pattern "photosd-v*"" is pushed - # See: [Note: Testing release workflows that are triggered by tags] + # Run when a tag matching the pattern "v*"" is pushed. + # + # See: [Note: Testing release workflows that are triggered by tags]. tags: - - "photosd-v*" + - "v*" jobs: release: @@ -29,6 +38,11 @@ jobs: - name: Checkout code uses: actions/checkout@v4 with: + # Checkout the tag photosd-v1.x.x from the source code + # repository when we're invoked for tag v1.x.x on the releases + # repository. + repository: ente-io/ente + ref: photosd-${{ github.ref }} submodules: recursive - name: Setup node @@ -44,7 +58,7 @@ jobs: # Import Apple API key for app notarization on macOS run: | mkdir -p ~/private_keys/ - echo '${{ secrets.APPLE_API_KEY }}' > ~/private_keys/AuthKey_${{ secrets.APPLE_API_KEY_ID }}.p8 + echo '${{ secrets.API_KEY }}' > ~/private_keys/AuthKey_${{ secrets.API_KEY_ID }}.p8 - name: Install libarchive-tools for pacman build if: startsWith(matrix.os, 'ubuntu') @@ -60,13 +74,13 @@ jobs: github_token: ${{ secrets.GITHUB_TOKEN }} # If the commit is tagged with a version (e.g. "v1.0.0"), - # release the app after building + # release the app after building. release: ${{ startsWith(github.ref, 'refs/tags/v') }} - mac_certs: ${{ secrets.MAC_OS_CERTIFICATE }} - mac_certs_password: ${{ secrets.MAC_OS_CERTIFICATE_PASSWORD }} + mac_certs: ${{ secrets.MAC_CERTS }} + mac_certs_password: ${{ secrets.MAC_CERTS_PASSWORD }} env: # macOS notarization API key details - API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} - API_KEY_ISSUER_ID: ${{ secrets.APPLE_API_KEY_ISSUER_ID }} + API_KEY_ID: ${{ secrets.API_KEY_ID }} + API_KEY_ISSUER_ID: ${{ secrets.API_KEY_ISSUER_ID }} USE_HARD_LINKS: false From fecfb4a8b7d12c4c0540d363bdd2ddad7f60753f Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 2 May 2024 19:52:39 +0530 Subject: [PATCH 221/240] Hopes and dreams --- desktop/docs/release.md | 74 ++++++++++++++++++++++++++--------------- 1 file changed, 47 insertions(+), 27 deletions(-) diff --git a/desktop/docs/release.md b/desktop/docs/release.md index 0a5c2970f5..59b2be10e0 100644 --- a/desktop/docs/release.md +++ b/desktop/docs/release.md @@ -1,33 +1,59 @@ ## Releases -The Github Action that builds the desktop binaries is triggered by pushing a tag -matching the pattern `photosd-v1.2.3`. This value should match the version in -`package.json`. +Conceptually, the release is straightforward: We push a tag, a GitHub workflow +gets triggered that creates a draft release with artifacts built from that tag. +We then publish that release. The download links on our website, and existing +apps already know how to check for the latest GitHub release and update +accordingly. -To make a new release +The complication comes by the fact that Electron Updater (the mechanism that we +use for auto updates) doesn't work well with monorepos. So we need to keep a +separate (non-mono) repository just for doing releases. -1. Create a new branch (can be named anything). On this branch, change the - `version` in `package.json` to `1.x.x` and finalize `CHANGELOG.md`. +- Source code lives here, in [ente-io/ente](https://github.com/ente-io/ente). -2. Commit, tag and push to remote. Note that the tag should have a `photosd-` - prefix: +- Releases are done from + [ente-io/photos-desktop](https://github.com/ente-io/photos-desktop). + +## Workflow + +The workflow is: + +1. Finalize the changes in the source repo. + + - Update the CHANGELOG. + - Update the version in `package.json` + - `git commit -m 'Release v1.x.x'` + - Open PR, merge into main. + + +2. Tag this commit with a tag matching the pattern `photosd-v1.2.3`, where + `1.2.3` is the version in `package.json` ```sh - git add CHANGELOG.md package.json - git commit -m 'Release v1.x.x' git tag photosd-v1.x.x git push origin photosd-v1.x.x ``` - This will trigger the GitHub action that will create a new draft release. +3. Head over to the releases repository, copy all relevant changes from the + source repository, commit and push the changes. -3. To wrap up, increase the version number in `package.json` the next release - train. That is, suppose we just released `v4.0.1`. Then we'll change the - version number in main to `v4.0.2-beta.0`. Each pre-release will modify the - `beta.0` part. Finally, at the time of the next release, this'll become - `v4.0.2`. + ```sh + cp ../ente/desktop/CHANGELOG.md CHANGELOG.md + git add CHANGELOG.md + git commit -m 'Release v1.x.x' + git push origin main + ``` -4. Open a PR for the branch to get it merged into main. +4. Tag this commit, but this time _don't_ use the `photosd-` prefix. Push the + tag to trigger the GitHub action. + + ```sh + git tag v1.x.x + git push origin v1.x.x + ``` + +## Post build The GitHub Action runs on Windows, Linux and macOS. It produces the artifacts defined in the `build` value in `package.json`. @@ -36,17 +62,11 @@ defined in the `build` value in `package.json`. - Linux - An AppImage, and 3 other packages (`.rpm`, `.deb`, `.pacman`) - macOS - A universal DMG -Additionally, the GitHub action notarizes the macOS DMG. For this it needs -credentials provided via GitHub secrets. +Additionally, the GitHub action notarizes and signs the macOS DMG (For this it +uses credentials provided via GitHub secrets). -To rollout the build, we need to publish the draft release. This needs to be -done in the old photos-desktop repository since that the Electron Updater -mechanism doesn't work well with monorepos. So we need to create a new tag with -changelog updates on -[photos-desktop](https://github.com/ente-io/photos-desktop/), use that to create -a new release, copying over all the artifacts. - -Thereafter, everything is automated: +To rollout the build, we need to publish the draft release. Thereafter, +everything is automated: - The website automatically redirects to the latest release on GitHub when people try to download. From 2f2d15c9f2127602e448cbedeb697d777933d134 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 2 May 2024 20:22:40 +0530 Subject: [PATCH 222/240] lint --- desktop/docs/release.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/desktop/docs/release.md b/desktop/docs/release.md index 59b2be10e0..0d1b11bc63 100644 --- a/desktop/docs/release.md +++ b/desktop/docs/release.md @@ -21,11 +21,10 @@ The workflow is: 1. Finalize the changes in the source repo. - - Update the CHANGELOG. - - Update the version in `package.json` - - `git commit -m 'Release v1.x.x'` - - Open PR, merge into main. - + - Update the CHANGELOG. + - Update the version in `package.json` + - `git commit -m 'Release v1.x.x'` + - Open PR, merge into main. 2. Tag this commit with a tag matching the pattern `photosd-v1.2.3`, where `1.2.3` is the version in `package.json` From 67eed1aa89c18a320c5473b74dea6bf68b470332 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 2 May 2024 21:20:26 +0530 Subject: [PATCH 223/240] Upgrade to Electron 30 This picks up the stream fix we need > Fixed data corruption when protocol.handle() processed incoming data asynchronously. #41933 (Also in 31) > > https://github.com/electron/electron/releases/tag/v30.0.2 --- desktop/package.json | 2 +- desktop/yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/desktop/package.json b/desktop/package.json index 5ec8b45be3..d9aaf133ef 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -43,7 +43,7 @@ "@typescript-eslint/eslint-plugin": "^7", "@typescript-eslint/parser": "^7", "concurrently": "^8", - "electron": "^29", + "electron": "^30", "electron-builder": "^24", "electron-builder-notarize": "^1.5", "eslint": "^8", diff --git a/desktop/yarn.lock b/desktop/yarn.lock index 2210d47450..d4338312bd 100644 --- a/desktop/yarn.lock +++ b/desktop/yarn.lock @@ -1199,10 +1199,10 @@ electron-updater@^6.1: semver "^7.3.8" tiny-typed-emitter "^2.1.0" -electron@^29: - version "29.3.1" - resolved "https://registry.yarnpkg.com/electron/-/electron-29.3.1.tgz#87c82b2cd2c326f78f036499377a5448bea5d4bb" - integrity sha512-auge1/6RVqgUd6TgIq88wKdUCJi2cjESi3jy7d+6X4JzvBGprKBqMJ8JSSFpu/Px1YJrFUKAxfy6SC+TQf1uLw== +electron@^30: + version "30.0.2" + resolved "https://registry.yarnpkg.com/electron/-/electron-30.0.2.tgz#95ba019216bf8be9f3097580123e33ea37497733" + integrity sha512-zv7T+GG89J/hyWVkQsLH4Y/rVEfqJG5M/wOBIGNaDdqd8UV9/YZPdS7CuFeaIj0H9LhCt95xkIQNpYB/3svOkQ== dependencies: "@electron/get" "^2.0.0" "@types/node" "^20.9.0" From 6a990020649965c377bd843a4bbb400e3faaa0fb Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 2 May 2024 21:22:58 +0530 Subject: [PATCH 224/240] Start using it --- desktop/src/main.ts | 8 -------- web/apps/photos/src/utils/native-stream.ts | 23 ++-------------------- 2 files changed, 2 insertions(+), 29 deletions(-) diff --git a/desktop/src/main.ts b/desktop/src/main.ts index eb1114cc4c..49b3162061 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -127,15 +127,7 @@ const registerPrivilegedSchemes = () => { { scheme: "stream", privileges: { - // TODO(MR): Remove the commented bits if we don't end up - // needing them by the time the IPC refactoring is done. - - // Prevent the insecure origin issues when fetching this - // secure: true, - // Allow the web fetch API in the renderer to use this scheme. supportFetchAPI: true, - // Allow it to be used with video tags. - // stream: true, }, }, ]); diff --git a/web/apps/photos/src/utils/native-stream.ts b/web/apps/photos/src/utils/native-stream.ts index 941c5a9888..4ed9da753a 100644 --- a/web/apps/photos/src/utils/native-stream.ts +++ b/web/apps/photos/src/utils/native-stream.ts @@ -93,40 +93,21 @@ export const writeStream = async ( const params = new URLSearchParams({ path }); const url = new URL(`stream://write?${params.toString()}`); - // TODO(MR): This doesn't currently work. - // - // Not sure what I'm doing wrong here; I've opened an issue upstream - // https://github.com/electron/electron/issues/41872 - // - // A gist with a minimal reproduction - // https://gist.github.com/mnvr/e08d9f4876fb8400b7615347b4d268eb - // - // Meanwhile, write the complete body in one go (this'll eventually run into - // memory failures with large files - just a temporary stopgap to get the - // code to work). - - /* // The duplex parameter needs to be set to 'half' when streaming requests. // // Currently browsers, and specifically in our case, since this code runs // only within our desktop (Electron) app, Chromium, don't support 'full' // duplex mode (i.e. streaming both the request and the response). // https://developer.chrome.com/docs/capabilities/web-apis/fetch-streaming-requests - const req = new Request(`stream://write${path}`, { + const req = new Request(url, { // GET can't have a body method: "POST", body: stream, - // --@ts-expect-error TypeScript's libdom.d.ts does not include the + // @ts-expect-error TypeScript's libdom.d.ts does not include the // "duplex" parameter, e.g. see // https://github.com/node-fetch/node-fetch/issues/1769. duplex: "half", }); - */ - - const req = new Request(url, { - method: "POST", - body: await new Response(stream).blob(), - }); const res = await fetch(req); if (!res.ok) From 0c4da8c86aea2aa00ea47df64a70f367a7410697 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 2 May 2024 21:38:02 +0530 Subject: [PATCH 225/240] POSIX paths --- desktop/src/preload.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index 589b17fab2..407e541ff7 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -217,7 +217,25 @@ const watchReset = async () => { // - Upload -const pathForFile = (file: File) => webUtils.getPathForFile(file); +const pathForFile = (file: File) => { + const path = webUtils.getPathForFile(file); + // The path that we get back from `webUtils.getPathForFile` on Windows uses + // "/" as the path separator. Convert them to POSIX separators. + // + // Note that we do not have access to the path or the os module in the + // preload script, thus this hand rolled transformation. + + // However that makes TypeScript fidgety since we it cannot find navigator, + // as we haven't included "lib": ["dom"] in our tsconfig to avoid making DOM + // APIs available to our main Node.js code. We could create a separate + // tsconfig just for the preload script, but for now let's go with a cast. + // + // @ts-expect-error navigator is not defined. + const platform = (navigator as { platform: string }).platform; + return platform.toLowerCase().includes("win") + ? path.split("\\").join("/") + : path; +}; const listZipItems = (zipPath: string) => ipcRenderer.invoke("listZipItems", zipPath); From d08c2b4fa0830962d0cdf0b2cb7483d60710c995 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Fri, 3 May 2024 01:40:38 +0000 Subject: [PATCH 226/240] New Crowdin translations by GitHub Action --- .../next/locales/pt-BR/translation.json | 18 +++++++++--------- .../next/locales/zh-CN/translation.json | 4 ++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/web/packages/next/locales/pt-BR/translation.json b/web/packages/next/locales/pt-BR/translation.json index dfe0030c56..9fc00517cf 100644 --- a/web/packages/next/locales/pt-BR/translation.json +++ b/web/packages/next/locales/pt-BR/translation.json @@ -239,7 +239,7 @@ "ENABLE_MAPS": "Habilitar mapa?", "ENABLE_MAP": "Habilitar mapa", "DISABLE_MAPS": "Desativar Mapas?", - "ENABLE_MAP_DESCRIPTION": "Isto mostrará suas fotos em um mapa do mundo.

Este mapa é hospedado pelo OpenStreetMap , e os exatos locais de suas fotos nunca são compartilhados.

Você pode desativar esse recurso a qualquer momento nas Configurações.

", + "ENABLE_MAP_DESCRIPTION": "

Isto mostrará suas fotos em um mapa do mundo.

Este mapa é hospedado pelo OpenStreetMap, e os exatos locais de suas fotos nunca são compartilhados.

Você pode desativar esse recurso a qualquer momento nas Configurações.

", "DISABLE_MAP_DESCRIPTION": "

Isto irá desativar a exibição de suas fotos em um mapa mundial.

Você pode ativar este recurso a qualquer momento nas Configurações.

", "DISABLE_MAP": "Desabilitar mapa", "DETAILS": "Detalhes", @@ -380,14 +380,14 @@ "LINK_EXPIRED_MESSAGE": "Este link expirou ou foi desativado!", "MANAGE_LINK": "Gerenciar link", "LINK_TOO_MANY_REQUESTS": "Desculpe, este álbum foi visualizado em muitos dispositivos!", - "FILE_DOWNLOAD": "Permitir transferências", + "FILE_DOWNLOAD": "Permitir downloads", "LINK_PASSWORD_LOCK": "Bloqueio de senha", "PUBLIC_COLLECT": "Permitir adicionar fotos", "LINK_DEVICE_LIMIT": "Limite de dispositivos", "NO_DEVICE_LIMIT": "Nenhum", "LINK_EXPIRY": "Expiração do link", "NEVER": "Nunca", - "DISABLE_FILE_DOWNLOAD": "Desabilitar transferência", + "DISABLE_FILE_DOWNLOAD": "Desabilitar download", "DISABLE_FILE_DOWNLOAD_MESSAGE": "

Tem certeza de que deseja desativar o botão de download para arquivos?

Os visualizadores ainda podem capturar imagens da tela ou salvar uma cópia de suas fotos usando ferramentas externas.

", "SHARED_USING": "Compartilhar usando ", "SHARING_REFERRAL_CODE": "Use o código {{referralCode}} para obter 10 GB de graça", @@ -408,8 +408,8 @@ "STOP_ALL_UPLOADS_MESSAGE": "Tem certeza que deseja parar todos os envios em andamento?", "STOP_UPLOADS_HEADER": "Parar envios?", "YES_STOP_UPLOADS": "Sim, parar envios", - "STOP_DOWNLOADS_HEADER": "Parar transferências?", - "YES_STOP_DOWNLOADS": "Sim, parar transferências", + "STOP_DOWNLOADS_HEADER": "Parar downloads?", + "YES_STOP_DOWNLOADS": "Sim, parar downloads", "STOP_ALL_DOWNLOADS_MESSAGE": "Tem certeza que deseja parar todos as transferências em andamento?", "albums_one": "1 Álbum", "albums_other": "{{count, number}} Álbuns", @@ -556,8 +556,8 @@ "SELECT_COLLECTION": "Selecionar álbum", "PIN_ALBUM": "Fixar álbum", "UNPIN_ALBUM": "Desafixar álbum", - "DOWNLOAD_COMPLETE": "Transferência concluída", - "DOWNLOADING_COLLECTION": "Transferindo {{name}}", + "DOWNLOAD_COMPLETE": "Download concluído", + "DOWNLOADING_COLLECTION": "Fazendo download de {{name}}", "DOWNLOAD_FAILED": "Falha ao baixar", "DOWNLOAD_PROGRESS": "{{progress.current}} / {{progress.total}} arquivos", "CHRISTMAS": "Natal", @@ -622,6 +622,6 @@ "TRY_AGAIN": "Tente novamente", "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "Siga os passos do seu navegador para continuar acessando.", "LOGIN_WITH_PASSKEY": "Entrar com a chave de acesso", - "autogenerated_first_album_name": "", - "autogenerated_default_album_name": "" + "autogenerated_first_album_name": "Meu Primeiro Álbum", + "autogenerated_default_album_name": "Novo Álbum" } diff --git a/web/packages/next/locales/zh-CN/translation.json b/web/packages/next/locales/zh-CN/translation.json index d2345f1ae7..c67018aaaf 100644 --- a/web/packages/next/locales/zh-CN/translation.json +++ b/web/packages/next/locales/zh-CN/translation.json @@ -622,6 +622,6 @@ "TRY_AGAIN": "重试", "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "按照浏览器中提示的步骤继续登录。", "LOGIN_WITH_PASSKEY": "使用通行密钥来登录", - "autogenerated_first_album_name": "", - "autogenerated_default_album_name": "" + "autogenerated_first_album_name": "我的第一个相册", + "autogenerated_default_album_name": "新建相册" } From 3eda263d26211418c95a3b2b4a0d1f4b10e06139 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 3 May 2024 09:38:58 +0530 Subject: [PATCH 227/240] Clarify cwd --- docs/docs/self-hosting/guides/custom-server/index.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/docs/self-hosting/guides/custom-server/index.md b/docs/docs/self-hosting/guides/custom-server/index.md index bf695af308..a5ce76cc2b 100644 --- a/docs/docs/self-hosting/guides/custom-server/index.md +++ b/docs/docs/self-hosting/guides/custom-server/index.md @@ -25,10 +25,13 @@ configure the endpoint the app should be connecting to. > You can download the CLI from > [here](https://github.com/ente-io/ente/releases?q=tag%3Acli-v0) -Define a config.yaml and put it either in the same directory as CLI or path -defined in env variable `ENTE_CLI_CONFIG_PATH` +Define a config.yaml and put it either in the same directory as where you run +the CLI from ("current working directory"), or in the path defined in env +variable `ENTE_CLI_CONFIG_PATH`: ```yaml endpoint: api: "http://localhost:8080" ``` + +(Another [example](https://github.com/ente-io/ente/blob/main/cli/config.yaml.example)) From 977d212be6151b54b94946ba3acbc97204a6380d Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 3 May 2024 09:42:17 +0530 Subject: [PATCH 228/240] Add a notice about ente account add --- cli/README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/cli/README.md b/cli/README.md index 8fc9aa6948..40858da0f8 100644 --- a/cli/README.md +++ b/cli/README.md @@ -36,7 +36,8 @@ ente --help ### Accounts -If you wish, you can add multiple accounts (your own and that of your family members) and export all data using this tool. +If you wish, you can add multiple accounts (your own and that of your family +members) and export all data using this tool. #### Add an account @@ -44,6 +45,12 @@ If you wish, you can add multiple accounts (your own and that of your family mem ente account add ``` +> [!NOTE] +> +> `ente account add` does not create new accounts, it just adds pre-existing +> accounts to the list of accounts that the CLI knows about so that you can use +> them for other actions. + #### List accounts ```shell From 024f160ca0735f67e3a649058f4bf1fb2290af62 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Fri, 3 May 2024 10:14:27 +0530 Subject: [PATCH 229/240] [mob] Improve log --- mobile/lib/utils/file_uploader.dart | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/mobile/lib/utils/file_uploader.dart b/mobile/lib/utils/file_uploader.dart index d77bc95d7e..f81f9d34bb 100644 --- a/mobile/lib/utils/file_uploader.dart +++ b/mobile/lib/utils/file_uploader.dart @@ -357,10 +357,16 @@ class FileUploader { final List connections = await (Connectivity().checkConnectivity()); bool canUploadUnderCurrentNetworkConditions = true; - if (connections.any((element) => element == ConnectivityResult.mobile)) { - canUploadUnderCurrentNetworkConditions = - Configuration.instance.shouldBackupOverMobileData(); + if (!Configuration.instance.shouldBackupOverMobileData()) { + if (connections.any((element) => element == ConnectivityResult.mobile)) { + canUploadUnderCurrentNetworkConditions = false; + } else { + _logger.info( + "mobileBackupDisabled, backing up with connections: ${connections.map((e) => e.name).toString()}", + ); + } } + if (!canUploadUnderCurrentNetworkConditions) { throw WiFiUnavailableError(); } From ddad863b313b3f9a56d8c2d6a7d00088f0ce47ce Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 3 May 2024 10:50:21 +0530 Subject: [PATCH 230/240] Prepare for release --- desktop/CHANGELOG.md | 8 ++++++++ desktop/package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/desktop/CHANGELOG.md b/desktop/CHANGELOG.md index 83d2123d86..eb118a424d 100644 --- a/desktop/CHANGELOG.md +++ b/desktop/CHANGELOG.md @@ -1,5 +1,13 @@ # CHANGELOG +## v1.7.0 (Unreleased) + +v1.7 is a major rewrite to improve the security of our app. We have enabled +sandboxing and disabled node integration for the renderer process. All this +required restructuring our IPC mechanisms, which resulted in a lot of under the +hood changes. The outcome is a more secure app that also uses the latest and +greatest Electron recommendations. + ## v1.6.63 ### New diff --git a/desktop/package.json b/desktop/package.json index d9aaf133ef..a57219aa35 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,6 +1,6 @@ { "name": "ente", - "version": "1.6.63", + "version": "1.7.0-beta+0", "private": true, "description": "Desktop client for Ente Photos", "author": "Ente ", From 647cc0d80348b1b7944bd903c316d21b173d3a94 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 3 May 2024 11:00:55 +0530 Subject: [PATCH 231/240] [desktop] Fix ref ref in action ref_name is the (from my understanding) the shorthand we need for prefixing. Untested, will do a test build. --- desktop/.github/workflows/desktop-release.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/desktop/.github/workflows/desktop-release.yml b/desktop/.github/workflows/desktop-release.yml index cb895fd4de..ef198ca48a 100644 --- a/desktop/.github/workflows/desktop-release.yml +++ b/desktop/.github/workflows/desktop-release.yml @@ -32,7 +32,9 @@ jobs: strategy: matrix: - os: [macos-latest, ubuntu-latest, windows-latest] + os: [macos-latest] + # Commented for testing + # os: [macos-latest, ubuntu-latest, windows-latest] steps: - name: Checkout code @@ -42,7 +44,7 @@ jobs: # repository when we're invoked for tag v1.x.x on the releases # repository. repository: ente-io/ente - ref: photosd-${{ github.ref }} + ref: photosd-${{ github.ref_name }} submodules: recursive - name: Setup node From 5d0e62cf5fb6d3802d9acc0720e9b9c95587a7df Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 3 May 2024 11:08:06 +0530 Subject: [PATCH 232/240] Use same convention as other preexisting tags in our repo --- .github/workflows/auth-release.yml | 4 ++-- desktop/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/auth-release.yml b/.github/workflows/auth-release.yml index 707bae895f..174b6c1d33 100644 --- a/.github/workflows/auth-release.yml +++ b/.github/workflows/auth-release.yml @@ -17,8 +17,8 @@ name: "Release (auth)" # We use a suffix like `-test` to indicate that these are test tags, and that # they belong to a pre-release. # -# If you need to do multiple tests, add a +x at the end of the tag. e.g. -# `auth-v1.2.3-test+1`. +# If you need to do multiple tests, add a .x at the end of the tag. e.g. +# `auth-v1.2.3-test.1`. # # Once the testing is done, also delete the tag(s) please. diff --git a/desktop/package.json b/desktop/package.json index a57219aa35..dc5ed9dba4 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,6 +1,6 @@ { "name": "ente", - "version": "1.7.0-beta+0", + "version": "1.7.0-beta.0", "private": true, "description": "Desktop client for Ente Photos", "author": "Ente ", From e9feec37d506772d5a9e2ce8654232cf42e34013 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 3 May 2024 11:28:06 +0530 Subject: [PATCH 233/240] Run the electron builder in the correct path --- desktop/.github/workflows/desktop-release.yml | 2 ++ desktop/docs/release.md | 17 +++-------------- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/desktop/.github/workflows/desktop-release.yml b/desktop/.github/workflows/desktop-release.yml index ef198ca48a..7013d3e579 100644 --- a/desktop/.github/workflows/desktop-release.yml +++ b/desktop/.github/workflows/desktop-release.yml @@ -71,6 +71,8 @@ jobs: - name: Build uses: ente-io/action-electron-builder@v1.0.0 with: + package_root: desktop + # GitHub token, automatically provided to the action # (No need to define this secret in the repo settings) github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/desktop/docs/release.md b/desktop/docs/release.md index 0d1b11bc63..da807b572c 100644 --- a/desktop/docs/release.md +++ b/desktop/docs/release.md @@ -34,22 +34,11 @@ The workflow is: git push origin photosd-v1.x.x ``` -3. Head over to the releases repository, copy all relevant changes from the - source repository, commit and push the changes. +3. Head over to the releases repository and run the trigger script, passing it + the tag _without_ the `photosd-` prefix. ```sh - cp ../ente/desktop/CHANGELOG.md CHANGELOG.md - git add CHANGELOG.md - git commit -m 'Release v1.x.x' - git push origin main - ``` - -4. Tag this commit, but this time _don't_ use the `photosd-` prefix. Push the - tag to trigger the GitHub action. - - ```sh - git tag v1.x.x - git push origin v1.x.x + ./.github/trigger-release.sh v1.x.x ``` ## Post build From 48f24d48b547c8aafe447de1d06eebb063036182 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Fri, 3 May 2024 12:11:35 +0530 Subject: [PATCH 234/240] [server] Move generateAlphaNumString to util --- .../pkg/controller/storagebonus/referral.go | 31 ++----------------- server/pkg/utils/random/generate.go | 27 ++++++++++++++++ 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/server/pkg/controller/storagebonus/referral.go b/server/pkg/controller/storagebonus/referral.go index b452484f41..5bdd951f8d 100644 --- a/server/pkg/controller/storagebonus/referral.go +++ b/server/pkg/controller/storagebonus/referral.go @@ -3,7 +3,7 @@ package storagebonus import ( "database/sql" "errors" - "fmt" + "github.com/ente-io/museum/pkg/utils/random" "github.com/ente-io/museum/ente" entity "github.com/ente-io/museum/ente/storagebonus" @@ -119,7 +119,7 @@ func (c *Controller) GetOrCreateReferralCode(ctx *gin.Context, userID int64) (*s if !errors.Is(err, sql.ErrNoRows) { return nil, stacktrace.Propagate(err, "failed to get storagebonus code") } - code, err := generateAlphaNumString(codeLength) + code, err := random.GenerateAlphaNumString(codeLength) if err != nil { return nil, stacktrace.Propagate(err, "") } @@ -131,30 +131,3 @@ func (c *Controller) GetOrCreateReferralCode(ctx *gin.Context, userID int64) (*s } return referralCode, nil } - -// generateAlphaNumString returns AlphaNumeric code of given length -// which exclude number 0 and letter O. The code always starts with an -// alphabet -func generateAlphaNumString(length int) (string, error) { - // Define the alphabet and numbers to be used in the string. - alphabet := "ABCDEFGHIJKLMNPQRSTUVWXYZ" - // Define the alphabet and numbers to be used in the string. - alphaNum := fmt.Sprintf("%s123456789", alphabet) - // Allocate a byte slice with the desired length. - result := make([]byte, length) - // Generate the first letter as an alphabet. - r0, err := auth.GenerateRandomInt(int64(len(alphabet))) - if err != nil { - return "", stacktrace.Propagate(err, "") - } - result[0] = alphabet[r0] - // Generate the remaining characters as alphanumeric. - for i := 1; i < length; i++ { - ri, err := auth.GenerateRandomInt(int64(len(alphaNum))) - if err != nil { - return "", stacktrace.Propagate(err, "") - } - result[i] = alphaNum[ri] - } - return string(result), nil -} diff --git a/server/pkg/utils/random/generate.go b/server/pkg/utils/random/generate.go index 47932b6603..75a811c8e1 100644 --- a/server/pkg/utils/random/generate.go +++ b/server/pkg/utils/random/generate.go @@ -13,3 +13,30 @@ func GenerateSixDigitOtp() (string, error) { } return fmt.Sprintf("%06d", n), nil } + +// GenerateAlphaNumString returns AlphaNumeric code of given length +// which exclude number 0 and letter O. The code always starts with an +// alphabet +func GenerateAlphaNumString(length int) (string, error) { + // Define the alphabet and numbers to be used in the string. + alphabet := "ABCDEFGHIJKLMNPQRSTUVWXYZ" + // Define the alphabet and numbers to be used in the string. + alphaNum := fmt.Sprintf("%s123456789", alphabet) + // Allocate a byte slice with the desired length. + result := make([]byte, length) + // Generate the first letter as an alphabet. + r0, err := auth.GenerateRandomInt(int64(len(alphabet))) + if err != nil { + return "", stacktrace.Propagate(err, "") + } + result[0] = alphabet[r0] + // Generate the remaining characters as alphanumeric. + for i := 1; i < length; i++ { + ri, err := auth.GenerateRandomInt(int64(len(alphaNum))) + if err != nil { + return "", stacktrace.Propagate(err, "") + } + result[i] = alphaNum[ri] + } + return string(result), nil +} From b9b928797c7cb141a70358384600a6f7068411c2 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Fri, 3 May 2024 12:24:52 +0530 Subject: [PATCH 235/240] [web][cast] Use server to generate deviceCode --- web/apps/cast/src/pages/index.tsx | 41 ++++++----------------------- web/packages/shared/network/cast.ts | 13 +++++---- 2 files changed, 16 insertions(+), 38 deletions(-) diff --git a/web/apps/cast/src/pages/index.tsx b/web/apps/cast/src/pages/index.tsx index 7ad310fe12..bbba9a1ad7 100644 --- a/web/apps/cast/src/pages/index.tsx +++ b/web/apps/cast/src/pages/index.tsx @@ -9,27 +9,8 @@ import { useEffect, useState } from "react"; import { storeCastData } from "services/cast/castService"; import { useCastReceiver } from "../utils/useCastReceiver"; -// Function to generate cryptographically secure digits -const generateSecureData = (length: number): Uint8Array => { - const array = new Uint8Array(length); - window.crypto.getRandomValues(array); - // Modulo operation to ensure each byte is a single digit - for (let i = 0; i < length; i++) { - array[i] = array[i] % 10; - } - return array; -}; - -const convertDataToDecimalString = (data: Uint8Array): string => { - let decimalString = ""; - for (let i = 0; i < data.length; i++) { - decimalString += data[i].toString(); // No need to pad, as each value is a single digit - } - return decimalString; -}; - export default function PairingMode() { - const [digits, setDigits] = useState([]); + const [deviceCode, setDeviceCode] = useState([]); const [publicKeyB64, setPublicKeyB64] = useState(""); const [privateKeyB64, setPrivateKeyB64] = useState(""); const [codePending, setCodePending] = useState(true); @@ -43,8 +24,6 @@ export default function PairingMode() { const init = async () => { try { - const data = generateSecureData(6); - setDigits(convertDataToDecimalString(data).split("")); const keypair = await generateKeyPair(); setPublicKeyB64(await toB64(keypair.publicKey)); setPrivateKeyB64(await toB64(keypair.privateKey)); @@ -107,7 +86,7 @@ export default function PairingMode() { "urn:x-cast:pair-request", message.senderId, { - code: digits.join(""), + code: deviceCode.join(""), }, ); } catch (e) { @@ -117,9 +96,7 @@ export default function PairingMode() { const generateKeyPair = async () => { await _sodium.ready; - const keypair = _sodium.crypto_box_keypair(); - return keypair; }; @@ -133,7 +110,7 @@ export default function PairingMode() { let devicePayload = ""; try { const encDastData = await castGateway.getCastData( - `${digits.join("")}`, + `${deviceCode.join("")}`, ); if (!encDastData) return; devicePayload = encDastData; @@ -157,10 +134,8 @@ export default function PairingMode() { const advertisePublicKey = async (publicKeyB64: string) => { // hey client, we exist! try { - await castGateway.registerDevice( - `${digits.join("")}`, - publicKeyB64, - ); + const codeValue = await castGateway.registerDevice(publicKeyB64); + setDeviceCode(codeValue.split("")); setCodePending(false); } catch (e) { // schedule re-try after 5 seconds @@ -175,7 +150,7 @@ export default function PairingMode() { useEffect(() => { console.log("useEffect for pairing called"); - if (digits.length < 1 || !publicKeyB64 || !privateKeyB64) return; + if (deviceCode.length < 1 || !publicKeyB64 || !privateKeyB64) return; const interval = setInterval(async () => { console.log("polling for cast data"); @@ -192,7 +167,7 @@ export default function PairingMode() { return () => { clearInterval(interval); }; - }, [digits, publicKeyB64, privateKeyB64, codePending]); + }, [deviceCode, publicKeyB64, privateKeyB64, codePending]); useEffect(() => { if (!publicKeyB64) return; @@ -235,7 +210,7 @@ export default function PairingMode() { ) : ( <> - + )}
diff --git a/web/packages/shared/network/cast.ts b/web/packages/shared/network/cast.ts index b240eab32d..a18767baa2 100644 --- a/web/packages/shared/network/cast.ts +++ b/web/packages/shared/network/cast.ts @@ -58,11 +58,14 @@ class CastGateway { return resp.data.publicKey; } - public async registerDevice(code: string, publicKey: string) { - await HTTPService.post(getEndpoint() + "/cast/device-info/", { - deviceCode: `${code}`, - publicKey: publicKey, - }); + public async registerDevice(publicKey: string): Promise { + const resp = await HTTPService.post( + getEndpoint() + "/cast/device-info/", + { + publicKey: publicKey, + }, + ); + return resp.data.deviceCode; } public async publishCastPayload( From 8a859325129d5d4a66f488bb4f1eca151c327952 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Fri, 3 May 2024 12:27:48 +0530 Subject: [PATCH 236/240] refactor --- web/apps/cast/src/pages/index.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/web/apps/cast/src/pages/index.tsx b/web/apps/cast/src/pages/index.tsx index bbba9a1ad7..b12bf1e765 100644 --- a/web/apps/cast/src/pages/index.tsx +++ b/web/apps/cast/src/pages/index.tsx @@ -10,7 +10,7 @@ import { storeCastData } from "services/cast/castService"; import { useCastReceiver } from "../utils/useCastReceiver"; export default function PairingMode() { - const [deviceCode, setDeviceCode] = useState([]); + const [deviceCode, setDeviceCode] = useState(""); const [publicKeyB64, setPublicKeyB64] = useState(""); const [privateKeyB64, setPrivateKeyB64] = useState(""); const [codePending, setCodePending] = useState(true); @@ -86,7 +86,7 @@ export default function PairingMode() { "urn:x-cast:pair-request", message.senderId, { - code: deviceCode.join(""), + code: deviceCode, }, ); } catch (e) { @@ -109,9 +109,7 @@ export default function PairingMode() { // then, we can decrypt this and store all the necessary info locally so we can play the collection slideshow. let devicePayload = ""; try { - const encDastData = await castGateway.getCastData( - `${deviceCode.join("")}`, - ); + const encDastData = await castGateway.getCastData(`${deviceCode}`); if (!encDastData) return; devicePayload = encDastData; } catch (e) { @@ -135,7 +133,7 @@ export default function PairingMode() { // hey client, we exist! try { const codeValue = await castGateway.registerDevice(publicKeyB64); - setDeviceCode(codeValue.split("")); + setDeviceCode(codeValue); setCodePending(false); } catch (e) { // schedule re-try after 5 seconds @@ -210,7 +208,7 @@ export default function PairingMode() { ) : ( <> - + )} From 99b13d18b0e84541bd682d739c3a5e26efc521ec Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Fri, 3 May 2024 12:29:32 +0530 Subject: [PATCH 237/240] [server][cast] Generate alphaNumeric deviceCode --- server/ente/cast/entity.go | 3 +-- server/pkg/controller/cast/controller.go | 2 +- server/pkg/repo/cast/repo.go | 14 ++------------ 3 files changed, 4 insertions(+), 15 deletions(-) diff --git a/server/ente/cast/entity.go b/server/ente/cast/entity.go index deffa90b97..a54d109fcc 100644 --- a/server/ente/cast/entity.go +++ b/server/ente/cast/entity.go @@ -9,8 +9,7 @@ type CastRequest struct { } type RegisterDeviceRequest struct { - DeviceCode *string `json:"deviceCode"` - PublicKey string `json:"publicKey" binding:"required"` + PublicKey string `json:"publicKey" binding:"required"` } type AuthContext struct { diff --git a/server/pkg/controller/cast/controller.go b/server/pkg/controller/cast/controller.go index 4432e149ff..e2d41101a9 100644 --- a/server/pkg/controller/cast/controller.go +++ b/server/pkg/controller/cast/controller.go @@ -28,7 +28,7 @@ func NewController(castRepo *castRepo.Repository, } func (c *Controller) RegisterDevice(ctx *gin.Context, request *cast.RegisterDeviceRequest) (string, error) { - return c.CastRepo.AddCode(ctx, request.DeviceCode, request.PublicKey, network.GetClientIP(ctx)) + return c.CastRepo.AddCode(ctx, request.PublicKey, network.GetClientIP(ctx)) } func (c *Controller) GetPublicKey(ctx *gin.Context, deviceCode string) (string, error) { diff --git a/server/pkg/repo/cast/repo.go b/server/pkg/repo/cast/repo.go index 89ebc40838..ee51f6f034 100644 --- a/server/pkg/repo/cast/repo.go +++ b/server/pkg/repo/cast/repo.go @@ -8,24 +8,14 @@ import ( "github.com/ente-io/stacktrace" "github.com/google/uuid" log "github.com/sirupsen/logrus" - "strings" ) type Repository struct { DB *sql.DB } -func (r *Repository) AddCode(ctx context.Context, code *string, pubKey string, ip string) (string, error) { - var codeValue string - var err error - if code == nil || *code == "" { - codeValue, err = random.GenerateSixDigitOtp() - if err != nil { - return "", stacktrace.Propagate(err, "") - } - } else { - codeValue = strings.TrimSpace(*code) - } +func (r *Repository) AddCode(ctx context.Context, pubKey string, ip string) (string, error) { + codeValue, err := random.GenerateAlphaNumString(6) _, err = r.DB.ExecContext(ctx, "INSERT INTO casting (code, public_key, id, ip) VALUES ($1, $2, $3, $4)", codeValue, pubKey, uuid.New(), ip) if err != nil { return "", err From ad5cfdc6db3d49eafc52fa66f3545f3b6ec9ebb7 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Fri, 3 May 2024 12:31:26 +0530 Subject: [PATCH 238/240] [server][cast] convert deviceCode input to upperCase --- server/pkg/api/cast.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/server/pkg/api/cast.go b/server/pkg/api/cast.go index 62d5c94784..9012624d32 100644 --- a/server/pkg/api/cast.go +++ b/server/pkg/api/cast.go @@ -1,16 +1,16 @@ package api import ( - entity "github.com/ente-io/museum/ente/cast" - "github.com/ente-io/museum/pkg/controller/cast" - "net/http" - "strconv" - "github.com/ente-io/museum/ente" + entity "github.com/ente-io/museum/ente/cast" "github.com/ente-io/museum/pkg/controller" + "github.com/ente-io/museum/pkg/controller/cast" "github.com/ente-io/museum/pkg/utils/handler" "github.com/ente-io/stacktrace" "github.com/gin-gonic/gin" + "net/http" + "strconv" + "strings" ) // CastHandler exposes request handlers for publicly accessible collections @@ -126,7 +126,7 @@ func (h *CastHandler) GetDiff(c *gin.Context) { } func getDeviceCode(c *gin.Context) string { - return c.Param("deviceCode") + return strings.ToUpper(c.Param("deviceCode")) } func (h *CastHandler) getFileForType(c *gin.Context, objectType ente.ObjectType) { From 627eab472c4b0cd5f41d2a0d87988abebe894497 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Fri, 3 May 2024 12:32:16 +0530 Subject: [PATCH 239/240] [server][cast] Only log ip mismatch instances --- server/pkg/controller/cast/controller.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/pkg/controller/cast/controller.go b/server/pkg/controller/cast/controller.go index e2d41101a9..2bb002f81d 100644 --- a/server/pkg/controller/cast/controller.go +++ b/server/pkg/controller/cast/controller.go @@ -2,7 +2,6 @@ package cast import ( "context" - "github.com/ente-io/museum/ente" "github.com/ente-io/museum/ente/cast" "github.com/ente-io/museum/pkg/controller/access" castRepo "github.com/ente-io/museum/pkg/repo/cast" @@ -42,7 +41,6 @@ func (c *Controller) GetPublicKey(ctx *gin.Context, deviceCode string) (string, "ip": ip, "clientIP": network.GetClientIP(ctx), }).Warn("GetPublicKey: IP mismatch") - return "", &ente.ErrCastIPMismatch } return pubKey, nil } From a62edad446ddd35c986beb0a9dba22a64f6eb96c Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Fri, 3 May 2024 12:36:02 +0530 Subject: [PATCH 240/240] [server][cast] Fix err handling --- server/pkg/repo/cast/repo.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/pkg/repo/cast/repo.go b/server/pkg/repo/cast/repo.go index ee51f6f034..2f4446c9d0 100644 --- a/server/pkg/repo/cast/repo.go +++ b/server/pkg/repo/cast/repo.go @@ -16,6 +16,9 @@ type Repository struct { func (r *Repository) AddCode(ctx context.Context, pubKey string, ip string) (string, error) { codeValue, err := random.GenerateAlphaNumString(6) + if err != nil { + return "", err + } _, err = r.DB.ExecContext(ctx, "INSERT INTO casting (code, public_key, id, ip) VALUES ($1, $2, $3, $4)", codeValue, pubKey, uuid.New(), ip) if err != nil { return "", err