diff --git a/.github/workflows/rust-lint.yml b/.github/workflows/rust-lint.yml
index 0f7694e0c6..802482c548 100644
--- a/.github/workflows/rust-lint.yml
+++ b/.github/workflows/rust-lint.yml
@@ -15,6 +15,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
+env:
+ RUSTFLAGS: -D warnings
+
jobs:
lint:
runs-on: ubuntu-latest
@@ -33,9 +36,9 @@ jobs:
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
-
+
- run: cargo fmt --check
- - run: cargo clippy
+ - run: cargo clippy --all-targets --all-features
- run: cargo build
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f4b90454e2..83203d474e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -48,7 +48,7 @@ See [docs/](docs/README.md) for how to edit these documents.
## Code contributions
-If you'd like to contribute code, it is best to start small. Consider some well-scoped changes, say like adding more [custom icons to auth](auth/docs/adding-icons.md), or fixing a specific bug.
+If you'd like to contribute code, it is best to start small. Consider some well-scoped changes, say like adding more [custom icons to auth](mobile/apps/auth/docs/adding-icons.md), or fixing a specific bug.
Code that changes the behaviour of the product might not get merged, at least not initially. The PR can serve as a discussion bed, but you might find it easier to just start a discussion instead, or post your perspective in the (likely) existing thread about the behaviour change or new feature you wish for.
diff --git a/mobile/apps/auth/README.md b/mobile/apps/auth/README.md
index 99ad0d1688..db0c84ea26 100644
--- a/mobile/apps/auth/README.md
+++ b/mobile/apps/auth/README.md
@@ -98,7 +98,7 @@ more, see [docs/adding-icons](docs/adding-icons.md).
The best way to support this project is by checking out [Ente
Photos](../mobile/README.md) or spreading the word.
-For more ways to contribute, see [../CONTRIBUTING.md](../../../CONTRIBUTING.md).
+For more ways to contribute, see [../../../CONTRIBUTING.md](../../../CONTRIBUTING.md).
## Certificate Fingerprints
diff --git a/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json b/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json
index fc558cf8f5..ba502b4046 100644
--- a/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json
+++ b/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json
@@ -60,6 +60,15 @@
"slug": "amtrak",
"hex": "003A5D"
},
+ {
+ "title": "Animal Crossing",
+ "slug:": "animal_crossing",
+ "altNames": [
+ "AnimalCrossing",
+ "Bell Tree Forums",
+ "BellTree Forums"
+ ]
+ },
{
"title": "Ankama",
"slug": "ankama"
@@ -81,6 +90,13 @@
"Docaposte AR24"
]
},
+ {
+ "title": "Art Fight",
+ "slug": "art_fight",
+ "altNames": [
+ "ArtFight"
+ ]
+ },
{
"title": "Aruba",
"slug": "aruba",
@@ -341,6 +357,9 @@
"slug": "cih",
"hex": "D14633"
},
+ {
+ "title": "Chucklefish"
+ },
{
"title": "Clipper",
"slug": "clippercard",
@@ -1505,6 +1524,9 @@
{
"title": "Skinport"
},
+ {
+ "title": "Smogon"
+ },
{
"title": "SMSPool",
"slug": "sms_pool_net",
@@ -1664,6 +1686,12 @@
{
"title": "TorGuard"
},
+ {
+ "title": "Toyhouse",
+ "altNames": [
+ "Toyhou.se"
+ ]
+ },
{
"title": "Trading 212"
},
@@ -1685,6 +1713,15 @@
"T Rowe Price Group, Inc"
]
},
+ {
+ "title": "TU Dresden",
+ "slug": "tu_dresden",
+ "altNames": [
+ "Technische Universität Dresden",
+ "Dresden University of Technology"
+ ],
+ "hex": "00305d"
+ },
{
"title": "Tweakers"
},
@@ -1699,6 +1736,12 @@
"Twitch tv"
]
},
+ {
+ "title": "Twitter",
+ "altNames": [
+ "X"
+ ]
+ },
{
"title": "Ubiquiti",
"slug": "ubiquiti",
@@ -1906,6 +1949,22 @@
{
"title": "Co-Wheels",
"slug": "cowheels"
+ },
+ {
+ "title": "Zivver",
+ "slug": "zivver"
+ },
+ {
+ "title": "Meesman Indexbeleggen",
+ "slug": "meesman"
+ },
+ {
+ "title": "Scouting Nederland",
+ "slug": "scoutingnederland"
+ },
+ {
+ "title": "ISC2",
+ "slug": "isc2"
}
]
-}
\ No newline at end of file
+}
diff --git a/mobile/apps/auth/assets/custom-icons/icons/animal_crossing.svg b/mobile/apps/auth/assets/custom-icons/icons/animal_crossing.svg
new file mode 100644
index 0000000000..6800a4d8f7
--- /dev/null
+++ b/mobile/apps/auth/assets/custom-icons/icons/animal_crossing.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/mobile/apps/auth/assets/custom-icons/icons/art_fight.svg b/mobile/apps/auth/assets/custom-icons/icons/art_fight.svg
new file mode 100644
index 0000000000..51c4274bd4
--- /dev/null
+++ b/mobile/apps/auth/assets/custom-icons/icons/art_fight.svg
@@ -0,0 +1,307 @@
+
+
+
+
diff --git a/mobile/apps/auth/assets/custom-icons/icons/best_buy.svg b/mobile/apps/auth/assets/custom-icons/icons/best_buy.svg
new file mode 100644
index 0000000000..86717212e2
--- /dev/null
+++ b/mobile/apps/auth/assets/custom-icons/icons/best_buy.svg
@@ -0,0 +1,13 @@
+
\ No newline at end of file
diff --git a/mobile/apps/auth/assets/custom-icons/icons/chucklefish.svg b/mobile/apps/auth/assets/custom-icons/icons/chucklefish.svg
new file mode 100644
index 0000000000..76be26a658
--- /dev/null
+++ b/mobile/apps/auth/assets/custom-icons/icons/chucklefish.svg
@@ -0,0 +1,75 @@
+
+
+
+
diff --git a/mobile/apps/auth/assets/custom-icons/icons/isc2.svg b/mobile/apps/auth/assets/custom-icons/icons/isc2.svg
new file mode 100644
index 0000000000..477c61750f
--- /dev/null
+++ b/mobile/apps/auth/assets/custom-icons/icons/isc2.svg
@@ -0,0 +1,4 @@
+
diff --git a/mobile/apps/auth/assets/custom-icons/icons/meesman.svg b/mobile/apps/auth/assets/custom-icons/icons/meesman.svg
new file mode 100644
index 0000000000..7c98ba0161
--- /dev/null
+++ b/mobile/apps/auth/assets/custom-icons/icons/meesman.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/mobile/apps/auth/assets/custom-icons/icons/scoutingnederland.svg b/mobile/apps/auth/assets/custom-icons/icons/scoutingnederland.svg
new file mode 100644
index 0000000000..ae617a6aee
--- /dev/null
+++ b/mobile/apps/auth/assets/custom-icons/icons/scoutingnederland.svg
@@ -0,0 +1,129 @@
+
+
+
+
diff --git a/mobile/apps/auth/assets/custom-icons/icons/smogon.svg b/mobile/apps/auth/assets/custom-icons/icons/smogon.svg
new file mode 100644
index 0000000000..6d4d656c88
--- /dev/null
+++ b/mobile/apps/auth/assets/custom-icons/icons/smogon.svg
@@ -0,0 +1,170 @@
+
+
+
\ No newline at end of file
diff --git a/mobile/apps/auth/assets/custom-icons/icons/toyhouse.svg b/mobile/apps/auth/assets/custom-icons/icons/toyhouse.svg
new file mode 100644
index 0000000000..f385ffbb27
--- /dev/null
+++ b/mobile/apps/auth/assets/custom-icons/icons/toyhouse.svg
@@ -0,0 +1,49 @@
+
+
+
+
diff --git a/mobile/apps/auth/assets/custom-icons/icons/tu_dresden.svg b/mobile/apps/auth/assets/custom-icons/icons/tu_dresden.svg
new file mode 100644
index 0000000000..2c53b913cf
--- /dev/null
+++ b/mobile/apps/auth/assets/custom-icons/icons/tu_dresden.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/mobile/apps/auth/assets/custom-icons/icons/twitter.svg b/mobile/apps/auth/assets/custom-icons/icons/twitter.svg
new file mode 100644
index 0000000000..d60af2b8c5
--- /dev/null
+++ b/mobile/apps/auth/assets/custom-icons/icons/twitter.svg
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/mobile/apps/auth/assets/custom-icons/icons/ubiquiti.svg b/mobile/apps/auth/assets/custom-icons/icons/ubiquiti.svg
index 992eb7986a..559fd81869 100644
--- a/mobile/apps/auth/assets/custom-icons/icons/ubiquiti.svg
+++ b/mobile/apps/auth/assets/custom-icons/icons/ubiquiti.svg
@@ -1,7 +1 @@
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/mobile/apps/auth/assets/custom-icons/icons/zivver.svg b/mobile/apps/auth/assets/custom-icons/icons/zivver.svg
new file mode 100644
index 0000000000..40d0977f98
--- /dev/null
+++ b/mobile/apps/auth/assets/custom-icons/icons/zivver.svg
@@ -0,0 +1,21 @@
+
+
+
diff --git a/mobile/apps/auth/assets/icons/auth-icon-monochrome.png b/mobile/apps/auth/assets/icons/auth-icon-monochrome.png
new file mode 100644
index 0000000000..09deb41b4c
Binary files /dev/null and b/mobile/apps/auth/assets/icons/auth-icon-monochrome.png differ
diff --git a/mobile/apps/auth/lib/main.dart b/mobile/apps/auth/lib/main.dart
index 54f8da92aa..2bca2feb0d 100644
--- a/mobile/apps/auth/lib/main.dart
+++ b/mobile/apps/auth/lib/main.dart
@@ -40,8 +40,10 @@ Future initSystemTray() async {
if (PlatformUtil.isMobile()) return;
String path = Platform.isWindows
? 'assets/icons/auth-icon.ico'
- : 'assets/icons/auth-icon.png';
- await trayManager.setIcon(path);
+ : Platform.isMacOS
+ ? 'assets/icons/auth-icon-monochrome.png'
+ : 'assets/icons/auth-icon.png';
+ await trayManager.setIcon(path, isTemplate: true);
Menu menu = Menu(
items: [
MenuItem(
diff --git a/mobile/apps/photos/README.md b/mobile/apps/photos/README.md
index 958f7c4215..45d096a77d 100644
--- a/mobile/apps/photos/README.md
+++ b/mobile/apps/photos/README.md
@@ -10,12 +10,12 @@ commit](https://github.com/ente-io/ente/commit/a8cdc811fd20ca4289d8e779c97f08ef5
Hello world
-To know more about Ente, see [our main README](../README.md) or visit
+To know more about Ente, see [our main README](../../../README.md) or visit
[ente.io](https://ente.io).
-To use Ente Photos on the web, see [../web](../web/README.md). To use Ente
-Photos on the desktop, see [../desktop](../desktop/README.md). There is a also a
-[CLI tool](../cli/README.md) for easy / automated exports.
+To use Ente Photos on the web, see [../../../web](../../../web/README.md). To use Ente
+Photos on the desktop, see [../../../desktop](../../../desktop/README.md). There is a also a
+[CLI tool](../../../cli/README.md) for easy / automated exports.
If you're looking for Ente Auth instead, see [../auth](../auth/README.md).
@@ -32,16 +32,16 @@ without relying on third party stores.
You can alternatively install the build from PlayStore or F-Droid.
-
+
-
+
### iOS
-
+
## 🧑💻 Building from source
@@ -99,4 +99,4 @@ apksigner verify --print-certs
## 💚 Contribute
-For more ways to contribute, see [../CONTRIBUTING.md](../CONTRIBUTING.md).
+For more ways to contribute, see [../../../CONTRIBUTING.md](../../../CONTRIBUTING.md).
diff --git a/mobile/apps/photos/ios/Podfile.lock b/mobile/apps/photos/ios/Podfile.lock
index 55224eaaf9..627d2debd6 100644
--- a/mobile/apps/photos/ios/Podfile.lock
+++ b/mobile/apps/photos/ios/Podfile.lock
@@ -12,6 +12,8 @@ PODS:
- Flutter
- device_info_plus (0.0.1):
- Flutter
+ - emoji_picker_flutter (0.0.1):
+ - Flutter
- ffmpeg_kit_custom (6.0.3)
- ffmpeg_kit_flutter (6.0.3):
- ffmpeg_kit_custom
@@ -127,9 +129,6 @@ PODS:
- libwebp/sharpyuv (1.5.0)
- libwebp/webp (1.5.0):
- libwebp/sharpyuv
- - local_auth_darwin (0.0.1):
- - Flutter
- - FlutterMacOS
- local_auth_ios (0.0.1):
- Flutter
- Mantle (2.2.0):
@@ -230,6 +229,8 @@ PODS:
- Flutter
- url_launcher_ios (0.0.1):
- Flutter
+ - vibration (1.7.5):
+ - Flutter
- video_player_avfoundation (0.0.1):
- Flutter
- FlutterMacOS
@@ -250,6 +251,7 @@ DEPENDENCIES:
- cupertino_http (from `.symlinks/plugins/cupertino_http/darwin`)
- dart_ui_isolate (from `.symlinks/plugins/dart_ui_isolate/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
+ - emoji_picker_flutter (from `.symlinks/plugins/emoji_picker_flutter/ios`)
- ffmpeg_kit_flutter (from `.symlinks/plugins/ffmpeg_kit_flutter/ios`)
- file_saver (from `.symlinks/plugins/file_saver/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
@@ -269,7 +271,6 @@ DEPENDENCIES:
- in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/darwin`)
- integration_test (from `.symlinks/plugins/integration_test/ios`)
- launcher_icon_switcher (from `.symlinks/plugins/launcher_icon_switcher/ios`)
- - local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
- local_auth_ios (from `.symlinks/plugins/local_auth_ios/ios`)
- maps_launcher (from `.symlinks/plugins/maps_launcher/ios`)
- media_extension (from `.symlinks/plugins/media_extension/ios`)
@@ -297,6 +298,7 @@ DEPENDENCIES:
- thermal (from `.symlinks/plugins/thermal/ios`)
- ua_client_hints (from `.symlinks/plugins/ua_client_hints/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
+ - vibration (from `.symlinks/plugins/vibration/ios`)
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
- video_thumbnail (from `.symlinks/plugins/video_thumbnail/ios`)
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
@@ -304,7 +306,7 @@ DEPENDENCIES:
- workmanager (from `.symlinks/plugins/workmanager/ios`)
SPEC REPOS:
- https://github.com/ente-io/ffmpeg-kit-custom-repo-ios:
+ https://github.com/ente-io/ffmpeg-kit-custom-repo-ios.git:
- ffmpeg_kit_custom
trunk:
- Firebase
@@ -339,6 +341,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/dart_ui_isolate/ios"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
+ emoji_picker_flutter:
+ :path: ".symlinks/plugins/emoji_picker_flutter/ios"
ffmpeg_kit_flutter:
:path: ".symlinks/plugins/ffmpeg_kit_flutter/ios"
file_saver:
@@ -377,8 +381,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/integration_test/ios"
launcher_icon_switcher:
:path: ".symlinks/plugins/launcher_icon_switcher/ios"
- local_auth_darwin:
- :path: ".symlinks/plugins/local_auth_darwin/darwin"
local_auth_ios:
:path: ".symlinks/plugins/local_auth_ios/ios"
maps_launcher:
@@ -433,6 +435,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/ua_client_hints/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
+ vibration:
+ :path: ".symlinks/plugins/vibration/ios"
video_player_avfoundation:
:path: ".symlinks/plugins/video_player_avfoundation/darwin"
video_thumbnail:
@@ -445,83 +449,84 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/workmanager/ios"
SPEC CHECKSUMS:
- app_links: 76b66b60cc809390ca1ad69bfd66b998d2387ac7
- battery_info: 83f3aae7be2fccefab1d2bf06b8aa96f11c8bcdd
- connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
- cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c
- dart_ui_isolate: 46f6714abe6891313267153ef6f9748d8ecfcab1
- device_info_plus: 335f3ce08d2e174b9fdc3db3db0f4e3b1f66bd89
+ app_links: f3e17e4ee5e357b39d8b95290a9b2c299fca71c6
+ battery_info: b6c551049266af31556b93c9d9b9452cfec0219f
+ connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d
+ cupertino_http: 947a233f40cfea55167a49f2facc18434ea117ba
+ dart_ui_isolate: d5bcda83ca4b04f129d70eb90110b7a567aece14
+ device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
+ emoji_picker_flutter: fe2e6151c5b548e975d546e6eeb567daf0962a58
ffmpeg_kit_custom: 682b4f2f1ff1f8abae5a92f6c3540f2441d5be99
- ffmpeg_kit_flutter: 915b345acc97d4142e8a9a8549d177ff10f043f5
- file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
+ ffmpeg_kit_flutter: 9dce4803991478c78c6fb9f972703301101095fe
+ file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
Firebase: d80354ed7f6df5f9aca55e9eb47cc4b634735eaf
- firebase_core: 6cbed78b4f298ed103a9fd034e6dbc846320480f
- firebase_messaging: 5e0adf2eb18b0ee59aa0c109314c091a0497ecac
+ firebase_core: 6e223dfa350b2edceb729cea505eaaef59330682
+ firebase_messaging: 07fde77ae28c08616a1d4d870450efc2b38cf40d
FirebaseCore: 99fe0c4b44a39f37d99e6404e02009d2db5d718d
FirebaseCoreInternal: df24ce5af28864660ecbd13596fc8dd3a8c34629
FirebaseInstallations: 6c963bd2a86aca0481eef4f48f5a4df783ae5917
FirebaseMessaging: 487b634ccdf6f7b7ff180fdcb2a9935490f764e8
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
- flutter_email_sender: aa1e9772696691d02cd91fea829856c11efb8e58
- flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1
- flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
- flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
- flutter_native_splash: 6cad9122ea0fad137d23137dd14b937f3e90b145
- flutter_secure_storage: 2c2ff13db9e0a5647389bff88b0ecac56e3f3418
- flutter_sodium: 7e4621538491834eba53bd524547854bcbbd6987
- flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544
- fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
+ flutter_email_sender: e03bdda7637bcd3539bfe718fddd980e9508efaa
+ flutter_image_compress_common: ec1d45c362c9d30a3f6a0426c297f47c52007e3e
+ flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
+ flutter_local_notifications: ff50f8405aaa0ccdc7dcfb9022ca192e8ad9688f
+ flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a
+ flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
+ flutter_sodium: a00383520fc689c688b66fd3092984174712493e
+ flutter_timezone: ac3da59ac941ff1c98a2e1f0293420e020120282
+ fluttertoast: 21eecd6935e7064cc1fcb733a4c5a428f3f24f0f
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
- home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
- image_editor_common: 3de87e7c4804f4ae24c8f8a998362b98c105cac1
- in_app_purchase_storekit: d1a48cb0f8b29dbf5f85f782f5dd79b21b90a5e6
- integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
- launcher_icon_switcher: 84c218d233505aa7d8655d8fa61a3ba802c022da
+ home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
+ image_editor_common: d6f6644ae4a6de80481e89fe6d0a8c49e30b4b43
+ in_app_purchase_storekit: a1ce04056e23eecc666b086040239da7619cd783
+ integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
+ launcher_icon_switcher: 8e0ad2131a20c51c1dd939896ee32e70cd845b37
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
- local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391
- local_auth_ios: f7a1841beef3151d140a967c2e46f30637cdf451
+ local_auth_ios: 5046a18c018dd973247a0564496c8898dbb5adf9
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
- maps_launcher: edf829809ba9e894d70e569bab11c16352dedb45
- media_extension: 671e2567880d96c95c65c9a82ccceed8f2e309fd
- media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
- media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
- motion_sensors: 741e702c17467b9569a92165dda8d4d88c6167f1
- motionphoto: 23e2aeb5c6380112f69468d71f970fa7438e5ed1
- move_to_background: 7e3467dd2a1d1013e98c9c1cb93fd53cd7ef9d84
+ maps_launcher: 2e5b6a2d664ec6c27f82ffa81b74228d770ab203
+ media_extension: 6618f07abd762cdbfaadf1b0c56a287e820f0c84
+ media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
+ media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
+ motion_sensors: 03f55b7c637a7e365a0b5f9697a449f9059d5d91
+ motionphoto: 8b65ce50c7d7ff3c767534fc3768b2eed9ac24e4
+ move_to_background: cd3091014529ec7829e342ad2d75c0a11f4378a5
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
- native_video_player: 6809dec117e8997161dbfb42a6f90d6df71a504d
- objective_c: 89e720c30d716b036faf9c9684022048eee1eee2
- onnxruntime: f9b296392c96c42882be020a59dbeac6310d81b2
+ native_video_player: 29ab24a926804ac8c4a57eb6d744c7d927c2bc3e
+ objective_c: 77e887b5ba1827970907e10e832eec1683f3431d
+ onnxruntime: e7c2ae44385191eaad5ae64c935a72debaddc997
onnxruntime-c: a909204639a1f035f575127ac406f781ac797c9c
onnxruntime-objc: b6fab0f1787aa6f7190c2013f03037df4718bd8b
- open_mail_app: 7314a609e88eed22d53671279e189af7a0ab0f11
+ open_mail_app: 70273c53f768beefdafbe310c3d9086e4da3cb02
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
- package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
- path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
- permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
- photo_manager: d2fbcc0f2d82458700ee6256a15018210a81d413
- privacy_screen: 3159a541f5d3a31bea916cfd4e58f9dc722b3fd4
+ package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
+ path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
+ permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
+ photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a
+ privacy_screen: 1a131c052ceb3c3659934b003b0d397c2381a24e
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
- receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
+ receive_sharing_intent: 79c848f5b045674ad60b9fea3bafea59962ad2c1
SDWebImage: f29024626962457f3470184232766516dee8dfea
SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
Sentry: da60d980b197a46db0b35ea12cb8f39af48d8854
- sentry_flutter: 27892878729f42701297c628eb90e7c6529f3684
- share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
- shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
- sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
+ sentry_flutter: 2df8b0aab7e4aba81261c230cbea31c82a62dd1b
+ share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
+ shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
+ sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
sqlite3: 3c950dc86011117c307eb0b28c4a7bb449dce9f1
- sqlite3_flutter_libs: 3c323550ef3b928bc0aa9513c841e45a7d242832
- system_info_plus: 555ce7047fbbf29154726db942ae785c29211740
- thermal: d4c48be750d1ddbab36b0e2dcb2471531bc8df41
- ua_client_hints: 92fe0d139619b73ec9fcb46cc7e079a26178f586
- url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
- video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b
- video_thumbnail: 584ccfa55d8fd2f3d5507218b0a18d84c839c620
- volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12
- wakelock_plus: 04623e3f525556020ebd4034310f20fe7fda8b49
- workmanager: 01be2de7f184bd15de93a1812936a2b7f42ef07e
+ sqlite3_flutter_libs: 069c435986dd4b63461aecd68f4b30be4a9e9daa
+ system_info_plus: 5393c8da281d899950d751713575fbf91c7709aa
+ thermal: a9261044101ae8f532fa29cab4e8270b51b3f55c
+ ua_client_hints: aeabd123262c087f0ce151ef96fa3ab77bfc8b38
+ url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
+ vibration: 7d883d141656a1c1a6d8d238616b2042a51a1241
+ video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3
+ video_thumbnail: 94ba6705afbaa120b77287080424930f23ea0c40
+ volume_controller: 2e3de73d6e7e81a0067310d17fb70f2f86d71ac7
+ wakelock_plus: 373cfe59b235a6dd5837d0fb88791d2f13a90d56
+ workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
PODFILE CHECKSUM: a8ef88ad74ba499756207e7592c6071a96756d18
diff --git a/mobile/apps/photos/ios/Runner.xcodeproj/project.pbxproj b/mobile/apps/photos/ios/Runner.xcodeproj/project.pbxproj
index d1c1bff9f4..db6d40da7d 100644
--- a/mobile/apps/photos/ios/Runner.xcodeproj/project.pbxproj
+++ b/mobile/apps/photos/ios/Runner.xcodeproj/project.pbxproj
@@ -532,6 +532,7 @@
"${BUILT_PRODUCTS_DIR}/cupertino_http/cupertino_http.framework",
"${BUILT_PRODUCTS_DIR}/dart_ui_isolate/dart_ui_isolate.framework",
"${BUILT_PRODUCTS_DIR}/device_info_plus/device_info_plus.framework",
+ "${BUILT_PRODUCTS_DIR}/emoji_picker_flutter/emoji_picker_flutter.framework",
"${BUILT_PRODUCTS_DIR}/file_saver/file_saver.framework",
"${BUILT_PRODUCTS_DIR}/flutter_email_sender/flutter_email_sender.framework",
"${BUILT_PRODUCTS_DIR}/flutter_image_compress_common/flutter_image_compress_common.framework",
@@ -548,7 +549,6 @@
"${BUILT_PRODUCTS_DIR}/integration_test/integration_test.framework",
"${BUILT_PRODUCTS_DIR}/launcher_icon_switcher/launcher_icon_switcher.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",
"${BUILT_PRODUCTS_DIR}/maps_launcher/maps_launcher.framework",
"${BUILT_PRODUCTS_DIR}/media_extension/media_extension.framework",
@@ -576,6 +576,7 @@
"${BUILT_PRODUCTS_DIR}/thermal/thermal.framework",
"${BUILT_PRODUCTS_DIR}/ua_client_hints/ua_client_hints.framework",
"${BUILT_PRODUCTS_DIR}/url_launcher_ios/url_launcher_ios.framework",
+ "${BUILT_PRODUCTS_DIR}/vibration/vibration.framework",
"${BUILT_PRODUCTS_DIR}/video_player_avfoundation/video_player_avfoundation.framework",
"${BUILT_PRODUCTS_DIR}/video_thumbnail/video_thumbnail.framework",
"${BUILT_PRODUCTS_DIR}/volume_controller/volume_controller.framework",
@@ -628,6 +629,7 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/cupertino_http.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/dart_ui_isolate.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_info_plus.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/emoji_picker_flutter.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_saver.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_email_sender.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_image_compress_common.framework",
@@ -644,7 +646,6 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/integration_test.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/launcher_icon_switcher.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",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/maps_launcher.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/media_extension.framework",
@@ -672,6 +673,7 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/thermal.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ua_client_hints.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher_ios.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/vibration.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/video_player_avfoundation.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/video_thumbnail.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/volume_controller.framework",
diff --git a/mobile/apps/photos/lib/db/upload_locks_db.dart b/mobile/apps/photos/lib/db/upload_locks_db.dart
index ab786d5c78..dc2bfab079 100644
--- a/mobile/apps/photos/lib/db/upload_locks_db.dart
+++ b/mobile/apps/photos/lib/db/upload_locks_db.dart
@@ -157,6 +157,23 @@ class UploadLocksDB {
);
}
+ Future getLockData(String id) async {
+ final db = await instance.database;
+ final rows = await db.query(
+ _uploadLocksTable.table,
+ where: '${_uploadLocksTable.columnID} = ?',
+ whereArgs: [id],
+ );
+ if (rows.isEmpty) {
+ return "No lock found for $id";
+ }
+ final row = rows.first;
+ final time = row[_uploadLocksTable.columnTime] as int;
+ final owner = row[_uploadLocksTable.columnOwner] as String;
+ final duration = DateTime.now().millisecondsSinceEpoch - time;
+ return "Lock for $id acquired by $owner since ${Duration(milliseconds: duration)}";
+ }
+
Future isLocked(String id, String owner) async {
final db = await instance.database;
final rows = await db.query(
diff --git a/mobile/apps/photos/lib/generated/l10n.dart b/mobile/apps/photos/lib/generated/l10n.dart
index 84cef71699..14b4c3aeea 100644
--- a/mobile/apps/photos/lib/generated/l10n.dart
+++ b/mobile/apps/photos/lib/generated/l10n.dart
@@ -12495,6 +12495,118 @@ class S {
args: [],
);
}
+
+ /// `Undo`
+ String get undo {
+ return Intl.message(
+ 'Undo',
+ name: 'undo',
+ desc: '',
+ args: [],
+ );
+ }
+
+ /// `Redo`
+ String get redo {
+ return Intl.message(
+ 'Redo',
+ name: 'redo',
+ desc: '',
+ args: [],
+ );
+ }
+
+ /// `Filter`
+ String get filter {
+ return Intl.message(
+ 'Filter',
+ name: 'filter',
+ desc: '',
+ args: [],
+ );
+ }
+
+ /// `Adjust`
+ String get adjust {
+ return Intl.message(
+ 'Adjust',
+ name: 'adjust',
+ desc: '',
+ args: [],
+ );
+ }
+
+ /// `Draw`
+ String get draw {
+ return Intl.message(
+ 'Draw',
+ name: 'draw',
+ desc: '',
+ args: [],
+ );
+ }
+
+ /// `Sticker`
+ String get sticker {
+ return Intl.message(
+ 'Sticker',
+ name: 'sticker',
+ desc: '',
+ args: [],
+ );
+ }
+
+ /// `Brush Color`
+ String get brushColor {
+ return Intl.message(
+ 'Brush Color',
+ name: 'brushColor',
+ desc: '',
+ args: [],
+ );
+ }
+
+ /// `Font`
+ String get font {
+ return Intl.message(
+ 'Font',
+ name: 'font',
+ desc: '',
+ args: [],
+ );
+ }
+
+ /// `Background`
+ String get background {
+ return Intl.message(
+ 'Background',
+ name: 'background',
+ desc: '',
+ args: [],
+ );
+ }
+
+ /// `Align`
+ String get align {
+ return Intl.message(
+ 'Align',
+ name: 'align',
+ desc: '',
+ args: [],
+ );
+ }
+
+ /// `{count, plural, =1{Added successfully to 1 album} other{Added successfully to {count} albums}}`
+ String addedToAlbums(int count) {
+ return Intl.plural(
+ count,
+ one: 'Added successfully to 1 album',
+ other: 'Added successfully to $count albums',
+ name: 'addedToAlbums',
+ desc: 'Message shown when items are added to albums',
+ args: [count],
+ );
+ }
}
class AppLocalizationDelegate extends LocalizationsDelegate {
diff --git a/mobile/apps/photos/lib/l10n/intl_en.arb b/mobile/apps/photos/lib/l10n/intl_en.arb
index 21998814da..017637a8b8 100644
--- a/mobile/apps/photos/lib/l10n/intl_en.arb
+++ b/mobile/apps/photos/lib/l10n/intl_en.arb
@@ -1808,5 +1808,24 @@
"automaticallyAnalyzeAndSplitGrouping": "We will automatically analyze the grouping to determine if there are multiple people present, and separate them out again. This may take a few seconds.",
"layout": "Layout",
"day": "Day",
- "peopleAutoAddDesc": "Select the people you want to automatically add to the album"
+ "peopleAutoAddDesc": "Select the people you want to automatically add to the album",
+ "undo": "Undo",
+ "redo": "Redo",
+ "filter": "Filter",
+ "adjust": "Adjust",
+ "draw": "Draw",
+ "sticker": "Sticker",
+ "brushColor": "Brush Color",
+ "font": "Font",
+ "background": "Background",
+ "align": "Align",
+ "addedToAlbums": "{count, plural, =1{Added successfully to 1 album} other{Added successfully to {count} albums}}",
+ "@addedToAlbums": {
+ "description": "Message shown when items are added to albums",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/mobile/apps/photos/lib/models/gallery/gallery_groups.dart b/mobile/apps/photos/lib/models/gallery/gallery_groups.dart
index a42ba360e0..3bb9e70c1d 100644
--- a/mobile/apps/photos/lib/models/gallery/gallery_groups.dart
+++ b/mobile/apps/photos/lib/models/gallery/gallery_groups.dart
@@ -224,18 +224,20 @@ class GalleryGroups {
int i = 0;
while (!endOfListReached) {
gridRowChildren.add(
- GalleryFileWidget(
+ RepaintBoundary(
key: ValueKey(
tagPrefix +
filesInGroup[firstIndexOfRowWrtFilesInGroup + i]
.tag,
),
- file: filesInGroup[firstIndexOfRowWrtFilesInGroup + i],
- selectedFiles: selectedFiles,
- limitSelectionToOne: limitSelectionToOne,
- tag: tagPrefix,
- photoGridSize: crossAxisCount,
- currentUserID: currentUserID,
+ child: GalleryFileWidget(
+ file: filesInGroup[firstIndexOfRowWrtFilesInGroup + i],
+ selectedFiles: selectedFiles,
+ limitSelectionToOne: limitSelectionToOne,
+ tag: tagPrefix,
+ photoGridSize: crossAxisCount,
+ currentUserID: currentUserID,
+ ),
),
);
@@ -247,18 +249,20 @@ class GalleryGroups {
} else {
for (int i = 0; i < crossAxisCount; i++) {
gridRowChildren.add(
- GalleryFileWidget(
+ RepaintBoundary(
key: ValueKey(
tagPrefix +
filesInGroup[firstIndexOfRowWrtFilesInGroup + i]
.tag,
),
- file: filesInGroup[firstIndexOfRowWrtFilesInGroup + i],
- selectedFiles: selectedFiles,
- limitSelectionToOne: limitSelectionToOne,
- tag: tagPrefix,
- photoGridSize: crossAxisCount,
- currentUserID: currentUserID,
+ child: GalleryFileWidget(
+ file: filesInGroup[firstIndexOfRowWrtFilesInGroup + i],
+ selectedFiles: selectedFiles,
+ limitSelectionToOne: limitSelectionToOne,
+ tag: tagPrefix,
+ photoGridSize: crossAxisCount,
+ currentUserID: currentUserID,
+ ),
),
);
}
diff --git a/mobile/apps/photos/lib/ui/collections/collection_action_sheet.dart b/mobile/apps/photos/lib/ui/collections/collection_action_sheet.dart
index d049be0102..64fe799e46 100644
--- a/mobile/apps/photos/lib/ui/collections/collection_action_sheet.dart
+++ b/mobile/apps/photos/lib/ui/collections/collection_action_sheet.dart
@@ -310,9 +310,7 @@ class _CollectionActionSheetState extends State {
if (result) {
showShortToast(
context,
- "Added successfully to " +
- _selectedCollections.length.toString() +
- " albums",
+ S.of(context).addedToAlbums(_selectedCollections.length),
);
widget.selectedFiles?.clearAll();
}
diff --git a/mobile/apps/photos/lib/ui/tools/editor/filtered_image.dart b/mobile/apps/photos/lib/ui/tools/editor/filtered_image.dart
deleted file mode 100644
index 31c74590ac..0000000000
--- a/mobile/apps/photos/lib/ui/tools/editor/filtered_image.dart
+++ /dev/null
@@ -1,110 +0,0 @@
-import 'dart:math';
-
-import 'package:flutter/widgets.dart';
-import 'package:image_editor/image_editor.dart';
-
-class FilteredImage extends StatelessWidget {
- const FilteredImage({
- required this.child,
- this.brightness,
- this.saturation,
- this.hue,
- super.key,
- });
-
- final double? brightness, saturation, hue;
- final Widget child;
-
- @override
- Widget build(BuildContext context) {
- return ColorFiltered(
- colorFilter: ColorFilter.matrix(
- ColorFilterGenerator.brightnessAdjustMatrix(
- value: brightness ?? 1,
- ),
- ),
- child: ColorFiltered(
- colorFilter: ColorFilter.matrix(
- ColorFilterGenerator.saturationAdjustMatrix(
- value: saturation ?? 1,
- ),
- ),
- child: ColorFiltered(
- colorFilter: ColorFilter.matrix(
- ColorFilterGenerator.hueAdjustMatrix(
- value: hue ?? 0,
- ),
- ),
- child: child,
- ),
- ),
- );
- }
-}
-
-class ColorFilterGenerator {
- static List hueAdjustMatrix({double value = 1}) {
- value = value * pi;
-
- if (value == 0) {
- return [
- 1,
- 0,
- 0,
- 0,
- 0,
- 0,
- 1,
- 0,
- 0,
- 0,
- 0,
- 0,
- 1,
- 0,
- 0,
- 0,
- 0,
- 0,
- 1,
- 0,
- ];
- }
- final double cosVal = cos(value);
- final double sinVal = sin(value);
- const double lumR = 0.213;
- const double lumG = 0.715;
- const double lumB = 0.072;
-
- return List.from([
- (lumR + (cosVal * (1 - lumR))) + (sinVal * (-lumR)),
- (lumG + (cosVal * (-lumG))) + (sinVal * (-lumG)),
- (lumB + (cosVal * (-lumB))) + (sinVal * (1 - lumB)),
- 0,
- 0,
- (lumR + (cosVal * (-lumR))) + (sinVal * 0.143),
- (lumG + (cosVal * (1 - lumG))) + (sinVal * 0.14),
- (lumB + (cosVal * (-lumB))) + (sinVal * (-0.283)),
- 0,
- 0,
- (lumR + (cosVal * (-lumR))) + (sinVal * (-(1 - lumR))),
- (lumG + (cosVal * (-lumG))) + (sinVal * lumG),
- (lumB + (cosVal * (1 - lumB))) + (sinVal * lumB),
- 0,
- 0,
- 0,
- 0,
- 0,
- 1,
- 0,
- ]).map((i) => i.toDouble()).toList();
- }
-
- static List brightnessAdjustMatrix({double value = 1}) {
- return ColorOption.brightness(value).matrix;
- }
-
- static List saturationAdjustMatrix({double value = 1}) {
- return ColorOption.saturation(value).matrix;
- }
-}
diff --git a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_app_bar.dart b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_app_bar.dart
index 03e84cf75d..730feb5d3a 100644
--- a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_app_bar.dart
+++ b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_app_bar.dart
@@ -1,7 +1,8 @@
import 'package:flutter/material.dart';
import "package:flutter_svg/svg.dart";
import "package:photos/ente_theme_data.dart";
-import "package:photos/theme/ente_theme.dart";
+import "package:photos/generated/l10n.dart";
+ import "package:photos/theme/ente_theme.dart";
import "package:pro_image_editor/models/editor_configs/pro_image_editor_configs.dart";
class ImageEditorAppBar extends StatelessWidget implements PreferredSizeWidget {
@@ -43,7 +44,7 @@ class ImageEditorAppBar extends StatelessWidget implements PreferredSizeWidget {
enableUndo ? close() : Navigator.of(context).pop();
},
child: Text(
- 'Cancel',
+ S.of(context).cancel,
style: getEnteTextTheme(context).body,
),
),
@@ -52,7 +53,7 @@ class ImageEditorAppBar extends StatelessWidget implements PreferredSizeWidget {
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
- tooltip: 'Undo',
+ tooltip: S.of(context).undo,
onPressed: () {
undo != null ? undo!() : null;
},
@@ -66,7 +67,7 @@ class ImageEditorAppBar extends StatelessWidget implements PreferredSizeWidget {
),
const SizedBox(width: 12),
IconButton(
- tooltip: 'Redo',
+ tooltip: S.of(context).redo,
onPressed: () {
redo != null ? redo!() : null;
},
@@ -88,7 +89,7 @@ class ImageEditorAppBar extends StatelessWidget implements PreferredSizeWidget {
key: ValueKey(isMainEditor ? 'save_copy' : 'done'),
onPressed: done,
child: Text(
- isMainEditor ? 'Save Copy' : 'Done',
+ isMainEditor ? S.of(context).saveCopy : S.of(context).done,
style: getEnteTextTheme(context).body.copyWith(
color: isMainEditor
? (enableUndo
diff --git a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_crop_rotate.dart b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_crop_rotate.dart
index a222ed92e4..10333cd3ce 100644
--- a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_crop_rotate.dart
+++ b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_crop_rotate.dart
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import "package:flutter_svg/svg.dart";
+import "package:photos/generated/l10n.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/tools/editor/image_editor/circular_icon_button.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_configs_mixin.dart";
@@ -113,7 +114,7 @@ class _ImageEditorCropRotateBarState extends State
children: [
CircularIconButton(
svgPath: "assets/image-editor/image-editor-crop-rotate.svg",
- label: "Rotate",
+ label: S.of(context).rotate,
onTap: () {
widget.editor.rotate();
},
@@ -121,7 +122,7 @@ class _ImageEditorCropRotateBarState extends State
const SizedBox(width: 6),
CircularIconButton(
svgPath: "assets/image-editor/image-editor-flip.svg",
- label: "Flip",
+ label: S.of(context).flip,
onTap: () {
widget.editor.flip();
},
diff --git a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_main_bottom_bar.dart b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_main_bottom_bar.dart
index 3321e645c1..32dc0a5ed8 100644
--- a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_main_bottom_bar.dart
+++ b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_main_bottom_bar.dart
@@ -1,6 +1,7 @@
import 'dart:math';
import 'package:flutter/material.dart';
+import "package:photos/generated/l10n.dart";
import "package:photos/ui/tools/editor/image_editor/circular_icon_button.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_configs_mixin.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_constants.dart";
@@ -90,7 +91,7 @@ class ImageEditorMainBottomBarState extends State
children: [
CircularIconButton(
svgPath: "assets/image-editor/image-editor-crop.svg",
- label: "Crop",
+ label: S.of(context).crop,
onTap: () {
widget.editor.openCropRotateEditor();
},
@@ -98,21 +99,21 @@ class ImageEditorMainBottomBarState extends State
CircularIconButton(
svgPath:
"assets/image-editor/image-editor-filter.svg",
- label: "Filter",
+ label: S.of(context).filter,
onTap: () {
widget.editor.openFilterEditor();
},
),
CircularIconButton(
svgPath: "assets/image-editor/image-editor-tune.svg",
- label: "Adjust",
+ label: S.of(context).adjust,
onTap: () {
widget.editor.openTuneEditor();
},
),
CircularIconButton(
svgPath: "assets/image-editor/image-editor-paint.svg",
- label: "Draw",
+ label: S.of(context).draw,
onTap: () {
widget.editor.openPaintingEditor();
},
@@ -120,7 +121,7 @@ class ImageEditorMainBottomBarState extends State
CircularIconButton(
svgPath:
"assets/image-editor/image-editor-sticker.svg",
- label: "Sticker",
+ label: S.of(context).sticker,
onTap: () {
widget.editor.openEmojiEditor();
},
diff --git a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_page_new.dart b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_page.dart
similarity index 99%
rename from mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_page_new.dart
rename to mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_page.dart
index f99799378e..e6bc90dbab 100644
--- a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_page_new.dart
+++ b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_page.dart
@@ -37,12 +37,12 @@ import "package:photos/utils/navigation_util.dart";
import "package:pro_image_editor/models/editor_configs/main_editor_configs.dart";
import 'package:pro_image_editor/pro_image_editor.dart';
-class NewImageEditor extends StatefulWidget {
+class ImageEditorPage extends StatefulWidget {
final ente.EnteFile originalFile;
final File file;
final DetailPageConfiguration detailPageConfig;
- const NewImageEditor({
+ const ImageEditorPage({
super.key,
required this.file,
required this.originalFile,
@@ -50,10 +50,10 @@ class NewImageEditor extends StatefulWidget {
});
@override
- State createState() => _NewImageEditorState();
+ State createState() => _ImageEditorPageState();
}
-class _NewImageEditorState extends State {
+class _ImageEditorPageState extends State {
final _mainEditorBarKey = GlobalKey();
final editorKey = GlobalKey();
final _logger = Logger("ImageEditor");
diff --git a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_paint_bar.dart b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_paint_bar.dart
index 0e4d1dd44f..5c7fe2c97d 100644
--- a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_paint_bar.dart
+++ b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_paint_bar.dart
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
+import "package:photos/generated/l10n.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_color_picker.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_configs_mixin.dart";
@@ -63,7 +64,7 @@ class _ImageEditorPaintBarState extends State
Padding(
padding: const EdgeInsets.only(left: 20.0),
child: Text(
- "Brush Color",
+ S.of(context).brushColor,
style: getEnteTextTheme(context).body,
),
),
diff --git a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_text_bar.dart b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_text_bar.dart
index 3115aa3933..d4327fb136 100644
--- a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_text_bar.dart
+++ b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_text_bar.dart
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import "package:flutter_svg/svg.dart";
+import "package:photos/generated/l10n.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/tools/editor/image_editor/circular_icon_button.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_color_picker.dart";
@@ -75,7 +76,7 @@ class _ImageEditorTextBarState extends State
children: [
CircularIconButton(
svgPath: "assets/image-editor/image-editor-text-color.svg",
- label: "Color",
+ label: S.of(context).color,
isSelected: selectedActionIndex == 0,
onTap: () {
_selectAction(0);
@@ -83,7 +84,7 @@ class _ImageEditorTextBarState extends State
),
CircularIconButton(
svgPath: "assets/image-editor/image-editor-text-font.svg",
- label: "Font",
+ label: S.of(context).font,
isSelected: selectedActionIndex == 1,
onTap: () {
_selectAction(1);
@@ -91,7 +92,7 @@ class _ImageEditorTextBarState extends State
),
CircularIconButton(
svgPath: "assets/image-editor/image-editor-text-background.svg",
- label: "Background",
+ label: S.of(context).background,
isSelected: selectedActionIndex == 2,
onTap: () {
setState(() {
@@ -101,7 +102,7 @@ class _ImageEditorTextBarState extends State
),
CircularIconButton(
svgPath: "assets/image-editor/image-editor-text-align-left.svg",
- label: "Align",
+ label: S.of(context).align,
isSelected: selectedActionIndex == 3,
onTap: () {
setState(() {
diff --git a/mobile/apps/photos/lib/ui/tools/editor/image_editor_page.dart b/mobile/apps/photos/lib/ui/tools/editor/image_editor_page.dart
deleted file mode 100644
index 99874d7c3a..0000000000
--- a/mobile/apps/photos/lib/ui/tools/editor/image_editor_page.dart
+++ /dev/null
@@ -1,553 +0,0 @@
-import "dart:async";
-import 'dart:io';
-import 'dart:math';
-import 'dart:typed_data';
-import 'dart:ui' as ui show Image;
-
-import 'package:extended_image/extended_image.dart';
-import 'package:flutter/material.dart';
-import "package:flutter_image_compress/flutter_image_compress.dart";
-import 'package:image_editor/image_editor.dart';
-import 'package:logging/logging.dart';
-import 'package:path/path.dart' as 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/models/file/file.dart' as ente;
-import 'package:photos/models/location/location.dart';
-import 'package:photos/services/sync/sync_service.dart';
-import 'package:photos/ui/common/loading_widget.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';
-import 'package:photos/ui/notification/toast.dart';
-import 'package:photos/ui/tools/editor/filtered_image.dart';
-import 'package:photos/ui/viewer/file/detail_page.dart';
-import 'package:photos/utils/dialog_util.dart';
-import 'package:photos/utils/navigation_util.dart';
-import 'package:syncfusion_flutter_core/theme.dart';
-import 'package:syncfusion_flutter_sliders/sliders.dart';
-
-class ImageEditorPage extends StatefulWidget {
- final ImageProvider imageProvider;
- final DetailPageConfiguration detailPageConfig;
- final ente.EnteFile originalFile;
-
- const ImageEditorPage(
- this.imageProvider,
- this.originalFile,
- this.detailPageConfig, {
- super.key,
- });
-
- @override
- State createState() => _ImageEditorPageState();
-}
-
-class _ImageEditorPageState extends State {
- static const double kBrightnessDefault = 1;
- static const double kBrightnessMin = 0;
- static const double kBrightnessMax = 2;
- static const double kSaturationDefault = 1;
- static const double kSaturationMin = 0;
- static const double kSaturationMax = 2;
-
- final _logger = Logger("ImageEditor");
- final GlobalKey editorKey =
- GlobalKey();
-
- double? _brightness = kBrightnessDefault;
- double? _saturation = kSaturationDefault;
- bool _hasEdited = false;
-
- @override
- Widget build(BuildContext context) {
- return PopScope(
- canPop: false,
- onPopInvokedWithResult: (didPop, _) async {
- if (_hasBeenEdited()) {
- await _showExitConfirmationDialog(context);
- } else {
- replacePage(context, DetailPage(widget.detailPageConfig));
- }
- },
- child: Scaffold(
- appBar: AppBar(
- backgroundColor: const Color(0x00000000),
- elevation: 0,
- actions: _hasBeenEdited()
- ? [
- IconButton(
- padding: const EdgeInsets.only(right: 16, left: 16),
- onPressed: () {
- editorKey.currentState!.reset();
- setState(() {
- _brightness = kBrightnessDefault;
- _saturation = kSaturationDefault;
- });
- },
- icon: const Icon(Icons.history),
- ),
- ]
- : [],
- ),
- body: Column(
- children: [
- Expanded(child: _buildImage()),
- const Padding(padding: EdgeInsets.all(4)),
- Column(
- children: [
- _buildBrightness(),
- _buildSat(),
- ],
- ),
- const Padding(padding: EdgeInsets.all(8)),
- SafeArea(child: _buildBottomBar()),
- Padding(padding: EdgeInsets.all(Platform.isIOS ? 16 : 6)),
- ],
- ),
- ),
- );
- }
-
- bool _hasBeenEdited() {
- return _hasEdited ||
- _saturation != kSaturationDefault ||
- _brightness != kBrightnessDefault;
- }
-
- Widget _buildImage() {
- return Hero(
- tag: widget.detailPageConfig.tagPrefix + widget.originalFile.tag,
- child: ExtendedImage(
- image: widget.imageProvider,
- extendedImageEditorKey: editorKey,
- mode: ExtendedImageMode.editor,
- fit: BoxFit.contain,
- initEditorConfigHandler: (_) => EditorConfig(
- maxScale: 8.0,
- cropRectPadding: const EdgeInsets.all(20.0),
- hitTestSize: 20.0,
- cornerColor: const Color.fromRGBO(45, 150, 98, 1),
- editActionDetailsIsChanged: (_) {
- setState(() {
- _hasEdited = true;
- });
- },
- ),
- loadStateChanged: (state) {
- if (state.extendedImageLoadState == LoadState.completed) {
- return FilteredImage(
- brightness: _brightness,
- saturation: _saturation,
- child: state.completedWidget,
- );
- }
- return const EnteLoadingWidget();
- },
- ),
- );
- }
-
- Widget _buildBottomBar() {
- return Row(
- mainAxisAlignment: MainAxisAlignment.spaceAround,
- children: [
- _buildFlipButton(),
- _buildRotateLeftButton(),
- _buildRotateRightButton(),
- _buildSaveButton(),
- ],
- );
- }
-
- Widget _buildFlipButton() {
- final TextStyle subtitle2 = Theme.of(context).textTheme.titleSmall!;
-
- return GestureDetector(
- behavior: HitTestBehavior.translucent,
- onTap: () {
- flip();
- },
- child: SizedBox(
- width: 80,
- child: Column(
- children: [
- Padding(
- padding: const EdgeInsets.only(bottom: 2),
- child: Icon(
- Icons.flip,
- color: Theme.of(context).iconTheme.color!.withOpacity(0.8),
- size: 20,
- ),
- ),
- const Padding(padding: EdgeInsets.all(2)),
- Text(
- S.of(context).flip,
- style: subtitle2.copyWith(
- color: subtitle2.color!.withOpacity(0.8),
- ),
- textAlign: TextAlign.center,
- ),
- ],
- ),
- ),
- );
- }
-
- Widget _buildRotateLeftButton() {
- final TextStyle subtitle2 = Theme.of(context).textTheme.titleSmall!;
-
- return GestureDetector(
- behavior: HitTestBehavior.translucent,
- onTap: () {
- rotate(false);
- },
- child: SizedBox(
- width: 80,
- child: Column(
- children: [
- Icon(
- Icons.rotate_left,
- color: Theme.of(context).iconTheme.color!.withOpacity(0.8),
- ),
- const Padding(padding: EdgeInsets.all(2)),
- Text(
- S.of(context).rotateLeft,
- style: subtitle2.copyWith(
- color: subtitle2.color!.withOpacity(0.8),
- ),
- textAlign: TextAlign.center,
- ),
- ],
- ),
- ),
- );
- }
-
- Widget _buildRotateRightButton() {
- final TextStyle subtitle2 = Theme.of(context).textTheme.titleSmall!;
-
- return GestureDetector(
- behavior: HitTestBehavior.translucent,
- onTap: () {
- rotate(true);
- },
- child: SizedBox(
- width: 80,
- child: Column(
- children: [
- Icon(
- Icons.rotate_right,
- color: Theme.of(context).iconTheme.color!.withOpacity(0.8),
- ),
- const Padding(padding: EdgeInsets.all(2)),
- Text(
- S.of(context).rotateRight,
- style: subtitle2.copyWith(
- color: subtitle2.color!.withOpacity(0.8),
- ),
- textAlign: TextAlign.center,
- ),
- ],
- ),
- ),
- );
- }
-
- Widget _buildSaveButton() {
- final TextStyle subtitle2 = Theme.of(context).textTheme.titleSmall!;
-
- return GestureDetector(
- behavior: HitTestBehavior.translucent,
- onTap: () {
- _saveEdits();
- },
- child: SizedBox(
- width: 80,
- child: Column(
- children: [
- Icon(
- Icons.save_alt_outlined,
- color: Theme.of(context).iconTheme.color!.withOpacity(0.8),
- ),
- const Padding(padding: EdgeInsets.all(2)),
- Text(
- S.of(context).saveCopy,
- style: subtitle2.copyWith(
- color: subtitle2.color!.withOpacity(0.8),
- ),
- textAlign: TextAlign.center,
- ),
- ],
- ),
- ),
- );
- }
-
- Future _saveEdits() async {
- final dialog = createProgressDialog(context, S.of(context).saving);
- await dialog.show();
- final ExtendedImageEditorState? state = editorKey.currentState;
- if (state == null) {
- return;
- }
- final Rect? rect = state.getCropRect();
- if (rect == null) {
- return;
- }
- final EditActionDetails action = state.editAction!;
- final double radian = action.rotateAngle;
-
- final bool flipHorizontal = action.flipY;
- final bool flipVertical = action.flipX;
- final Uint8List img = state.rawImageData;
-
- // ignore: unnecessary_null_comparison
- if (img == null) {
- _logger.severe("null rawImageData");
- showToast(context, S.of(context).somethingWentWrong);
- return;
- }
-
- final ImageEditorOption option = ImageEditorOption();
-
- option.addOption(ClipOption.fromRect(rect));
- option.addOption(
- FlipOption(horizontal: flipHorizontal, vertical: flipVertical),
- );
- if (action.hasRotateAngle) {
- option.addOption(RotateOption(radian.toInt()));
- }
-
- option.addOption(ColorOption.saturation(_saturation!));
- option.addOption(ColorOption.brightness(_brightness!));
-
- option.outputFormat = const OutputFormat.jpeg(100);
-
- final DateTime start = DateTime.now();
- Uint8List? result = await ImageEditor.editImage(
- image: img,
- imageEditorOption: option,
- );
- if (result == null) {
- _logger.severe("null result");
- showToast(context, S.of(context).somethingWentWrong);
- return;
- }
- _logger.info('Size before compression = ${result.length}');
-
- final ui.Image decodedResult = await decodeImageFromList(result);
- result = await FlutterImageCompress.compressWithList(
- result,
- minWidth: decodedResult.width,
- minHeight: decodedResult.height,
- );
- _logger.info('Size after compression = ${result.length}');
- final Duration diff = DateTime.now().difference(start);
- _logger.info('image_editor time : $diff');
-
- try {
- final fileName =
- path.basenameWithoutExtension(widget.originalFile.title!) +
- "_edited_" +
- DateTime.now().microsecondsSinceEpoch.toString() +
- ".JPEG";
- //Disabling notifications for assets changing to insert the file into
- //files db before triggering a sync.
- await PhotoManager.stopChangeNotify();
- final AssetEntity newAsset =
- await (PhotoManager.editor.saveImage(result, filename: fileName));
- final newFile = await ente.EnteFile.fromAsset(
- widget.originalFile.deviceFolder ?? '',
- newAsset,
- );
-
- newFile.creationTime = widget.originalFile.creationTime;
- newFile.collectionID = widget.originalFile.collectionID;
- newFile.location = widget.originalFile.location;
- if (!newFile.hasLocation && widget.originalFile.localID != null) {
- final assetEntity = await widget.originalFile.getAsset;
- if (assetEntity != null) {
- final latLong = await assetEntity.latlngAsync();
- newFile.location = Location(
- latitude: latLong.latitude,
- longitude: latLong.longitude,
- );
- }
- }
- newFile.generatedID = await FilesDB.instance.insertAndGetId(newFile);
- Bus.instance.fire(LocalPhotosUpdatedEvent([newFile], source: "editSave"));
- unawaited(SyncService.instance.sync());
- showShortToast(context, S.of(context).editsSaved);
- _logger.info("Original file " + widget.originalFile.toString());
- _logger.info("Saved edits to file " + newFile.toString());
- final files = widget.detailPageConfig.files;
-
- // the index could be -1 if the files fetched doesn't contain the newly
- // edited files
- int selectionIndex =
- files.indexWhere((file) => file.generatedID == newFile.generatedID);
- if (selectionIndex == -1) {
- files.add(newFile);
- selectionIndex = files.length - 1;
- }
- await dialog.hide();
- replacePage(
- context,
- DetailPage(
- widget.detailPageConfig.copyWith(
- files: files,
- selectedIndex: min(selectionIndex, files.length - 1),
- ),
- ),
- );
- } catch (e, s) {
- await dialog.hide();
- showToast(context, S.of(context).oopsCouldNotSaveEdits);
- _logger.severe(e, s);
- } finally {
- await PhotoManager.startChangeNotify();
- }
- }
-
- void flip() {
- editorKey.currentState?.flip();
- }
-
- void rotate(bool right) {
- editorKey.currentState?.rotate(right: right);
- }
-
- Widget _buildSat() {
- final TextStyle subtitle2 = Theme.of(context).textTheme.titleSmall!;
-
- return Container(
- padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
- child: Row(
- children: [
- SizedBox(
- width: 42,
- child: FittedBox(
- fit: BoxFit.scaleDown,
- alignment: Alignment.centerLeft,
- child: Text(
- S.of(context).color,
- style: subtitle2.copyWith(
- color: subtitle2.color!.withOpacity(0.8),
- ),
- ),
- ),
- ),
- Expanded(
- child: SfSliderTheme(
- data: SfSliderThemeData(
- activeTrackHeight: 4,
- inactiveTrackHeight: 2,
- inactiveTrackColor: Colors.grey[900],
- activeTrackColor: const Color.fromRGBO(45, 150, 98, 1),
- thumbColor: const Color.fromRGBO(45, 150, 98, 1),
- thumbRadius: 10,
- tooltipBackgroundColor: Colors.grey[900],
- ),
- child: SfSlider(
- onChanged: (value) {
- setState(() {
- _saturation = value;
- });
- },
- value: _saturation,
- enableTooltip: true,
- stepSize: 0.01,
- min: kSaturationMin,
- max: kSaturationMax,
- ),
- ),
- ),
- ],
- ),
- );
- }
-
- Widget _buildBrightness() {
- final TextStyle subtitle2 = Theme.of(context).textTheme.titleSmall!;
-
- return Container(
- padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
- child: Row(
- children: [
- SizedBox(
- width: 42,
- child: FittedBox(
- fit: BoxFit.scaleDown,
- alignment: Alignment.centerLeft,
- child: Text(
- S.of(context).light,
- style: subtitle2.copyWith(
- color: subtitle2.color!.withOpacity(0.8),
- ),
- ),
- ),
- ),
- Expanded(
- child: SfSliderTheme(
- data: SfSliderThemeData(
- activeTrackHeight: 4,
- inactiveTrackHeight: 2,
- activeTrackColor: const Color.fromRGBO(45, 150, 98, 1),
- inactiveTrackColor: Colors.grey[900],
- thumbColor: const Color.fromRGBO(45, 150, 98, 1),
- thumbRadius: 10,
- tooltipBackgroundColor: Colors.grey[900],
- ),
- child: SfSlider(
- onChanged: (value) {
- setState(() {
- _brightness = value;
- });
- },
- value: _brightness,
- enableTooltip: true,
- stepSize: 0.01,
- min: kBrightnessMin,
- max: kBrightnessMax,
- ),
- ),
- ),
- ],
- ),
- );
- }
-
- Future _showExitConfirmationDialog(BuildContext context) async {
- final actionResult = await showActionSheet(
- context: context,
- buttons: [
- ButtonWidget(
- labelText: S.of(context).yesDiscardChanges,
- buttonType: ButtonType.critical,
- buttonSize: ButtonSize.large,
- shouldStickToDarkTheme: true,
- buttonAction: ButtonAction.first,
- isInAlert: true,
- ),
- ButtonWidget(
- labelText: S.of(context).no,
- buttonType: ButtonType.secondary,
- buttonSize: ButtonSize.large,
- buttonAction: ButtonAction.second,
- shouldStickToDarkTheme: true,
- isInAlert: true,
- ),
- ],
- body: S.of(context).doYouWantToDiscardTheEditsYouHaveMade,
- actionSheetType: ActionSheetType.defaultActionSheet,
- );
- if (actionResult?.action != null &&
- actionResult!.action == ButtonAction.first) {
- replacePage(context, DetailPage(widget.detailPageConfig));
- }
- }
-}
diff --git a/mobile/apps/photos/lib/ui/viewer/file/detail_page.dart b/mobile/apps/photos/lib/ui/viewer/file/detail_page.dart
index 5cb0d8fcbe..18b50f6357 100644
--- a/mobile/apps/photos/lib/ui/viewer/file/detail_page.dart
+++ b/mobile/apps/photos/lib/ui/viewer/file/detail_page.dart
@@ -19,8 +19,7 @@ import "package:photos/services/local_authentication_service.dart";
import "package:photos/states/detail_page_state.dart";
import "package:photos/ui/common/fast_scroll_physics.dart";
import 'package:photos/ui/notification/toast.dart';
-import "package:photos/ui/tools/editor/image_editor/image_editor_page_new.dart";
-import 'package:photos/ui/tools/editor/image_editor_page.dart';
+import "package:photos/ui/tools/editor/image_editor/image_editor_page.dart";
import "package:photos/ui/tools/editor/video_editor_page.dart";
import "package:photos/ui/viewer/file/file_app_bar.dart";
import "package:photos/ui/viewer/file/file_bottom_bar.dart";
@@ -64,16 +63,30 @@ class DetailPageConfiguration {
}
}
-class DetailPage extends StatefulWidget {
+class DetailPage extends StatelessWidget {
final DetailPageConfiguration config;
const DetailPage(this.config, {super.key});
@override
- State createState() => _DetailPageState();
+ Widget build(BuildContext context) {
+ // Separating body to a different widget to avoid
+ // unnecessary reinitialization of the InheritedDetailPageState
+ // when the body is rebuilt, which can reset state stored in it.
+ return InheritedDetailPageState(child: _Body(config));
+ }
}
-class _DetailPageState extends State {
+class _Body extends StatefulWidget {
+ final DetailPageConfiguration config;
+
+ const _Body(this.config);
+
+ @override
+ State<_Body> createState() => _BodyState();
+}
+
+class _BodyState extends State<_Body> {
final _logger = Logger("DetailPageState");
bool _shouldDisableScroll = false;
List? _files;
@@ -137,102 +150,100 @@ class _DetailPageState extends State {
_files!.length.toString() +
" files .",
);
- return InheritedDetailPageState(
- child: PopScope(
- canPop: !isGuestView,
- onPopInvokedWithResult: (didPop, _) async {
- if (isGuestView) {
- final authenticated = await _requestAuthentication();
- if (authenticated) {
- Bus.instance.fire(GuestViewEvent(false, false));
- await localSettings.setOnGuestView(false);
- }
+ return PopScope(
+ canPop: !isGuestView,
+ onPopInvokedWithResult: (didPop, _) async {
+ if (isGuestView) {
+ final authenticated = await _requestAuthentication();
+ if (authenticated) {
+ Bus.instance.fire(GuestViewEvent(false, false));
+ await localSettings.setOnGuestView(false);
}
- },
- child: Scaffold(
- appBar: PreferredSize(
- preferredSize: const Size.fromHeight(80),
- child: ValueListenableBuilder(
- builder: (BuildContext context, int selectedIndex, _) {
- return FileAppBar(
- _files![selectedIndex],
- _onFileRemoved,
- widget.config.mode == DetailPageMode.full,
- enableFullScreenNotifier: InheritedDetailPageState.of(context)
- .enableFullScreenNotifier,
- );
- },
- valueListenable: _selectedIndexNotifier,
- ),
+ }
+ },
+ child: Scaffold(
+ appBar: PreferredSize(
+ preferredSize: const Size.fromHeight(80),
+ child: ValueListenableBuilder(
+ builder: (BuildContext context, int selectedIndex, _) {
+ return FileAppBar(
+ _files![selectedIndex],
+ _onFileRemoved,
+ widget.config.mode == DetailPageMode.full,
+ enableFullScreenNotifier: InheritedDetailPageState.of(context)
+ .enableFullScreenNotifier,
+ );
+ },
+ valueListenable: _selectedIndexNotifier,
),
- extendBodyBehindAppBar: true,
- resizeToAvoidBottomInset: false,
- backgroundColor: Colors.black,
- body: Center(
- child: Stack(
- children: [
- _buildPageView(),
- ValueListenableBuilder(
- builder: (BuildContext context, int selectedIndex, _) {
- return FileBottomBar(
- _files![selectedIndex],
- _onNewImageEditor,
- widget.config.mode == DetailPageMode.minimalistic &&
- !isGuestView,
- onFileRemoved: _onFileRemoved,
- userID: Configuration.instance.getUserID(),
- enableFullScreenNotifier:
- InheritedDetailPageState.of(context)
- .enableFullScreenNotifier,
- );
- },
- valueListenable: _selectedIndexNotifier,
- ),
- ValueListenableBuilder(
- valueListenable: _selectedIndexNotifier,
- builder: (BuildContext context, int selectedIndex, _) {
- if (_files![selectedIndex].isPanorama() == true) {
- return ValueListenableBuilder(
- valueListenable: InheritedDetailPageState.of(context)
+ ),
+ extendBodyBehindAppBar: true,
+ resizeToAvoidBottomInset: false,
+ backgroundColor: Colors.black,
+ body: Center(
+ child: Stack(
+ children: [
+ _buildPageView(),
+ ValueListenableBuilder(
+ builder: (BuildContext context, int selectedIndex, _) {
+ return FileBottomBar(
+ _files![selectedIndex],
+ _onEditFileRequested,
+ widget.config.mode == DetailPageMode.minimalistic &&
+ !isGuestView,
+ onFileRemoved: _onFileRemoved,
+ userID: Configuration.instance.getUserID(),
+ enableFullScreenNotifier:
+ InheritedDetailPageState.of(context)
.enableFullScreenNotifier,
- builder: (context, value, child) {
- return IgnorePointer(
- ignoring: value,
- child: AnimatedOpacity(
- duration: const Duration(milliseconds: 200),
- opacity: !value ? 1.0 : 0.0,
- child: Align(
- alignment: Alignment.center,
- child: Tooltip(
- message: S.of(context).panorama,
- child: IconButton(
- style: IconButton.styleFrom(
- backgroundColor: const Color(0xAA252525),
- fixedSize: const Size(44, 44),
- ),
- icon: const Icon(
- Icons.threesixty,
- color: Colors.white,
- size: 26,
- ),
- onPressed: () async {
- await openPanoramaViewerPage(
- _files![selectedIndex],
- );
- },
+ );
+ },
+ valueListenable: _selectedIndexNotifier,
+ ),
+ ValueListenableBuilder(
+ valueListenable: _selectedIndexNotifier,
+ builder: (BuildContext context, int selectedIndex, _) {
+ if (_files![selectedIndex].isPanorama() == true) {
+ return ValueListenableBuilder(
+ valueListenable: InheritedDetailPageState.of(context)
+ .enableFullScreenNotifier,
+ builder: (context, value, child) {
+ return IgnorePointer(
+ ignoring: value,
+ child: AnimatedOpacity(
+ duration: const Duration(milliseconds: 200),
+ opacity: !value ? 1.0 : 0.0,
+ child: Align(
+ alignment: Alignment.center,
+ child: Tooltip(
+ message: S.of(context).panorama,
+ child: IconButton(
+ style: IconButton.styleFrom(
+ backgroundColor: const Color(0xAA252525),
+ fixedSize: const Size(44, 44),
),
+ icon: const Icon(
+ Icons.threesixty,
+ color: Colors.white,
+ size: 26,
+ ),
+ onPressed: () async {
+ await openPanoramaViewerPage(
+ _files![selectedIndex],
+ );
+ },
),
),
),
- );
- },
- );
- }
- return const SizedBox();
- },
- ),
- ],
- ),
+ ),
+ );
+ },
+ );
+ }
+ return const SizedBox();
+ },
+ ),
+ ],
),
),
),
@@ -358,68 +369,6 @@ class _DetailPageState extends State {
}
}
- Future _onNewImageEditor(EnteFile file) async {
- if (file.uploadedFileID != null &&
- file.ownerID != Configuration.instance.getUserID()) {
- _logger.severe(
- "Attempt to edit unowned file",
- UnauthorizedEditError(),
- StackTrace.current,
- );
- // ignore: unawaited_futures
- showErrorDialog(
- context,
- S.of(context).sorry,
- S.of(context).weDontSupportEditingPhotosAndAlbumsThatYouDont,
- );
- return;
- }
- final dialog = createProgressDialog(context, S.of(context).pleaseWait);
- await dialog.show();
-
- try {
- final ioFile = await getFile(file);
- if (ioFile == null) {
- showShortToast(context, S.of(context).failedToFetchOriginalForEdit);
- await dialog.hide();
- return;
- }
- if (file.fileType == FileType.video) {
- await dialog.hide();
- replacePage(
- context,
- VideoEditorPage(
- file: file,
- ioFile: ioFile,
- detailPageConfig: widget.config.copyWith(
- files: _files,
- selectedIndex: _selectedIndexNotifier.value,
- ),
- ),
- );
- return;
- }
- final imageProvider =
- ExtendedFileImageProvider(ioFile, cacheRawData: true);
- await precacheImage(imageProvider, context);
- await dialog.hide();
- replacePage(
- context,
- NewImageEditor(
- originalFile: file,
- file: ioFile,
- detailPageConfig: widget.config.copyWith(
- files: _files,
- selectedIndex: _selectedIndexNotifier.value,
- ),
- ),
- );
- } catch (e) {
- await dialog.hide();
- _logger.warning("Failed to initiate edit", e);
- }
- }
-
Future _onEditFileRequested(EnteFile file) async {
if (file.uploadedFileID != null &&
file.ownerID != Configuration.instance.getUserID()) {
@@ -438,6 +387,7 @@ class _DetailPageState extends State {
}
final dialog = createProgressDialog(context, S.of(context).pleaseWait);
await dialog.show();
+
try {
final ioFile = await getFile(file);
if (ioFile == null) {
@@ -467,9 +417,9 @@ class _DetailPageState extends State {
replacePage(
context,
ImageEditorPage(
- imageProvider,
- file,
- widget.config.copyWith(
+ originalFile: file,
+ file: ioFile,
+ detailPageConfig: widget.config.copyWith(
files: _files,
selectedIndex: _selectedIndexNotifier.value,
),
diff --git a/mobile/apps/photos/lib/utils/file_uploader.dart b/mobile/apps/photos/lib/utils/file_uploader.dart
index 9bb28eeee4..890b167fea 100644
--- a/mobile/apps/photos/lib/utils/file_uploader.dart
+++ b/mobile/apps/photos/lib/utils/file_uploader.dart
@@ -519,7 +519,8 @@ class FileUploader {
DateTime.now().microsecondsSinceEpoch,
);
} catch (e) {
- _logger.warning("Lock was already taken for " + file.toString());
+ final lockInfo = await _uploadLocks.getLockData(lockKey);
+ _logger.warning("Lock was already taken ($lockInfo) for " + file.tag);
throw LockAlreadyAcquiredError();
}
diff --git a/mobile/apps/photos/pubspec.lock b/mobile/apps/photos/pubspec.lock
index 13f6212d27..84aef16996 100644
--- a/mobile/apps/photos/pubspec.lock
+++ b/mobile/apps/photos/pubspec.lock
@@ -5,10 +5,10 @@ packages:
dependency: transitive
description:
name: _fe_analyzer_shared
- sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834
+ sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab"
url: "https://pub.dev"
source: hosted
- version: "72.0.0"
+ version: "76.0.0"
_flutterfire_internals:
dependency: transitive
description:
@@ -21,7 +21,7 @@ packages:
dependency: transitive
description: dart
source: sdk
- version: "0.3.2"
+ version: "0.3.3"
adaptive_theme:
dependency: "direct main"
description:
@@ -34,10 +34,10 @@ packages:
dependency: transitive
description:
name: analyzer
- sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139
+ sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e"
url: "https://pub.dev"
source: hosted
- version: "6.7.0"
+ version: "6.11.0"
android_intent_plus:
dependency: "direct main"
description:
@@ -317,10 +317,10 @@ packages:
dependency: "direct main"
description:
name: collection
- sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
+ sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
url: "https://pub.dev"
source: hosted
- version: "1.18.0"
+ version: "1.19.0"
computer:
dependency: "direct main"
description:
@@ -1271,38 +1271,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.3.0"
- image_editor:
- dependency: "direct main"
- description:
- name: image_editor
- sha256: "38070067264fd9fea4328ca630d2ff7bd65ebe6aa4ed375d983b732d2ae7146b"
- url: "https://pub.dev"
- source: hosted
- version: "1.6.0"
- image_editor_common:
- dependency: transitive
- description:
- name: image_editor_common
- sha256: "93d2f5c8b636f862775dd62a9ec20d09c8272598daa02f935955a4640e1844ee"
- url: "https://pub.dev"
- source: hosted
- version: "1.2.0"
- image_editor_ohos:
- dependency: transitive
- description:
- name: image_editor_ohos
- sha256: "06756859586d5acefec6e3b4f356f9b1ce05ef09213bcb9a0ce1680ecea2d054"
- url: "https://pub.dev"
- source: hosted
- version: "0.0.9"
- image_editor_platform_interface:
- dependency: transitive
- description:
- name: image_editor_platform_interface
- sha256: "474517efc770464f7d99942472d8cfb369a3c378e95466ec17f74d2b80bd40de"
- url: "https://pub.dev"
- source: hosted
- version: "1.1.0"
in_app_purchase:
dependency: "direct main"
description:
@@ -1424,18 +1392,18 @@ packages:
dependency: transitive
description:
name: leak_tracker
- sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
+ sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
url: "https://pub.dev"
source: hosted
- version: "10.0.5"
+ version: "10.0.7"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
- sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
+ sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
url: "https://pub.dev"
source: hosted
- version: "3.0.5"
+ version: "3.0.8"
leak_tracker_testing:
dependency: transitive
description:
@@ -1544,10 +1512,10 @@ packages:
dependency: transitive
description:
name: macros
- sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
+ sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
url: "https://pub.dev"
source: hosted
- version: "0.1.2-main.4"
+ version: "0.1.3-main.0"
maps_launcher:
dependency: "direct main"
description:
@@ -1586,24 +1554,24 @@ packages:
description:
path: media_kit
ref: HEAD
- resolved-ref: "3c4ff28c43d20e68f8d587956b2f525292c25a80"
+ resolved-ref: c9617f570b8c0ba02857e721997f78c053a856c1
url: "https://github.com/media-kit/media-kit"
source: git
- version: "1.1.11"
+ version: "1.2.0"
media_kit_libs_android_video:
dependency: transitive
description:
name: media_kit_libs_android_video
- sha256: "9dd8012572e4aff47516e55f2597998f0a378e3d588d0fad0ca1f11a53ae090c"
+ sha256: adff9b571b8ead0867f9f91070f8df39562078c0eb3371d88b9029a2d547d7b7
url: "https://pub.dev"
source: hosted
- version: "1.3.6"
+ version: "1.3.7"
media_kit_libs_ios_video:
dependency: "direct main"
description:
path: "libs/ios/media_kit_libs_ios_video"
ref: HEAD
- resolved-ref: "3c4ff28c43d20e68f8d587956b2f525292c25a80"
+ resolved-ref: c9617f570b8c0ba02857e721997f78c053a856c1
url: "https://github.com/media-kit/media-kit"
source: git
version: "1.1.4"
@@ -1611,10 +1579,10 @@ packages:
dependency: transitive
description:
name: media_kit_libs_linux
- sha256: e186891c31daa6bedab4d74dcdb4e8adfccc7d786bfed6ad81fe24a3b3010310
+ sha256: "2b473399a49ec94452c4d4ae51cfc0f6585074398d74216092bf3d54aac37ecf"
url: "https://pub.dev"
source: hosted
- version: "1.1.3"
+ version: "1.2.1"
media_kit_libs_macos_video:
dependency: transitive
description:
@@ -1628,27 +1596,27 @@ packages:
description:
path: "libs/universal/media_kit_libs_video"
ref: HEAD
- resolved-ref: "3c4ff28c43d20e68f8d587956b2f525292c25a80"
+ resolved-ref: c9617f570b8c0ba02857e721997f78c053a856c1
url: "https://github.com/media-kit/media-kit"
source: git
- version: "1.0.5"
+ version: "1.0.6"
media_kit_libs_windows_video:
dependency: transitive
description:
name: media_kit_libs_windows_video
- sha256: "32654572167825c42c55466f5d08eee23ea11061c84aa91b09d0e0f69bdd0887"
+ sha256: dff76da2778729ab650229e6b4ec6ec111eb5151431002cbd7ea304ff1f112ab
url: "https://pub.dev"
source: hosted
- version: "1.0.10"
+ version: "1.0.11"
media_kit_video:
dependency: "direct main"
description:
path: media_kit_video
ref: HEAD
- resolved-ref: "3c4ff28c43d20e68f8d587956b2f525292c25a80"
+ resolved-ref: c9617f570b8c0ba02857e721997f78c053a856c1
url: "https://github.com/media-kit/media-kit"
source: git
- version: "1.2.5"
+ version: "1.3.0"
meta:
dependency: transitive
description:
@@ -1712,7 +1680,7 @@ packages:
description:
path: "."
ref: HEAD
- resolved-ref: "7814e2c61ee1fa74cef73b946eb08519c35bdaa5"
+ resolved-ref: "64e47a446bf3b64f012f2076481cebea51ca27cf"
url: "https://github.com/ente-io/motionphoto.git"
source: git
version: "0.0.1"
@@ -2325,7 +2293,7 @@ packages:
dependency: transitive
description: flutter
source: sdk
- version: "0.0.99"
+ version: "0.0.0"
source_gen:
dependency: transitive
description:
@@ -2450,10 +2418,10 @@ packages:
dependency: transitive
description:
name: stack_trace
- sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
+ sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
url: "https://pub.dev"
source: hosted
- version: "1.11.1"
+ version: "1.12.0"
step_progress_indicator:
dependency: "direct main"
description:
@@ -2482,10 +2450,10 @@ packages:
dependency: transitive
description:
name: string_scanner
- sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
+ sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
url: "https://pub.dev"
source: hosted
- version: "1.2.0"
+ version: "1.3.0"
styled_text:
dependency: "direct main"
description:
@@ -2546,26 +2514,26 @@ packages:
dependency: "direct dev"
description:
name: test
- sha256: "7ee44229615f8f642b68120165ae4c2a75fe77ae2065b1e55ae4711f6cf0899e"
+ sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f"
url: "https://pub.dev"
source: hosted
- version: "1.25.7"
+ version: "1.25.8"
test_api:
dependency: transitive
description:
name: test_api
- sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
+ sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
url: "https://pub.dev"
source: hosted
- version: "0.7.2"
+ version: "0.7.3"
test_core:
dependency: transitive
description:
name: test_core
- sha256: "55ea5a652e38a1dfb32943a7973f3681a60f872f8c3a05a14664ad54ef9c6696"
+ sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d"
url: "https://pub.dev"
source: hosted
- version: "0.6.4"
+ version: "0.6.5"
thermal:
dependency: "direct main"
description:
@@ -2845,10 +2813,10 @@ packages:
dependency: transitive
description:
name: vm_service
- sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
+ sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
url: "https://pub.dev"
source: hosted
- version: "14.2.5"
+ version: "14.3.0"
volume_controller:
dependency: transitive
description:
@@ -2909,10 +2877,10 @@ packages:
dependency: transitive
description:
name: webdriver
- sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e"
+ sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8"
url: "https://pub.dev"
source: hosted
- version: "3.0.3"
+ version: "3.0.4"
webkit_inspection_protocol:
dependency: transitive
description:
diff --git a/mobile/apps/photos/pubspec.yaml b/mobile/apps/photos/pubspec.yaml
index 80dd4a978b..245f2f4e48 100644
--- a/mobile/apps/photos/pubspec.yaml
+++ b/mobile/apps/photos/pubspec.yaml
@@ -114,7 +114,6 @@ dependencies:
html_unescape: ^2.0.0
http: ^1.1.0
image: ^4.0.17
- image_editor: ^1.6.0
in_app_purchase: ^3.0.7
intl: ^0.19.0
latlong2: ^0.9.0
diff --git a/server/cmd/museum/main.go b/server/cmd/museum/main.go
index 5e5daef7ed..87d720530a 100644
--- a/server/cmd/museum/main.go
+++ b/server/cmd/museum/main.go
@@ -5,6 +5,9 @@ import (
"database/sql"
b64 "encoding/base64"
"fmt"
+ "github.com/ente-io/museum/pkg/controller/collections"
+ publicCtrl "github.com/ente-io/museum/pkg/controller/public"
+ "github.com/ente-io/museum/pkg/repo/public"
"net/http"
"os"
"os/signal"
@@ -14,8 +17,6 @@ import (
"syscall"
"time"
- "github.com/ente-io/museum/pkg/controller/collections"
-
"github.com/ente-io/museum/ente/base"
"github.com/ente-io/museum/pkg/controller/emergency"
"github.com/ente-io/museum/pkg/controller/file_copy"
@@ -97,6 +98,7 @@ func main() {
}
viper.SetDefault("apps.public-albums", "https://albums.ente.io")
+ viper.SetDefault("apps.public-locker", "https://locker.ente.io")
viper.SetDefault("apps.accounts", "https://accounts.ente.io")
viper.SetDefault("apps.cast", "https://cast.ente.io")
viper.SetDefault("apps.family", "https://family.ente.io")
@@ -174,11 +176,13 @@ func main() {
fileRepo := &repo.FileRepository{DB: db, S3Config: s3Config, QueueRepo: queueRepo,
ObjectRepo: objectRepo, ObjectCleanupRepo: objectCleanupRepo,
ObjectCopiesRepo: objectCopiesRepo, UsageRepo: usageRepo}
+ fileLinkRepo := public.NewFileLinkRepo(db)
fileDataRepo := &fileDataRepo.Repository{DB: db, ObjectCleanupRepo: objectCleanupRepo}
familyRepo := &repo.FamilyRepository{DB: db}
- trashRepo := &repo.TrashRepository{DB: db, ObjectRepo: objectRepo, FileRepo: fileRepo, QueueRepo: queueRepo}
- publicCollectionRepo := repo.NewPublicCollectionRepository(db, viper.GetString("apps.public-albums"))
- collectionRepo := &repo.CollectionRepository{DB: db, FileRepo: fileRepo, PublicCollectionRepo: publicCollectionRepo,
+ trashRepo := &repo.TrashRepository{DB: db, ObjectRepo: objectRepo, FileRepo: fileRepo, QueueRepo: queueRepo, FileLinkRepo: fileLinkRepo}
+ collectionLinkRepo := public.NewCollectionLinkRepository(db, viper.GetString("apps.public-albums"))
+
+ collectionRepo := &repo.CollectionRepository{DB: db, FileRepo: fileRepo, CollectionLinkRepo: collectionLinkRepo,
TrashRepo: trashRepo, SecretEncryptionKey: secretEncryptionKeyBytes, QueueRepo: queueRepo, LatencyLogger: latencyLogger}
pushRepo := &repo.PushTokenRepository{DB: db}
kexRepo := &kex.Repository{
@@ -300,26 +304,27 @@ func main() {
UsageRepo: usageRepo,
}
- publicCollectionCtrl := &controller.PublicCollectionController{
+ collectionLinkCtrl := &publicCtrl.CollectionLinkController{
FileController: fileController,
EmailNotificationCtrl: emailNotificationCtrl,
- PublicCollectionRepo: publicCollectionRepo,
+ CollectionLinkRepo: collectionLinkRepo,
+ FileLinkRepo: fileLinkRepo,
CollectionRepo: collectionRepo,
UserRepo: userRepo,
JwtSecret: jwtSecretBytes,
}
collectionController := &collections.CollectionController{
- CollectionRepo: collectionRepo,
- EmailCtrl: emailNotificationCtrl,
- AccessCtrl: accessCtrl,
- PublicCollectionCtrl: publicCollectionCtrl,
- UserRepo: userRepo,
- FileRepo: fileRepo,
- CastRepo: &castDb,
- BillingCtrl: billingController,
- QueueRepo: queueRepo,
- TaskRepo: taskLockingRepo,
+ CollectionRepo: collectionRepo,
+ EmailCtrl: emailNotificationCtrl,
+ AccessCtrl: accessCtrl,
+ CollectionLinkCtrl: collectionLinkCtrl,
+ UserRepo: userRepo,
+ FileRepo: fileRepo,
+ CastRepo: &castDb,
+ BillingCtrl: billingController,
+ QueueRepo: queueRepo,
+ TaskRepo: taskLockingRepo,
}
kexCtrl := &kexCtrl.Controller{
@@ -351,6 +356,12 @@ func main() {
userCache,
userCacheCtrl,
)
+ fileLinkCtrl := &publicCtrl.FileLinkController{
+ FileController: fileController,
+ FileLinkRepo: fileLinkRepo,
+ FileRepo: fileRepo,
+ JwtSecret: jwtSecretBytes,
+ }
passkeyCtrl := &controller.PasskeyController{
Repo: passkeysRepo,
@@ -358,14 +369,21 @@ func main() {
}
authMiddleware := middleware.AuthMiddleware{UserAuthRepo: userAuthRepo, Cache: authCache, UserController: userController}
- accessTokenMiddleware := middleware.AccessTokenMiddleware{
- PublicCollectionRepo: publicCollectionRepo,
- PublicCollectionCtrl: publicCollectionCtrl,
+ collectionLinkMiddleware := middleware.CollectionLinkMiddleware{
+ CollectionLinkRepo: collectionLinkRepo,
+ PublicCollectionCtrl: collectionLinkCtrl,
CollectionRepo: collectionRepo,
Cache: accessTokenCache,
BillingCtrl: billingController,
DiscordController: discordController,
}
+ fileLinkMiddleware := &middleware.FileLinkMiddleware{
+ FileLinkRepo: fileLinkRepo,
+ FileLinkCtrl: fileLinkCtrl,
+ Cache: accessTokenCache,
+ BillingCtrl: billingController,
+ DiscordController: discordController,
+ }
if environment != "local" {
gin.SetMode(gin.ReleaseMode)
@@ -404,7 +422,9 @@ func main() {
familiesJwtAuthAPI.Use(rateLimiter.GlobalRateLimiter(), authMiddleware.TokenAuthMiddleware(jwt.FAMILIES.Ptr()), rateLimiter.APIRateLimitForUserMiddleware(urlSanitizer))
publicCollectionAPI := server.Group("/public-collection")
- publicCollectionAPI.Use(rateLimiter.GlobalRateLimiter(), accessTokenMiddleware.AccessTokenAuthMiddleware(urlSanitizer))
+ publicCollectionAPI.Use(rateLimiter.GlobalRateLimiter(), collectionLinkMiddleware.Authenticate(urlSanitizer))
+ fileLinkApi := server.GET("/file-link")
+ fileLinkApi.Use(rateLimiter.GlobalRateLimiter(), fileLinkMiddleware.Authenticate(urlSanitizer))
healthCheckHandler := &api.HealthCheckHandler{
DB: db,
@@ -432,6 +452,7 @@ func main() {
Controller: fileController,
FileCopyCtrl: fileCopyCtrl,
FileDataCtrl: fileDataCtrl,
+ FileUrlCtrl: fileLinkCtrl,
}
privateAPI.GET("/files/upload-urls", fileHandler.GetUploadURLs)
privateAPI.GET("/files/multipart-upload-urls", fileHandler.GetMultipartUploadURLs)
@@ -440,6 +461,11 @@ func main() {
privateAPI.GET("/files/preview/:fileID", fileHandler.GetThumbnail)
privateAPI.GET("/files/preview/v2/:fileID", fileHandler.GetThumbnail)
+ privateAPI.POST("/files/share-url", fileHandler.ShareUrl)
+ privateAPI.PUT("/files/share-url", fileHandler.UpdateFileURL)
+ privateAPI.DELETE("/files/share-url/:fileID", fileHandler.DisableUrl)
+ privateAPI.GET("/files/share-urls/", fileHandler.GetUrls)
+
privateAPI.PUT("/files/data", fileHandler.PutFileData)
privateAPI.PUT("/files/video-data", fileHandler.PutVideoData)
privateAPI.POST("/files/data/status-diff", fileHandler.FileDataStatusDiff)
@@ -566,13 +592,19 @@ func main() {
privateAPI.PUT("/collections/sharee-magic-metadata", collectionHandler.ShareeMagicMetadataUpdate)
publicCollectionHandler := &api.PublicCollectionHandler{
- Controller: publicCollectionCtrl,
+ Controller: collectionLinkCtrl,
FileCtrl: fileController,
CollectionCtrl: collectionController,
FileDataCtrl: fileDataCtrl,
StorageBonusController: storageBonusCtrl,
}
+ fileLinkApi.GET("/info", fileHandler.LinkInfo)
+ fileLinkApi.GET("/pass-info", fileHandler.PasswordInfo)
+ fileLinkApi.GET("/thumbnail", fileHandler.LinkThumbnail)
+ fileLinkApi.GET("/file", fileHandler.LinkFile)
+ fileLinkApi.POST("/verify-password", fileHandler.VerifyPassword)
+
publicCollectionAPI.GET("/files/preview/:fileID", publicCollectionHandler.GetThumbnail)
publicCollectionAPI.GET("/files/download/:fileID", publicCollectionHandler.GetFile)
publicCollectionAPI.GET("/files/data/fetch", publicCollectionHandler.GetFileData)
@@ -770,7 +802,7 @@ func main() {
setKnownAPIs(server.Routes())
setupAndStartBackgroundJobs(objectCleanupController, replicationController3, fileDataCtrl)
setupAndStartCrons(
- userAuthRepo, publicCollectionRepo, twoFactorRepo, passkeysRepo, fileController, taskLockingRepo, emailNotificationCtrl,
+ userAuthRepo, collectionLinkRepo, fileLinkRepo, twoFactorRepo, passkeysRepo, fileController, taskLockingRepo, emailNotificationCtrl,
trashController, pushController, objectController, dataCleanupController, storageBonusCtrl, emergencyCtrl,
embeddingController, healthCheckHandler, kexCtrl, castDb)
@@ -899,7 +931,8 @@ func setupAndStartBackgroundJobs(
objectCleanupController.StartClearingOrphanObjects()
}
-func setupAndStartCrons(userAuthRepo *repo.UserAuthRepository, publicCollectionRepo *repo.PublicCollectionRepository,
+func setupAndStartCrons(userAuthRepo *repo.UserAuthRepository, collectionLinkRepo *public.CollectionLinkRepo,
+ fileLinkRepo *public.FileLinkRepository,
twoFactorRepo *repo.TwoFactorRepository, passkeysRepo *passkey.Repository, fileController *controller.FileController,
taskRepo *repo.TaskLockRepository, emailNotificationCtrl *email.EmailNotificationController,
trashController *controller.TrashController, pushController *controller.PushController,
@@ -925,7 +958,8 @@ func setupAndStartCrons(userAuthRepo *repo.UserAuthRepository, publicCollectionR
schedule(c, "@every 24h", func() {
_ = userAuthRepo.RemoveDeletedTokens(timeUtil.MicrosecondsBeforeDays(30))
_ = castDb.DeleteOldSessions(context.Background(), timeUtil.MicrosecondsBeforeDays(7))
- _ = publicCollectionRepo.CleanupAccessHistory(context.Background())
+ _ = collectionLinkRepo.CleanupAccessHistory(context.Background())
+ _ = fileLinkRepo.CleanupAccessHistory(context.Background())
})
schedule(c, "@every 1m", func() {
diff --git a/server/configurations/local.yaml b/server/configurations/local.yaml
index b6b1d567eb..a16f560e43 100644
--- a/server/configurations/local.yaml
+++ b/server/configurations/local.yaml
@@ -79,9 +79,14 @@ http:
apps:
# Default is https://albums.ente.io
#
- # If you're running a self hosted instance and wish to serve public links,
+ # If you're running a self hosted instance and wish to serve public links for photos,
# set this to the URL where your albums web app is running.
public-albums:
+ # Default is https://locker.ente.io
+ #
+ # If you're running a self-hosted instance and wish to serve public links for locker,
+ # set this to the URL where your albums web app is running.
+ public-locker:
# Default is https://cast.ente.io
cast:
# Default is https://accounts.ente.io
diff --git a/server/ente/errors.go b/server/ente/errors.go
index 4a5a02fb47..2370ab7fe8 100644
--- a/server/ente/errors.go
+++ b/server/ente/errors.go
@@ -97,8 +97,8 @@ var ErrUserDeleted = errors.New("user account has been deleted")
// ErrLockUnavailable is thrown when a lock could not be acquired
var ErrLockUnavailable = errors.New("could not acquire lock")
-// ErrActiveLinkAlreadyExists is thrown when the collection already has active public link
-var ErrActiveLinkAlreadyExists = errors.New("Collection already has active public link")
+// ErrActiveLinkAlreadyExists is thrown when an active link already exists for entity
+var ErrActiveLinkAlreadyExists = errors.New("link already exists for this entity")
// ErrNotImplemented indicates that the action that we tried to perform is not
// available at this museum instance. e.g. this could be something that is not
@@ -176,6 +176,11 @@ var ErrMaxPasskeysReached = ApiError{
Message: "Max passkeys limit reached",
HttpStatusCode: http.StatusConflict,
}
+var ErrPassProtectedResource = ApiError{
+ Code: "PASS_PROTECTED_RESOURCE",
+ Message: "This resource is password protected",
+ HttpStatusCode: http.StatusForbidden,
+}
var ErrCastPermissionDenied = ApiError{
Code: "CAST_PERMISSION_DENIED",
diff --git a/server/ente/file_link.go b/server/ente/file_link.go
new file mode 100644
index 0000000000..e817e35da0
--- /dev/null
+++ b/server/ente/file_link.go
@@ -0,0 +1,94 @@
+package ente
+
+import (
+ "fmt"
+ "github.com/ente-io/museum/pkg/utils/time"
+)
+
+// CreateFileUrl represents an encrypted file in the system
+type CreateFileUrl struct {
+ FileID int64 `json:"fileID" binding:"required"`
+ App App `json:"app" binding:"required"`
+}
+
+// UpdateFileUrl ..
+type UpdateFileUrl struct {
+ LinkID string `json:"linkID" binding:"required"`
+ FileID int64 `json:"fileID" binding:"required"`
+ ValidTill *int64 `json:"validTill"`
+ DeviceLimit *int `json:"deviceLimit"`
+ PassHash *string
+ Nonce *string
+ MemLimit *int64
+ OpsLimit *int64
+ EnableDownload *bool `json:"enableDownload"`
+ DisablePassword *bool `json:"disablePassword"`
+}
+
+func (ut *UpdateFileUrl) Validate() error {
+ if ut.DeviceLimit == nil && ut.ValidTill == nil && ut.DisablePassword == nil &&
+ ut.Nonce == nil && ut.PassHash == nil && ut.EnableDownload == nil {
+ return NewBadRequestWithMessage("all parameters are missing")
+ }
+
+ if ut.DeviceLimit != nil && (*ut.DeviceLimit < 0 || *ut.DeviceLimit > 50) {
+ return NewBadRequestWithMessage(fmt.Sprintf("device limit: %d out of range [0-50]", *ut.DeviceLimit))
+ }
+
+ if ut.ValidTill != nil && *ut.ValidTill != 0 && *ut.ValidTill < time.Microseconds() {
+ return NewBadRequestWithMessage("valid till should be greater than current timestamp")
+ }
+
+ var allPassParamsMissing = ut.Nonce == nil && ut.PassHash == nil && ut.MemLimit == nil && ut.OpsLimit == nil
+ var allPassParamsPresent = ut.Nonce != nil && ut.PassHash != nil && ut.MemLimit != nil && ut.OpsLimit != nil
+
+ if !(allPassParamsMissing || allPassParamsPresent) {
+ return NewBadRequestWithMessage("all password params should be either present or missing")
+ }
+
+ if allPassParamsPresent && ut.DisablePassword != nil && *ut.DisablePassword {
+ return NewBadRequestWithMessage("can not set and disable password in same request")
+ }
+ return nil
+}
+
+type FileLinkRow struct {
+ LinkID string
+ OwnerID int64
+ FileID int64
+ Token string
+ DeviceLimit int
+ ValidTill int64
+ IsDisabled bool
+ PassHash *string
+ Nonce *string
+ MemLimit *int64
+ OpsLimit *int64
+ EnableDownload bool
+ CreatedAt int64
+ UpdatedAt int64
+}
+
+type FileUrl struct {
+ LinkID string `json:"linkID" binding:"required"`
+ URL string `json:"url" binding:"required"`
+ OwnerID int64 `json:"ownerID" binding:"required"`
+ FileID int64 `json:"fileID" binding:"required"`
+ ValidTill int64 `json:"validTill"`
+ DeviceLimit int `json:"deviceLimit"`
+ PasswordEnabled bool `json:"passwordEnabled"`
+ // Nonce contains the nonce value for the password if the link is password protected.
+ Nonce *string `json:"nonce,omitempty"`
+ MemLimit *int64 `json:"memLimit,omitempty"`
+ OpsLimit *int64 `json:"opsLimit,omitempty"`
+ EnableDownload bool `json:"enableDownload"`
+ CreatedAt int64 `json:"createdAt"`
+}
+
+type FileLinkAccessContext struct {
+ LinkID string
+ IP string
+ UserAgent string
+ FileID int64
+ OwnerID int64
+}
diff --git a/server/ente/jwt/jwt.go b/server/ente/jwt/jwt.go
index 94cfa995f2..c4d210b66c 100644
--- a/server/ente/jwt/jwt.go
+++ b/server/ente/jwt/jwt.go
@@ -40,13 +40,13 @@ func (w WebCommonJWTClaim) Valid() error {
return nil
}
-// PublicAlbumPasswordClaim refer to token granted post public album password verification
-type PublicAlbumPasswordClaim struct {
+// LinkPasswordClaim refer to token granted post link password verification
+type LinkPasswordClaim struct {
PassHash string `json:"passKey"`
ExpiryTime int64 `json:"expiryTime"`
}
-func (c PublicAlbumPasswordClaim) Valid() error {
+func (c LinkPasswordClaim) Valid() error {
if c.ExpiryTime < time.Microseconds() {
return errors.New("token expired")
}
diff --git a/server/ente/public_collection.go b/server/ente/public_collection.go
index eb1bd8c385..5f3867e6d0 100644
--- a/server/ente/public_collection.go
+++ b/server/ente/public_collection.go
@@ -3,7 +3,7 @@ package ente
import (
"database/sql/driver"
"encoding/json"
-
+ "fmt"
"github.com/ente-io/museum/pkg/utils/time"
"github.com/ente-io/stacktrace"
)
@@ -32,6 +32,33 @@ type UpdatePublicAccessTokenRequest struct {
EnableJoin *bool `json:"enableJoin"`
}
+func (ut *UpdatePublicAccessTokenRequest) Validate() error {
+ if ut.DeviceLimit == nil && ut.ValidTill == nil && ut.DisablePassword == nil &&
+ ut.Nonce == nil && ut.PassHash == nil && ut.EnableDownload == nil && ut.EnableCollect == nil {
+ return NewBadRequestWithMessage("all parameters are missing")
+ }
+
+ if ut.DeviceLimit != nil && (*ut.DeviceLimit < 0 || *ut.DeviceLimit > 50) {
+ return NewBadRequestWithMessage(fmt.Sprintf("device limit: %d out of range [0-50]", *ut.DeviceLimit))
+ }
+
+ if ut.ValidTill != nil && *ut.ValidTill != 0 && *ut.ValidTill < time.Microseconds() {
+ return NewBadRequestWithMessage("valid till should be greater than current timestamp")
+ }
+
+ var allPassParamsMissing = ut.Nonce == nil && ut.PassHash == nil && ut.MemLimit == nil && ut.OpsLimit == nil
+ var allPassParamsPresent = ut.Nonce != nil && ut.PassHash != nil && ut.MemLimit != nil && ut.OpsLimit != nil
+
+ if !(allPassParamsMissing || allPassParamsPresent) {
+ return NewBadRequestWithMessage("all password params should be either present or missing")
+ }
+
+ if allPassParamsPresent && ut.DisablePassword != nil && *ut.DisablePassword {
+ return NewBadRequestWithMessage("can not set and disable password in same request")
+ }
+ return nil
+}
+
type VerifyPasswordRequest struct {
PassHash string `json:"passHash" binding:"required"`
}
@@ -40,8 +67,8 @@ type VerifyPasswordResponse struct {
JWTToken string `json:"jwtToken"`
}
-// PublicCollectionToken represents row entity for public_collection_token table
-type PublicCollectionToken struct {
+// CollectionLinkRow represents row entity for public_collection_token table
+type CollectionLinkRow struct {
ID int64
CollectionID int64
Token string
@@ -57,7 +84,7 @@ type PublicCollectionToken struct {
EnableJoin bool
}
-func (p PublicCollectionToken) CanJoin() error {
+func (p CollectionLinkRow) CanJoin() error {
if p.IsDisabled {
return NewBadRequestWithMessage("link disabled")
}
diff --git a/server/migrations/103_single_file_url.down.sql b/server/migrations/103_single_file_url.down.sql
new file mode 100644
index 0000000000..2efd3e0053
--- /dev/null
+++ b/server/migrations/103_single_file_url.down.sql
@@ -0,0 +1,3 @@
+
+DROP TABLE IF EXISTS public_file_tokens_access_history;
+DROP TABLE IF EXISTS public_file_tokens;
diff --git a/server/migrations/103_single_file_url.up.sql b/server/migrations/103_single_file_url.up.sql
new file mode 100644
index 0000000000..3145e46aad
--- /dev/null
+++ b/server/migrations/103_single_file_url.up.sql
@@ -0,0 +1,46 @@
+
+
+CREATE TABLE IF NOT EXISTS public_file_tokens
+(
+ id text primary key,
+ file_id bigint NOT NULL,
+ owner_id bigint NOT NULL,
+ app text NOT NULL,
+ access_token text not null,
+ valid_till bigint not null DEFAULT 0,
+ device_limit int not null DEFAULT 0,
+ is_disabled bool not null DEFAULT FALSE,
+ enable_download bool not null DEFAULT TRUE,
+ pw_hash TEXT,
+ pw_nonce TEXT,
+ mem_limit BIGINT,
+ ops_limit BIGINT,
+ created_at bigint NOT NULL DEFAULT now_utc_micro_seconds(),
+ updated_at bigint NOT NULL DEFAULT now_utc_micro_seconds()
+);
+
+
+CREATE OR REPLACE TRIGGER update_public_file_tokens_updated_at
+ BEFORE UPDATE
+ ON public_file_tokens
+ FOR EACH ROW
+EXECUTE PROCEDURE
+ trigger_updated_at_microseconds_column();
+
+
+CREATE TABLE IF NOT EXISTS public_file_tokens_access_history
+(
+ id text NOT NULL,
+ ip text not null,
+ user_agent text not null,
+ created_at bigint NOT NULL DEFAULT now_utc_micro_seconds(),
+ CONSTRAINT unique_access_id_ip_ua UNIQUE (id, ip, user_agent),
+ CONSTRAINT fk_public_file_history_token_id
+ FOREIGN KEY (id)
+ REFERENCES public_file_tokens (id)
+ ON DELETE CASCADE
+);
+
+CREATE UNIQUE INDEX IF NOT EXISTS public_file_token_unique_idx ON public_file_tokens (access_token) WHERE is_disabled = FALSE;
+CREATE INDEX IF NOT EXISTS public_file_tokens_owner_id_updated_at_idx ON public_file_tokens (owner_id, updated_at);
+CREATE UNIQUE INDEX IF NOT EXISTS public_active_file_link_unique_idx ON public_file_tokens (file_id, is_disabled) WHERE is_disabled = FALSE;
diff --git a/server/pkg/api/collection.go b/server/pkg/api/collection.go
index 9318f5c329..6c198652c7 100644
--- a/server/pkg/api/collection.go
+++ b/server/pkg/api/collection.go
@@ -10,7 +10,6 @@ import (
log "github.com/sirupsen/logrus"
"github.com/ente-io/museum/ente"
- "github.com/ente-io/museum/pkg/controller"
"github.com/ente-io/museum/pkg/utils/auth"
"github.com/ente-io/museum/pkg/utils/handler"
"github.com/ente-io/museum/pkg/utils/time"
@@ -172,35 +171,6 @@ func (h *CollectionHandler) UpdateShareURL(c *gin.Context) {
handler.Error(c, stacktrace.Propagate(err, ""))
return
}
- if req.DeviceLimit == nil && req.ValidTill == nil && req.DisablePassword == nil &&
- req.Nonce == nil && req.PassHash == nil && req.EnableDownload == nil && req.EnableCollect == nil {
- handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, "all parameters are missing"))
- return
- }
-
- if req.DeviceLimit != nil && (*req.DeviceLimit < 0 || *req.DeviceLimit > controller.DeviceLimitThreshold) {
- handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("device limit: %d out of range", *req.DeviceLimit)))
- return
- }
-
- if req.ValidTill != nil && *req.ValidTill != 0 && *req.ValidTill < time.Microseconds() {
- handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, "valid till should be greater than current timestamp"))
- return
- }
-
- var allPassParamsMissing = req.Nonce == nil && req.PassHash == nil && req.MemLimit == nil && req.OpsLimit == nil
- var allPassParamsPresent = req.Nonce != nil && req.PassHash != nil && req.MemLimit != nil && req.OpsLimit != nil
-
- if !(allPassParamsMissing || allPassParamsPresent) {
- handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, "all password params should be either present or missing"))
- return
- }
-
- if allPassParamsPresent && req.DisablePassword != nil && *req.DisablePassword {
- handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, "can not set and disable password in same request"))
- return
- }
-
response, err := h.Controller.UpdateShareURL(c, auth.GetUserID(c.Request.Header), req)
if err != nil {
handler.Error(c, stacktrace.Propagate(err, ""))
diff --git a/server/pkg/api/file.go b/server/pkg/api/file.go
index 2e15ade325..4ec205d1bb 100644
--- a/server/pkg/api/file.go
+++ b/server/pkg/api/file.go
@@ -4,6 +4,7 @@ import (
"fmt"
"github.com/ente-io/museum/pkg/controller/file_copy"
"github.com/ente-io/museum/pkg/controller/filedata"
+ "github.com/ente-io/museum/pkg/controller/public"
"net/http"
"os"
"strconv"
@@ -24,6 +25,7 @@ import (
// FileHandler exposes request handlers for all encrypted file related requests
type FileHandler struct {
Controller *controller.FileController
+ FileUrlCtrl *public.FileLinkController
FileCopyCtrl *file_copy.FileCopyController
FileDataCtrl *filedata.Controller
}
diff --git a/server/pkg/api/file_link.go b/server/pkg/api/file_link.go
new file mode 100644
index 0000000000..d243a87532
--- /dev/null
+++ b/server/pkg/api/file_link.go
@@ -0,0 +1,141 @@
+package api
+
+import (
+ "github.com/ente-io/museum/ente"
+ "github.com/ente-io/museum/pkg/utils/auth"
+ "github.com/ente-io/museum/pkg/utils/handler"
+ "github.com/ente-io/stacktrace"
+ "github.com/gin-gonic/gin"
+ "net/http"
+ "strconv"
+)
+
+// ShareUrl a sharable url for the file
+func (h *FileHandler) ShareUrl(c *gin.Context) {
+ var file ente.CreateFileUrl
+ if err := c.ShouldBindJSON(&file); err != nil {
+ handler.Error(c, stacktrace.Propagate(err, ""))
+ return
+ }
+
+ response, err := h.FileUrlCtrl.CreateLink(c, file)
+ if err != nil {
+ handler.Error(c, stacktrace.Propagate(err, ""))
+ return
+ }
+ c.JSON(http.StatusOK, response)
+}
+
+func (h *FileHandler) LinkInfo(c *gin.Context) {
+ resp, err := h.FileUrlCtrl.Info(c)
+ if err != nil {
+ handler.Error(c, stacktrace.Propagate(err, ""))
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{
+ "file": resp,
+ })
+}
+
+func (h *FileHandler) PasswordInfo(c *gin.Context) {
+ resp, err := h.FileUrlCtrl.PassInfo(c)
+ if err != nil {
+ handler.Error(c, stacktrace.Propagate(err, ""))
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{
+ "nonce": resp.Nonce,
+ "opsLimit": resp.OpsLimit,
+ "memLimit": resp.MemLimit,
+ })
+}
+
+func (h *FileHandler) LinkThumbnail(c *gin.Context) {
+ linkCtx := auth.MustGetFileLinkAccessContext(c)
+ url, err := h.Controller.GetThumbnailURL(c, linkCtx.OwnerID, linkCtx.FileID)
+ if err != nil {
+ handler.Error(c, stacktrace.Propagate(err, ""))
+ return
+ }
+ c.Redirect(http.StatusTemporaryRedirect, url)
+}
+
+func (h *FileHandler) LinkFile(c *gin.Context) {
+ linkCtx := auth.MustGetFileLinkAccessContext(c)
+ url, err := h.Controller.GetFileURL(c, linkCtx.OwnerID, linkCtx.FileID)
+ if err != nil {
+ handler.Error(c, stacktrace.Propagate(err, ""))
+ return
+ }
+ c.Redirect(http.StatusTemporaryRedirect, url)
+}
+
+func (h *FileHandler) DisableUrl(c *gin.Context) {
+ cID, err := strconv.ParseInt(c.Param("fileID"), 10, 64)
+ if err != nil {
+ handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, ""))
+ return
+ }
+ err = h.FileUrlCtrl.Disable(c, cID)
+ if err != nil {
+ handler.Error(c, stacktrace.Propagate(err, ""))
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{})
+}
+
+func (h *FileHandler) GetUrls(c *gin.Context) {
+ sinceTime, err := strconv.ParseInt(c.Query("sinceTime"), 10, 64)
+ if err != nil {
+ handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, "sinceTime parsing failed"))
+ return
+ }
+ limit := 500
+ if c.Query("limit") != "" {
+ limit, err = strconv.Atoi(c.Query("limit"))
+ if err != nil || limit < 1 {
+ handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, ""))
+ return
+ }
+ }
+ response, err := h.FileUrlCtrl.GetUrls(c, sinceTime, int64(limit))
+ if err != nil {
+ handler.Error(c, stacktrace.Propagate(err, ""))
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{
+ "diff": response,
+ })
+}
+
+// VerifyPassword verifies the password for given link access token and return signed jwt token if it's valid
+func (h *FileHandler) VerifyPassword(c *gin.Context) {
+ var req ente.VerifyPasswordRequest
+ if err := c.ShouldBindJSON(&req); err != nil {
+ handler.Error(c, stacktrace.Propagate(err, ""))
+ return
+ }
+ resp, err := h.FileUrlCtrl.VerifyPassword(c, req)
+ if err != nil {
+ handler.Error(c, stacktrace.Propagate(err, ""))
+ return
+ }
+ c.JSON(http.StatusOK, resp)
+}
+
+// UpdateFileURL updates the share URL for a file
+func (h *FileHandler) UpdateFileURL(c *gin.Context) {
+ var req ente.UpdateFileUrl
+ if err := c.ShouldBindJSON(&req); err != nil {
+ handler.Error(c, stacktrace.Propagate(err, ""))
+ return
+ }
+ response, err := h.FileUrlCtrl.UpdateSharedUrl(c, req)
+ if err != nil {
+ handler.Error(c, stacktrace.Propagate(err, ""))
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{
+ "result": response,
+ })
+}
diff --git a/server/pkg/api/public_collection.go b/server/pkg/api/public_collection.go
index 9f61ba788e..81e1836f90 100644
--- a/server/pkg/api/public_collection.go
+++ b/server/pkg/api/public_collection.go
@@ -5,6 +5,7 @@ import (
fileData "github.com/ente-io/museum/ente/filedata"
"github.com/ente-io/museum/pkg/controller/collections"
"github.com/ente-io/museum/pkg/controller/filedata"
+ "github.com/ente-io/museum/pkg/controller/public"
"net/http"
"strconv"
@@ -20,7 +21,7 @@ import (
// PublicCollectionHandler exposes request handlers for publicly accessible collections
type PublicCollectionHandler struct {
- Controller *controller.PublicCollectionController
+ Controller *public.CollectionLinkController
FileCtrl *controller.FileController
CollectionCtrl *collections.CollectionController
FileDataCtrl *filedata.Controller
diff --git a/server/pkg/controller/collections/collection.go b/server/pkg/controller/collections/collection.go
index 5f096bc133..5b86d38d0a 100644
--- a/server/pkg/controller/collections/collection.go
+++ b/server/pkg/controller/collections/collection.go
@@ -6,6 +6,7 @@ import (
"github.com/ente-io/museum/pkg/controller"
"github.com/ente-io/museum/pkg/controller/access"
"github.com/ente-io/museum/pkg/controller/email"
+ "github.com/ente-io/museum/pkg/controller/public"
"github.com/ente-io/museum/pkg/repo/cast"
"github.com/ente-io/museum/pkg/utils/array"
"github.com/ente-io/museum/pkg/utils/auth"
@@ -24,16 +25,16 @@ const (
// CollectionController encapsulates logic that deals with collections
type CollectionController struct {
- PublicCollectionCtrl *controller.PublicCollectionController
- EmailCtrl *email.EmailNotificationController
- AccessCtrl access.Controller
- BillingCtrl *controller.BillingController
- CollectionRepo *repo.CollectionRepository
- UserRepo *repo.UserRepository
- FileRepo *repo.FileRepository
- QueueRepo *repo.QueueRepository
- CastRepo *cast.Repository
- TaskRepo *repo.TaskLockRepository
+ CollectionLinkCtrl *public.CollectionLinkController
+ EmailCtrl *email.EmailNotificationController
+ AccessCtrl access.Controller
+ BillingCtrl *controller.BillingController
+ CollectionRepo *repo.CollectionRepository
+ UserRepo *repo.UserRepository
+ FileRepo *repo.FileRepository
+ QueueRepo *repo.QueueRepository
+ CastRepo *cast.Repository
+ TaskRepo *repo.TaskLockRepository
}
// Create creates a collection
@@ -148,7 +149,7 @@ func (c *CollectionController) TrashV3(ctx *gin.Context, req ente.TrashCollectio
}
}
- err = c.PublicCollectionCtrl.Disable(ctx, cID)
+ err = c.CollectionLinkCtrl.Disable(ctx, cID)
if err != nil {
return stacktrace.Propagate(err, "failed to disabled public share url")
}
@@ -209,7 +210,7 @@ func (c *CollectionController) HandleAccountDeletion(ctx context.Context, userID
if err != nil {
return stacktrace.Propagate(err, "failed to revoke cast token for user")
}
- err = c.PublicCollectionCtrl.HandleAccountDeletion(ctx, userID, logger)
+ err = c.CollectionLinkCtrl.HandleAccountDeletion(ctx, userID, logger)
return stacktrace.Propagate(err, "")
}
diff --git a/server/pkg/controller/collections/share.go b/server/pkg/controller/collections/share.go
index ced64f0fdf..6002c7b493 100644
--- a/server/pkg/controller/collections/share.go
+++ b/server/pkg/controller/collections/share.go
@@ -70,21 +70,21 @@ func (c *CollectionController) JoinViaLink(ctx *gin.Context, req ente.JoinCollec
if !collection.AllowSharing() {
return stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("joining %s is not allowed", collection.Type))
}
- publicCollectionToken, err := c.PublicCollectionCtrl.GetActivePublicCollectionToken(ctx, req.CollectionID)
+ collectionLinkToken, err := c.CollectionLinkCtrl.GetActiveCollectionLinkToken(ctx, req.CollectionID)
if err != nil {
return stacktrace.Propagate(err, "")
}
- if canJoin := publicCollectionToken.CanJoin(); canJoin != nil {
+ if canJoin := collectionLinkToken.CanJoin(); canJoin != nil {
return stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("can not join collection: %s", canJoin.Error()))
}
accessToken := auth.GetAccessToken(ctx)
- if publicCollectionToken.Token != accessToken {
+ if collectionLinkToken.Token != accessToken {
return stacktrace.Propagate(ente.ErrPermissionDenied, "token doesn't match collection")
}
- if publicCollectionToken.PassHash != nil && *publicCollectionToken.PassHash != "" {
+ if collectionLinkToken.PassHash != nil && *collectionLinkToken.PassHash != "" {
accessTokenJWT := auth.GetAccessTokenJWT(ctx)
- if passCheckErr := c.PublicCollectionCtrl.ValidateJWTToken(ctx, accessTokenJWT, *publicCollectionToken.PassHash); passCheckErr != nil {
+ if passCheckErr := c.CollectionLinkCtrl.ValidateJWTToken(ctx, accessTokenJWT, *collectionLinkToken.PassHash); passCheckErr != nil {
return stacktrace.Propagate(passCheckErr, "")
}
}
@@ -93,7 +93,7 @@ func (c *CollectionController) JoinViaLink(ctx *gin.Context, req ente.JoinCollec
return stacktrace.Propagate(err, "")
}
role := ente.VIEWER
- if publicCollectionToken.EnableCollect {
+ if collectionLinkToken.EnableCollect {
role = ente.COLLABORATOR
}
joinErr := c.CollectionRepo.Share(req.CollectionID, collection.Owner.ID, userID, req.EncryptedKey, role, time.Microseconds())
@@ -197,7 +197,7 @@ func (c *CollectionController) ShareURL(ctx context.Context, userID int64, req e
if err != nil {
return ente.PublicURL{}, stacktrace.Propagate(err, "")
}
- response, err := c.PublicCollectionCtrl.CreateAccessToken(ctx, req)
+ response, err := c.CollectionLinkCtrl.CreateLink(ctx, req)
if err != nil {
return ente.PublicURL{}, stacktrace.Propagate(err, "")
}
@@ -205,20 +205,26 @@ func (c *CollectionController) ShareURL(ctx context.Context, userID int64, req e
}
// UpdateShareURL updates the shared url configuration
-func (c *CollectionController) UpdateShareURL(ctx context.Context, userID int64, req ente.UpdatePublicAccessTokenRequest) (
- ente.PublicURL, error) {
+func (c *CollectionController) UpdateShareURL(
+ ctx context.Context,
+ userID int64,
+ req ente.UpdatePublicAccessTokenRequest,
+) (*ente.PublicURL, error) {
+ if err := req.Validate(); err != nil {
+ return nil, stacktrace.Propagate(err, "")
+ }
if err := c.verifyOwnership(req.CollectionID, userID); err != nil {
- return ente.PublicURL{}, stacktrace.Propagate(err, "")
+ return nil, stacktrace.Propagate(err, "")
}
err := c.BillingCtrl.HasActiveSelfOrFamilySubscription(userID, true)
if err != nil {
- return ente.PublicURL{}, stacktrace.Propagate(err, "")
+ return nil, stacktrace.Propagate(err, "")
}
- response, err := c.PublicCollectionCtrl.UpdateSharedUrl(ctx, req)
+ response, err := c.CollectionLinkCtrl.UpdateSharedUrl(ctx, req)
if err != nil {
- return ente.PublicURL{}, stacktrace.Propagate(err, "")
+ return nil, stacktrace.Propagate(err, "")
}
- return response, nil
+ return &response, nil
}
// DisableSharedURL disable a public auth-token for the given collectionID
@@ -226,7 +232,7 @@ func (c *CollectionController) DisableSharedURL(ctx context.Context, userID int6
if err := c.verifyOwnership(cID, userID); err != nil {
return stacktrace.Propagate(err, "")
}
- err := c.PublicCollectionCtrl.Disable(ctx, cID)
+ err := c.CollectionLinkCtrl.Disable(ctx, cID)
return stacktrace.Propagate(err, "")
}
diff --git a/server/pkg/controller/public_collection.go b/server/pkg/controller/public/collection_link.go
similarity index 69%
rename from server/pkg/controller/public_collection.go
rename to server/pkg/controller/public/collection_link.go
index 022a08812e..ead744bfbf 100644
--- a/server/pkg/controller/public_collection.go
+++ b/server/pkg/controller/public/collection_link.go
@@ -1,12 +1,13 @@
-package controller
+package public
import (
"context"
"errors"
"fmt"
+ "github.com/ente-io/museum/pkg/controller"
+ "github.com/ente-io/museum/pkg/repo/public"
"github.com/ente-io/museum/ente"
- enteJWT "github.com/ente-io/museum/ente/jwt"
emailCtrl "github.com/ente-io/museum/pkg/controller/email"
"github.com/ente-io/museum/pkg/repo"
"github.com/ente-io/museum/pkg/utils/auth"
@@ -14,7 +15,6 @@ import (
"github.com/ente-io/museum/pkg/utils/time"
"github.com/ente-io/stacktrace"
"github.com/gin-gonic/gin"
- "github.com/golang-jwt/jwt"
"github.com/lithammer/shortuuid/v3"
"github.com/sirupsen/logrus"
)
@@ -49,23 +49,24 @@ const (
AbuseLimitExceededTemplate = "report_limit_exceeded_alert.html"
)
-// PublicCollectionController controls share collection operations
-type PublicCollectionController struct {
- FileController *FileController
+// CollectionLinkController controls share collection operations
+type CollectionLinkController struct {
+ FileController *controller.FileController
EmailNotificationCtrl *emailCtrl.EmailNotificationController
- PublicCollectionRepo *repo.PublicCollectionRepository
+ CollectionLinkRepo *public.CollectionLinkRepo
+ FileLinkRepo *public.FileLinkRepository
CollectionRepo *repo.CollectionRepository
UserRepo *repo.UserRepository
JwtSecret []byte
}
-func (c *PublicCollectionController) CreateAccessToken(ctx context.Context, req ente.CreatePublicAccessTokenRequest) (ente.PublicURL, error) {
+func (c *CollectionLinkController) CreateLink(ctx context.Context, req ente.CreatePublicAccessTokenRequest) (ente.PublicURL, error) {
accessToken := shortuuid.New()[0:AccessTokenLength]
- err := c.PublicCollectionRepo.
+ err := c.CollectionLinkRepo.
Insert(ctx, req.CollectionID, accessToken, req.ValidTill, req.DeviceLimit, req.EnableCollect, req.EnableJoin)
if err != nil {
if errors.Is(err, ente.ErrActiveLinkAlreadyExists) {
- collectionToPubUrlMap, err2 := c.PublicCollectionRepo.GetCollectionToActivePublicURLMap(ctx, []int64{req.CollectionID})
+ collectionToPubUrlMap, err2 := c.CollectionLinkRepo.GetCollectionToActivePublicURLMap(ctx, []int64{req.CollectionID})
if err2 != nil {
return ente.PublicURL{}, stacktrace.Propagate(err2, "")
}
@@ -81,7 +82,7 @@ func (c *PublicCollectionController) CreateAccessToken(ctx context.Context, req
}
}
response := ente.PublicURL{
- URL: c.PublicCollectionRepo.GetAlbumUrl(accessToken),
+ URL: c.CollectionLinkRepo.GetAlbumUrl(accessToken),
ValidTill: req.ValidTill,
DeviceLimit: req.DeviceLimit,
EnableDownload: true,
@@ -91,11 +92,11 @@ func (c *PublicCollectionController) CreateAccessToken(ctx context.Context, req
return response, nil
}
-func (c *PublicCollectionController) GetActivePublicCollectionToken(ctx context.Context, collectionID int64) (ente.PublicCollectionToken, error) {
- return c.PublicCollectionRepo.GetActivePublicCollectionToken(ctx, collectionID)
+func (c *CollectionLinkController) GetActiveCollectionLinkToken(ctx context.Context, collectionID int64) (ente.CollectionLinkRow, error) {
+ return c.CollectionLinkRepo.GetActiveCollectionLinkRow(ctx, collectionID)
}
-func (c *PublicCollectionController) CreateFile(ctx *gin.Context, file ente.File, app ente.App) (ente.File, error) {
+func (c *CollectionLinkController) CreateFile(ctx *gin.Context, file ente.File, app ente.App) (ente.File, error) {
collection, err := c.GetPublicCollection(ctx, true)
if err != nil {
return ente.File{}, stacktrace.Propagate(err, "")
@@ -118,13 +119,13 @@ func (c *PublicCollectionController) CreateFile(ctx *gin.Context, file ente.File
}
// Disable all public accessTokens generated for the given cID till date.
-func (c *PublicCollectionController) Disable(ctx context.Context, cID int64) error {
- err := c.PublicCollectionRepo.DisableSharing(ctx, cID)
+func (c *CollectionLinkController) Disable(ctx context.Context, cID int64) error {
+ err := c.CollectionLinkRepo.DisableSharing(ctx, cID)
return stacktrace.Propagate(err, "")
}
-func (c *PublicCollectionController) UpdateSharedUrl(ctx context.Context, req ente.UpdatePublicAccessTokenRequest) (ente.PublicURL, error) {
- publicCollectionToken, err := c.PublicCollectionRepo.GetActivePublicCollectionToken(ctx, req.CollectionID)
+func (c *CollectionLinkController) UpdateSharedUrl(ctx context.Context, req ente.UpdatePublicAccessTokenRequest) (ente.PublicURL, error) {
+ publicCollectionToken, err := c.CollectionLinkRepo.GetActiveCollectionLinkRow(ctx, req.CollectionID)
if err != nil {
return ente.PublicURL{}, err
}
@@ -154,12 +155,12 @@ func (c *PublicCollectionController) UpdateSharedUrl(ctx context.Context, req en
if req.EnableJoin != nil {
publicCollectionToken.EnableJoin = *req.EnableJoin
}
- err = c.PublicCollectionRepo.UpdatePublicCollectionToken(ctx, publicCollectionToken)
+ err = c.CollectionLinkRepo.UpdatePublicCollectionToken(ctx, publicCollectionToken)
if err != nil {
return ente.PublicURL{}, stacktrace.Propagate(err, "")
}
return ente.PublicURL{
- URL: c.PublicCollectionRepo.GetAlbumUrl(publicCollectionToken.Token),
+ URL: c.CollectionLinkRepo.GetAlbumUrl(publicCollectionToken.Token),
DeviceLimit: publicCollectionToken.DeviceLimit,
ValidTill: publicCollectionToken.ValidTill,
EnableDownload: publicCollectionToken.EnableDownload,
@@ -176,58 +177,23 @@ func (c *PublicCollectionController) UpdateSharedUrl(ctx context.Context, req en
// used by the client to pass in other requests for public collection.
// Having a separate endpoint for password validation allows us to easily rate-limit the attempts for brute-force
// attack for guessing password.
-func (c *PublicCollectionController) VerifyPassword(ctx *gin.Context, req ente.VerifyPasswordRequest) (*ente.VerifyPasswordResponse, error) {
+func (c *CollectionLinkController) VerifyPassword(ctx *gin.Context, req ente.VerifyPasswordRequest) (*ente.VerifyPasswordResponse, error) {
accessContext := auth.MustGetPublicAccessContext(ctx)
- publicCollectionToken, err := c.PublicCollectionRepo.GetActivePublicCollectionToken(ctx, accessContext.CollectionID)
+ collectionLinkRow, err := c.CollectionLinkRepo.GetActiveCollectionLinkRow(ctx, accessContext.CollectionID)
if err != nil {
return nil, stacktrace.Propagate(err, "failed to get public collection info")
}
- if publicCollectionToken.PassHash == nil || *publicCollectionToken.PassHash == "" {
- return nil, stacktrace.Propagate(ente.ErrBadRequest, "password is not configured for the link")
- }
- if req.PassHash != *publicCollectionToken.PassHash {
- return nil, stacktrace.Propagate(ente.ErrInvalidPassword, "incorrect password for link")
- }
- token := jwt.NewWithClaims(jwt.SigningMethodHS256, &enteJWT.PublicAlbumPasswordClaim{
- PassHash: req.PassHash,
- ExpiryTime: time.NDaysFromNow(365),
- })
- // Sign and get the complete encoded token as a string using the secret
- tokenString, err := token.SignedString(c.JwtSecret)
-
- if err != nil {
- return nil, stacktrace.Propagate(err, "")
- }
- return &ente.VerifyPasswordResponse{
- JWTToken: tokenString,
- }, nil
+ return verifyPassword(c.JwtSecret, collectionLinkRow.PassHash, req)
}
-func (c *PublicCollectionController) ValidateJWTToken(ctx *gin.Context, jwtToken string, passwordHash string) error {
- token, err := jwt.ParseWithClaims(jwtToken, &enteJWT.PublicAlbumPasswordClaim{}, func(token *jwt.Token) (interface{}, error) {
- if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
- return stacktrace.Propagate(fmt.Errorf("unexpected signing method: %v", token.Header["alg"]), ""), nil
- }
- return c.JwtSecret, nil
- })
- if err != nil {
- return stacktrace.Propagate(err, "JWT parsed failed")
- }
- claims, ok := token.Claims.(*enteJWT.PublicAlbumPasswordClaim)
-
- if !ok {
- return stacktrace.Propagate(errors.New("no claim in jwt token"), "")
- }
- if token.Valid && claims.PassHash == passwordHash {
- return nil
- }
- return ente.ErrInvalidPassword
+func (c *CollectionLinkController) ValidateJWTToken(ctx *gin.Context, jwtToken string, passwordHash string) error {
+ return validateJWTToken(c.JwtSecret, jwtToken, passwordHash)
}
// ReportAbuse captures abuse report for a publicly shared collection.
// It will also disable the accessToken for the collection if total abuse reports for the said collection
// reaches AutoDisableAbuseThreshold
-func (c *PublicCollectionController) ReportAbuse(ctx *gin.Context, req ente.AbuseReportRequest) error {
+func (c *CollectionLinkController) ReportAbuse(ctx *gin.Context, req ente.AbuseReportRequest) error {
accessContext := auth.MustGetPublicAccessContext(ctx)
readableReason, found := AllowedReasons[req.Reason]
if !found {
@@ -235,11 +201,11 @@ func (c *PublicCollectionController) ReportAbuse(ctx *gin.Context, req ente.Abus
}
logrus.WithField("collectionID", accessContext.CollectionID).Error("CRITICAL: received abuse report")
- err := c.PublicCollectionRepo.RecordAbuseReport(ctx, accessContext, req.URL, req.Reason, req.Details)
+ err := c.CollectionLinkRepo.RecordAbuseReport(ctx, accessContext, req.URL, req.Reason, req.Details)
if err != nil {
return stacktrace.Propagate(err, "")
}
- count, err := c.PublicCollectionRepo.GetAbuseReportCount(ctx, accessContext)
+ count, err := c.CollectionLinkRepo.GetAbuseReportCount(ctx, accessContext)
if err != nil {
return stacktrace.Propagate(err, "")
}
@@ -253,7 +219,7 @@ func (c *PublicCollectionController) ReportAbuse(ctx *gin.Context, req ente.Abus
return nil
}
-func (c *PublicCollectionController) onAbuseReportReceived(collectionID int64, report ente.AbuseReportRequest, readableReason string, abuseCount int64) {
+func (c *CollectionLinkController) onAbuseReportReceived(collectionID int64, report ente.AbuseReportRequest, readableReason string, abuseCount int64) {
collection, err := c.CollectionRepo.Get(collectionID)
if err != nil {
logrus.Error("Could not get collection for abuse report")
@@ -292,9 +258,9 @@ func (c *PublicCollectionController) onAbuseReportReceived(collectionID int64, r
}
}
-func (c *PublicCollectionController) HandleAccountDeletion(ctx context.Context, userID int64, logger *logrus.Entry) error {
+func (c *CollectionLinkController) HandleAccountDeletion(ctx context.Context, userID int64, logger *logrus.Entry) error {
logger.Info("updating public collection on account deletion")
- collectionIDs, err := c.PublicCollectionRepo.GetActivePublicTokenForUser(ctx, userID)
+ collectionIDs, err := c.CollectionLinkRepo.GetActivePublicTokenForUser(ctx, userID)
if err != nil {
return stacktrace.Propagate(err, "")
}
@@ -305,12 +271,12 @@ func (c *PublicCollectionController) HandleAccountDeletion(ctx context.Context,
return stacktrace.Propagate(err, "")
}
}
- return nil
+ return c.FileLinkRepo.DisableLinksForUser(ctx, userID)
}
// GetPublicCollection will return collection info for a public url.
// is mustAllowCollect is set to true but the underlying collection doesn't allow uploading
-func (c *PublicCollectionController) GetPublicCollection(ctx *gin.Context, mustAllowCollect bool) (ente.Collection, error) {
+func (c *CollectionLinkController) GetPublicCollection(ctx *gin.Context, mustAllowCollect bool) (ente.Collection, error) {
accessContext := auth.MustGetPublicAccessContext(ctx)
collection, err := c.CollectionRepo.Get(accessContext.CollectionID)
if err != nil {
diff --git a/server/pkg/controller/public/file_link.go b/server/pkg/controller/public/file_link.go
new file mode 100644
index 0000000000..be015bc719
--- /dev/null
+++ b/server/pkg/controller/public/file_link.go
@@ -0,0 +1,162 @@
+package public
+
+import (
+ "github.com/ente-io/museum/ente"
+ "github.com/ente-io/museum/pkg/controller"
+ "github.com/ente-io/museum/pkg/repo"
+ "github.com/ente-io/museum/pkg/repo/public"
+ "github.com/ente-io/museum/pkg/utils/auth"
+ "github.com/ente-io/stacktrace"
+ "github.com/gin-gonic/gin"
+ "github.com/lithammer/shortuuid/v3"
+)
+
+// FileLinkController controls share collection operations
+type FileLinkController struct {
+ FileController *controller.FileController
+ FileLinkRepo *public.FileLinkRepository
+ FileRepo *repo.FileRepository
+ JwtSecret []byte
+}
+
+func (c *FileLinkController) CreateLink(ctx *gin.Context, req ente.CreateFileUrl) (*ente.FileUrl, error) {
+ actorUserID := auth.GetUserID(ctx.Request.Header)
+ app := auth.GetApp(ctx)
+ if req.App != app {
+ return nil, stacktrace.Propagate(ente.NewBadRequestWithMessage("app mismatch"), "app mismatch")
+ }
+ file, err := c.FileRepo.GetFileAttributes(req.FileID)
+ if err != nil {
+ return nil, stacktrace.Propagate(err, "failed to get file attributes")
+ }
+ if actorUserID != file.OwnerID {
+ return nil, stacktrace.Propagate(ente.NewPermissionDeniedError("not file owner"), "")
+ }
+ accessToken := shortuuid.New()[0:AccessTokenLength]
+ _, err = c.FileLinkRepo.Insert(ctx, req.FileID, actorUserID, accessToken, app)
+ if err == nil || err == ente.ErrActiveLinkAlreadyExists {
+ row, rowErr := c.FileLinkRepo.GetFileUrlRowByFileID(ctx, req.FileID)
+ if rowErr != nil {
+ return nil, stacktrace.Propagate(rowErr, "failed to get active file url token")
+ }
+ return c.mapRowToFileUrl(ctx, row), nil
+ }
+ return nil, stacktrace.Propagate(err, "failed to create public file link")
+}
+
+// Disable all public accessTokens generated for the given fileID till date.
+func (c *FileLinkController) Disable(ctx *gin.Context, fileID int64) error {
+ userID := auth.GetUserID(ctx.Request.Header)
+ file, err := c.FileRepo.GetFileAttributes(fileID)
+ if err != nil {
+ return stacktrace.Propagate(err, "failed to get file attributes")
+ }
+ if userID != file.OwnerID {
+ return stacktrace.Propagate(ente.NewPermissionDeniedError("not file owner"), "")
+ }
+ return c.FileLinkRepo.DisableLinkForFiles(ctx, []int64{fileID})
+}
+
+func (c *FileLinkController) GetUrls(ctx *gin.Context, sinceTime int64, limit int64) ([]*ente.FileUrl, error) {
+ userID := auth.GetUserID(ctx.Request.Header)
+ app := auth.GetApp(ctx)
+ fileLinks, err := c.FileLinkRepo.GetFileUrls(ctx, userID, sinceTime, limit, app)
+ if err != nil {
+ return nil, stacktrace.Propagate(err, "failed to get file urls")
+ }
+ var fileUrls []*ente.FileUrl
+ for _, row := range fileLinks {
+ fileUrls = append(fileUrls, c.mapRowToFileUrl(ctx, row))
+ }
+ return fileUrls, nil
+}
+
+func (c *FileLinkController) UpdateSharedUrl(ctx *gin.Context, req ente.UpdateFileUrl) (*ente.FileUrl, error) {
+ if err := req.Validate(); err != nil {
+ return nil, stacktrace.Propagate(err, "invalid request")
+ }
+ fileLinkRow, err := c.FileLinkRepo.GetActiveFileUrlToken(ctx, req.FileID)
+ if err != nil {
+ return nil, stacktrace.Propagate(err, "failed to get file link info")
+ }
+ if fileLinkRow.OwnerID != auth.GetUserID(ctx.Request.Header) {
+ return nil, stacktrace.Propagate(ente.NewPermissionDeniedError("not file owner"), "")
+ }
+ if req.ValidTill != nil {
+ fileLinkRow.ValidTill = *req.ValidTill
+ }
+ if req.DeviceLimit != nil {
+ fileLinkRow.DeviceLimit = *req.DeviceLimit
+ }
+ if req.PassHash != nil && req.Nonce != nil && req.OpsLimit != nil && req.MemLimit != nil {
+ fileLinkRow.PassHash = req.PassHash
+ fileLinkRow.Nonce = req.Nonce
+ fileLinkRow.OpsLimit = req.OpsLimit
+ fileLinkRow.MemLimit = req.MemLimit
+ } else if req.DisablePassword != nil && *req.DisablePassword {
+ fileLinkRow.PassHash = nil
+ fileLinkRow.Nonce = nil
+ fileLinkRow.OpsLimit = nil
+ fileLinkRow.MemLimit = nil
+ }
+ if req.EnableDownload != nil {
+ fileLinkRow.EnableDownload = *req.EnableDownload
+ }
+
+ err = c.FileLinkRepo.UpdateLink(ctx, *fileLinkRow)
+ if err != nil {
+ return nil, stacktrace.Propagate(err, "")
+ }
+ return c.mapRowToFileUrl(ctx, fileLinkRow), nil
+}
+
+func (c *FileLinkController) Info(ctx *gin.Context) (*ente.File, error) {
+ accessContext := auth.MustGetFileLinkAccessContext(ctx)
+ return c.FileRepo.GetFileAttributes(accessContext.FileID)
+}
+
+func (c *FileLinkController) PassInfo(ctx *gin.Context) (*ente.FileLinkRow, error) {
+ accessContext := auth.MustGetFileLinkAccessContext(ctx)
+ return c.FileLinkRepo.GetFileUrlRowByFileID(ctx, accessContext.FileID)
+}
+
+// VerifyPassword verifies if the user has provided correct pw hash. If yes, it returns a signed jwt token which can be
+// used by the client to pass in other requests for public collection.
+// Having a separate endpoint for password validation allows us to easily rate-limit the attempts for brute-force
+// attack for guessing password.
+func (c *FileLinkController) VerifyPassword(ctx *gin.Context, req ente.VerifyPasswordRequest) (*ente.VerifyPasswordResponse, error) {
+ accessContext := auth.MustGetFileLinkAccessContext(ctx)
+ collectionLinkRow, err := c.FileLinkRepo.GetActiveFileUrlToken(ctx, accessContext.FileID)
+ if err != nil {
+ return nil, stacktrace.Propagate(err, "failed to get public collection info")
+ }
+ return verifyPassword(c.JwtSecret, collectionLinkRow.PassHash, req)
+}
+
+func (c *FileLinkController) ValidateJWTToken(ctx *gin.Context, jwtToken string, passwordHash string) error {
+ return validateJWTToken(c.JwtSecret, jwtToken, passwordHash)
+}
+
+func (c *FileLinkController) mapRowToFileUrl(ctx *gin.Context, row *ente.FileLinkRow) *ente.FileUrl {
+ app := auth.GetApp(ctx)
+ var url string
+ if app == ente.Locker {
+ url = c.FileLinkRepo.LockerFileLink(row.Token)
+ } else {
+ url = c.FileLinkRepo.PhotoLink(row.Token)
+ }
+ return &ente.FileUrl{
+ LinkID: row.LinkID,
+ FileID: row.FileID,
+ URL: url,
+ OwnerID: row.OwnerID,
+ ValidTill: row.ValidTill,
+ DeviceLimit: row.DeviceLimit,
+ PasswordEnabled: row.PassHash != nil,
+ Nonce: row.Nonce,
+ OpsLimit: row.OpsLimit,
+ MemLimit: row.MemLimit,
+ EnableDownload: row.EnableDownload,
+ CreatedAt: row.CreatedAt,
+ }
+}
diff --git a/server/pkg/controller/public/link_common.go b/server/pkg/controller/public/link_common.go
new file mode 100644
index 0000000000..9a56334a0e
--- /dev/null
+++ b/server/pkg/controller/public/link_common.go
@@ -0,0 +1,54 @@
+package public
+
+import (
+ "errors"
+ "fmt"
+ "github.com/ente-io/museum/ente"
+ enteJWT "github.com/ente-io/museum/ente/jwt"
+ "github.com/ente-io/museum/pkg/utils/time"
+ "github.com/ente-io/stacktrace"
+ "github.com/golang-jwt/jwt"
+)
+
+func validateJWTToken(secret []byte, jwtToken string, passwordHash string) error {
+ token, err := jwt.ParseWithClaims(jwtToken, &enteJWT.LinkPasswordClaim{}, func(token *jwt.Token) (interface{}, error) {
+ if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
+ return stacktrace.Propagate(fmt.Errorf("unexpected signing method: %v", token.Header["alg"]), ""), nil
+ }
+ return secret, nil
+ })
+ if err != nil {
+ return stacktrace.Propagate(err, "JWT parsed failed")
+ }
+ claims, ok := token.Claims.(*enteJWT.LinkPasswordClaim)
+
+ if !ok {
+ return stacktrace.Propagate(errors.New("no claim in jwt token"), "")
+ }
+ if token.Valid && claims.PassHash == passwordHash {
+ return nil
+ }
+ return ente.ErrInvalidPassword
+}
+
+func verifyPassword(secret []byte, expectedPassHash *string, req ente.VerifyPasswordRequest) (*ente.VerifyPasswordResponse, error) {
+ if expectedPassHash == nil || *expectedPassHash == "" {
+ return nil, stacktrace.Propagate(ente.ErrBadRequest, "password is not configured for the link")
+ }
+ if req.PassHash != *expectedPassHash {
+ return nil, stacktrace.Propagate(ente.ErrInvalidPassword, "incorrect password for link")
+ }
+ token := jwt.NewWithClaims(jwt.SigningMethodHS256, &enteJWT.LinkPasswordClaim{
+ PassHash: req.PassHash,
+ ExpiryTime: time.NDaysFromNow(365),
+ })
+ // Sign and get the complete encoded token as a string using the secret
+ tokenString, err := token.SignedString(secret)
+
+ if err != nil {
+ return nil, stacktrace.Propagate(err, "")
+ }
+ return &ente.VerifyPasswordResponse{
+ JWTToken: tokenString,
+ }, nil
+}
diff --git a/server/pkg/middleware/access_token.go b/server/pkg/middleware/collection_link.go
similarity index 81%
rename from server/pkg/middleware/access_token.go
rename to server/pkg/middleware/collection_link.go
index 702af77db8..5b11a5ec07 100644
--- a/server/pkg/middleware/access_token.go
+++ b/server/pkg/middleware/collection_link.go
@@ -5,6 +5,8 @@ import (
"context"
"crypto/sha256"
"fmt"
+ public2 "github.com/ente-io/museum/pkg/controller/public"
+ "github.com/ente-io/museum/pkg/repo/public"
"net/http"
"github.com/ente-io/museum/ente"
@@ -24,20 +26,20 @@ import (
var passwordWhiteListedURLs = []string{"/public-collection/info", "/public-collection/report-abuse", "/public-collection/verify-password"}
var whitelistedCollectionShareIDs = []int64{111}
-// AccessTokenMiddleware intercepts and authenticates incoming requests
-type AccessTokenMiddleware struct {
- PublicCollectionRepo *repo.PublicCollectionRepository
- PublicCollectionCtrl *controller.PublicCollectionController
+// CollectionLinkMiddleware intercepts and authenticates incoming requests
+type CollectionLinkMiddleware struct {
+ CollectionLinkRepo *public.CollectionLinkRepo
+ PublicCollectionCtrl *public2.CollectionLinkController
CollectionRepo *repo.CollectionRepository
Cache *cache.Cache
BillingCtrl *controller.BillingController
DiscordController *discord.DiscordController
}
-// AccessTokenAuthMiddleware returns a middle ware that extracts the `X-Auth-Access-Token`
+// Authenticate returns a middle ware that extracts the `X-Auth-Access-Token`
// within the header of a request and uses it to validate the access token and set the
// ente.PublicAccessContext with auth.PublicAccessKey as key
-func (m *AccessTokenMiddleware) AccessTokenAuthMiddleware(urlSanitizer func(_ *gin.Context) string) gin.HandlerFunc {
+func (m *CollectionLinkMiddleware) Authenticate(urlSanitizer func(_ *gin.Context) string) gin.HandlerFunc {
return func(c *gin.Context) {
accessToken := auth.GetAccessToken(c)
if accessToken == "" {
@@ -52,7 +54,7 @@ func (m *AccessTokenMiddleware) AccessTokenAuthMiddleware(urlSanitizer func(_ *g
cacheKey := computeHashKeyForList([]string{accessToken, clientIP, userAgent}, ":")
cachedValue, cacheHit := m.Cache.Get(cacheKey)
if !cacheHit {
- publicCollectionSummary, err = m.PublicCollectionRepo.GetCollectionSummaryByToken(c, accessToken)
+ publicCollectionSummary, err = m.CollectionLinkRepo.GetCollectionSummaryByToken(c, accessToken)
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
return
@@ -112,7 +114,7 @@ func (m *AccessTokenMiddleware) AccessTokenAuthMiddleware(urlSanitizer func(_ *g
c.Next()
}
}
-func (m *AccessTokenMiddleware) validateOwnersSubscription(cID int64) error {
+func (m *CollectionLinkMiddleware) validateOwnersSubscription(cID int64) error {
userID, err := m.CollectionRepo.GetOwnerID(cID)
if err != nil {
return stacktrace.Propagate(err, "")
@@ -120,7 +122,7 @@ func (m *AccessTokenMiddleware) validateOwnersSubscription(cID int64) error {
return m.BillingCtrl.HasActiveSelfOrFamilySubscription(userID, false)
}
-func (m *AccessTokenMiddleware) isDeviceLimitReached(ctx context.Context,
+func (m *CollectionLinkMiddleware) isDeviceLimitReached(ctx context.Context,
collectionSummary ente.PublicCollectionSummary, ip string, ua string) (bool, error) {
// skip deviceLimit check & record keeping for requests via CF worker
if network.IsCFWorkerIP(ip) {
@@ -128,7 +130,7 @@ func (m *AccessTokenMiddleware) isDeviceLimitReached(ctx context.Context,
}
sharedID := collectionSummary.ID
- hasAccessedInPast, err := m.PublicCollectionRepo.AccessedInPast(ctx, sharedID, ip, ua)
+ hasAccessedInPast, err := m.CollectionLinkRepo.AccessedInPast(ctx, sharedID, ip, ua)
if err != nil {
return false, stacktrace.Propagate(err, "")
}
@@ -136,17 +138,17 @@ func (m *AccessTokenMiddleware) isDeviceLimitReached(ctx context.Context,
if hasAccessedInPast {
return false, nil
}
- count, err := m.PublicCollectionRepo.GetUniqueAccessCount(ctx, sharedID)
+ count, err := m.CollectionLinkRepo.GetUniqueAccessCount(ctx, sharedID)
if err != nil {
return false, stacktrace.Propagate(err, "failed to get unique access count")
}
deviceLimit := int64(collectionSummary.DeviceLimit)
- if deviceLimit == controller.DeviceLimitThreshold {
- deviceLimit = controller.DeviceLimitThresholdMultiplier * controller.DeviceLimitThreshold
+ if deviceLimit == public2.DeviceLimitThreshold {
+ deviceLimit = public2.DeviceLimitThresholdMultiplier * public2.DeviceLimitThreshold
}
- if count >= controller.DeviceLimitWarningThreshold {
+ if count >= public2.DeviceLimitWarningThreshold {
if !array.Int64InList(sharedID, whitelistedCollectionShareIDs) {
m.DiscordController.NotifyPotentialAbuse(
fmt.Sprintf("Album exceeds warning threshold: {CollectionID: %d, ShareID: %d}",
@@ -157,12 +159,12 @@ func (m *AccessTokenMiddleware) isDeviceLimitReached(ctx context.Context,
if deviceLimit > 0 && count >= deviceLimit {
return true, nil
}
- err = m.PublicCollectionRepo.RecordAccessHistory(ctx, sharedID, ip, ua)
+ err = m.CollectionLinkRepo.RecordAccessHistory(ctx, sharedID, ip, ua)
return false, stacktrace.Propagate(err, "failed to record access history")
}
// validatePassword will verify if the user is provided correct password for the public album
-func (m *AccessTokenMiddleware) validatePassword(c *gin.Context, reqPath string,
+func (m *CollectionLinkMiddleware) validatePassword(c *gin.Context, reqPath string,
collectionSummary ente.PublicCollectionSummary) error {
if array.StringInList(reqPath, passwordWhiteListedURLs) {
return nil
diff --git a/server/pkg/middleware/file_link.go b/server/pkg/middleware/file_link.go
new file mode 100644
index 0000000000..72b095bbc0
--- /dev/null
+++ b/server/pkg/middleware/file_link.go
@@ -0,0 +1,168 @@
+package middleware
+
+import (
+ "context"
+ "fmt"
+ publicCtrl "github.com/ente-io/museum/pkg/controller/public"
+ "github.com/ente-io/museum/pkg/repo/public"
+ "github.com/ente-io/museum/pkg/utils/array"
+ "net/http"
+
+ "github.com/ente-io/museum/ente"
+ "github.com/ente-io/museum/pkg/controller"
+ "github.com/ente-io/museum/pkg/controller/discord"
+ "github.com/ente-io/museum/pkg/utils/auth"
+ "github.com/ente-io/museum/pkg/utils/network"
+ "github.com/ente-io/museum/pkg/utils/time"
+ "github.com/ente-io/stacktrace"
+ "github.com/gin-gonic/gin"
+ "github.com/patrickmn/go-cache"
+ "github.com/sirupsen/logrus"
+)
+
+var filePasswordWhiteListedURLs = []string{"/file-link/pass-info", "/file-link/verify-password"}
+
+// FileLinkMiddleware intercepts and authenticates incoming requests
+type FileLinkMiddleware struct {
+ FileLinkRepo *public.FileLinkRepository
+ FileLinkCtrl *publicCtrl.FileLinkController
+ Cache *cache.Cache
+ BillingCtrl *controller.BillingController
+ DiscordController *discord.DiscordController
+}
+
+// Authenticate returns a middle ware that extracts the `X-Auth-Access-Token`
+// within the header of a request and uses it to validate the access token and set the
+// ente.PublicAccessContext with auth.PublicAccessKey as key
+func (m *FileLinkMiddleware) Authenticate(urlSanitizer func(_ *gin.Context) string) gin.HandlerFunc {
+ return func(c *gin.Context) {
+ accessToken := auth.GetAccessToken(c)
+ if accessToken == "" {
+ c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing accessToken"})
+ return
+ }
+ clientIP := network.GetClientIP(c)
+ userAgent := c.GetHeader("User-Agent")
+
+ cacheKey := computeHashKeyForList([]string{accessToken, clientIP, userAgent}, ":")
+ cachedValue, cacheHit := m.Cache.Get(cacheKey)
+ var fileLinkRow *ente.FileLinkRow
+ var err error
+ if !cacheHit {
+ fileLinkRow, err = m.FileLinkRepo.GetFileUrlRowByToken(c, accessToken)
+ if err != nil {
+ logrus.WithError(err).Info("failed to get file link row by token")
+ c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
+ return
+ }
+ if fileLinkRow.IsDisabled {
+ c.AbortWithStatusJSON(http.StatusGone, gin.H{"error": "disabled token"})
+ return
+ }
+ // validate if user still has active paid subscription
+ if err = m.BillingCtrl.HasActiveSelfOrFamilySubscription(fileLinkRow.OwnerID, true); err != nil {
+ logrus.WithError(err).Info("failed to verify active paid subscription")
+ c.AbortWithStatusJSON(http.StatusGone, gin.H{"error": "no active subscription"})
+ return
+ }
+
+ // validate device limit
+ reached, limitErr := m.isDeviceLimitReached(c, fileLinkRow, clientIP, userAgent)
+ if limitErr != nil {
+ logrus.WithError(limitErr).Error("failed to check device limit")
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "something went wrong"})
+ return
+ }
+ if reached {
+ c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"error": "reached device limit"})
+ return
+ }
+ } else {
+ fileLinkRow = cachedValue.(*ente.FileLinkRow)
+ }
+
+ if fileLinkRow.ValidTill > 0 && // expiry time is defined, 0 indicates no expiry
+ fileLinkRow.ValidTill < time.Microseconds() {
+ c.AbortWithStatusJSON(http.StatusGone, gin.H{"error": "expired token"})
+ return
+ }
+
+ // checks password protected public collection
+ if fileLinkRow.PassHash != nil && *fileLinkRow.PassHash != "" {
+ reqPath := urlSanitizer(c)
+ if err = m.validatePassword(c, reqPath, fileLinkRow); err != nil {
+ logrus.WithError(err).Warn("password validation failed")
+ c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": err})
+ return
+ }
+ }
+
+ if !cacheHit {
+ m.Cache.Set(cacheKey, fileLinkRow, cache.DefaultExpiration)
+ }
+
+ c.Set(auth.FileLinkAccessKey, &ente.FileLinkAccessContext{
+ LinkID: fileLinkRow.LinkID,
+ IP: clientIP,
+ UserAgent: userAgent,
+ FileID: fileLinkRow.FileID,
+ OwnerID: fileLinkRow.OwnerID,
+ })
+ c.Next()
+ }
+}
+
+func (m *FileLinkMiddleware) isDeviceLimitReached(ctx context.Context,
+ collectionSummary *ente.FileLinkRow, ip string, ua string) (bool, error) {
+ // skip deviceLimit check & record keeping for requests via CF worker
+ if network.IsCFWorkerIP(ip) {
+ return false, nil
+ }
+
+ sharedID := collectionSummary.LinkID
+ hasAccessedInPast, err := m.FileLinkRepo.AccessedInPast(ctx, sharedID, ip, ua)
+ if err != nil {
+ return false, stacktrace.Propagate(err, "")
+ }
+ // if the device has accessed the url in the past, let it access it now as well, irrespective of device limit.
+ if hasAccessedInPast {
+ return false, nil
+ }
+ count, err := m.FileLinkRepo.GetUniqueAccessCount(ctx, sharedID)
+ if err != nil {
+ return false, stacktrace.Propagate(err, "failed to get unique access count")
+ }
+
+ deviceLimit := int64(collectionSummary.DeviceLimit)
+ if deviceLimit == publicCtrl.DeviceLimitThreshold {
+ deviceLimit = publicCtrl.DeviceLimitThresholdMultiplier * publicCtrl.DeviceLimitThreshold
+ }
+
+ if count >= publicCtrl.DeviceLimitWarningThreshold {
+ m.DiscordController.NotifyPotentialAbuse(
+ fmt.Sprintf("FileLink exceeds warning threshold: {FileID: %d, ShareID: %s}",
+ collectionSummary.FileID, collectionSummary.LinkID))
+ }
+
+ if deviceLimit > 0 && count >= deviceLimit {
+ return true, nil
+ }
+ err = m.FileLinkRepo.RecordAccessHistory(ctx, sharedID, ip, ua)
+ return false, stacktrace.Propagate(err, "failed to record access history")
+}
+
+// validatePassword will verify if the user is provided correct password for the public album
+func (m *FileLinkMiddleware) validatePassword(
+ c *gin.Context,
+ reqPath string,
+ fileLinkRow *ente.FileLinkRow,
+) error {
+ accessTokenJWT := auth.GetAccessTokenJWT(c)
+ if accessTokenJWT == "" {
+ if array.StringInList(reqPath, filePasswordWhiteListedURLs) {
+ return nil
+ }
+ return &ente.ErrPassProtectedResource
+ }
+ return m.FileLinkCtrl.ValidateJWTToken(c, accessTokenJWT, *fileLinkRow.PassHash)
+}
diff --git a/server/pkg/middleware/rate_limit.go b/server/pkg/middleware/rate_limit.go
index 14d3c92a00..bf4c403cfe 100644
--- a/server/pkg/middleware/rate_limit.go
+++ b/server/pkg/middleware/rate_limit.go
@@ -140,6 +140,7 @@ func (r *RateLimitMiddleware) getLimiter(reqPath string, reqMethod string) *limi
reqPath == "/users/verify-email" ||
reqPath == "/user/change-email" ||
reqPath == "/public-collection/verify-password" ||
+ reqPath == "/file-link/verify-password" ||
reqPath == "/family/accept-invite" ||
reqPath == "/users/srp/attributes" ||
(reqPath == "/cast/device-info" && reqMethod == "POST") ||
diff --git a/server/pkg/repo/collection.go b/server/pkg/repo/collection.go
index 3f9af70268..c76b40da50 100644
--- a/server/pkg/repo/collection.go
+++ b/server/pkg/repo/collection.go
@@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"fmt"
+ "github.com/ente-io/museum/pkg/repo/public"
"strconv"
t "time"
@@ -22,13 +23,13 @@ import (
// CollectionRepository defines the methods for inserting, updating and
// retrieving collection entities from the underlying repository
type CollectionRepository struct {
- DB *sql.DB
- FileRepo *FileRepository
- PublicCollectionRepo *PublicCollectionRepository
- TrashRepo *TrashRepository
- SecretEncryptionKey []byte
- QueueRepo *QueueRepository
- LatencyLogger *prometheus.HistogramVec
+ DB *sql.DB
+ FileRepo *FileRepository
+ CollectionLinkRepo *public.CollectionLinkRepo
+ TrashRepo *TrashRepository
+ SecretEncryptionKey []byte
+ QueueRepo *QueueRepository
+ LatencyLogger *prometheus.HistogramVec
}
type SharedCollection struct {
@@ -74,7 +75,7 @@ func (repo *CollectionRepository) Get(collectionID int64) (ente.Collection, erro
c.EncryptedName = encryptedName.String
c.NameDecryptionNonce = nameDecryptionNonce.String
}
- urlMap, err := repo.PublicCollectionRepo.GetCollectionToActivePublicURLMap(context.Background(), []int64{collectionID})
+ urlMap, err := repo.CollectionLinkRepo.GetCollectionToActivePublicURLMap(context.Background(), []int64{collectionID})
if err != nil {
return ente.Collection{}, stacktrace.Propagate(err, "failed to get publicURL info")
}
@@ -174,7 +175,7 @@ pct.access_token, pct.valid_till, pct.device_limit, pct.created_at, pct.updated_
if _, ok := addPublicUrlMap[pctToken.String]; !ok {
addPublicUrlMap[pctToken.String] = true
url := ente.PublicURL{
- URL: repo.PublicCollectionRepo.GetAlbumUrl(pctToken.String),
+ URL: repo.CollectionLinkRepo.GetAlbumUrl(pctToken.String),
DeviceLimit: int(pctDeviceLimit.Int32),
ValidTill: pctValidTill.Int64,
EnableDownload: pctEnableDownload.Bool,
diff --git a/server/pkg/repo/file.go b/server/pkg/repo/file.go
index 2ae4eafdca..50945cf6b1 100644
--- a/server/pkg/repo/file.go
+++ b/server/pkg/repo/file.go
@@ -638,6 +638,16 @@ func (repo *FileRepository) GetFileAttributesForCopy(fileIDs []int64) ([]ente.Fi
return result, nil
}
+func (repo *FileRepository) GetFileAttributes(fileID int64) (*ente.File, error) {
+ rows := repo.DB.QueryRow(`SELECT file_id, owner_id, file_decryption_header, thumbnail_decryption_header, metadata_decryption_header, encrypted_metadata, pub_magic_metadata FROM files WHERE file_id = $1`, fileID)
+ var file ente.File
+ err := rows.Scan(&file.ID, &file.OwnerID, &file.File.DecryptionHeader, &file.Thumbnail.DecryptionHeader, &file.Metadata.DecryptionHeader, &file.Metadata.EncryptedData, &file.PubicMagicMetadata)
+ if err != nil {
+ return nil, stacktrace.Propagate(err, "")
+ }
+ return &file, nil
+}
+
// GetUsage gets the Storage usage of a user
// Deprecated: GetUsage is deprecated, use UsageRepository.GetUsage
func (repo *FileRepository) GetUsage(userID int64) (int64, error) {
diff --git a/server/pkg/repo/public_collection.go b/server/pkg/repo/public/collection_link.go
similarity index 78%
rename from server/pkg/repo/public_collection.go
rename to server/pkg/repo/public/collection_link.go
index f5ae8f2d72..fafcd4cb11 100644
--- a/server/pkg/repo/public_collection.go
+++ b/server/pkg/repo/public/collection_link.go
@@ -1,4 +1,4 @@
-package repo
+package public
import (
"context"
@@ -13,29 +13,29 @@ import (
const BaseShareURL = "https://albums.ente.io/?t=%s"
-// PublicCollectionRepository defines the methods for inserting, updating and
+// CollectionLinkRepo defines the methods for inserting, updating and
// retrieving entities related to public collections
-type PublicCollectionRepository struct {
+type CollectionLinkRepo struct {
DB *sql.DB
albumHost string
}
-// NewPublicCollectionRepository ..
-func NewPublicCollectionRepository(db *sql.DB, albumHost string) *PublicCollectionRepository {
+// NewCollectionLinkRepository ..
+func NewCollectionLinkRepository(db *sql.DB, albumHost string) *CollectionLinkRepo {
if albumHost == "" {
albumHost = "https://albums.ente.io"
}
- return &PublicCollectionRepository{
+ return &CollectionLinkRepo{
DB: db,
albumHost: albumHost,
}
}
-func (pcr *PublicCollectionRepository) GetAlbumUrl(token string) string {
+func (pcr *CollectionLinkRepo) GetAlbumUrl(token string) string {
return fmt.Sprintf("%s/?t=%s", pcr.albumHost, token)
}
-func (pcr *PublicCollectionRepository) Insert(ctx context.Context,
+func (pcr *CollectionLinkRepo) Insert(ctx context.Context,
cID int64, token string, validTill int64, deviceLimit int, enableCollect bool, enableJoin *bool) error {
// default value for enableJoin is true
join := true
@@ -51,7 +51,7 @@ func (pcr *PublicCollectionRepository) Insert(ctx context.Context,
return stacktrace.Propagate(err, "failed to insert")
}
-func (pcr *PublicCollectionRepository) DisableSharing(ctx context.Context, cID int64) error {
+func (pcr *CollectionLinkRepo) DisableSharing(ctx context.Context, cID int64) error {
_, err := pcr.DB.ExecContext(ctx, `UPDATE public_collection_tokens SET is_disabled = true where
collection_id = $1 and is_disabled = false`, cID)
return stacktrace.Propagate(err, "failed to disable sharing")
@@ -59,7 +59,7 @@ func (pcr *PublicCollectionRepository) DisableSharing(ctx context.Context, cID i
// GetCollectionToActivePublicURLMap will return map of collectionID to PublicURLs which are not disabled yet.
// Note: The url could be expired or deviceLimit is already reached
-func (pcr *PublicCollectionRepository) GetCollectionToActivePublicURLMap(ctx context.Context, collectionIDs []int64) (map[int64][]ente.PublicURL, error) {
+func (pcr *CollectionLinkRepo) GetCollectionToActivePublicURLMap(ctx context.Context, collectionIDs []int64) (map[int64][]ente.PublicURL, error) {
rows, err := pcr.DB.QueryContext(ctx, `SELECT collection_id, access_token, valid_till, device_limit, enable_download, enable_collect, enable_join, pw_nonce, mem_limit, ops_limit FROM
public_collection_tokens WHERE collection_id = ANY($1) and is_disabled = FALSE`,
pq.Array(collectionIDs))
@@ -92,26 +92,26 @@ func (pcr *PublicCollectionRepository) GetCollectionToActivePublicURLMap(ctx con
return result, nil
}
-// GetActivePublicCollectionToken will return ente.PublicCollectionToken for given collection ID
+// GetActiveCollectionLinkRow will return ente.CollectionLinkRow for given collection ID
// Note: The token could be expired or deviceLimit is already reached
-func (pcr *PublicCollectionRepository) GetActivePublicCollectionToken(ctx context.Context, collectionID int64) (ente.PublicCollectionToken, error) {
+func (pcr *CollectionLinkRepo) GetActiveCollectionLinkRow(ctx context.Context, collectionID int64) (ente.CollectionLinkRow, error) {
row := pcr.DB.QueryRowContext(ctx, `SELECT id, collection_id, access_token, valid_till, device_limit,
is_disabled, pw_hash, pw_nonce, mem_limit, ops_limit, enable_download, enable_collect, enable_join FROM
public_collection_tokens WHERE collection_id = $1 and is_disabled = FALSE`,
collectionID)
//defer rows.Close()
- ret := ente.PublicCollectionToken{}
+ ret := ente.CollectionLinkRow{}
err := row.Scan(&ret.ID, &ret.CollectionID, &ret.Token, &ret.ValidTill, &ret.DeviceLimit,
&ret.IsDisabled, &ret.PassHash, &ret.Nonce, &ret.MemLimit, &ret.OpsLimit, &ret.EnableDownload, &ret.EnableCollect, &ret.EnableJoin)
if err != nil {
- return ente.PublicCollectionToken{}, stacktrace.Propagate(err, "")
+ return ente.CollectionLinkRow{}, stacktrace.Propagate(err, "")
}
return ret, nil
}
// UpdatePublicCollectionToken will update the row for corresponding public collection token
-func (pcr *PublicCollectionRepository) UpdatePublicCollectionToken(ctx context.Context, pct ente.PublicCollectionToken) error {
+func (pcr *CollectionLinkRepo) UpdatePublicCollectionToken(ctx context.Context, pct ente.CollectionLinkRow) error {
_, err := pcr.DB.ExecContext(ctx, `UPDATE public_collection_tokens SET valid_till = $1, device_limit = $2,
pw_hash = $3, pw_nonce = $4, mem_limit = $5, ops_limit = $6, enable_download = $7, enable_collect = $8, enable_join = $9
where id = $10`,
@@ -119,7 +119,7 @@ func (pcr *PublicCollectionRepository) UpdatePublicCollectionToken(ctx context.C
return stacktrace.Propagate(err, "failed to update public collection token")
}
-func (pcr *PublicCollectionRepository) RecordAbuseReport(ctx context.Context, accessCtx ente.PublicAccessContext,
+func (pcr *CollectionLinkRepo) RecordAbuseReport(ctx context.Context, accessCtx ente.PublicAccessContext,
url string, reason string, details ente.AbuseReportDetails) error {
_, err := pcr.DB.ExecContext(ctx, `INSERT INTO public_abuse_report
(share_id, ip, user_agent, url, reason, details) VALUES ($1, $2, $3, $4, $5, $6)
@@ -128,7 +128,7 @@ func (pcr *PublicCollectionRepository) RecordAbuseReport(ctx context.Context, ac
return stacktrace.Propagate(err, "failed to record abuse report")
}
-func (pcr *PublicCollectionRepository) GetAbuseReportCount(ctx context.Context, accessCtx ente.PublicAccessContext) (int64, error) {
+func (pcr *CollectionLinkRepo) GetAbuseReportCount(ctx context.Context, accessCtx ente.PublicAccessContext) (int64, error) {
row := pcr.DB.QueryRowContext(ctx, `SELECT count(*) FROM public_abuse_report WHERE share_id = $1`, accessCtx.ID)
var count int64 = 0
err := row.Scan(&count)
@@ -138,7 +138,7 @@ func (pcr *PublicCollectionRepository) GetAbuseReportCount(ctx context.Context,
return count, nil
}
-func (pcr *PublicCollectionRepository) GetUniqueAccessCount(ctx context.Context, shareId int64) (int64, error) {
+func (pcr *CollectionLinkRepo) GetUniqueAccessCount(ctx context.Context, shareId int64) (int64, error) {
row := pcr.DB.QueryRowContext(ctx, `SELECT count(*) FROM public_collection_access_history WHERE share_id = $1`, shareId)
var count int64 = 0
err := row.Scan(&count)
@@ -148,7 +148,7 @@ func (pcr *PublicCollectionRepository) GetUniqueAccessCount(ctx context.Context,
return count, nil
}
-func (pcr *PublicCollectionRepository) RecordAccessHistory(ctx context.Context, shareID int64, ip string, ua string) error {
+func (pcr *CollectionLinkRepo) RecordAccessHistory(ctx context.Context, shareID int64, ip string, ua string) error {
_, err := pcr.DB.ExecContext(ctx, `INSERT INTO public_collection_access_history
(share_id, ip, user_agent) VALUES ($1, $2, $3)
ON CONFLICT ON CONSTRAINT unique_access_sid_ip_ua DO NOTHING;`,
@@ -157,7 +157,7 @@ func (pcr *PublicCollectionRepository) RecordAccessHistory(ctx context.Context,
}
// AccessedInPast returns true if the given ip, ua agent combination has accessed the url in the past
-func (pcr *PublicCollectionRepository) AccessedInPast(ctx context.Context, shareID int64, ip string, ua string) (bool, error) {
+func (pcr *CollectionLinkRepo) AccessedInPast(ctx context.Context, shareID int64, ip string, ua string) (bool, error) {
row := pcr.DB.QueryRowContext(ctx, `select share_id from public_collection_access_history where share_id =$1 and ip = $2 and user_agent = $3`,
shareID, ip, ua)
var tempID int64
@@ -168,7 +168,7 @@ func (pcr *PublicCollectionRepository) AccessedInPast(ctx context.Context, share
return true, stacktrace.Propagate(err, "failed to record access history")
}
-func (pcr *PublicCollectionRepository) GetCollectionSummaryByToken(ctx context.Context, accessToken string) (ente.PublicCollectionSummary, error) {
+func (pcr *CollectionLinkRepo) GetCollectionSummaryByToken(ctx context.Context, accessToken string) (ente.PublicCollectionSummary, error) {
row := pcr.DB.QueryRowContext(ctx,
`SELECT sct.id, sct.collection_id, sct.is_disabled, sct.valid_till, sct.device_limit, sct.pw_hash,
sct.created_at, sct.updated_at, count(ah.share_id)
@@ -185,7 +185,7 @@ func (pcr *PublicCollectionRepository) GetCollectionSummaryByToken(ctx context.C
return result, nil
}
-func (pcr *PublicCollectionRepository) GetActivePublicTokenForUser(ctx context.Context, userID int64) ([]int64, error) {
+func (pcr *CollectionLinkRepo) GetActivePublicTokenForUser(ctx context.Context, userID int64) ([]int64, error) {
rows, err := pcr.DB.QueryContext(ctx, `select pt.collection_id from public_collection_tokens pt left join collections c on pt.collection_id = c.collection_id where pt.is_disabled = FALSE and c.owner_id= $1;`, userID)
if err != nil {
return nil, stacktrace.Propagate(err, "")
@@ -204,7 +204,7 @@ func (pcr *PublicCollectionRepository) GetActivePublicTokenForUser(ctx context.C
}
// CleanupAccessHistory public_collection_access_history where public_collection_tokens is disabled and the last updated time is older than 30 days
-func (pcr *PublicCollectionRepository) CleanupAccessHistory(ctx context.Context) error {
+func (pcr *CollectionLinkRepo) CleanupAccessHistory(ctx context.Context) error {
_, err := pcr.DB.ExecContext(ctx, `DELETE FROM public_collection_access_history WHERE share_id IN (SELECT id FROM public_collection_tokens WHERE is_disabled = TRUE AND updated_at < (now_utc_micro_seconds() - (24::BIGINT * 30 * 60 * 60 * 1000 * 1000)))`)
if err != nil {
return stacktrace.Propagate(err, "failed to clean up public collection access history")
diff --git a/server/pkg/repo/public/file_link.go b/server/pkg/repo/public/file_link.go
new file mode 100644
index 0000000000..fbb2f4e072
--- /dev/null
+++ b/server/pkg/repo/public/file_link.go
@@ -0,0 +1,218 @@
+package public
+
+import (
+ "context"
+ "database/sql"
+ "errors"
+ "fmt"
+ "github.com/ente-io/museum/ente/base"
+ "github.com/lib/pq"
+ "github.com/spf13/viper"
+
+ "github.com/ente-io/museum/ente"
+ "github.com/ente-io/stacktrace"
+)
+
+// FileLinkRepository defines the methods for inserting, updating and
+// retrieving entities related to public file
+type FileLinkRepository struct {
+ DB *sql.DB
+ photoHost string
+ lockerHost string
+}
+
+// NewFileLinkRepo ..
+func NewFileLinkRepo(db *sql.DB) *FileLinkRepository {
+ albumHost := viper.GetString("apps.public-albums")
+ if albumHost == "" {
+ albumHost = "https://albums.ente.io"
+ }
+ lockerHost := viper.GetString("apps.public-locker")
+ if lockerHost == "" {
+ lockerHost = "https://locker.ente.io"
+ }
+ return &FileLinkRepository{
+ DB: db,
+ photoHost: albumHost,
+ lockerHost: lockerHost,
+ }
+}
+
+func (pcr *FileLinkRepository) PhotoLink(token string) string {
+ return fmt.Sprintf("%s/?t=%s", pcr.photoHost, token)
+}
+
+func (pcr *FileLinkRepository) LockerFileLink(token string) string {
+ return fmt.Sprintf("%s/?t=%s", pcr.lockerHost, token)
+}
+
+func (pcr *FileLinkRepository) Insert(
+ ctx context.Context,
+ fileID int64,
+ ownerID int64,
+ token string,
+ app ente.App,
+) (*string, error) {
+ id, err := base.NewID("pft")
+ if err != nil {
+ return nil, stacktrace.Propagate(err, "failed to generate new ID for public file token")
+ }
+ _, err = pcr.DB.ExecContext(ctx, `INSERT INTO public_file_tokens
+ (id, file_id, owner_id, access_token, app) VALUES ($1, $2, $3, $4, $5)`,
+ id, fileID, ownerID, token, string(app))
+ if err != nil {
+ if err.Error() == "pq: duplicate key value violates unique constraint \"public_active_file_link_unique_idx\"" {
+ return nil, ente.ErrActiveLinkAlreadyExists
+ }
+ return nil, stacktrace.Propagate(err, "failed to insert")
+ }
+ return id, nil
+}
+
+// GetActiveFileUrlToken will return ente.CollectionLinkRow for given collection ID
+// Note: The token could be expired or deviceLimit is already reached
+func (pcr *FileLinkRepository) GetActiveFileUrlToken(ctx context.Context, fileID int64) (*ente.FileLinkRow, error) {
+ row := pcr.DB.QueryRowContext(ctx, `SELECT id, file_id, owner_id, access_token, valid_till, device_limit,
+ is_disabled, pw_hash, pw_nonce, mem_limit, ops_limit, enable_download FROM
+ public_file_tokens WHERE file_id = $1 and is_disabled = FALSE`,
+ fileID)
+
+ ret := ente.FileLinkRow{}
+ err := row.Scan(&ret.LinkID, &ret.FileID, ret.OwnerID, &ret.Token, &ret.ValidTill, &ret.DeviceLimit,
+ &ret.IsDisabled, &ret.PassHash, &ret.Nonce, &ret.MemLimit, &ret.OpsLimit, &ret.EnableDownload)
+ if err != nil {
+ return nil, stacktrace.Propagate(err, "")
+ }
+ return &ret, nil
+}
+func (pcr *FileLinkRepository) GetFileUrls(ctx context.Context, userID int64, sinceTime int64, limit int64, app ente.App) ([]*ente.FileLinkRow, error) {
+ if limit <= 0 {
+ limit = 500
+ }
+ query := `SELECT id, file_id, owner_id, is_disabled, valid_till, device_limit, enable_download, pw_hash, pw_nonce, mem_limit, ops_limit,
+ created_at, updated_at FROM public_file_tokens
+ WHERE owner_id = $1 AND created_at > $2 AND app = $3 ORDER BY updated_at DESC LIMIT $4`
+ rows, err := pcr.DB.QueryContext(ctx, query, userID, sinceTime, string(app), limit)
+ if err != nil {
+ return nil, stacktrace.Propagate(err, "failed to get public file urls")
+ }
+ defer rows.Close()
+
+ var result []*ente.FileLinkRow
+ for rows.Next() {
+ var row ente.FileLinkRow
+ err = rows.Scan(&row.LinkID, &row.FileID, &row.OwnerID, &row.IsDisabled,
+ &row.ValidTill, &row.DeviceLimit, &row.EnableDownload,
+ &row.PassHash, &row.Nonce, &row.MemLimit,
+ &row.OpsLimit, &row.CreatedAt, &row.UpdatedAt)
+ if err != nil {
+ return nil, stacktrace.Propagate(err, "failed to scan public file url row")
+ }
+ result = append(result, &row)
+ }
+ return result, nil
+}
+
+func (pcr *FileLinkRepository) DisableLinkForFiles(ctx context.Context, fileIDs []int64) error {
+ if len(fileIDs) == 0 {
+ return nil
+ }
+ query := `UPDATE public_file_tokens SET is_disabled = TRUE WHERE file_id = ANY($1)`
+ _, err := pcr.DB.ExecContext(ctx, query, pq.Array(fileIDs))
+ if err != nil {
+ return stacktrace.Propagate(err, "failed to disable public file links")
+ }
+ return nil
+}
+
+// DisableLinksForUser will disable all public file links for the given user
+func (pcr *FileLinkRepository) DisableLinksForUser(ctx context.Context, userID int64) error {
+ _, err := pcr.DB.ExecContext(ctx, `UPDATE public_file_tokens SET is_disabled = TRUE WHERE owner_id = $1`, userID)
+ if err != nil {
+ return stacktrace.Propagate(err, "failed to disable public file link")
+ }
+ return nil
+}
+
+func (pcr *FileLinkRepository) GetFileUrlRowByToken(ctx context.Context, accessToken string) (*ente.FileLinkRow, error) {
+ row := pcr.DB.QueryRowContext(ctx,
+ `SELECT id, file_id, owner_id, is_disabled, valid_till, device_limit, enable_download, pw_hash, pw_nonce, mem_limit, ops_limit
+ created_at, updated_at
+ from public_file_tokens
+ where access_token = $1
+`, accessToken)
+ var result = ente.FileLinkRow{}
+ err := row.Scan(&result.LinkID, &result.FileID, &result.OwnerID, &result.IsDisabled, &result.EnableDownload, &result.ValidTill, &result.DeviceLimit, &result.PassHash, &result.Nonce, &result.MemLimit, &result.OpsLimit, &result.CreatedAt, &result.UpdatedAt)
+ if err != nil {
+ if err == sql.ErrNoRows {
+ return nil, ente.ErrNotFound
+ }
+ return nil, stacktrace.Propagate(err, "failed to get public file url summary by token")
+ }
+ return &result, nil
+}
+
+func (pcr *FileLinkRepository) GetFileUrlRowByFileID(ctx context.Context, fileID int64) (*ente.FileLinkRow, error) {
+ row := pcr.DB.QueryRowContext(ctx,
+ `SELECT id, file_id, access_token, owner_id, is_disabled, enable_download, valid_till, device_limit, pw_hash, pw_nonce, mem_limit, ops_limit,
+ created_at, updated_at
+ from public_file_tokens
+ where file_id = $1 and is_disabled = FALSE`, fileID)
+ var result = ente.FileLinkRow{}
+ err := row.Scan(&result.LinkID, &result.FileID, &result.Token, &result.OwnerID, &result.IsDisabled, &result.EnableDownload, &result.ValidTill, &result.DeviceLimit, &result.PassHash, &result.Nonce, &result.MemLimit, &result.OpsLimit, &result.CreatedAt, &result.UpdatedAt)
+ if err != nil {
+ if err == sql.ErrNoRows {
+ return nil, ente.ErrNotFound
+ }
+ return nil, stacktrace.Propagate(err, "failed to get public file url summary by file ID")
+ }
+ return &result, nil
+}
+
+// UpdateLink will update the row for corresponding public file token
+func (pcr *FileLinkRepository) UpdateLink(ctx context.Context, pct ente.FileLinkRow) error {
+ _, err := pcr.DB.ExecContext(ctx, `UPDATE public_file_tokens SET valid_till = $1, device_limit = $2,
+ pw_hash = $3, pw_nonce = $4, mem_limit = $5, ops_limit = $6, enable_download = $7
+ where id = $8`,
+ pct.ValidTill, pct.DeviceLimit, pct.PassHash, pct.Nonce, pct.MemLimit, pct.OpsLimit, pct.EnableDownload, pct.LinkID)
+ return stacktrace.Propagate(err, "failed to update public file token")
+}
+
+func (pcr *FileLinkRepository) GetUniqueAccessCount(ctx context.Context, linkId string) (int64, error) {
+ row := pcr.DB.QueryRowContext(ctx, `SELECT count(*) FROM public_file_tokens_access_history WHERE id = $1`, linkId)
+ var count int64 = 0
+ err := row.Scan(&count)
+ if err != nil {
+ return -1, stacktrace.Propagate(err, "")
+ }
+ return count, nil
+}
+
+func (pcr *FileLinkRepository) RecordAccessHistory(ctx context.Context, shareID string, ip string, ua string) error {
+ _, err := pcr.DB.ExecContext(ctx, `INSERT INTO public_file_tokens_access_history
+ (id, ip, user_agent) VALUES ($1, $2, $3)
+ ON CONFLICT ON CONSTRAINT unique_access_id_ip_ua DO NOTHING;`,
+ shareID, ip, ua)
+ return stacktrace.Propagate(err, "failed to record access history")
+}
+
+// AccessedInPast returns true if the given ip, ua agent combination has accessed the url in the past
+func (pcr *FileLinkRepository) AccessedInPast(ctx context.Context, shareID string, ip string, ua string) (bool, error) {
+ row := pcr.DB.QueryRowContext(ctx, `select id from public_file_tokens_access_history where id =$1 and ip = $2 and user_agent = $3`,
+ shareID, ip, ua)
+ var tempID int64
+ err := row.Scan(&tempID)
+ if errors.Is(err, sql.ErrNoRows) {
+ return false, nil
+ }
+ return true, stacktrace.Propagate(err, "failed to record access history")
+}
+
+// CleanupAccessHistory public_file_tokens_access_history where public_collection_tokens is disabled and the last updated time is older than 30 days
+func (pcr *FileLinkRepository) CleanupAccessHistory(ctx context.Context) error {
+ _, err := pcr.DB.ExecContext(ctx, `DELETE FROM public_file_tokens_access_history WHERE id IN (SELECT id FROM public_file_tokens WHERE is_disabled = TRUE AND updated_at < (now_utc_micro_seconds() - (24::BIGINT * 30 * 60 * 60 * 1000 * 1000)))`)
+ if err != nil {
+ return stacktrace.Propagate(err, "failed to clean up public file access history")
+ }
+ return nil
+}
diff --git a/server/pkg/repo/trash.go b/server/pkg/repo/trash.go
index f1ab3c2289..3781e0716e 100644
--- a/server/pkg/repo/trash.go
+++ b/server/pkg/repo/trash.go
@@ -5,6 +5,7 @@ import (
"database/sql"
"errors"
"fmt"
+ "github.com/ente-io/museum/pkg/repo/public"
"strings"
"github.com/ente-io/museum/ente"
@@ -32,10 +33,11 @@ type FileWithUpdatedAt struct {
}
type TrashRepository struct {
- DB *sql.DB
- ObjectRepo *ObjectRepository
- FileRepo *FileRepository
- QueueRepo *QueueRepository
+ DB *sql.DB
+ ObjectRepo *ObjectRepository
+ FileRepo *FileRepository
+ QueueRepo *QueueRepository
+ FileLinkRepo *public.FileLinkRepository
}
func (t *TrashRepository) InsertItems(ctx context.Context, tx *sql.Tx, userID int64, items []ente.TrashItemRequest) error {
@@ -156,6 +158,13 @@ func (t *TrashRepository) TrashFiles(fileIDs []int64, userID int64, trash ente.T
return stacktrace.Propagate(err, "")
}
err = tx.Commit()
+
+ if err == nil {
+ removeLinkErr := t.FileLinkRepo.DisableLinkForFiles(ctx, fileIDs)
+ if removeLinkErr != nil {
+ return stacktrace.Propagate(removeLinkErr, "failed to disable file links for files being trashed")
+ }
+ }
return stacktrace.Propagate(err, "")
}
diff --git a/server/pkg/utils/auth/auth.go b/server/pkg/utils/auth/auth.go
index 6f8091998b..8b52808e36 100644
--- a/server/pkg/utils/auth/auth.go
+++ b/server/pkg/utils/auth/auth.go
@@ -17,8 +17,9 @@ import (
)
const (
- PublicAccessKey = "X-Public-Access-ID"
- CastContext = "X-Cast-Context"
+ PublicAccessKey = "X-Public-Access-ID"
+ FileLinkAccessKey = "X-Public-FileLink-Access-ID"
+ CastContext = "X-Cast-Context"
)
// GenerateRandomBytes returns securely generated random bytes.
@@ -120,6 +121,8 @@ func GetCastToken(c *gin.Context) string {
return token
}
+// GetAccessTokenJWT fetches the JWT access token from the request header or query parameters.
+// This token is issued by server on password verification of links that are protected by password.
func GetAccessTokenJWT(c *gin.Context) string {
token := c.GetHeader("X-Auth-Access-Token-JWT")
if token == "" {
@@ -132,6 +135,10 @@ func MustGetPublicAccessContext(c *gin.Context) ente.PublicAccessContext {
return c.MustGet(PublicAccessKey).(ente.PublicAccessContext)
}
+func MustGetFileLinkAccessContext(c *gin.Context) *ente.FileLinkAccessContext {
+ return c.MustGet(FileLinkAccessKey).(*ente.FileLinkAccessContext)
+}
+
func GetCastCtx(c *gin.Context) cast.AuthContext {
return c.MustGet(CastContext).(cast.AuthContext)
}