Compare commits
142 Commits
auth-v2.0.
...
update_doc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6146350aae | ||
|
|
f8d8550b10 | ||
|
|
bc4fa44edd | ||
|
|
92de88e778 | ||
|
|
7814cbcc91 | ||
|
|
518b947808 | ||
|
|
077ba04664 | ||
|
|
e42422407c | ||
|
|
2711a227fc | ||
|
|
4325de6fde | ||
|
|
c7d7d436c3 | ||
|
|
f7077c2b11 | ||
|
|
8f525cb88d | ||
|
|
be3b4dc7ba | ||
|
|
0c1c0ad400 | ||
|
|
773f4cdca2 | ||
|
|
96bb79b9e9 | ||
|
|
d5164693ff | ||
|
|
26b162c8dc | ||
|
|
297148dc67 | ||
|
|
46522c329c | ||
|
|
8358eef34e | ||
|
|
4326409046 | ||
|
|
687d575bf4 | ||
|
|
0678e3129a | ||
|
|
b164b0df21 | ||
|
|
0d38346722 | ||
|
|
51d3238a52 | ||
|
|
ddd89aa1d1 | ||
|
|
f21a627a71 | ||
|
|
063e980280 | ||
|
|
d7d42b6854 | ||
|
|
260a7fbcaa | ||
|
|
55e0ec39ed | ||
|
|
9c04a7102b | ||
|
|
a5e6f0cc30 | ||
|
|
2322b41f67 | ||
|
|
685e75d97d | ||
|
|
cde87716a1 | ||
|
|
dff0af3397 | ||
|
|
ca771993ee | ||
|
|
c8b9b4cd8f | ||
|
|
d7cd2cecbc | ||
|
|
e219197e2f | ||
|
|
3eb84ceba8 | ||
|
|
e358738c35 | ||
|
|
b15901df67 | ||
|
|
ee7d90e55b | ||
|
|
22b744aa96 | ||
|
|
5d01931402 | ||
|
|
206ad46950 | ||
|
|
9b6e47d291 | ||
|
|
70cddfdf0b | ||
|
|
c0a2347b80 | ||
|
|
dd556f8f72 | ||
|
|
2fc1a96c8b | ||
|
|
8a7f64b889 | ||
|
|
c153b28ed6 | ||
|
|
a9557df240 | ||
|
|
8c23090abd | ||
|
|
6efe2cd5fd | ||
|
|
4feea01879 | ||
|
|
51f19cf2fd | ||
|
|
dbc50760af | ||
|
|
638de0a769 | ||
|
|
0ab1c0ee89 | ||
|
|
cb8738287a | ||
|
|
ddedaf2d0e | ||
|
|
cfa4077b5c | ||
|
|
1d2de8d9b8 | ||
|
|
e843ea6669 | ||
|
|
9f2a66e0ef | ||
|
|
944ef2e564 | ||
|
|
00f45ef39d | ||
|
|
84926cbee1 | ||
|
|
27c1b66c08 | ||
|
|
027ae1cfb9 | ||
|
|
621f81355b | ||
|
|
849b61c5cf | ||
|
|
267ad0d11f | ||
|
|
e272722d6e | ||
|
|
a73f3cc52b | ||
|
|
748d18cc2a | ||
|
|
88741083fe | ||
|
|
829406fa62 | ||
|
|
df13eac6ef | ||
|
|
25dda3598c | ||
|
|
9a8e76b494 | ||
|
|
c6120f33de | ||
|
|
200504dc01 | ||
|
|
2a33707db2 | ||
|
|
3fd5af8134 | ||
|
|
bb68b22adb | ||
|
|
5accf4c6a8 | ||
|
|
28335700e3 | ||
|
|
e3826695c5 | ||
|
|
6fdfa24e89 | ||
|
|
fb2abd8afc | ||
|
|
bd84b54c5a | ||
|
|
af4eaac158 | ||
|
|
de166645b0 | ||
|
|
ae67f0d67b | ||
|
|
fab16a7947 | ||
|
|
9711e0e29e | ||
|
|
d7292dc670 | ||
|
|
6930aaf220 | ||
|
|
12903a3748 | ||
|
|
412c872266 | ||
|
|
8fab6b5e48 | ||
|
|
4af3030c81 | ||
|
|
c32f0a28f1 | ||
|
|
38e8f7c8d7 | ||
|
|
72aa597f85 | ||
|
|
f759ce07ae | ||
|
|
976a76ae23 | ||
|
|
89d761a096 | ||
|
|
e667eef951 | ||
|
|
192caedeb9 | ||
|
|
fc482609b6 | ||
|
|
e1e0c45d88 | ||
|
|
8f384247ba | ||
|
|
b0d396a5bd | ||
|
|
2354f5bc7e | ||
|
|
73b4f54d42 | ||
|
|
ccd9e2ecaf | ||
|
|
4f3d9d0798 | ||
|
|
5ffa8ffe2b | ||
|
|
50cb7f7aaf | ||
|
|
8daa7d8a8e | ||
|
|
5dbc300056 | ||
|
|
c7a4507f96 | ||
|
|
b812827480 | ||
|
|
f9051c94da | ||
|
|
4c3642526e | ||
|
|
7c86e8f903 | ||
|
|
b4cf5761fa | ||
|
|
881ece525f | ||
|
|
5acef45118 | ||
|
|
96d1b09147 | ||
|
|
fc390d69c7 | ||
|
|
c49cee8be6 | ||
|
|
8c9a11fc62 |
3
.gitattributes
vendored
Normal file
3
.gitattributes
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Set line endings of shell scripts to LF, even on Windows, otherwise execution
|
||||
# within Docker fails.
|
||||
*.sh text eol=lf
|
||||
6
.github/workflows/auth-lint.yml
vendored
6
.github/workflows/auth-lint.yml
vendored
@@ -1,11 +1,9 @@
|
||||
name: "Lint (auth)"
|
||||
|
||||
on:
|
||||
# Run on every push to branches (this also covers pull requests)
|
||||
# Run on every push to a branch other than main that changes auth/
|
||||
push:
|
||||
# See: [Note: Specify branch when specifying a path filter]
|
||||
branches: ["**"]
|
||||
# Only run if something changes in these paths
|
||||
branches-ignore: [main]
|
||||
paths:
|
||||
- "auth/**"
|
||||
- ".github/workflows/auth-lint.yml"
|
||||
|
||||
2
.github/workflows/cli-release.yml
vendored
2
.github/workflows/cli-release.yml
vendored
@@ -49,6 +49,6 @@ jobs:
|
||||
project_path: "./cli"
|
||||
pre_command: export CGO_ENABLED=0
|
||||
build_flags: "-trimpath"
|
||||
ldflags: "-s -w"
|
||||
ldflags: "-X main.AppVersion=${{ github.ref_name }} -s -w"
|
||||
md5sum: false
|
||||
sha256sum: true
|
||||
|
||||
37
.github/workflows/docs-verify-build.yml
vendored
Normal file
37
.github/workflows/docs-verify-build.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: "Verify build (docs)"
|
||||
|
||||
# Preflight build of docs. This allows us to ensure that yarn build is
|
||||
# succeeding before we merge the PR into main.
|
||||
|
||||
on:
|
||||
# Run on every push to a branch other than main that changes docs/
|
||||
push:
|
||||
branches-ignore: [main]
|
||||
paths:
|
||||
- "docs/**"
|
||||
- ".github/workflows/docs-verify-build.yml"
|
||||
|
||||
jobs:
|
||||
verify-build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: docs
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup node and enable yarn caching
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: "yarn"
|
||||
cache-dependency-path: "docs/yarn.lock"
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
|
||||
- name: Build production site
|
||||
run: yarn build
|
||||
6
.github/workflows/mobile-lint.yml
vendored
6
.github/workflows/mobile-lint.yml
vendored
@@ -1,11 +1,9 @@
|
||||
name: "Lint (mobile)"
|
||||
|
||||
on:
|
||||
# Run on every push (this also covers pull requests)
|
||||
# Run on every push to a branch other than main that changes mobile/
|
||||
push:
|
||||
# See: [Note: Specify branch when specifying a path filter]
|
||||
branches: ["**"]
|
||||
# Only run if something changes in these paths
|
||||
branches-ignore: [main]
|
||||
paths:
|
||||
- "mobile/**"
|
||||
- ".github/workflows/mobile-lint.yml"
|
||||
|
||||
6
.github/workflows/mobile-release.yml
vendored
6
.github/workflows/mobile-release.yml
vendored
@@ -39,7 +39,9 @@ jobs:
|
||||
encodedString: ${{ secrets.SIGNING_KEY_PHOTOS }}
|
||||
|
||||
- name: Build independent APK
|
||||
run: flutter build apk --release --flavor independent && mv build/app/outputs/flutter-apk/app-independent-release.apk build/app/outputs/flutter-apk/ente.apk
|
||||
run: |
|
||||
flutter build apk --release --flavor independent
|
||||
mv build/app/outputs/flutter-apk/app-independent-release.apk build/app/outputs/flutter-apk/ente-${{ github.ref_name }}.apk
|
||||
env:
|
||||
SIGNING_KEY_PATH: "/home/runner/work/_temp/keystore/ente_photos_key.jks"
|
||||
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS_PHOTOS }}
|
||||
@@ -52,5 +54,5 @@ jobs:
|
||||
- name: Create a draft GitHub release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
artifacts: "mobile/build/app/outputs/flutter-apk/ente.apk,mobile/build/app/outputs/flutter-apk/sha256sum"
|
||||
artifacts: "mobile/build/app/outputs/flutter-apk/ente-${{ github.ref_name }}.apk,mobile/build/app/outputs/flutter-apk/sha256sum"
|
||||
draft: true
|
||||
|
||||
6
.github/workflows/server-lint.yml
vendored
6
.github/workflows/server-lint.yml
vendored
@@ -1,11 +1,9 @@
|
||||
name: "Lint (server)"
|
||||
|
||||
on:
|
||||
# Run on every push (this also covers pull requests)
|
||||
# Run on every push to a branch other than main that changes server/
|
||||
push:
|
||||
# See: [Note: Specify branch when specifying a path filter]
|
||||
branches: ["**"]
|
||||
# Only run if something changes in these paths
|
||||
branches-ignore: [main]
|
||||
paths:
|
||||
- "server/**"
|
||||
- ".github/workflows/server-lint.yml"
|
||||
|
||||
15
.github/workflows/web-lint.yml
vendored
15
.github/workflows/web-lint.yml
vendored
@@ -1,20 +1,9 @@
|
||||
name: "Lint (web)"
|
||||
|
||||
on:
|
||||
# Run on every push (this also covers pull requests)
|
||||
# Run on every push to a branch other than main that changes web/
|
||||
push:
|
||||
# [Note: Specify branch when specifying a path filter]
|
||||
#
|
||||
# Path filters are ignored for tag pushes, which causes this workflow to
|
||||
# always run when we push a tag. Defining an explicit branch solves the
|
||||
# issue. From GitHub's docs:
|
||||
#
|
||||
# > if you define both branches/branches-ignore and paths/paths-ignore,
|
||||
# > the workflow will only run when both filters are satisfied.
|
||||
#
|
||||
# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions
|
||||
branches: ["**"]
|
||||
# Only run if something changes in these paths
|
||||
branches-ignore: [main]
|
||||
paths:
|
||||
- "web/**"
|
||||
- ".github/workflows/web-lint.yml"
|
||||
|
||||
2
.github/workflows/web-nightly.yml
vendored
2
.github/workflows/web-nightly.yml
vendored
@@ -80,6 +80,8 @@ jobs:
|
||||
|
||||
- name: Build photos
|
||||
run: yarn build:photos
|
||||
env:
|
||||
NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT: https://albums.ente.sh
|
||||
|
||||
- name: Publish photos
|
||||
uses: cloudflare/pages-action@1
|
||||
|
||||
52
.github/workflows/web-preview.yml
vendored
Normal file
52
.github/workflows/web-preview.yml
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
name: "Preview (web)"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
app:
|
||||
description: "App to build and deploy"
|
||||
type: choice
|
||||
required: true
|
||||
default: "photos"
|
||||
options:
|
||||
- "accounts"
|
||||
- "auth"
|
||||
- "cast"
|
||||
- "photos"
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: web
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup node and enable yarn caching
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: "yarn"
|
||||
cache-dependency-path: "docs/yarn.lock"
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
|
||||
- name: Build ${{ inputs.app }}
|
||||
run: yarn build:${{ inputs.app }}
|
||||
|
||||
- name: Publish ${{ inputs.app }} to preview
|
||||
uses: cloudflare/pages-action@1
|
||||
with:
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
projectName: ente
|
||||
branch: preview
|
||||
directory: web/apps/${{ inputs.app }}/out
|
||||
wranglerVersion: "3"
|
||||
4
.gitmodules
vendored
4
.gitmodules
vendored
@@ -9,10 +9,6 @@
|
||||
[submodule "auth/assets/simple-icons"]
|
||||
path = auth/assets/simple-icons
|
||||
url = https://github.com/simple-icons/simple-icons.git
|
||||
[submodule "desktop/thirdparty/next-electron-server"]
|
||||
path = desktop/thirdparty/next-electron-server
|
||||
url = https://github.com/ente-io/next-electron-server.git
|
||||
branch = desktop
|
||||
[submodule "mobile/thirdparty/flutter"]
|
||||
path = mobile/thirdparty/flutter
|
||||
url = https://github.com/flutter/flutter.git
|
||||
|
||||
@@ -37,8 +37,7 @@
|
||||
{
|
||||
"title": "BorgBase",
|
||||
"altNames": ["borg"],
|
||||
"slug": "BorgBase",
|
||||
"hex": "222C31"
|
||||
"slug": "BorgBase"
|
||||
},
|
||||
{
|
||||
"title": "Brave Creators",
|
||||
@@ -109,8 +108,7 @@
|
||||
{
|
||||
"title": "Gosuslugi",
|
||||
"altNames": ["Госуслуги"],
|
||||
"slug": "Gosuslugi",
|
||||
"hex": "EE2F53"
|
||||
"slug": "Gosuslugi"
|
||||
},
|
||||
{
|
||||
"title": "Healthchecks.io",
|
||||
@@ -127,13 +125,11 @@
|
||||
},
|
||||
{
|
||||
"title": "IVPN",
|
||||
"slug": "IVPN",
|
||||
"hex": "FA3243"
|
||||
"slug": "IVPN"
|
||||
},
|
||||
{
|
||||
"title": "IceDrive",
|
||||
"slug": "Icedrive",
|
||||
"hex": "1F4FD0"
|
||||
"slug": "Icedrive"
|
||||
},
|
||||
{
|
||||
"title": "Jagex",
|
||||
@@ -154,8 +150,7 @@
|
||||
"title": "Kite"
|
||||
},
|
||||
{
|
||||
"title": "Koofr",
|
||||
"hex": "71BA05"
|
||||
"title": "Koofr"
|
||||
},
|
||||
{
|
||||
"title": "Kraken",
|
||||
@@ -184,8 +179,7 @@
|
||||
{
|
||||
"title": "Murena",
|
||||
"altNames": ["eCloud"],
|
||||
"slug": "ecloud",
|
||||
"hex": "EC6A55"
|
||||
"slug": "ecloud"
|
||||
},
|
||||
{
|
||||
"title": "Microsoft"
|
||||
@@ -230,8 +224,7 @@
|
||||
},
|
||||
{
|
||||
"title": "pCloud",
|
||||
"slug": "pCloud",
|
||||
"hex": "1EBCC5"
|
||||
"slug": "pCloud"
|
||||
},
|
||||
{
|
||||
"title": "Peerberry",
|
||||
@@ -371,8 +364,7 @@
|
||||
{
|
||||
"title": "Yandex",
|
||||
"altNames": ["Ya", "Яндекс"],
|
||||
"slug": "Yandex",
|
||||
"hex": "FC3F1D"
|
||||
"slug": "Yandex"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -71,8 +71,6 @@ PODS:
|
||||
- move_to_background (0.0.1):
|
||||
- Flutter
|
||||
- MTBBarcodeScanner (5.0.11)
|
||||
- open_filex (0.0.2):
|
||||
- Flutter
|
||||
- OrderedSet (5.0.0)
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
@@ -126,7 +124,6 @@ DEPENDENCIES:
|
||||
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
||||
- local_auth_ios (from `.symlinks/plugins/local_auth_ios/ios`)
|
||||
- move_to_background (from `.symlinks/plugins/move_to_background/ios`)
|
||||
- open_filex (from `.symlinks/plugins/open_filex/ios`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- privacy_screen (from `.symlinks/plugins/privacy_screen/ios`)
|
||||
@@ -183,8 +180,6 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/local_auth_ios/ios"
|
||||
move_to_background:
|
||||
:path: ".symlinks/plugins/move_to_background/ios"
|
||||
open_filex:
|
||||
:path: ".symlinks/plugins/open_filex/ios"
|
||||
package_info_plus:
|
||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||
path_provider_foundation:
|
||||
@@ -214,7 +209,7 @@ SPEC CHECKSUMS:
|
||||
file_picker: ce3938a0df3cc1ef404671531facef740d03f920
|
||||
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
|
||||
fk_user_agent: 1f47ec39291e8372b1d692b50084b0d54103c545
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||
flutter_email_sender: 02d7443217d8c41483223627972bfdc09f74276b
|
||||
flutter_inappwebview: acd4fc0f012cefd09015000c241137d82f01ba62
|
||||
flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743
|
||||
@@ -226,7 +221,6 @@ SPEC CHECKSUMS:
|
||||
local_auth_ios: c6cf091ded637a88f24f86a8875d8b0f526e2605
|
||||
move_to_background: 39a5b79b26d577b0372cbe8a8c55e7aa9fcd3a2d
|
||||
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
|
||||
open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4
|
||||
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
|
||||
package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7
|
||||
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
|
||||
|
||||
@@ -144,7 +144,8 @@
|
||||
"enterCodeHint": "Enter the 6-digit code from\nyour authenticator app",
|
||||
"lostDeviceTitle": "Lost device?",
|
||||
"twoFactorAuthTitle": "Two-factor authentication",
|
||||
"passkeyAuthTitle": "Passkey authentication",
|
||||
"passkeyAuthTitle": "Passkey verification",
|
||||
"verifyPasskey": "Verify passkey",
|
||||
"recoverAccount": "Recover account",
|
||||
"enterRecoveryKeyHint": "Enter your recovery key",
|
||||
"recover": "Recover",
|
||||
@@ -407,7 +408,7 @@
|
||||
"hearUsWhereTitle": "How did you hear about Ente? (optional)",
|
||||
"hearUsExplanation": "We don't track app installs. It'd help if you told us where you found us!",
|
||||
"waitingForBrowserRequest": "Waiting for browser request...",
|
||||
"launchPasskeyUrlAgain": "Launch passkey URL again",
|
||||
"waitingForVerification": "Waiting for verification...",
|
||||
"passkey": "Passkey",
|
||||
"developerSettingsWarning":"Are you sure that you want to modify Developer settings?",
|
||||
"developerSettings": "Developer settings",
|
||||
|
||||
13
auth/lib/models/account/two_factor.dart
Normal file
13
auth/lib/models/account/two_factor.dart
Normal file
@@ -0,0 +1,13 @@
|
||||
enum TwoFactorType { totp, passkey }
|
||||
|
||||
// ToString for TwoFactorType
|
||||
String twoFactorTypeToString(TwoFactorType type) {
|
||||
switch (type) {
|
||||
case TwoFactorType.totp:
|
||||
return "totp";
|
||||
case TwoFactorType.passkey:
|
||||
return "passkey";
|
||||
default:
|
||||
return type.name;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,28 @@ class PasskeyService {
|
||||
return response.data!["accountsToken"] as String;
|
||||
}
|
||||
|
||||
Future<bool> isPasskeyRecoveryEnabled() async {
|
||||
final response = await _enteDio.get(
|
||||
"/users/two-factor/recovery-status",
|
||||
);
|
||||
return response.data!["isPasskeyRecoveryEnabled"] as bool;
|
||||
}
|
||||
|
||||
Future<void> configurePasskeyRecovery(
|
||||
String secret,
|
||||
String userEncryptedSecret,
|
||||
String userSecretNonce,
|
||||
) async {
|
||||
await _enteDio.post(
|
||||
"/users/two-factor/passkeys/configure-recovery",
|
||||
data: {
|
||||
"secret": secret,
|
||||
"userSecretCipher": userEncryptedSecret,
|
||||
"userSecretNonce": userSecretNonce,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> openPasskeyPage(BuildContext context) async {
|
||||
try {
|
||||
final jwtToken = await getJwtToken();
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:ente_auth/core/event_bus.dart';
|
||||
import 'package:ente_auth/core/network.dart';
|
||||
import 'package:ente_auth/events/user_details_changed_event.dart';
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/models/account/two_factor.dart';
|
||||
import 'package:ente_auth/models/api/user/srp.dart';
|
||||
import 'package:ente_auth/models/delete_account.dart';
|
||||
import 'package:ente_auth/models/key_attributes.dart';
|
||||
@@ -762,7 +763,11 @@ class UserService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> recoverTwoFactor(BuildContext context, String sessionID) async {
|
||||
Future<void> recoverTwoFactor(
|
||||
BuildContext context,
|
||||
String sessionID,
|
||||
TwoFactorType type,
|
||||
) async {
|
||||
final dialog = createProgressDialog(context, context.l10n.pleaseWait);
|
||||
await dialog.show();
|
||||
try {
|
||||
@@ -770,6 +775,7 @@ class UserService {
|
||||
_config.getHttpEndpoint() + "/users/two-factor/recover",
|
||||
queryParameters: {
|
||||
"sessionID": sessionID,
|
||||
"twoFactorType": twoFactorTypeToString(type),
|
||||
},
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
@@ -778,6 +784,7 @@ class UserService {
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return TwoFactorRecoveryPage(
|
||||
type,
|
||||
sessionID,
|
||||
response.data["encryptedSecret"],
|
||||
response.data["secretDecryptionNonce"],
|
||||
@@ -788,6 +795,7 @@ class UserService {
|
||||
);
|
||||
}
|
||||
} on DioError catch (e) {
|
||||
await dialog.hide();
|
||||
_logger.severe(e);
|
||||
if (e.response != null && e.response!.statusCode == 404) {
|
||||
showToast(context, context.l10n.sessionExpired);
|
||||
@@ -809,6 +817,7 @@ class UserService {
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
await dialog.hide();
|
||||
_logger.severe(e);
|
||||
// ignore: unawaited_futures
|
||||
showErrorDialog(
|
||||
@@ -823,6 +832,7 @@ class UserService {
|
||||
|
||||
Future<void> removeTwoFactor(
|
||||
BuildContext context,
|
||||
TwoFactorType type,
|
||||
String sessionID,
|
||||
String recoveryKey,
|
||||
String encryptedSecret,
|
||||
@@ -862,6 +872,7 @@ class UserService {
|
||||
data: {
|
||||
"sessionID": sessionID,
|
||||
"secret": secret,
|
||||
"twoFactorType": twoFactorTypeToString(type),
|
||||
},
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
@@ -881,6 +892,7 @@ class UserService {
|
||||
);
|
||||
}
|
||||
} on DioError catch (e) {
|
||||
await dialog.hide();
|
||||
_logger.severe(e);
|
||||
if (e.response != null && e.response!.statusCode == 404) {
|
||||
showToast(context, "Session expired");
|
||||
@@ -902,6 +914,7 @@ class UserService {
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
await dialog.hide();
|
||||
_logger.severe(e);
|
||||
// ignore: unawaited_futures
|
||||
showErrorDialog(
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/ente_theme_data.dart';
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/models/account/two_factor.dart';
|
||||
import 'package:ente_auth/services/user_service.dart';
|
||||
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
|
||||
import 'package:ente_auth/ui/components/models/button_type.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
@@ -99,30 +101,50 @@ class _PasskeyPageState extends State<PasskeyPage> {
|
||||
}
|
||||
|
||||
Widget _getBody() {
|
||||
final l10n = context.l10n;
|
||||
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
l10n.waitingForBrowserRequest,
|
||||
style: const TextStyle(
|
||||
height: 1.4,
|
||||
fontSize: 16,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.waitingForVerification,
|
||||
style: const TextStyle(
|
||||
height: 1.4,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: ElevatedButton(
|
||||
style: Theme.of(context).colorScheme.optionalActionButtonStyle,
|
||||
onPressed: launchPasskey,
|
||||
child: Text(l10n.launchPasskeyUrlAgain),
|
||||
const SizedBox(height: 16),
|
||||
ButtonWidget(
|
||||
buttonType: ButtonType.primary,
|
||||
labelText: context.l10n.verifyPasskey,
|
||||
onTap: () => launchPasskey(),
|
||||
),
|
||||
),
|
||||
],
|
||||
const Padding(padding: EdgeInsets.all(30)),
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
UserService.instance.recoverTwoFactor(
|
||||
context,
|
||||
widget.sessionID,
|
||||
TwoFactorType.passkey,
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Center(
|
||||
child: Text(
|
||||
context.l10n.recoverAccount,
|
||||
style: const TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/core/network.dart';
|
||||
import 'package:ente_auth/ente_theme_data.dart';
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/services/update_service.dart';
|
||||
import 'package:ente_auth/theme/ente_theme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:open_filex/open_filex.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class AppUpdateDialog extends StatefulWidget {
|
||||
@@ -114,116 +107,3 @@ class _AppUpdateDialogState extends State<AppUpdateDialog> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ApkDownloaderDialog extends StatefulWidget {
|
||||
final LatestVersionInfo? versionInfo;
|
||||
|
||||
const ApkDownloaderDialog(this.versionInfo, {Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<ApkDownloaderDialog> createState() => _ApkDownloaderDialogState();
|
||||
}
|
||||
|
||||
class _ApkDownloaderDialogState extends State<ApkDownloaderDialog> {
|
||||
String? _saveUrl;
|
||||
double? _downloadProgress;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_saveUrl = Configuration.instance.getTempDirectory() +
|
||||
"ente-" +
|
||||
widget.versionInfo!.name! +
|
||||
".apk";
|
||||
_downloadApk();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WillPopScope(
|
||||
onWillPop: () async => false,
|
||||
child: AlertDialog(
|
||||
title: const Text(
|
||||
"Downloading...",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
content: LinearProgressIndicator(
|
||||
value: _downloadProgress,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Theme.of(context).colorScheme.alternativeColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _downloadApk() async {
|
||||
try {
|
||||
if (!File(_saveUrl!).existsSync()) {
|
||||
await Network.instance.getDio().download(
|
||||
widget.versionInfo!.url!,
|
||||
_saveUrl,
|
||||
onReceiveProgress: (count, _) {
|
||||
setState(() {
|
||||
_downloadProgress = count / widget.versionInfo!.size!;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
Navigator.of(context, rootNavigator: true).pop('dialog');
|
||||
// ignore: unawaited_futures
|
||||
OpenFilex.open(_saveUrl);
|
||||
} catch (e) {
|
||||
Logger("ApkDownloader").severe(e);
|
||||
final AlertDialog alert = AlertDialog(
|
||||
title: const Text("Sorry"),
|
||||
content: const Text("The download could not be completed"),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text(
|
||||
"Ignore",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context, rootNavigator: true).pop('dialog');
|
||||
Navigator.of(context, rootNavigator: true).pop('dialog');
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: Text(
|
||||
"Retry",
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.alternativeColor,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context, rootNavigator: true).pop('dialog');
|
||||
Navigator.of(context, rootNavigator: true).pop('dialog');
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return ApkDownloaderDialog(widget.versionInfo);
|
||||
},
|
||||
barrierDismissible: false,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
// ignore: unawaited_futures
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return alert;
|
||||
},
|
||||
barrierColor: Colors.black87,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:ente_auth/utils/navigation_util.dart';
|
||||
import 'package:ente_auth/utils/toast_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
class SecuritySectionWidget extends StatefulWidget {
|
||||
const SecuritySectionWidget({Key? key}) : super(key: key);
|
||||
@@ -32,6 +33,7 @@ class SecuritySectionWidget extends StatefulWidget {
|
||||
class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
|
||||
final _config = Configuration.instance;
|
||||
late bool _hasLoggedIn;
|
||||
final Logger _logger = Logger('SecuritySectionWidget');
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -75,7 +77,7 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
|
||||
pressedColor: getEnteColorScheme(context).fillFaint,
|
||||
trailingIcon: Icons.chevron_right_outlined,
|
||||
trailingIconIsMuted: true,
|
||||
onTap: () => PasskeyService.instance.openPasskeyPage(context),
|
||||
onTap: () async => await onPasskeyClick(context),
|
||||
),
|
||||
sectionOptionSpacing,
|
||||
MenuItemWidget(
|
||||
@@ -159,6 +161,31 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> onPasskeyClick(BuildContext buildContext) async {
|
||||
try {
|
||||
final isPassKeyResetEnabled =
|
||||
await PasskeyService.instance.isPasskeyRecoveryEnabled();
|
||||
if (!isPassKeyResetEnabled) {
|
||||
final Uint8List recoveryKey = Configuration.instance.getRecoveryKey();
|
||||
final resetKey = CryptoUtil.generateKey();
|
||||
final resetKeyBase64 = CryptoUtil.bin2base64(resetKey);
|
||||
final encryptionResult = CryptoUtil.encryptSync(
|
||||
resetKey,
|
||||
recoveryKey,
|
||||
);
|
||||
await PasskeyService.instance.configurePasskeyRecovery(
|
||||
resetKeyBase64,
|
||||
CryptoUtil.bin2base64(encryptionResult.encryptedData!),
|
||||
CryptoUtil.bin2base64(encryptionResult.nonce!),
|
||||
);
|
||||
}
|
||||
PasskeyService.instance.openPasskeyPage(buildContext).ignore();
|
||||
} catch (e, s) {
|
||||
_logger.severe("failed to open passkey page", e, s);
|
||||
await showGenericErrorDialog(context: context);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateEmailMFA(bool enableEmailMFA) async {
|
||||
try {
|
||||
final UserDetails details =
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/models/account/two_factor.dart';
|
||||
import 'package:ente_auth/services/user_service.dart';
|
||||
import 'package:ente_auth/ui/lifecycle_event_handler.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -129,7 +130,11 @@ class _TwoFactorAuthenticationPageState
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
UserService.instance.recoverTwoFactor(context, widget.sessionID);
|
||||
UserService.instance.recoverTwoFactor(
|
||||
context,
|
||||
widget.sessionID,
|
||||
TwoFactorType.totp,
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/models/account/two_factor.dart';
|
||||
import 'package:ente_auth/services/user_service.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -9,8 +10,10 @@ class TwoFactorRecoveryPage extends StatefulWidget {
|
||||
final String sessionID;
|
||||
final String encryptedSecret;
|
||||
final String secretDecryptionNonce;
|
||||
final TwoFactorType type;
|
||||
|
||||
const TwoFactorRecoveryPage(
|
||||
this.type,
|
||||
this.sessionID,
|
||||
this.encryptedSecret,
|
||||
this.secretDecryptionNonce, {
|
||||
@@ -72,6 +75,7 @@ class _TwoFactorRecoveryPageState extends State<TwoFactorRecoveryPage> {
|
||||
? () async {
|
||||
await UserService.instance.removeTwoFactor(
|
||||
context,
|
||||
widget.type,
|
||||
widget.sessionID,
|
||||
_recoveryKey.text,
|
||||
widget.encryptedSecret,
|
||||
|
||||
@@ -879,14 +879,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
open_filex:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: open_filex
|
||||
sha256: "854aefd72dfd74219dc8c8d1767c34ec1eae64b8399a5be317bddb1ec2108915"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.3.2"
|
||||
otp:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: ente_auth
|
||||
description: ente two-factor authenticator
|
||||
version: 2.0.40+240
|
||||
version: 2.0.42+242
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
@@ -54,13 +54,11 @@ dependencies:
|
||||
intl: ^0.18.0
|
||||
json_annotation: ^4.5.0
|
||||
local_auth: ^2.1.7
|
||||
|
||||
local_auth_android: ^1.0.31
|
||||
local_auth_ios: ^1.1.3
|
||||
logging: ^1.0.1
|
||||
modal_bottom_sheet: ^3.0.0-pre
|
||||
move_to_background: ^1.0.2
|
||||
open_filex: ^4.3.2
|
||||
otp: ^3.1.1
|
||||
package_info_plus: ^4.1.0
|
||||
password_strength: ^0.2.0
|
||||
|
||||
@@ -64,7 +64,14 @@ ente account update --email email@domain.com --dir ~/photos
|
||||
ente export
|
||||
```
|
||||
|
||||
---
|
||||
### CLI Docs
|
||||
You can view more cli documents at [docs](docs/generated/ente.md).
|
||||
To update the docs, run the following command:
|
||||
|
||||
```shell
|
||||
go run main.go docs
|
||||
```
|
||||
|
||||
|
||||
## Docker
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ var updateAccCmd = &cobra.Command{
|
||||
fmt.Printf("invalid app. Accepted values are 'photos', 'locker', 'auth'")
|
||||
|
||||
}
|
||||
err := ctrl.UpdateAccount(context.Background(), model.UpdateAccountParams{
|
||||
err := ctrl.UpdateAccount(context.Background(), model.AccountCommandParams{
|
||||
Email: email,
|
||||
App: api.StringToApp(app),
|
||||
ExportDir: &exportDir,
|
||||
@@ -73,12 +73,49 @@ var updateAccCmd = &cobra.Command{
|
||||
},
|
||||
}
|
||||
|
||||
// Subcommand for 'account update'
|
||||
var getTokenCmd = &cobra.Command{
|
||||
Use: "get-token",
|
||||
Short: "Get token for an account for a specific app",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
recoverWithLog()
|
||||
app, _ := cmd.Flags().GetString("app")
|
||||
email, _ := cmd.Flags().GetString("email")
|
||||
if email == "" {
|
||||
|
||||
fmt.Println("email must be specified, use --help for more information")
|
||||
// print help
|
||||
return
|
||||
}
|
||||
|
||||
validApps := map[string]bool{
|
||||
"photos": true,
|
||||
"locker": true,
|
||||
"auth": true,
|
||||
}
|
||||
|
||||
if !validApps[app] {
|
||||
fmt.Printf("invalid app. Accepted values are 'photos', 'locker', 'auth'")
|
||||
|
||||
}
|
||||
err := ctrl.GetToken(context.Background(), model.AccountCommandParams{
|
||||
Email: email,
|
||||
App: api.StringToApp(app),
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("Error updating account: %v\n", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Add 'config' subcommands to the root command
|
||||
rootCmd.AddCommand(accountCmd)
|
||||
// Add 'config' subcommands to the 'config' command
|
||||
updateAccCmd.Flags().String("dir", "", "update export directory")
|
||||
updateAccCmd.Flags().String("email", "", "email address of the account to update")
|
||||
updateAccCmd.Flags().String("email", "", "email address of the account")
|
||||
updateAccCmd.Flags().String("app", "photos", "Specify the app, default is 'photos'")
|
||||
accountCmd.AddCommand(listAccCmd, addAccCmd, updateAccCmd)
|
||||
getTokenCmd.Flags().String("email", "", "email address of the account")
|
||||
getTokenCmd.Flags().String("app", "photos", "Specify the app, default is 'photos'")
|
||||
accountCmd.AddCommand(listAccCmd, addAccCmd, updateAccCmd, getTokenCmd)
|
||||
}
|
||||
|
||||
90
cli/cmd/admin.go
Normal file
90
cli/cmd/admin.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/ente-io/cli/pkg/model"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var _adminCmd = &cobra.Command{
|
||||
Use: "admin",
|
||||
Short: "Commands for admin actions",
|
||||
Long: "Commands for admin actions like disable or enabling 2fa, bumping up the storage limit, etc.",
|
||||
}
|
||||
|
||||
var _userDetailsCmd = &cobra.Command{
|
||||
Use: "get-user-id",
|
||||
Short: "Get user id",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
recoverWithLog()
|
||||
var flags = &model.AdminActionForUser{}
|
||||
cmd.Flags().VisitAll(func(f *pflag.Flag) {
|
||||
if f.Name == "admin-user" {
|
||||
flags.AdminEmail = f.Value.String()
|
||||
}
|
||||
if f.Name == "user" {
|
||||
flags.UserEmail = f.Value.String()
|
||||
}
|
||||
})
|
||||
return ctrl.GetUserId(context.Background(), *flags)
|
||||
},
|
||||
}
|
||||
|
||||
var _disable2faCmd = &cobra.Command{
|
||||
Use: "disable-2fa",
|
||||
Short: "Disable 2fa for a user",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
recoverWithLog()
|
||||
var flags = &model.AdminActionForUser{}
|
||||
cmd.Flags().VisitAll(func(f *pflag.Flag) {
|
||||
if f.Name == "admin-user" {
|
||||
flags.AdminEmail = f.Value.String()
|
||||
}
|
||||
if f.Name == "user" {
|
||||
flags.UserEmail = f.Value.String()
|
||||
}
|
||||
})
|
||||
fmt.Println("Not supported yet")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var _updateFreeUserStorage = &cobra.Command{
|
||||
Use: "update-subscription",
|
||||
Short: "Update subscription for the free user",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
recoverWithLog()
|
||||
var flags = &model.AdminActionForUser{}
|
||||
noLimit := false
|
||||
cmd.Flags().VisitAll(func(f *pflag.Flag) {
|
||||
if f.Name == "admin-user" {
|
||||
flags.AdminEmail = f.Value.String()
|
||||
}
|
||||
if f.Name == "user" {
|
||||
flags.UserEmail = f.Value.String()
|
||||
}
|
||||
if f.Name == "no-limit" {
|
||||
noLimit = strings.ToLower(f.Value.String()) == "true"
|
||||
}
|
||||
})
|
||||
return ctrl.UpdateFreeStorage(context.Background(), *flags, noLimit)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(_adminCmd)
|
||||
_ = _userDetailsCmd.MarkFlagRequired("admin-user")
|
||||
_ = _userDetailsCmd.MarkFlagRequired("user")
|
||||
_userDetailsCmd.Flags().StringP("admin-user", "a", "", "The email of the admin user. (required)")
|
||||
_userDetailsCmd.Flags().StringP("user", "u", "", "The email of the user to fetch details for. (required)")
|
||||
_disable2faCmd.Flags().StringP("admin-user", "a", "", "The email of the admin user. (required)")
|
||||
_disable2faCmd.Flags().StringP("user", "u", "", "The email of the user to disable 2FA for. (required)")
|
||||
_updateFreeUserStorage.Flags().StringP("admin-user", "a", "", "The email of the admin user. (required)")
|
||||
_updateFreeUserStorage.Flags().StringP("user", "u", "", "The email of the user to update subscription for. (required)")
|
||||
// add a flag with no value --no-limit
|
||||
_updateFreeUserStorage.Flags().String("no-limit", "True", "When true, sets 100TB as storage limit, and expiry to current date + 100 years")
|
||||
_adminCmd.AddCommand(_userDetailsCmd, _disable2faCmd, _updateFreeUserStorage)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package cmd
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/ente-io/cli/pkg"
|
||||
"github.com/spf13/cobra/doc"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
@@ -11,7 +12,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const AppVersion = "0.1.11"
|
||||
var version string
|
||||
|
||||
var ctrl *pkg.ClICtrl
|
||||
|
||||
@@ -27,10 +28,15 @@ var rootCmd = &cobra.Command{
|
||||
},
|
||||
}
|
||||
|
||||
func GenerateDocs() error {
|
||||
return doc.GenMarkdownTree(rootCmd, "./docs/generated")
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||
func Execute(controller *pkg.ClICtrl) {
|
||||
func Execute(controller *pkg.ClICtrl, ver string) {
|
||||
ctrl = controller
|
||||
version = ver
|
||||
err := rootCmd.Execute()
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
|
||||
@@ -12,7 +12,7 @@ var versionCmd = &cobra.Command{
|
||||
Short: "Prints the current version",
|
||||
Long: ``,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Printf("Version %s\n", AppVersion)
|
||||
fmt.Printf("Version %s\n", version)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
10
cli/config.yaml.example
Normal file
10
cli/config.yaml.example
Normal file
@@ -0,0 +1,10 @@
|
||||
# You can put this configuration file in the following locations:
|
||||
# - $HOME/.ente/config.yaml
|
||||
# - config.yaml in the current working directory
|
||||
# - $ENTE_CLI_CONFIG_PATH/config.yaml
|
||||
|
||||
endpoint:
|
||||
api: "http://localhost:8080"
|
||||
|
||||
log:
|
||||
http: false # log status code & time taken by requests
|
||||
28
cli/docs/generated/ente.md
Normal file
28
cli/docs/generated/ente.md
Normal file
@@ -0,0 +1,28 @@
|
||||
## ente
|
||||
|
||||
CLI tool for exporting your photos from ente.io
|
||||
|
||||
### Synopsis
|
||||
|
||||
Start by creating a config file in your home directory:
|
||||
|
||||
```
|
||||
ente [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for ente
|
||||
-t, --toggle Help message for toggle
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [ente account](ente_account.md) - Manage account settings
|
||||
* [ente admin](ente_admin.md) - Commands for admin actions
|
||||
* [ente auth](ente_auth.md) - Authenticator commands
|
||||
* [ente export](ente_export.md) - Starts the export process
|
||||
* [ente version](ente_version.md) - Prints the current version
|
||||
|
||||
###### Auto generated by spf13/cobra on 13-Mar-2024
|
||||
19
cli/docs/generated/ente_account.md
Normal file
19
cli/docs/generated/ente_account.md
Normal file
@@ -0,0 +1,19 @@
|
||||
## ente account
|
||||
|
||||
Manage account settings
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for account
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [ente](ente.md) - CLI tool for exporting your photos from ente.io
|
||||
* [ente account add](ente_account_add.md) - Add a new account
|
||||
* [ente account get-token](ente_account_get-token.md) - Get token for an account for a specific app
|
||||
* [ente account list](ente_account_list.md) - list configured accounts
|
||||
* [ente account update](ente_account_update.md) - Update an existing account's export directory
|
||||
|
||||
###### Auto generated by spf13/cobra on 13-Mar-2024
|
||||
19
cli/docs/generated/ente_account_add.md
Normal file
19
cli/docs/generated/ente_account_add.md
Normal file
@@ -0,0 +1,19 @@
|
||||
## ente account add
|
||||
|
||||
Add a new account
|
||||
|
||||
```
|
||||
ente account add [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for add
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [ente account](ente_account.md) - Manage account settings
|
||||
|
||||
###### Auto generated by spf13/cobra on 13-Mar-2024
|
||||
21
cli/docs/generated/ente_account_get-token.md
Normal file
21
cli/docs/generated/ente_account_get-token.md
Normal file
@@ -0,0 +1,21 @@
|
||||
## ente account get-token
|
||||
|
||||
Get token for an account for a specific app
|
||||
|
||||
```
|
||||
ente account get-token [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
--app string Specify the app, default is 'photos' (default "photos")
|
||||
--email string email address of the account
|
||||
-h, --help help for get-token
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [ente account](ente_account.md) - Manage account settings
|
||||
|
||||
###### Auto generated by spf13/cobra on 13-Mar-2024
|
||||
19
cli/docs/generated/ente_account_list.md
Normal file
19
cli/docs/generated/ente_account_list.md
Normal file
@@ -0,0 +1,19 @@
|
||||
## ente account list
|
||||
|
||||
list configured accounts
|
||||
|
||||
```
|
||||
ente account list [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for list
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [ente account](ente_account.md) - Manage account settings
|
||||
|
||||
###### Auto generated by spf13/cobra on 13-Mar-2024
|
||||
22
cli/docs/generated/ente_account_update.md
Normal file
22
cli/docs/generated/ente_account_update.md
Normal file
@@ -0,0 +1,22 @@
|
||||
## ente account update
|
||||
|
||||
Update an existing account's export directory
|
||||
|
||||
```
|
||||
ente account update [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
--app string Specify the app, default is 'photos' (default "photos")
|
||||
--dir string update export directory
|
||||
--email string email address of the account
|
||||
-h, --help help for update
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [ente account](ente_account.md) - Manage account settings
|
||||
|
||||
###### Auto generated by spf13/cobra on 13-Mar-2024
|
||||
22
cli/docs/generated/ente_admin.md
Normal file
22
cli/docs/generated/ente_admin.md
Normal file
@@ -0,0 +1,22 @@
|
||||
## ente admin
|
||||
|
||||
Commands for admin actions
|
||||
|
||||
### Synopsis
|
||||
|
||||
Commands for admin actions like disable or enabling 2fa, bumping up the storage limit, etc.
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for admin
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [ente](ente.md) - CLI tool for exporting your photos from ente.io
|
||||
* [ente admin disable-2fa](ente_admin_disable-2fa.md) - Disable 2fa for a user
|
||||
* [ente admin get-user-id](ente_admin_get-user-id.md) - Get user id
|
||||
* [ente admin update-subscription](ente_admin_update-subscription.md) - Update subscription for the free user
|
||||
|
||||
###### Auto generated by spf13/cobra on 13-Mar-2024
|
||||
21
cli/docs/generated/ente_admin_disable-2fa.md
Normal file
21
cli/docs/generated/ente_admin_disable-2fa.md
Normal file
@@ -0,0 +1,21 @@
|
||||
## ente admin disable-2fa
|
||||
|
||||
Disable 2fa for a user
|
||||
|
||||
```
|
||||
ente admin disable-2fa [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-a, --admin-user string The email of the admin user. (required)
|
||||
-h, --help help for disable-2fa
|
||||
-u, --user string The email of the user to disable 2FA for. (required)
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [ente admin](ente_admin.md) - Commands for admin actions
|
||||
|
||||
###### Auto generated by spf13/cobra on 13-Mar-2024
|
||||
21
cli/docs/generated/ente_admin_get-user-id.md
Normal file
21
cli/docs/generated/ente_admin_get-user-id.md
Normal file
@@ -0,0 +1,21 @@
|
||||
## ente admin get-user-id
|
||||
|
||||
Get user id
|
||||
|
||||
```
|
||||
ente admin get-user-id [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-a, --admin-user string The email of the admin user. (required)
|
||||
-h, --help help for get-user-id
|
||||
-u, --user string The email of the user to fetch details for. (required)
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [ente admin](ente_admin.md) - Commands for admin actions
|
||||
|
||||
###### Auto generated by spf13/cobra on 13-Mar-2024
|
||||
22
cli/docs/generated/ente_admin_update-subscription.md
Normal file
22
cli/docs/generated/ente_admin_update-subscription.md
Normal file
@@ -0,0 +1,22 @@
|
||||
## ente admin update-subscription
|
||||
|
||||
Update subscription for the free user
|
||||
|
||||
```
|
||||
ente admin update-subscription [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-a, --admin-user string The email of the admin user. (required)
|
||||
-h, --help help for update-subscription
|
||||
--no-limit string When true, sets 100TB as storage limit, and expiry to current date + 100 years (default "True")
|
||||
-u, --user string The email of the user to update subscription for. (required)
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [ente admin](ente_admin.md) - Commands for admin actions
|
||||
|
||||
###### Auto generated by spf13/cobra on 13-Mar-2024
|
||||
16
cli/docs/generated/ente_auth.md
Normal file
16
cli/docs/generated/ente_auth.md
Normal file
@@ -0,0 +1,16 @@
|
||||
## ente auth
|
||||
|
||||
Authenticator commands
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for auth
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [ente](ente.md) - CLI tool for exporting your photos from ente.io
|
||||
* [ente auth decrypt](ente_auth_decrypt.md) - Decrypt authenticator export
|
||||
|
||||
###### Auto generated by spf13/cobra on 13-Mar-2024
|
||||
19
cli/docs/generated/ente_auth_decrypt.md
Normal file
19
cli/docs/generated/ente_auth_decrypt.md
Normal file
@@ -0,0 +1,19 @@
|
||||
## ente auth decrypt
|
||||
|
||||
Decrypt authenticator export
|
||||
|
||||
```
|
||||
ente auth decrypt [input] [output] [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for decrypt
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [ente auth](ente_auth.md) - Authenticator commands
|
||||
|
||||
###### Auto generated by spf13/cobra on 13-Mar-2024
|
||||
19
cli/docs/generated/ente_export.md
Normal file
19
cli/docs/generated/ente_export.md
Normal file
@@ -0,0 +1,19 @@
|
||||
## ente export
|
||||
|
||||
Starts the export process
|
||||
|
||||
```
|
||||
ente export [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for export
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [ente](ente.md) - CLI tool for exporting your photos from ente.io
|
||||
|
||||
###### Auto generated by spf13/cobra on 13-Mar-2024
|
||||
19
cli/docs/generated/ente_version.md
Normal file
19
cli/docs/generated/ente_version.md
Normal file
@@ -0,0 +1,19 @@
|
||||
## ente version
|
||||
|
||||
Prints the current version
|
||||
|
||||
```
|
||||
ente version [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for version
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [ente](ente.md) - CLI tool for exporting your photos from ente.io
|
||||
|
||||
###### Auto generated by spf13/cobra on 13-Mar-2024
|
||||
27
cli/docs/selfhost.md
Normal file
27
cli/docs/selfhost.md
Normal file
@@ -0,0 +1,27 @@
|
||||
## Self Hosting
|
||||
If you are self-hosting the server, you can still configure CLI to export data & perform basic admin actions.
|
||||
|
||||
To do this, first configure the CLI to point to your server.
|
||||
Define a config.yaml and put it either in the same directory as CLI binary or path defined in env variable `ENTE_CLI_CONFIG_PATH`
|
||||
|
||||
```yaml
|
||||
endpoint:
|
||||
api: "http://localhost:8080"
|
||||
```
|
||||
|
||||
You should be able to [add an account](https://github.com/ente-io/ente/blob/main/cli/docs/generated/ente_account_add.md), and subsequently increase the [storage and account validity](https://github.com/ente-io/ente/blob/main/cli/docs/generated/ente_admin_update-subscription.md) using the CLI.
|
||||
|
||||
|
||||
For the admin actions, you first need to whitelist admin users. You can create `server/museum.yaml`, and whitelist add the admin userID `internal.admins`. See [local.yaml](https://github.com/ente-io/ente/blob/main/server/configurations/local.yaml#L211C1-L232C1) in the server source code for details about how to define this.
|
||||
|
||||
You can use [account list](https://github.com/ente-io/ente/blob/main/cli/docs/generated/ente_account_list.md) command to find the user id of any account.
|
||||
|
||||
```yaml
|
||||
# ....
|
||||
|
||||
internal:
|
||||
admins:
|
||||
# - 1580559962386440
|
||||
|
||||
# ....
|
||||
```
|
||||
@@ -12,10 +12,12 @@ require (
|
||||
|
||||
require (
|
||||
github.com/alessio/shellescape v1.4.1 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/danieljoos/wincred v1.2.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
|
||||
@@ -48,6 +48,7 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE=
|
||||
github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec=
|
||||
@@ -168,6 +169,7 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
|
||||
github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
|
||||
|
||||
57
cli/internal/api/admin.go
Normal file
57
cli/internal/api/admin.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/ente-io/cli/internal/api/models"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (c *Client) GetUserIdFromEmail(ctx context.Context, email string) (*models.UserDetails, error) {
|
||||
var res models.UserDetails
|
||||
r, err := c.restClient.R().
|
||||
SetContext(ctx).
|
||||
SetResult(&res).
|
||||
SetQueryParam("email", email).
|
||||
Get("/admin/user/")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r.IsError() {
|
||||
return nil, &ApiError{
|
||||
StatusCode: r.StatusCode(),
|
||||
Message: r.String(),
|
||||
}
|
||||
}
|
||||
return &res, nil
|
||||
}
|
||||
func (c *Client) UpdateFreePlanSub(ctx context.Context, userDetails *models.UserDetails, storageInBytes int64, expiryTimeInMicro int64) error {
|
||||
var res interface{}
|
||||
if userDetails.Subscription.ProductID != "free" {
|
||||
return fmt.Errorf("user is not on free plan")
|
||||
}
|
||||
payload := map[string]interface{}{
|
||||
"userID": userDetails.User.ID,
|
||||
"expiryTime": expiryTimeInMicro,
|
||||
"transactionID": fmt.Sprintf("cli-on-%d", time.Now().Unix()),
|
||||
"productID": "free",
|
||||
"paymentProvider": "",
|
||||
"storage": storageInBytes,
|
||||
}
|
||||
r, err := c.restClient.R().
|
||||
SetContext(ctx).
|
||||
SetResult(&res).
|
||||
SetBody(payload).
|
||||
Put("/admin/user/subscription")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if r.IsError() {
|
||||
return &ApiError{
|
||||
StatusCode: r.StatusCode(),
|
||||
Message: r.String(),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
@@ -2,19 +2,30 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/ente-io/cli/utils/constants"
|
||||
"github.com/spf13/viper"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
downloadHost = "https://files.ente.io/?fileID="
|
||||
)
|
||||
|
||||
func downloadUrl(fileID int64) string {
|
||||
apiEndpoint := viper.GetString("endpoint.api")
|
||||
if apiEndpoint == "" || strings.Compare(apiEndpoint, constants.EnteApiUrl) == 0 {
|
||||
return downloadHost + strconv.FormatInt(fileID, 10)
|
||||
}
|
||||
return apiEndpoint + "/files/download/" + strconv.FormatInt(fileID, 10)
|
||||
}
|
||||
|
||||
func (c *Client) DownloadFile(ctx context.Context, fileID int64, absolutePath string) error {
|
||||
req := c.downloadClient.R().
|
||||
SetContext(ctx).
|
||||
SetOutput(absolutePath)
|
||||
attachToken(req)
|
||||
r, err := req.Get(downloadHost + strconv.FormatInt(fileID, 10))
|
||||
r, err := req.Get(downloadUrl(fileID))
|
||||
if r.IsError() {
|
||||
return &ApiError{
|
||||
StatusCode: r.StatusCode(),
|
||||
|
||||
@@ -30,6 +30,16 @@ func logRequest(req *resty.Request) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// log query params if present
|
||||
if len(req.QueryParam) > 0 {
|
||||
fmt.Println(color.GreenString("Query Params:"))
|
||||
for k, v := range req.QueryParam {
|
||||
if k == TokenQuery {
|
||||
v = []string{"REDACTED"}
|
||||
}
|
||||
fmt.Printf("%s: %s\n", color.CyanString(k), color.YellowString(strings.Join(v, ",")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func logResponse(resp *resty.Response) {
|
||||
|
||||
16
cli/internal/api/models/user_details.go
Normal file
16
cli/internal/api/models/user_details.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package models
|
||||
|
||||
type UserDetails struct {
|
||||
User struct {
|
||||
ID int64 `json:"id"`
|
||||
} `json:"user"`
|
||||
Usage int64 `json:"usage"`
|
||||
Email string `json:"email"`
|
||||
|
||||
Subscription struct {
|
||||
ExpiryTime int64 `json:"expiryTime"`
|
||||
Storage int64 `json:"storage"`
|
||||
ProductID string `json:"productID"`
|
||||
PaymentProvider string `json:"paymentProvider"`
|
||||
} `json:"subscription"`
|
||||
}
|
||||
@@ -5,11 +5,12 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/ente-io/cli/internal/api"
|
||||
"golang.org/x/term"
|
||||
"log"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
func GetSensitiveField(label string) (string, error) {
|
||||
@@ -81,6 +82,79 @@ func GetCode(promptText string, length int) (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// parseStorageSize parses a string representing a storage size (e.g., "500MB", "2GB") into bytes.
|
||||
func parseStorageSize(input string) (int64, error) {
|
||||
units := map[string]int64{
|
||||
"MB": 1 << 20,
|
||||
"GB": 1 << 30,
|
||||
"TB": 1 << 40,
|
||||
}
|
||||
re := regexp.MustCompile(`(?i)^(\d+(?:\.\d+)?)(MB|GB|TB)$`)
|
||||
matches := re.FindStringSubmatch(input)
|
||||
|
||||
if matches == nil {
|
||||
return 0, errors.New("invalid format")
|
||||
}
|
||||
|
||||
number, err := strconv.ParseFloat(matches[1], 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid number: %s", matches[1])
|
||||
}
|
||||
|
||||
unit := strings.ToUpper(matches[2])
|
||||
bytes := int64(number * float64(units[unit]))
|
||||
|
||||
return bytes, nil
|
||||
}
|
||||
|
||||
func ConfirmAction(promptText string) (bool, error) {
|
||||
for {
|
||||
input, err := GetUserInput(promptText)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if input == "" {
|
||||
log.Fatal("No input entered")
|
||||
return false, errors.New("invalid input. Please enter 'y' or 'n'")
|
||||
}
|
||||
if input == "c" {
|
||||
return false, errors.New("cancelled")
|
||||
}
|
||||
if input == "y" {
|
||||
return true, nil
|
||||
}
|
||||
if input == "n" {
|
||||
return false, nil
|
||||
}
|
||||
fmt.Println("Invalid input. Please enter 'y' or 'n'.")
|
||||
}
|
||||
}
|
||||
|
||||
// GetStorageSize prompts the user for a storage size and returns the size in bytes.
|
||||
func GetStorageSize(promptText string) (int64, error) {
|
||||
for {
|
||||
input, err := GetUserInput(promptText)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if input == "" {
|
||||
log.Fatal("No storage size entered")
|
||||
return 0, errors.New("no storage size entered")
|
||||
}
|
||||
if input == "c" {
|
||||
return 0, errors.New("storage size entry cancelled")
|
||||
}
|
||||
|
||||
bytes, err := parseStorageSize(input)
|
||||
if err != nil {
|
||||
fmt.Println("Invalid storage size format. Please use a valid format like '500MB', '2GB'.")
|
||||
continue
|
||||
}
|
||||
|
||||
return bytes, nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetExportDir() string {
|
||||
for {
|
||||
exportDir, err := GetUserInput("Enter export directory")
|
||||
|
||||
36
cli/main.go
36
cli/main.go
@@ -8,12 +8,15 @@ import (
|
||||
"github.com/ente-io/cli/pkg"
|
||||
"github.com/ente-io/cli/pkg/secrets"
|
||||
"github.com/ente-io/cli/utils/constants"
|
||||
"github.com/spf13/viper"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var AppVersion = "0.1.12"
|
||||
|
||||
func main() {
|
||||
cliDBPath, err := GetCLIConfigPath()
|
||||
if secrets.IsRunningInContainer() {
|
||||
@@ -23,10 +26,10 @@ func main() {
|
||||
log.Fatalf("Please mount a volume to %s to persist cli data\n%v\n", cliDBPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Could not create cli config path\n%v\n", err)
|
||||
}
|
||||
initConfig(cliDBPath)
|
||||
newCliPath := fmt.Sprintf("%s/ente-cli.db", cliDBPath)
|
||||
if !strings.HasPrefix(cliDBPath, "/") {
|
||||
oldCliPath := fmt.Sprintf("%sente-cli.db", cliDBPath)
|
||||
@@ -48,8 +51,8 @@ func main() {
|
||||
}
|
||||
ctrl := pkg.ClICtrl{
|
||||
Client: api.NewClient(api.Params{
|
||||
Debug: false,
|
||||
//Host: "http://localhost:8080",
|
||||
Debug: viper.GetBool("log.http"),
|
||||
Host: viper.GetString("endpoint.api"),
|
||||
}),
|
||||
DB: db,
|
||||
KeyHolder: secrets.NewKeyHolder(secrets.GetOrCreateClISecret()),
|
||||
@@ -63,7 +66,32 @@ func main() {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
cmd.Execute(&ctrl)
|
||||
|
||||
if len(os.Args) == 2 && os.Args[1] == "docs" {
|
||||
log.Println("Generating docs")
|
||||
err = cmd.GenerateDocs()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
cmd.Execute(&ctrl, AppVersion)
|
||||
}
|
||||
|
||||
func initConfig(cliConfigPath string) {
|
||||
viper.SetConfigName("config") // name of config file (without extension)
|
||||
viper.SetConfigType("yaml") // REQUIRED if the config file does not have the extension in the name
|
||||
viper.AddConfigPath(cliConfigPath + "/") // path to look for the config file in
|
||||
viper.AddConfigPath(".") // optionally look for config in the working directory
|
||||
|
||||
viper.SetDefault("endpoint.api", constants.EnteApiUrl)
|
||||
viper.SetDefault("log.http", false)
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
|
||||
} else {
|
||||
// Config file was found but another error was produced
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetCLIConfigPath returns the path to the .ente-cli folder and creates it if it doesn't exist.
|
||||
|
||||
@@ -142,7 +142,7 @@ func (c *ClICtrl) ListAccounts(cxt context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ClICtrl) UpdateAccount(ctx context.Context, params model.UpdateAccountParams) error {
|
||||
func (c *ClICtrl) UpdateAccount(ctx context.Context, params model.AccountCommandParams) error {
|
||||
accounts, err := c.GetAccounts(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -177,5 +177,27 @@ func (c *ClICtrl) UpdateAccount(ctx context.Context, params model.UpdateAccountP
|
||||
return b.Put([]byte(accountKey), accInfoBytes)
|
||||
})
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
func (c *ClICtrl) GetToken(ctx context.Context, params model.AccountCommandParams) error {
|
||||
accounts, err := c.GetAccounts(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var acc *model.Account
|
||||
for _, a := range accounts {
|
||||
if a.Email == params.Email && a.App == params.App {
|
||||
acc = &a
|
||||
break
|
||||
}
|
||||
}
|
||||
if acc == nil {
|
||||
return fmt.Errorf("account not found, use `account list` to list accounts")
|
||||
}
|
||||
secretInfo, err := c.KeyHolder.LoadSecrets(*acc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(secretInfo.TokenStr())
|
||||
return nil
|
||||
}
|
||||
|
||||
114
cli/pkg/admin_actions.go
Normal file
114
cli/pkg/admin_actions.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/ente-io/cli/internal"
|
||||
"github.com/ente-io/cli/pkg/model"
|
||||
"github.com/ente-io/cli/utils"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (c *ClICtrl) GetUserId(ctx context.Context, params model.AdminActionForUser) error {
|
||||
accountCtx, err := c.buildAdminContext(ctx, params.AdminEmail)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := c.Client.GetUserIdFromEmail(accountCtx, params.UserEmail)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(id.User.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ClICtrl) UpdateFreeStorage(ctx context.Context, params model.AdminActionForUser, noLimit bool) error {
|
||||
accountCtx, err := c.buildAdminContext(ctx, params.AdminEmail)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
userDetails, err := c.Client.GetUserIdFromEmail(accountCtx, params.UserEmail)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if noLimit {
|
||||
// set storage to 100TB and expiry to + 100 years
|
||||
err := c.Client.UpdateFreePlanSub(accountCtx, userDetails, 100*1024*1024*1024*1024, time.Now().AddDate(100, 0, 0).UnixMicro())
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
fmt.Println("Successfully updated storage and expiry date for user")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
storageSize, err := internal.GetStorageSize("Enter a storage size (e.g.'5MB', '10GB', '2Tb'): ")
|
||||
if err != nil {
|
||||
log.Fatalf("Error: %v", err)
|
||||
}
|
||||
dateStr, err := internal.GetUserInput("Enter sub expiry date in YYYY-MM-DD format (e.g.'2040-12-31')")
|
||||
if err != nil {
|
||||
log.Fatalf("Error: %v", err)
|
||||
}
|
||||
date, err := _parseDateOrDateTime(dateStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Updating storage for user %s to %s (old %s) with new expirty %s (old %s) \n",
|
||||
params.UserEmail,
|
||||
utils.ByteCountDecimalGIB(storageSize), utils.ByteCountDecimalGIB(userDetails.Subscription.Storage),
|
||||
date.Format("2006-01-02"),
|
||||
time.UnixMicro(userDetails.Subscription.ExpiryTime).Format("2006-01-02"))
|
||||
// press y to confirm
|
||||
confirmed, _ := internal.ConfirmAction("Are you sure you want to update the storage ('y' or 'n')?")
|
||||
if !confirmed {
|
||||
return nil
|
||||
} else {
|
||||
err := c.Client.UpdateFreePlanSub(accountCtx, userDetails, storageSize, date.UnixMicro())
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
fmt.Println("Successfully updated storage and expiry date for user")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ClICtrl) buildAdminContext(ctx context.Context, adminEmail string) (context.Context, error) {
|
||||
accounts, err := c.GetAccounts(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var acc *model.Account
|
||||
for _, a := range accounts {
|
||||
if a.Email == adminEmail {
|
||||
acc = &a
|
||||
break
|
||||
}
|
||||
}
|
||||
if acc == nil {
|
||||
return nil, fmt.Errorf("account not found for %s, use `account list` to list accounts", adminEmail)
|
||||
}
|
||||
secretInfo, err := c.KeyHolder.LoadSecrets(*acc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
accountCtx := c.buildRequestContext(ctx, *acc)
|
||||
c.Client.AddToken(acc.AccountKey(), secretInfo.TokenStr())
|
||||
return accountCtx, nil
|
||||
}
|
||||
|
||||
func _parseDateOrDateTime(input string) (time.Time, error) {
|
||||
var layout string
|
||||
if strings.Contains(input, " ") {
|
||||
// If the input contains a space, assume it's a date-time format
|
||||
layout = "2006-01-02 15:04:05"
|
||||
} else {
|
||||
// If there's no space, assume it's just a date
|
||||
layout = "2006-01-02"
|
||||
}
|
||||
return time.Parse(layout, input)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"github.com/ente-io/cli/internal/api"
|
||||
)
|
||||
@@ -17,7 +18,7 @@ type Account struct {
|
||||
ExportDir string `json:"exportDir"`
|
||||
}
|
||||
|
||||
type UpdateAccountParams struct {
|
||||
type AccountCommandParams struct {
|
||||
Email string
|
||||
App api.App
|
||||
ExportDir *string
|
||||
@@ -37,3 +38,7 @@ type AccSecretInfo struct {
|
||||
Token []byte
|
||||
PublicKey []byte
|
||||
}
|
||||
|
||||
func (a *AccSecretInfo) TokenStr() string {
|
||||
return base64.URLEncoding.EncodeToString(a.Token)
|
||||
}
|
||||
|
||||
6
cli/pkg/model/admin.go
Normal file
6
cli/pkg/model/admin.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package model
|
||||
|
||||
type AdminActionForUser struct {
|
||||
UserEmail string
|
||||
AdminEmail string
|
||||
}
|
||||
@@ -1,5 +1,16 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Fetch the latest tag that starts with "cli-"
|
||||
# shellcheck disable=SC2046
|
||||
# shellcheck disable=SC2006
|
||||
LATEST_TAG=$(git describe --tags `git rev-list --tags='cli-*' --max-count=1`)
|
||||
|
||||
# Check if the LATEST_TAG variable is empty
|
||||
if [ -z "$LATEST_TAG" ]; then
|
||||
echo "No 'cli-' tag found. Exiting..."
|
||||
exit 1
|
||||
fi
|
||||
VERSION=${LATEST_TAG#cli-}
|
||||
# Create a "bin" directory if it doesn't exist
|
||||
mkdir -p bin
|
||||
|
||||
@@ -29,7 +40,7 @@ do
|
||||
fi
|
||||
|
||||
# Build the binary and place it in the "bin" directory
|
||||
go build -ldflags="-s -w" -trimpath -o "bin/$BINARY_NAME" main.go
|
||||
go build -ldflags="-X main.AppVersion=${VERSION} -s -w" -trimpath -o "bin/$BINARY_NAME" main.go
|
||||
|
||||
# Print a message indicating the build is complete for the current OS and architecture
|
||||
echo "Built for $OS ($ARCH) as bin/$BINARY_NAME"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
package constants
|
||||
|
||||
const CliDataPath = "/cli-data/"
|
||||
const EnteApiUrl = "https://api.ente.io"
|
||||
|
||||
31
cli/utils/convert.go
Normal file
31
cli/utils/convert.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func ByteCountDecimal(b int64) string {
|
||||
const unit = 1000
|
||||
if b < unit {
|
||||
return fmt.Sprintf("%d B", b)
|
||||
}
|
||||
div, exp := int64(unit), 0
|
||||
for n := b / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp])
|
||||
}
|
||||
|
||||
func ByteCountDecimalGIB(b int64) string {
|
||||
const unit = 1024
|
||||
if b < unit {
|
||||
return fmt.Sprintf("%d B", b)
|
||||
}
|
||||
div, exp := int64(unit), 0
|
||||
for n := b / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp])
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
@@ -10,16 +9,3 @@ func TimeTrack(start time.Time, name string) {
|
||||
elapsed := time.Since(start)
|
||||
log.Printf("%s took %s", name, elapsed)
|
||||
}
|
||||
|
||||
func ByteCountDecimal(b int64) string {
|
||||
const unit = 1000
|
||||
if b < unit {
|
||||
return fmt.Sprintf("%d B", b)
|
||||
}
|
||||
div, exp := int64(unit), 0
|
||||
for n := b / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp])
|
||||
}
|
||||
|
||||
3
desktop/.github/workflows/build.yml
vendored
3
desktop/.github/workflows/build.yml
vendored
@@ -52,7 +52,4 @@ jobs:
|
||||
# macOS notarization API key
|
||||
API_KEY_ID: ${{ secrets.api_key_id }}
|
||||
API_KEY_ISSUER_ID: ${{ secrets.api_key_issuer_id}}
|
||||
# setry crash reporting token
|
||||
SENTRY_AUTH_TOKEN: ${{secrets.sentry_auth_token}}
|
||||
NEXT_PUBLIC_DISABLE_SENTRY: ${{secrets.next_public_disable_sentry}}
|
||||
USE_HARD_LINKS: false
|
||||
|
||||
29
desktop/.gitignore
vendored
29
desktop/.gitignore
vendored
@@ -1,12 +1,21 @@
|
||||
node_modules
|
||||
app
|
||||
.next/
|
||||
dist
|
||||
.vscode
|
||||
buildingSteps.md
|
||||
# Node
|
||||
node_modules/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
.idea/
|
||||
build/.DS_Store
|
||||
|
||||
# Editors
|
||||
.vscode/
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
.electron-symbols/
|
||||
models/
|
||||
.env.*.local
|
||||
|
||||
# tsc transpiles src/**/*.ts and emits the generated JS into app
|
||||
app/
|
||||
|
||||
# out is a symlink to the photos web app's dir
|
||||
out
|
||||
|
||||
# electron-builder
|
||||
dist/
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
branch="$(git rev-parse --abbrev-ref HEAD)"
|
||||
|
||||
if [ "$branch" = "main" ]; then
|
||||
echo "You can't commit directly to main branch"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
npx lint-staged
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"tabWidth": 4,
|
||||
"trailingComma": "es5",
|
||||
"singleQuote": true,
|
||||
"bracketSameLine": true
|
||||
}
|
||||
"plugins": [
|
||||
"prettier-plugin-organize-imports",
|
||||
"prettier-plugin-packagejson"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
network-timeout 500000
|
||||
@@ -4,128 +4,128 @@
|
||||
|
||||
### New
|
||||
|
||||
- Option to select file download location.
|
||||
- Add support for searching popular cities
|
||||
- Sorted duplicates in desecending order of size
|
||||
- Add Counter to upload section
|
||||
- Display full name and collection name on hover on dedupe screen photos
|
||||
- Option to select file download location.
|
||||
- Add support for searching popular cities
|
||||
- Sorted duplicates in desecending order of size
|
||||
- Add Counter to upload section
|
||||
- Display full name and collection name on hover on dedupe screen photos
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix add to album padding issue
|
||||
- Fix double uncategorized album issue
|
||||
- Hide Hidden collection files from all section
|
||||
- Fix add to album padding issue
|
||||
- Fix double uncategorized album issue
|
||||
- Hide Hidden collection files from all section
|
||||
|
||||
## v1.6.62
|
||||
|
||||
### New
|
||||
|
||||
- Integrated onnx clip runner
|
||||
- Integrated onnx clip runner
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixes login button requiring double click issue
|
||||
- Fixes Collection sort state not preserved issue
|
||||
- Fixes continuous export causing app crash
|
||||
- Improves ML related copies for better distinction from clip
|
||||
- Added Better favicon for light mode
|
||||
- Fixed face indexing issues
|
||||
- Fixed thumbnail load issue
|
||||
- Fixes login button requiring double click issue
|
||||
- Fixes Collection sort state not preserved issue
|
||||
- Fixes continuous export causing app crash
|
||||
- Improves ML related copies for better distinction from clip
|
||||
- Added Better favicon for light mode
|
||||
- Fixed face indexing issues
|
||||
- Fixed thumbnail load issue
|
||||
|
||||
## v1.6.60
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix Thumbnail Orientation issue
|
||||
- Fix ML logging issue
|
||||
- Fix Thumbnail Orientation issue
|
||||
- Fix ML logging issue
|
||||
|
||||
## v1.6.59
|
||||
|
||||
### New
|
||||
|
||||
- Added arm64 builds for linux
|
||||
- Added arm64 builds for linux
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix Editor file not loading issue
|
||||
- Fix ML results missing thumbnail issue
|
||||
- Fix Editor file not loading issue
|
||||
- Fix ML results missing thumbnail issue
|
||||
|
||||
## v1.6.58
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix File load issue
|
||||
- Fix File load issue
|
||||
|
||||
## v1.6.57
|
||||
|
||||
### New Features
|
||||
|
||||
- Added encrypted Disk caching for files
|
||||
- Added option to customize cache folder location
|
||||
- Added encrypted Disk caching for files
|
||||
- Added option to customize cache folder location
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed caching issue,causing multiple download of file during ml sync
|
||||
- Fixed caching issue,causing multiple download of file during ml sync
|
||||
|
||||
## v1.6.55
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Added manage family portal option if add-on is active
|
||||
- Fixed filename date parsing issue
|
||||
- Fixed storage limit ui glitch
|
||||
- Fixed dedupe page layout issue
|
||||
- Fixed ElectronAPI refactoring issue
|
||||
- Fixed Search related issues
|
||||
- Added manage family portal option if add-on is active
|
||||
- Fixed filename date parsing issue
|
||||
- Fixed storage limit ui glitch
|
||||
- Fixed dedupe page layout issue
|
||||
- Fixed ElectronAPI refactoring issue
|
||||
- Fixed Search related issues
|
||||
|
||||
## v1.6.54
|
||||
|
||||
### New Features
|
||||
|
||||
- Added support for HEIC and raw image in photo editor
|
||||
- Added support for HEIC and raw image in photo editor
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed 16bit HDR HEIC images support
|
||||
- Fixed blocked login due safe storage issue
|
||||
- Fixed Search related issues
|
||||
- Fixed issue of watch folder not cleared on logout
|
||||
- other under the hood ui/ux improvements
|
||||
- Fixed 16bit HDR HEIC images support
|
||||
- Fixed blocked login due safe storage issue
|
||||
- Fixed Search related issues
|
||||
- Fixed issue of watch folder not cleared on logout
|
||||
- other under the hood ui/ux improvements
|
||||
|
||||
## v1.6.53
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed watch folder disabled issue
|
||||
- Fixed BF Add on related issues
|
||||
- Fixed clip sync issue and added better logging
|
||||
- Fixed mov file upload
|
||||
- Fixed clip extraction related issue
|
||||
- Fixed watch folder disabled issue
|
||||
- Fixed BF Add on related issues
|
||||
- Fixed clip sync issue and added better logging
|
||||
- Fixed mov file upload
|
||||
- Fixed clip extraction related issue
|
||||
|
||||
## v1.6.52
|
||||
|
||||
### New Features
|
||||
|
||||
- Added Clip Desktop on windows
|
||||
- Added Clip Desktop on windows
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fixed google json matching issue
|
||||
- other under-the-hood changes to improve performance and bug fixes
|
||||
- fixed google json matching issue
|
||||
- other under-the-hood changes to improve performance and bug fixes
|
||||
|
||||
## v1.6.50
|
||||
|
||||
### New Features
|
||||
|
||||
- Added Clip desktop
|
||||
- Added Clip desktop
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed desktop downloaded file had extra dot in the name
|
||||
- Cleanup error messages
|
||||
- fix the motion photo clustering issue
|
||||
- Add option to disable cf proxy locally
|
||||
- other under-the-hood changes to improve UX
|
||||
- Fixed desktop downloaded file had extra dot in the name
|
||||
- Cleanup error messages
|
||||
- fix the motion photo clustering issue
|
||||
- Add option to disable cf proxy locally
|
||||
- other under-the-hood changes to improve UX
|
||||
|
||||
## v1.6.49
|
||||
|
||||
@@ -137,54 +137,54 @@ Check out our [blog](https://ente.io/blog/introducing-web-desktop-photo-editor/)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed misaligned icons in photo-viewer
|
||||
- Fixed issue with Motion photo upload
|
||||
- Fixed issue with Live-photo upload
|
||||
- other minor ux improvement
|
||||
- Fixed misaligned icons in photo-viewer
|
||||
- Fixed issue with Motion photo upload
|
||||
- Fixed issue with Live-photo upload
|
||||
- other minor ux improvement
|
||||
|
||||
## v1.6.46
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixes OOM crashes during file upload [#1379](https://github.com/ente-io/photos-web/pull/1379)
|
||||
- Fixes OOM crashes during file upload [#1379](https://github.com/ente-io/photos-web/pull/1379)
|
||||
|
||||
## v1.6.45
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed app keeps reloading issue [#235](https://github.com/ente-io/photos-desktop/pull/235)
|
||||
- Fixed dng and arw preview issue [#1378](https://github.com/ente-io/photos-web/pull/1378)
|
||||
- Added view crash report option (help menu) for user to share electron crash report locally
|
||||
- Fixed app keeps reloading issue [#235](https://github.com/ente-io/photos-desktop/pull/235)
|
||||
- Fixed dng and arw preview issue [#1378](https://github.com/ente-io/photos-web/pull/1378)
|
||||
- Added view crash report option (help menu) for user to share electron crash report locally
|
||||
|
||||
## v1.6.44
|
||||
|
||||
- Upgraded electron to get latest security patches and other improvements.
|
||||
- Upgraded electron to get latest security patches and other improvements.
|
||||
|
||||
## v1.6.43
|
||||
|
||||
### Added
|
||||
|
||||
- #### Check for update and changelog option
|
||||
- #### Check for update and changelog option
|
||||
|
||||
Added options to check for update manually and a view changelog via the app menubar
|
||||
|
||||
- #### Opt out of crash reporting
|
||||
- #### Opt out of crash reporting
|
||||
|
||||
Added option to out of a crash reporting, it can accessed from the settings -> preferences -> disable crash reporting
|
||||
|
||||
- #### Type search
|
||||
- #### Type search
|
||||
|
||||
Added new search option to search files based on file type i.e, image, video, live-photo.
|
||||
|
||||
- #### Manual Convert Button
|
||||
- #### Manual Convert Button
|
||||
|
||||
In case the video is not playable, Now there is a convert button which can be used to trigger conversion of the video to supported format.
|
||||
|
||||
- #### File Download Progress
|
||||
- #### File Download Progress
|
||||
|
||||
The file loader now also shows the exact percentage download progress, instead of just a simple loader.
|
||||
|
||||
- #### Bug fixes & other enhancements
|
||||
- #### Bug fixes & other enhancements
|
||||
|
||||
We have squashed a few pesky bugs that were reported by our community
|
||||
|
||||
@@ -192,21 +192,21 @@ Check out our [blog](https://ente.io/blog/introducing-web-desktop-photo-editor/)
|
||||
|
||||
### Added
|
||||
|
||||
- #### Hidden albums
|
||||
- #### Hidden albums
|
||||
|
||||
You can now hide albums, just like individual memories.
|
||||
|
||||
- #### Email verification
|
||||
- #### Email verification
|
||||
|
||||
We have now made email verification optional, so you can sign in with just your email address and password, without waiting for a verification code.
|
||||
|
||||
You can opt in / out of email verification from Settings > Security.
|
||||
|
||||
- #### Download Album
|
||||
- #### Download Album
|
||||
|
||||
You can now chose the download location for downloading albums. Along with that we have also added progress bar for album download.
|
||||
|
||||
- #### Bug fixes & other enhancements
|
||||
- #### Bug fixes & other enhancements
|
||||
|
||||
We have squashed a few pesky bugs that were reported by our community
|
||||
|
||||
|
||||
@@ -10,33 +10,28 @@ To know more about Ente, see [our main README](../README.md) or visit
|
||||
|
||||
## Building from source
|
||||
|
||||
> [!CAUTION]
|
||||
>
|
||||
> We moved a few things around when switching to a monorepo recently, so this
|
||||
> folder might not build with the instructions below. Hang tight, we're on it,
|
||||
> will fix things if.
|
||||
|
||||
Fetch submodules
|
||||
|
||||
```sh
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
Install dependencies
|
||||
|
||||
```sh
|
||||
yarn install
|
||||
```
|
||||
|
||||
Run the app
|
||||
Run in development mode (with hot reload)
|
||||
|
||||
```sh
|
||||
yarn start
|
||||
yarn dev
|
||||
```
|
||||
|
||||
To recompile automatically using electron-reload, run this in a separate
|
||||
terminal:
|
||||
> [!CAUTION]
|
||||
>
|
||||
> `yarn dev` is currently not working (we'll fix soon). If you just want to
|
||||
> build from source and use the generated binary, use `yarn build`.
|
||||
|
||||
```bash
|
||||
yarn watch
|
||||
Or create a binary for your platform
|
||||
|
||||
```sh
|
||||
yarn build
|
||||
```
|
||||
|
||||
That's the gist of it. For more development related documentation, see
|
||||
[docs](docs/README.md).
|
||||
|
||||
@@ -1,20 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ente Photos</title>
|
||||
</head>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ente Photos</title>
|
||||
</head>
|
||||
|
||||
<body style="background-color: black;">
|
||||
<div style=" height: 95vh;width: 96vw; display: grid; place-items: center; color: white;">
|
||||
<div>
|
||||
<div style="margin-bottom: 10px;">Site unreachable, please try again later</div>
|
||||
<button onClick="window[`ElectronAPIs`].reloadWindow()">Reload</button>
|
||||
<body style="background-color: black">
|
||||
<div
|
||||
style="
|
||||
height: 95vh;
|
||||
width: 96vw;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: white;
|
||||
"
|
||||
>
|
||||
<div>
|
||||
<div style="margin-bottom: 10px">
|
||||
Site unreachable, please try again later
|
||||
</div>
|
||||
<button onClick="window[`ElectronAPIs`].reloadWindow()">
|
||||
Reload
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,30 +1,50 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ente Photos</title>
|
||||
</head>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ente Photos</title>
|
||||
</head>
|
||||
|
||||
<body style="background-color: black;">
|
||||
<div style="display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 90vh;">
|
||||
<div style="width:64px;"><svg version="1.1" id="L9" xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 100 100"
|
||||
enable-background="new 0 0 0 0" xml:space="preserve">
|
||||
<path fill="#2dc262"
|
||||
d="M73,50c0-12.7-10.3-23-23-23S27,37.3,27,50 M30.9,50c0-10.5,8.5-19.1,19.1-19.1S69.1,39.5,69.1,50">
|
||||
<animateTransform attributeName="transform" attributeType="XML" type="rotate" dur="1s"
|
||||
from="0 50 50" to="360 50 50" repeatCount="indefinite" />
|
||||
</path>
|
||||
</svg>
|
||||
<body style="background-color: black">
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 90vh;
|
||||
"
|
||||
>
|
||||
<div style="width: 64px">
|
||||
<svg
|
||||
version="1.1"
|
||||
id="L9"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 100 100"
|
||||
enable-background="new 0 0 0 0"
|
||||
xml:space="preserve"
|
||||
>
|
||||
<path
|
||||
fill="#2dc262"
|
||||
d="M73,50c0-12.7-10.3-23-23-23S27,37.3,27,50 M30.9,50c0-10.5,8.5-19.1,19.1-19.1S69.1,39.5,69.1,50"
|
||||
>
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
attributeType="XML"
|
||||
type="rotate"
|
||||
dur="1s"
|
||||
from="0 50 50"
|
||||
to="360 50 50"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Electron Updater Example</title>
|
||||
</head>
|
||||
<body>
|
||||
Current version: <span id="version">vX.Y.Z</span>
|
||||
<div id="messages"></div>
|
||||
<script>
|
||||
// Display the current version
|
||||
let version = window.location.hash.substring(1);
|
||||
document.getElementById('version').innerText = version;
|
||||
<head>
|
||||
<title>Electron Updater Example</title>
|
||||
</head>
|
||||
<body>
|
||||
Current version: <span id="version">vX.Y.Z</span>
|
||||
<div id="messages"></div>
|
||||
<script>
|
||||
// Display the current version
|
||||
let version = window.location.hash.substring(1);
|
||||
document.getElementById("version").innerText = version;
|
||||
|
||||
// Listen for messages
|
||||
const {ipcRenderer} = require('electron');
|
||||
ipcRenderer.on('message', function(event, text) {
|
||||
var container = document.getElementById('messages');
|
||||
var message = document.createElement('div');
|
||||
message.innerHTML = text;
|
||||
container.appendChild(message);
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
// Listen for messages
|
||||
const { ipcRenderer } = require("electron");
|
||||
ipcRenderer.on("message", function (event, text) {
|
||||
var container = document.getElementById("messages");
|
||||
var message = document.createElement("div");
|
||||
message.innerHTML = text;
|
||||
container.appendChild(message);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
Notes on how to upload electron symbols directly to sentry instance (bypassing the CF limits) cc @abhi just for future reference
|
||||
|
||||
To upload electron symbols
|
||||
|
||||
1. Create a tunnel
|
||||
```
|
||||
ssh -p 7426 -N -L 8080:localhost:9000 sentry
|
||||
```
|
||||
|
||||
2. Add the following env file
|
||||
```
|
||||
NEXT_PUBLIC_IS_SENTRY_ENABLED = yes
|
||||
SENTRY_ORG = ente
|
||||
SENTRY_PROJECT = bhari-frame
|
||||
SENTRY_URL2 = https://sentry.ente.io/
|
||||
SENTRY_URL = http://localhost:8080/
|
||||
SENTRY_AUTH_TOKEN = xxx
|
||||
SENTRY_LOG_LEVEL = debug
|
||||
```
|
||||
|
||||
3. Run
|
||||
|
||||
```
|
||||
node sentry-symbols.js
|
||||
```
|
||||
11
desktop/docs/README.md
Normal file
11
desktop/docs/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Developer docs
|
||||
|
||||
If you just want to run the Ente Photos desktop app locally or develop it, you
|
||||
can do:
|
||||
|
||||
yarn install
|
||||
yarn dev
|
||||
|
||||
The docs in this directory provide more details that some developers might find
|
||||
useful. You might also find the developer docs for
|
||||
[web](../../web/docs/README.md) useful.
|
||||
14
desktop/docs/dependencies.md
Normal file
14
desktop/docs/dependencies.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Dependencies
|
||||
|
||||
See [web/docs/dependencies.md](../../web/docs/dependencies.md) for general web
|
||||
specific dependencies. See [electron.md](electron.md) for our main dependency,
|
||||
Electron. The rest of this document describes the remaining, desktop specific
|
||||
dependencies that are used by the Photos desktop app.
|
||||
|
||||
## Electron related
|
||||
|
||||
### next-electron-server
|
||||
|
||||
This spins up a server for serving files using a protocol handler inside our
|
||||
Electron process. This allows us to directly use the output produced by `next
|
||||
build` for loading into our renderer process.
|
||||
4
desktop/docs/dev.md
Normal file
4
desktop/docs/dev.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# Development tips
|
||||
|
||||
- `yarn build:quick` is a variant of `yarn build` that uses the
|
||||
`--config.compression=store` flag to (slightly) speed up electron-builder.
|
||||
21
desktop/docs/electron.md
Normal file
21
desktop/docs/electron.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Electron
|
||||
|
||||
[Electron](https://www.electronjs.org) is a cross-platform (Linux, Windows,
|
||||
macOS) way for creating desktop apps using TypeScript.
|
||||
|
||||
Electron embeds Chromium and Node.js in the generated app's binary. The
|
||||
generated app thus consists of two separate processes - the _main_ process, and
|
||||
a _renderer_ process.
|
||||
|
||||
- The _main_ process is runs the embedded node. This process can deal with the
|
||||
host OS - it is conceptually like a `node` repl running on your machine. In our
|
||||
case, the TypeScript code (in the `src/` directory) gets transpiled by `tsc`
|
||||
into JavaScript in the `build/app/` directory, which gets bundled in the
|
||||
generated app's binary and is loaded by the node (main) process when the app
|
||||
starts.
|
||||
|
||||
- The _renderer_ process is a regular web app that gets loaded into the embedded
|
||||
Chromium. When the main process starts, it creates a new "window" that shows
|
||||
this embedded Chromium. In our case, we build and bundle a static export of
|
||||
the [Photos web app](../web/README.md) in the generated app. This gets loaded
|
||||
by the embedded Chromium at runtime, acting as the app's UI.
|
||||
@@ -1,10 +1,65 @@
|
||||
{
|
||||
"name": "ente",
|
||||
"productName": "ente",
|
||||
"version": "1.6.63",
|
||||
"private": true,
|
||||
"description": "Desktop client for ente.io",
|
||||
"description": "Desktop client for Ente Photos",
|
||||
"author": "Ente <code@ente.io>",
|
||||
"main": "app/main.js",
|
||||
"scripts": {
|
||||
"build": "yarn build-renderer && yarn build-main",
|
||||
"build-main": "tsc && electron-builder",
|
||||
"build-main:quick": "tsc && electron-builder --config.compression=store",
|
||||
"build-renderer": "cd ../web && yarn install && yarn build:photos && cd ../desktop && rm -f out && ln -sf ../web/apps/photos/out",
|
||||
"build:quick": "yarn build-renderer && yarn build-main:quick",
|
||||
"dev": "concurrently --names 'main,rndr,tscw' \"yarn dev-main\" \"yarn dev-renderer\" \"yarn dev-main-watch\"",
|
||||
"dev-main": "tsc && electron app/main.js",
|
||||
"dev-main-watch": "tsc --watch --preserveWatchOutput",
|
||||
"dev-renderer": "cd ../web && yarn install && yarn dev:photos",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"lint": "yarn prettier --check . && eslint \"src/**/*.ts\"",
|
||||
"lint-fix": "yarn prettier --write . && eslint --fix src"
|
||||
},
|
||||
"dependencies": {
|
||||
"any-shell-escape": "^0.1.1",
|
||||
"auto-launch": "^5.0.5",
|
||||
"chokidar": "^3.5.3",
|
||||
"compare-versions": "^6.1.0",
|
||||
"electron-log": "^4.3.5",
|
||||
"electron-reload": "^2.0.0-alpha.1",
|
||||
"electron-store": "^8.0.1",
|
||||
"electron-updater": "^4.3.8",
|
||||
"ffmpeg-static": "^5.1.0",
|
||||
"get-folder-size": "^2.0.1",
|
||||
"html-entities": "^2.4.0",
|
||||
"jpeg-js": "^0.4.4",
|
||||
"next-electron-server": "^1",
|
||||
"node-fetch": "^2.6.7",
|
||||
"node-stream-zip": "^1.15.0",
|
||||
"onnxruntime-node": "^1.16.3",
|
||||
"promise-fs": "^2.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/auto-launch": "^5.0.2",
|
||||
"@types/ffmpeg-static": "^3.0.1",
|
||||
"@types/get-folder-size": "^2.0.0",
|
||||
"@types/node": "18.15.0",
|
||||
"@types/node-fetch": "^2.6.2",
|
||||
"@types/promise-fs": "^2.1.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.28.0",
|
||||
"@typescript-eslint/parser": "^5.28.0",
|
||||
"concurrently": "^7.0.0",
|
||||
"electron": "^25.8.4",
|
||||
"electron-builder": "^24.6.4",
|
||||
"electron-builder-notarize": "^1.2.0",
|
||||
"electron-download": "^4.1.1",
|
||||
"eslint": "^7.23.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"prettier": "^3",
|
||||
"prettier-plugin-organize-imports": "^3.2",
|
||||
"prettier-plugin-packagejson": "^2.4",
|
||||
"typescript": "^4.2.3"
|
||||
},
|
||||
"build": {
|
||||
"appId": "io.ente.bhari-frame",
|
||||
"artifactName": "${productName}-${version}-${arch}.${ext}",
|
||||
@@ -42,7 +97,7 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"icon": "./build/icon.icns",
|
||||
"icon": "./resources/icon.icns",
|
||||
"category": "Photography"
|
||||
},
|
||||
"mac": {
|
||||
@@ -57,98 +112,24 @@
|
||||
"x64ArchFiles": "Contents/Resources/ggmlclip-mac"
|
||||
},
|
||||
"afterSign": "electron-builder-notarize",
|
||||
"extraFiles": [
|
||||
{
|
||||
"from": "build",
|
||||
"to": "resources",
|
||||
"filter": [
|
||||
"**/*"
|
||||
]
|
||||
}
|
||||
],
|
||||
"asarUnpack": [
|
||||
"node_modules/ffmpeg-static/bin/${os}/${arch}/ffmpeg",
|
||||
"node_modules/ffmpeg-static/index.js",
|
||||
"node_modules/ffmpeg-static/package.json"
|
||||
],
|
||||
"extraFiles": [
|
||||
{
|
||||
"from": "build",
|
||||
"to": "resources"
|
||||
}
|
||||
],
|
||||
"files": [
|
||||
"app/**/*",
|
||||
{
|
||||
"from": "ui/apps/photos",
|
||||
"to": "ui",
|
||||
"filter": [
|
||||
"!**/*",
|
||||
"out/**/*"
|
||||
]
|
||||
}
|
||||
"out"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"prebuild": "eslint \"src/**/*.{js,jsx,ts,tsx}\"",
|
||||
"prepare": "husky install",
|
||||
"lint": "eslint -c .eslintrc --ext .ts src",
|
||||
"watch": "tsc -w",
|
||||
"build-main": "yarn install && tsc",
|
||||
"start-main": "yarn build-main && electron app/main.js",
|
||||
"start-renderer": "cd ui && yarn install && yarn dev:photos",
|
||||
"start": "concurrently \"yarn start-main\" \"yarn start-renderer\"",
|
||||
"build-renderer": "cd ui && yarn install && yarn export:photos",
|
||||
"build": "yarn build-renderer && yarn build-main",
|
||||
"test-release": "cross-env IS_TEST_RELEASE=true yarn build && electron-builder --config.compression=store"
|
||||
},
|
||||
"author": "ente <code@ente.io>",
|
||||
"devDependencies": {
|
||||
"@sentry/cli": "^1.68.0",
|
||||
"@types/auto-launch": "^5.0.2",
|
||||
"@types/ffmpeg-static": "^3.0.1",
|
||||
"@types/get-folder-size": "^2.0.0",
|
||||
"@types/node": "18.15.0",
|
||||
"@types/node-fetch": "^2.6.2",
|
||||
"@types/promise-fs": "^2.1.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.28.0",
|
||||
"@typescript-eslint/parser": "^5.28.0",
|
||||
"concurrently": "^7.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"electron": "^25.8.4",
|
||||
"electron-builder": "^24.6.4",
|
||||
"electron-builder-notarize": "^1.2.0",
|
||||
"electron-download": "^4.1.1",
|
||||
"eslint": "^7.23.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"husky": "^8.0.1",
|
||||
"lint-staged": "^13.0.1",
|
||||
"prettier": "2.5.1",
|
||||
"typescript": "^4.2.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sentry/electron": "^2.5.1",
|
||||
"any-shell-escape": "^0.1.1",
|
||||
"auto-launch": "^5.0.5",
|
||||
"chokidar": "^3.5.3",
|
||||
"compare-versions": "^6.1.0",
|
||||
"electron-log": "^4.3.5",
|
||||
"electron-reload": "^2.0.0-alpha.1",
|
||||
"electron-store": "^8.0.1",
|
||||
"electron-updater": "^4.3.8",
|
||||
"ffmpeg-static": "^5.1.0",
|
||||
"get-folder-size": "^2.0.1",
|
||||
"html-entities": "^2.4.0",
|
||||
"jpeg-js": "^0.4.4",
|
||||
"next-electron-server": "file:./thirdparty/next-electron-server",
|
||||
"node-fetch": "^2.6.7",
|
||||
"node-stream-zip": "^1.15.0",
|
||||
"onnxruntime-node": "^1.16.3",
|
||||
"promise-fs": "^2.1.1"
|
||||
},
|
||||
"productName": "ente",
|
||||
"standard": {
|
||||
"parser": "babel-eslint"
|
||||
},
|
||||
"lint-staged": {
|
||||
"src/**/*.{js,jsx,ts,tsx}": [
|
||||
"eslint --fix",
|
||||
"prettier --write --ignore-unknown"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
let SentryCli;
|
||||
let download;
|
||||
|
||||
try {
|
||||
SentryCli = require('@sentry/cli');
|
||||
download = require('electron-download');
|
||||
} catch (e) {
|
||||
console.error('ERROR: Missing required packages, please run:');
|
||||
console.error('npm install --save-dev @sentry/cli electron-download');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const SYMBOL_CACHE_FOLDER = '.electron-symbols';
|
||||
const sentryCli = new SentryCli('./sentry.properties');
|
||||
|
||||
async function main() {
|
||||
const version = getElectronVersion();
|
||||
if (!version) {
|
||||
console.error('Cannot detect electron version, check that electron is installed');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('We are starting to download all possible electron symbols');
|
||||
console.log('We need it in order to symbolicate native crashes');
|
||||
console.log(
|
||||
'This step is only needed once whenever you update your electron version',
|
||||
);
|
||||
console.log('Just call this script again it should do everything for you.');
|
||||
|
||||
let zipPath = await downloadSymbols({
|
||||
version,
|
||||
platform: 'darwin',
|
||||
arch: 'x64',
|
||||
dsym: true,
|
||||
});
|
||||
await sentryCli.execute(['upload-dif', '-t', 'dsym', zipPath], true);
|
||||
|
||||
zipPath = await downloadSymbols({
|
||||
version,
|
||||
platform: 'win32',
|
||||
arch: 'ia32',
|
||||
symbols: true,
|
||||
});
|
||||
await sentryCli.execute(['upload-dif', '-t', 'breakpad', zipPath], true);
|
||||
|
||||
zipPath = await downloadSymbols({
|
||||
version,
|
||||
platform: 'win32',
|
||||
arch: 'x64',
|
||||
symbols: true,
|
||||
});
|
||||
await sentryCli.execute(['upload-dif', '-t', 'breakpad', zipPath], true);
|
||||
|
||||
zipPath = await downloadSymbols({
|
||||
version,
|
||||
platform: 'linux',
|
||||
arch: 'x64',
|
||||
symbols: true,
|
||||
});
|
||||
await sentryCli.execute(['upload-dif', '-t', 'breakpad', zipPath], true);
|
||||
|
||||
console.log('Finished downloading and uploading to Sentry');
|
||||
console.log(`Feel free to delete the ${SYMBOL_CACHE_FOLDER}`);
|
||||
}
|
||||
|
||||
function getElectronVersion() {
|
||||
try {
|
||||
return require('electron/package.json').version;
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadSymbols(options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
download(
|
||||
{
|
||||
...options,
|
||||
cache: SYMBOL_CACHE_FOLDER,
|
||||
},
|
||||
(err, zipPath) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(zipPath);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
main().catch(e => console.error(e));
|
||||
@@ -1,3 +0,0 @@
|
||||
defaults.url=https://sentry.ente.io/
|
||||
defaults.org=ente
|
||||
defaults.project=desktop-photos
|
||||
@@ -1,16 +1,16 @@
|
||||
import { ipcRenderer } from 'electron/renderer';
|
||||
import path from 'path';
|
||||
import { existsSync, mkdir, rmSync } from 'promise-fs';
|
||||
import { DiskCache } from '../services/diskCache';
|
||||
import { ipcRenderer } from "electron/renderer";
|
||||
import path from "path";
|
||||
import { existsSync, mkdir, rmSync } from "promise-fs";
|
||||
import { DiskCache } from "../services/diskCache";
|
||||
|
||||
const ENTE_CACHE_DIR_NAME = 'ente';
|
||||
const ENTE_CACHE_DIR_NAME = "ente";
|
||||
|
||||
export const getCacheDirectory = async () => {
|
||||
const customCacheDir = await getCustomCacheDirectory();
|
||||
if (customCacheDir && existsSync(customCacheDir)) {
|
||||
return customCacheDir;
|
||||
}
|
||||
const defaultSystemCacheDir = await ipcRenderer.invoke('get-path', 'cache');
|
||||
const defaultSystemCacheDir = await ipcRenderer.invoke("get-path", "cache");
|
||||
return path.join(defaultSystemCacheDir, ENTE_CACHE_DIR_NAME);
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ const getCacheBucketDir = async (cacheName: string) => {
|
||||
|
||||
export async function openDiskCache(
|
||||
cacheName: string,
|
||||
cacheLimitInBytes?: number
|
||||
cacheLimitInBytes?: number,
|
||||
) {
|
||||
const cacheBucketDir = await getCacheBucketDir(cacheName);
|
||||
if (!existsSync(cacheBucketDir)) {
|
||||
@@ -42,11 +42,11 @@ export async function deleteDiskCache(cacheName: string) {
|
||||
}
|
||||
|
||||
export async function setCustomCacheDirectory(
|
||||
directory: string
|
||||
directory: string,
|
||||
): Promise<void> {
|
||||
await ipcRenderer.invoke('set-custom-cache-directory', directory);
|
||||
await ipcRenderer.invoke("set-custom-cache-directory", directory);
|
||||
}
|
||||
|
||||
async function getCustomCacheDirectory(): Promise<string> {
|
||||
return await ipcRenderer.invoke('get-custom-cache-directory');
|
||||
return await ipcRenderer.invoke("get-custom-cache-directory");
|
||||
}
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { writeStream } from '../services/fs';
|
||||
import { isExecError } from '../utils/error';
|
||||
import { parseExecError } from '../utils/error';
|
||||
import { Model } from '../types';
|
||||
import { ipcRenderer } from "electron";
|
||||
import { writeStream } from "../services/fs";
|
||||
import { Model } from "../types";
|
||||
import { isExecError, parseExecError } from "../utils/error";
|
||||
|
||||
export async function computeImageEmbedding(
|
||||
model: Model,
|
||||
imageData: Uint8Array
|
||||
imageData: Uint8Array,
|
||||
): Promise<Float32Array> {
|
||||
let tempInputFilePath = null;
|
||||
try {
|
||||
tempInputFilePath = await ipcRenderer.invoke('get-temp-file-path', '');
|
||||
tempInputFilePath = await ipcRenderer.invoke("get-temp-file-path", "");
|
||||
const imageStream = new Response(imageData.buffer).body;
|
||||
await writeStream(tempInputFilePath, imageStream);
|
||||
const embedding = await ipcRenderer.invoke(
|
||||
'compute-image-embedding',
|
||||
"compute-image-embedding",
|
||||
model,
|
||||
tempInputFilePath
|
||||
tempInputFilePath,
|
||||
);
|
||||
return embedding;
|
||||
} catch (err) {
|
||||
@@ -28,20 +27,20 @@ export async function computeImageEmbedding(
|
||||
}
|
||||
} finally {
|
||||
if (tempInputFilePath) {
|
||||
await ipcRenderer.invoke('remove-temp-file', tempInputFilePath);
|
||||
await ipcRenderer.invoke("remove-temp-file", tempInputFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function computeTextEmbedding(
|
||||
model: Model,
|
||||
text: string
|
||||
text: string,
|
||||
): Promise<Float32Array> {
|
||||
try {
|
||||
const embedding = await ipcRenderer.invoke(
|
||||
'compute-text-embedding',
|
||||
"compute-text-embedding",
|
||||
model,
|
||||
text
|
||||
text,
|
||||
);
|
||||
return embedding;
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,44 +1,39 @@
|
||||
import { ipcRenderer } from 'electron/renderer';
|
||||
import { logError } from '../services/logging';
|
||||
import { ipcRenderer } from "electron/renderer";
|
||||
import { logError } from "../services/logging";
|
||||
|
||||
export const selectDirectory = async (): Promise<string> => {
|
||||
try {
|
||||
return await ipcRenderer.invoke('select-dir');
|
||||
return await ipcRenderer.invoke("select-dir");
|
||||
} catch (e) {
|
||||
logError(e, 'error while selecting root directory');
|
||||
logError(e, "error while selecting root directory");
|
||||
}
|
||||
};
|
||||
|
||||
export const getAppVersion = async (): Promise<string> => {
|
||||
try {
|
||||
return await ipcRenderer.invoke('get-app-version');
|
||||
return await ipcRenderer.invoke("get-app-version");
|
||||
} catch (e) {
|
||||
logError(e, 'failed to get release version');
|
||||
logError(e, "failed to get release version");
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const openDirectory = async (dirPath: string): Promise<void> => {
|
||||
try {
|
||||
await ipcRenderer.invoke('open-dir', dirPath);
|
||||
await ipcRenderer.invoke("open-dir", dirPath);
|
||||
} catch (e) {
|
||||
logError(e, 'error while opening directory');
|
||||
logError(e, "error while opening directory");
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const getPlatform = async (): Promise<'mac' | 'windows' | 'linux'> => {
|
||||
export const getPlatform = async (): Promise<"mac" | "windows" | "linux"> => {
|
||||
try {
|
||||
return await ipcRenderer.invoke('get-platform');
|
||||
return await ipcRenderer.invoke("get-platform");
|
||||
} catch (e) {
|
||||
logError(e, 'failed to get platform');
|
||||
logError(e, "failed to get platform");
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export {
|
||||
logToDisk,
|
||||
openLogDirectory,
|
||||
getSentryUserID,
|
||||
updateOptOutOfCrashReports,
|
||||
} from '../services/logging';
|
||||
export { logToDisk, openLogDirectory } from "../services/logging";
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { keysStore } from '../stores/keys.store';
|
||||
import { safeStorageStore } from '../stores/safeStorage.store';
|
||||
import { uploadStatusStore } from '../stores/upload.store';
|
||||
import { logError } from '../services/logging';
|
||||
import { userPreferencesStore } from '../stores/userPreferences.store';
|
||||
import { watchStore } from '../stores/watch.store';
|
||||
import { logError } from "../services/logging";
|
||||
import { keysStore } from "../stores/keys.store";
|
||||
import { safeStorageStore } from "../stores/safeStorage.store";
|
||||
import { uploadStatusStore } from "../stores/upload.store";
|
||||
import { watchStore } from "../stores/watch.store";
|
||||
|
||||
export const clearElectronStore = () => {
|
||||
try {
|
||||
@@ -11,9 +10,8 @@ export const clearElectronStore = () => {
|
||||
keysStore.clear();
|
||||
safeStorageStore.clear();
|
||||
watchStore.clear();
|
||||
userPreferencesStore.delete('optOutOfCrashReports');
|
||||
} catch (e) {
|
||||
logError(e, 'error while clearing electron store');
|
||||
logError(e, "error while clearing electron store");
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { writeStream } from './../services/fs';
|
||||
import * as fs from 'promise-fs';
|
||||
import * as fs from "promise-fs";
|
||||
import { writeStream } from "./../services/fs";
|
||||
|
||||
export const exists = (path: string) => {
|
||||
return fs.existsSync(path);
|
||||
@@ -13,7 +13,7 @@ export const checkExistsAndCreateDir = async (dirPath: string) => {
|
||||
|
||||
export const saveStreamToDisk = async (
|
||||
filePath: string,
|
||||
fileStream: ReadableStream<Uint8Array>
|
||||
fileStream: ReadableStream<Uint8Array>,
|
||||
) => {
|
||||
await writeStream(filePath, fileStream);
|
||||
};
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { existsSync } from 'fs';
|
||||
import { writeStream } from '../services/fs';
|
||||
import { logError } from '../services/logging';
|
||||
import { ElectronFile } from '../types';
|
||||
import { ipcRenderer } from "electron";
|
||||
import { existsSync } from "fs";
|
||||
import { writeStream } from "../services/fs";
|
||||
import { logError } from "../services/logging";
|
||||
import { ElectronFile } from "../types";
|
||||
|
||||
export async function runFFmpegCmd(
|
||||
cmd: string[],
|
||||
inputFile: File | ElectronFile,
|
||||
outputFileName: string,
|
||||
dontTimeout?: boolean
|
||||
dontTimeout?: boolean,
|
||||
) {
|
||||
let inputFilePath = null;
|
||||
let createdTempInputFile = null;
|
||||
try {
|
||||
if (!existsSync(inputFile.path)) {
|
||||
const tempFilePath = await ipcRenderer.invoke(
|
||||
'get-temp-file-path',
|
||||
inputFile.name
|
||||
"get-temp-file-path",
|
||||
inputFile.name,
|
||||
);
|
||||
await writeStream(tempFilePath, await inputFile.stream());
|
||||
inputFilePath = tempFilePath;
|
||||
@@ -25,19 +25,19 @@ export async function runFFmpegCmd(
|
||||
inputFilePath = inputFile.path;
|
||||
}
|
||||
const outputFileData = await ipcRenderer.invoke(
|
||||
'run-ffmpeg-cmd',
|
||||
"run-ffmpeg-cmd",
|
||||
cmd,
|
||||
inputFilePath,
|
||||
outputFileName,
|
||||
dontTimeout
|
||||
dontTimeout,
|
||||
);
|
||||
return new File([outputFileData], outputFileName);
|
||||
} finally {
|
||||
if (createdTempInputFile) {
|
||||
try {
|
||||
await ipcRenderer.invoke('remove-temp-file', inputFilePath);
|
||||
await ipcRenderer.invoke("remove-temp-file", inputFilePath);
|
||||
} catch (e) {
|
||||
logError(e, 'failed to deleteTempFile');
|
||||
logError(e, "failed to deleteTempFile");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getElectronFile, getDirFilePaths } from '../services/fs';
|
||||
import { getDirFilePaths, getElectronFile } from "../services/fs";
|
||||
|
||||
export async function getDirFiles(dirPath: string) {
|
||||
const files = await getDirFilePaths(dirPath);
|
||||
@@ -6,10 +6,10 @@ export async function getDirFiles(dirPath: string) {
|
||||
return electronFiles;
|
||||
}
|
||||
export {
|
||||
deleteFile,
|
||||
deleteFolder,
|
||||
isFolder,
|
||||
moveFile,
|
||||
deleteFolder,
|
||||
deleteFile,
|
||||
rename,
|
||||
readTextFile,
|
||||
} from '../services/fs';
|
||||
rename,
|
||||
} from "../services/fs";
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import { CustomErrors } from '../constants/errors';
|
||||
import { ipcRenderer } from 'electron/renderer';
|
||||
import { existsSync } from 'fs';
|
||||
import { writeStream } from '../services/fs';
|
||||
import { logError } from '../services/logging';
|
||||
import { ElectronFile } from '../types';
|
||||
import { isPlatform } from '../utils/common/platform';
|
||||
import { ipcRenderer } from "electron/renderer";
|
||||
import { existsSync } from "fs";
|
||||
import { CustomErrors } from "../constants/errors";
|
||||
import { writeStream } from "../services/fs";
|
||||
import { logError } from "../services/logging";
|
||||
import { ElectronFile } from "../types";
|
||||
import { isPlatform } from "../utils/common/platform";
|
||||
|
||||
export async function convertToJPEG(
|
||||
fileData: Uint8Array,
|
||||
filename: string
|
||||
filename: string,
|
||||
): Promise<Uint8Array> {
|
||||
if (isPlatform('windows')) {
|
||||
if (isPlatform("windows")) {
|
||||
throw Error(CustomErrors.WINDOWS_NATIVE_IMAGE_PROCESSING_NOT_SUPPORTED);
|
||||
}
|
||||
const convertedFileData = await ipcRenderer.invoke(
|
||||
'convert-to-jpeg',
|
||||
"convert-to-jpeg",
|
||||
fileData,
|
||||
filename
|
||||
filename,
|
||||
);
|
||||
return convertedFileData;
|
||||
}
|
||||
@@ -24,20 +24,20 @@ export async function convertToJPEG(
|
||||
export async function generateImageThumbnail(
|
||||
inputFile: File | ElectronFile,
|
||||
maxDimension: number,
|
||||
maxSize: number
|
||||
maxSize: number,
|
||||
): Promise<Uint8Array> {
|
||||
let inputFilePath = null;
|
||||
let createdTempInputFile = null;
|
||||
try {
|
||||
if (isPlatform('windows')) {
|
||||
if (isPlatform("windows")) {
|
||||
throw Error(
|
||||
CustomErrors.WINDOWS_NATIVE_IMAGE_PROCESSING_NOT_SUPPORTED
|
||||
CustomErrors.WINDOWS_NATIVE_IMAGE_PROCESSING_NOT_SUPPORTED,
|
||||
);
|
||||
}
|
||||
if (!existsSync(inputFile.path)) {
|
||||
const tempFilePath = await ipcRenderer.invoke(
|
||||
'get-temp-file-path',
|
||||
inputFile.name
|
||||
"get-temp-file-path",
|
||||
inputFile.name,
|
||||
);
|
||||
await writeStream(tempFilePath, await inputFile.stream());
|
||||
inputFilePath = tempFilePath;
|
||||
@@ -46,18 +46,18 @@ export async function generateImageThumbnail(
|
||||
inputFilePath = inputFile.path;
|
||||
}
|
||||
const thumbnail = await ipcRenderer.invoke(
|
||||
'generate-image-thumbnail',
|
||||
"generate-image-thumbnail",
|
||||
inputFilePath,
|
||||
maxDimension,
|
||||
maxSize
|
||||
maxSize,
|
||||
);
|
||||
return thumbnail;
|
||||
} finally {
|
||||
if (createdTempInputFile) {
|
||||
try {
|
||||
await ipcRenderer.invoke('remove-temp-file', inputFilePath);
|
||||
await ipcRenderer.invoke("remove-temp-file", inputFilePath);
|
||||
} catch (e) {
|
||||
logError(e, 'failed to deleteTempFile');
|
||||
logError(e, "failed to deleteTempFile");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { safeStorageStore } from '../stores/safeStorage.store';
|
||||
import { logError } from '../services/logging';
|
||||
import { ipcRenderer } from "electron";
|
||||
import { logError } from "../services/logging";
|
||||
import { safeStorageStore } from "../stores/safeStorage.store";
|
||||
|
||||
export async function setEncryptionKey(encryptionKey: string) {
|
||||
try {
|
||||
const encryptedKey: Buffer = await ipcRenderer.invoke(
|
||||
'safeStorage-encrypt',
|
||||
encryptionKey
|
||||
"safeStorage-encrypt",
|
||||
encryptionKey,
|
||||
);
|
||||
const b64EncryptedKey = Buffer.from(encryptedKey).toString('base64');
|
||||
safeStorageStore.set('encryptionKey', b64EncryptedKey);
|
||||
const b64EncryptedKey = Buffer.from(encryptedKey).toString("base64");
|
||||
safeStorageStore.set("encryptionKey", b64EncryptedKey);
|
||||
} catch (e) {
|
||||
logError(e, 'setEncryptionKey failed');
|
||||
logError(e, "setEncryptionKey failed");
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getEncryptionKey(): Promise<string> {
|
||||
try {
|
||||
const b64EncryptedKey = safeStorageStore.get('encryptionKey');
|
||||
const b64EncryptedKey = safeStorageStore.get("encryptionKey");
|
||||
if (b64EncryptedKey) {
|
||||
const keyBuffer = new Uint8Array(
|
||||
Buffer.from(b64EncryptedKey, 'base64')
|
||||
Buffer.from(b64EncryptedKey, "base64"),
|
||||
);
|
||||
return await ipcRenderer.invoke('safeStorage-decrypt', keyBuffer);
|
||||
return await ipcRenderer.invoke("safeStorage-decrypt", keyBuffer);
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e, 'getEncryptionKey failed');
|
||||
logError(e, "getEncryptionKey failed");
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { AppUpdateInfo } from '../types';
|
||||
import { ipcRenderer } from "electron";
|
||||
import { AppUpdateInfo } from "../types";
|
||||
|
||||
export const sendNotification = (content: string) => {
|
||||
ipcRenderer.send('send-notification', content);
|
||||
ipcRenderer.send("send-notification", content);
|
||||
};
|
||||
export const reloadWindow = () => {
|
||||
ipcRenderer.send('reload-window');
|
||||
ipcRenderer.send("reload-window");
|
||||
};
|
||||
|
||||
export const registerUpdateEventListener = (
|
||||
showUpdateDialog: (updateInfo: AppUpdateInfo) => void
|
||||
showUpdateDialog: (updateInfo: AppUpdateInfo) => void,
|
||||
) => {
|
||||
ipcRenderer.removeAllListeners('show-update-dialog');
|
||||
ipcRenderer.on('show-update-dialog', (_, updateInfo: AppUpdateInfo) => {
|
||||
ipcRenderer.removeAllListeners("show-update-dialog");
|
||||
ipcRenderer.on("show-update-dialog", (_, updateInfo: AppUpdateInfo) => {
|
||||
showUpdateDialog(updateInfo);
|
||||
});
|
||||
};
|
||||
|
||||
export const registerForegroundEventListener = (onForeground: () => void) => {
|
||||
ipcRenderer.removeAllListeners('app-in-foreground');
|
||||
ipcRenderer.on('app-in-foreground', () => {
|
||||
ipcRenderer.removeAllListeners("app-in-foreground");
|
||||
ipcRenderer.on("app-in-foreground", () => {
|
||||
onForeground();
|
||||
});
|
||||
};
|
||||
|
||||
export const updateAndRestart = () => {
|
||||
ipcRenderer.send('update-and-restart');
|
||||
ipcRenderer.send("update-and-restart");
|
||||
};
|
||||
|
||||
export const skipAppUpdate = (version: string) => {
|
||||
ipcRenderer.send('skip-app-update', version);
|
||||
ipcRenderer.send("skip-app-update", version);
|
||||
};
|
||||
|
||||
export const muteUpdateNotification = (version: string) => {
|
||||
ipcRenderer.send('mute-update-notification', version);
|
||||
ipcRenderer.send("mute-update-notification", version);
|
||||
};
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { getElectronFile } from './../services/fs';
|
||||
import { uploadStatusStore } from '../stores/upload.store';
|
||||
import { ElectronFile, FILE_PATH_TYPE } from '../types';
|
||||
import { logError } from '../services/logging';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { ipcRenderer } from "electron";
|
||||
import { logError } from "../services/logging";
|
||||
import {
|
||||
getElectronFilesFromGoogleZip,
|
||||
getSavedFilePaths,
|
||||
} from '../services/upload';
|
||||
} from "../services/upload";
|
||||
import { uploadStatusStore } from "../stores/upload.store";
|
||||
import { ElectronFile, FILE_PATH_TYPE } from "../types";
|
||||
import { getElectronFile } from "./../services/fs";
|
||||
|
||||
export const getPendingUploads = async () => {
|
||||
const filePaths = getSavedFilePaths(FILE_PATH_TYPE.FILES);
|
||||
const zipPaths = getSavedFilePaths(FILE_PATH_TYPE.ZIPS);
|
||||
const collectionName = uploadStatusStore.get('collectionName');
|
||||
const collectionName = uploadStatusStore.get("collectionName");
|
||||
|
||||
let files: ElectronFile[] = [];
|
||||
let type: FILE_PATH_TYPE;
|
||||
@@ -39,31 +39,31 @@ export const getPendingUploads = async () => {
|
||||
export const showUploadDirsDialog = async () => {
|
||||
try {
|
||||
const filePaths: string[] = await ipcRenderer.invoke(
|
||||
'show-upload-dirs-dialog'
|
||||
"show-upload-dirs-dialog",
|
||||
);
|
||||
const files = await Promise.all(filePaths.map(getElectronFile));
|
||||
return files;
|
||||
} catch (e) {
|
||||
logError(e, 'error while selecting folders');
|
||||
logError(e, "error while selecting folders");
|
||||
}
|
||||
};
|
||||
|
||||
export const showUploadFilesDialog = async () => {
|
||||
try {
|
||||
const filePaths: string[] = await ipcRenderer.invoke(
|
||||
'show-upload-files-dialog'
|
||||
"show-upload-files-dialog",
|
||||
);
|
||||
const files = await Promise.all(filePaths.map(getElectronFile));
|
||||
return files;
|
||||
} catch (e) {
|
||||
logError(e, 'error while selecting files');
|
||||
logError(e, "error while selecting files");
|
||||
}
|
||||
};
|
||||
|
||||
export const showUploadZipDialog = async () => {
|
||||
try {
|
||||
const filePaths: string[] = await ipcRenderer.invoke(
|
||||
'show-upload-zip-dialog'
|
||||
"show-upload-zip-dialog",
|
||||
);
|
||||
let files: ElectronFile[] = [];
|
||||
|
||||
@@ -79,12 +79,12 @@ export const showUploadZipDialog = async () => {
|
||||
files,
|
||||
};
|
||||
} catch (e) {
|
||||
logError(e, 'error while selecting zips');
|
||||
logError(e, "error while selecting zips");
|
||||
}
|
||||
};
|
||||
|
||||
export {
|
||||
setToUploadFiles,
|
||||
getElectronFilesFromGoogleZip,
|
||||
setToUploadCollection,
|
||||
} from '../services/upload';
|
||||
setToUploadFiles,
|
||||
} from "../services/upload";
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { isMappingPresent } from '../utils/watch';
|
||||
import path from 'path';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { ElectronFile, WatchMapping } from '../types';
|
||||
import { getElectronFile } from '../services/fs';
|
||||
import { getWatchMappings, setWatchMappings } from '../services/watch';
|
||||
import ElectronLog from 'electron-log';
|
||||
import { ipcRenderer } from "electron";
|
||||
import ElectronLog from "electron-log";
|
||||
import path from "path";
|
||||
import { getElectronFile } from "../services/fs";
|
||||
import { getWatchMappings, setWatchMappings } from "../services/watch";
|
||||
import { ElectronFile, WatchMapping } from "../types";
|
||||
import { isMappingPresent } from "../utils/watch";
|
||||
|
||||
export async function addWatchMapping(
|
||||
rootFolderName: string,
|
||||
folderPath: string,
|
||||
uploadStrategy: number
|
||||
uploadStrategy: number,
|
||||
) {
|
||||
ElectronLog.log(`Adding watch mapping: ${folderPath}`);
|
||||
const watchMappings = getWatchMappings();
|
||||
@@ -17,7 +17,7 @@ export async function addWatchMapping(
|
||||
throw new Error(`Watch mapping already exists`);
|
||||
}
|
||||
|
||||
await ipcRenderer.invoke('add-watcher', {
|
||||
await ipcRenderer.invoke("add-watcher", {
|
||||
dir: folderPath,
|
||||
});
|
||||
|
||||
@@ -35,19 +35,19 @@ export async function addWatchMapping(
|
||||
export async function removeWatchMapping(folderPath: string) {
|
||||
let watchMappings = getWatchMappings();
|
||||
const watchMapping = watchMappings.find(
|
||||
(mapping) => mapping.folderPath === folderPath
|
||||
(mapping) => mapping.folderPath === folderPath,
|
||||
);
|
||||
|
||||
if (!watchMapping) {
|
||||
throw new Error(`Watch mapping does not exist`);
|
||||
}
|
||||
|
||||
await ipcRenderer.invoke('remove-watcher', {
|
||||
await ipcRenderer.invoke("remove-watcher", {
|
||||
dir: watchMapping.folderPath,
|
||||
});
|
||||
|
||||
watchMappings = watchMappings.filter(
|
||||
(mapping) => mapping.folderPath !== watchMapping.folderPath
|
||||
(mapping) => mapping.folderPath !== watchMapping.folderPath,
|
||||
);
|
||||
|
||||
setWatchMappings(watchMappings);
|
||||
@@ -55,11 +55,11 @@ export async function removeWatchMapping(folderPath: string) {
|
||||
|
||||
export function updateWatchMappingSyncedFiles(
|
||||
folderPath: string,
|
||||
files: WatchMapping['syncedFiles']
|
||||
files: WatchMapping["syncedFiles"],
|
||||
): void {
|
||||
const watchMappings = getWatchMappings();
|
||||
const watchMapping = watchMappings.find(
|
||||
(mapping) => mapping.folderPath === folderPath
|
||||
(mapping) => mapping.folderPath === folderPath,
|
||||
);
|
||||
|
||||
if (!watchMapping) {
|
||||
@@ -72,11 +72,11 @@ export function updateWatchMappingSyncedFiles(
|
||||
|
||||
export function updateWatchMappingIgnoredFiles(
|
||||
folderPath: string,
|
||||
files: WatchMapping['ignoredFiles']
|
||||
files: WatchMapping["ignoredFiles"],
|
||||
): void {
|
||||
const watchMappings = getWatchMappings();
|
||||
const watchMapping = watchMappings.find(
|
||||
(mapping) => mapping.folderPath === folderPath
|
||||
(mapping) => mapping.folderPath === folderPath,
|
||||
);
|
||||
|
||||
if (!watchMapping) {
|
||||
@@ -90,25 +90,25 @@ export function updateWatchMappingIgnoredFiles(
|
||||
export function registerWatcherFunctions(
|
||||
addFile: (file: ElectronFile) => Promise<void>,
|
||||
removeFile: (path: string) => Promise<void>,
|
||||
removeFolder: (folderPath: string) => Promise<void>
|
||||
removeFolder: (folderPath: string) => Promise<void>,
|
||||
) {
|
||||
ipcRenderer.removeAllListeners('watch-add');
|
||||
ipcRenderer.removeAllListeners('watch-change');
|
||||
ipcRenderer.removeAllListeners('watch-unlink-dir');
|
||||
ipcRenderer.on('watch-add', async (_, filePath: string) => {
|
||||
ipcRenderer.removeAllListeners("watch-add");
|
||||
ipcRenderer.removeAllListeners("watch-change");
|
||||
ipcRenderer.removeAllListeners("watch-unlink-dir");
|
||||
ipcRenderer.on("watch-add", async (_, filePath: string) => {
|
||||
filePath = filePath.split(path.sep).join(path.posix.sep);
|
||||
|
||||
await addFile(await getElectronFile(filePath));
|
||||
});
|
||||
ipcRenderer.on('watch-unlink', async (_, filePath: string) => {
|
||||
ipcRenderer.on("watch-unlink", async (_, filePath: string) => {
|
||||
filePath = filePath.split(path.sep).join(path.posix.sep);
|
||||
|
||||
await removeFile(filePath);
|
||||
});
|
||||
ipcRenderer.on('watch-unlink-dir', async (_, folderPath: string) => {
|
||||
ipcRenderer.on("watch-unlink-dir", async (_, folderPath: string) => {
|
||||
folderPath = folderPath.split(path.sep).join(path.posix.sep);
|
||||
await removeFolder(folderPath);
|
||||
});
|
||||
}
|
||||
|
||||
export { getWatchMappings } from '../services/watch';
|
||||
export { getWatchMappings } from "../services/watch";
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
const PROD_HOST_URL: string = 'ente://app';
|
||||
const RENDERER_OUTPUT_DIR: string = './ui/out';
|
||||
const LOG_FILENAME = 'ente.log';
|
||||
const MAX_LOG_SIZE = 50 * 1024 * 1024; // 50MB
|
||||
|
||||
const FILE_STREAM_CHUNK_SIZE: number = 4 * 1024 * 1024;
|
||||
|
||||
const SENTRY_DSN = 'https://759d8498487a81ac33a0c2efa2a42c4f@sentry.ente.io/9';
|
||||
|
||||
const RELEASE_VERSION = require('../../package.json').version;
|
||||
|
||||
export {
|
||||
PROD_HOST_URL,
|
||||
RENDERER_OUTPUT_DIR,
|
||||
FILE_STREAM_CHUNK_SIZE,
|
||||
LOG_FILENAME,
|
||||
MAX_LOG_SIZE,
|
||||
SENTRY_DSN,
|
||||
RELEASE_VERSION,
|
||||
};
|
||||
@@ -1,12 +1,12 @@
|
||||
export const CustomErrors = {
|
||||
WINDOWS_NATIVE_IMAGE_PROCESSING_NOT_SUPPORTED:
|
||||
'Windows native image processing is not supported',
|
||||
"Windows native image processing is not supported",
|
||||
INVALID_OS: (os: string) => `Invalid OS - ${os}`,
|
||||
WAIT_TIME_EXCEEDED: 'Wait time exceeded',
|
||||
WAIT_TIME_EXCEEDED: "Wait time exceeded",
|
||||
UNSUPPORTED_PLATFORM: (platform: string, arch: string) =>
|
||||
`Unsupported platform - ${platform} ${arch}`,
|
||||
MODEL_DOWNLOAD_PENDING:
|
||||
'Model download pending, skipping clip search request',
|
||||
INVALID_FILE_PATH: 'Invalid file path',
|
||||
"Model download pending, skipping clip search request",
|
||||
INVALID_FILE_PATH: "Invalid file path",
|
||||
INVALID_CLIP_MODEL: (model: string) => `Invalid Clip model - ${model}`,
|
||||
};
|
||||
|
||||
@@ -1,27 +1,25 @@
|
||||
import { app, BrowserWindow } from 'electron';
|
||||
import { createWindow } from './utils/createWindow';
|
||||
import setupIpcComs from './utils/ipcComms';
|
||||
import { initWatcher } from './services/chokidar';
|
||||
import { addAllowOriginHeader } from './utils/cors';
|
||||
import { app, BrowserWindow } from "electron";
|
||||
import electronReload from "electron-reload";
|
||||
import serveNextAt from "next-electron-server";
|
||||
import { initWatcher } from "./services/chokidar";
|
||||
import { isDev } from "./utils/common";
|
||||
import { addAllowOriginHeader } from "./utils/cors";
|
||||
import { createWindow } from "./utils/createWindow";
|
||||
import { setupAppEventEmitter } from "./utils/events";
|
||||
import setupIpcComs from "./utils/ipcComms";
|
||||
import { setupLogging } from "./utils/logging";
|
||||
import {
|
||||
setupTrayItem,
|
||||
handleDownloads,
|
||||
setupMacWindowOnDockIconClick,
|
||||
setupMainMenu,
|
||||
setupMainHotReload,
|
||||
setupNextElectronServe,
|
||||
enableSharedArrayBufferSupport,
|
||||
handleDockIconHideOnAutoLaunch,
|
||||
handleDownloads,
|
||||
handleExternalLinks,
|
||||
handleUpdates,
|
||||
logSystemInfo,
|
||||
handleExternalLinks,
|
||||
} from './utils/main';
|
||||
import { initSentry } from './services/sentry';
|
||||
import { setupLogging } from './utils/logging';
|
||||
import { isDev } from './utils/common';
|
||||
import { setupMainProcessStatsLogger } from './utils/processStats';
|
||||
import { setupAppEventEmitter } from './utils/events';
|
||||
import { getOptOutOfCrashReports } from './services/userPreference';
|
||||
setupMacWindowOnDockIconClick,
|
||||
setupMainMenu,
|
||||
setupTrayItem,
|
||||
} from "./utils/main";
|
||||
import { setupMainProcessStatsLogger } from "./utils/processStats";
|
||||
|
||||
let mainWindow: BrowserWindow;
|
||||
|
||||
@@ -29,8 +27,6 @@ let appIsQuitting = false;
|
||||
|
||||
let updateIsAvailable = false;
|
||||
|
||||
let optedOutOfCrashReports = false;
|
||||
|
||||
export const isAppQuitting = (): boolean => {
|
||||
return appIsQuitting;
|
||||
};
|
||||
@@ -42,22 +38,48 @@ export const setIsAppQuitting = (value: boolean): void => {
|
||||
export const isUpdateAvailable = (): boolean => {
|
||||
return updateIsAvailable;
|
||||
};
|
||||
|
||||
export const setIsUpdateAvailable = (value: boolean): void => {
|
||||
updateIsAvailable = value;
|
||||
};
|
||||
|
||||
export const hasOptedOutOfCrashReports = (): boolean => {
|
||||
return optedOutOfCrashReports;
|
||||
/**
|
||||
* Hot reload the main process if anything changes in the source directory that
|
||||
* we're running from.
|
||||
*
|
||||
* In particular, this gets triggered when the `tsc -w` rebuilds JS files in the
|
||||
* `app/` directory when we change the TS files in the `src/` directory.
|
||||
*/
|
||||
const setupMainHotReload = () => {
|
||||
if (isDev) {
|
||||
electronReload(__dirname, {});
|
||||
}
|
||||
};
|
||||
|
||||
export const updateOptOutOfCrashReports = (value: boolean): void => {
|
||||
optedOutOfCrashReports = value;
|
||||
/**
|
||||
* The URL where the renderer HTML is being served from.
|
||||
*/
|
||||
export const rendererURL = "next://app";
|
||||
|
||||
/**
|
||||
* next-electron-server allows up to directly use the output of `next build` in
|
||||
* production mode and `next dev` in development mode, whilst keeping the rest
|
||||
* of our code the same.
|
||||
*
|
||||
* It uses protocol handlers to serve files from the "next://app" protocol
|
||||
*
|
||||
* - In development this is proxied to http://localhost:3000
|
||||
* - In production it serves files from the `/out` directory
|
||||
*
|
||||
* For more details, see this comparison:
|
||||
* https://github.com/HaNdTriX/next-electron-server/issues/5
|
||||
*/
|
||||
const setupRendererServer = () => {
|
||||
serveNextAt(rendererURL);
|
||||
};
|
||||
|
||||
setupMainHotReload();
|
||||
|
||||
setupNextElectronServe();
|
||||
|
||||
setupRendererServer();
|
||||
setupLogging(isDev);
|
||||
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
@@ -66,7 +88,7 @@ if (!gotTheLock) {
|
||||
} else {
|
||||
handleDockIconHideOnAutoLaunch();
|
||||
enableSharedArrayBufferSupport();
|
||||
app.on('second-instance', () => {
|
||||
app.on("second-instance", () => {
|
||||
// Someone tried to run a second instance, we should focus our window.
|
||||
if (mainWindow) {
|
||||
mainWindow.show();
|
||||
@@ -80,14 +102,9 @@ if (!gotTheLock) {
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.on('ready', async () => {
|
||||
app.on("ready", async () => {
|
||||
logSystemInfo();
|
||||
setupMainProcessStatsLogger();
|
||||
const hasOptedOutOfCrashReports = getOptOutOfCrashReports();
|
||||
updateOptOutOfCrashReports(hasOptedOutOfCrashReports);
|
||||
if (!hasOptedOutOfCrashReports) {
|
||||
initSentry();
|
||||
}
|
||||
mainWindow = await createWindow();
|
||||
const tray = setupTrayItem(mainWindow);
|
||||
const watcher = initWatcher(mainWindow);
|
||||
@@ -101,5 +118,5 @@ if (!gotTheLock) {
|
||||
setupAppEventEmitter(mainWindow);
|
||||
});
|
||||
|
||||
app.on('before-quit', () => setIsAppQuitting(true));
|
||||
app.on("before-quit", () => setIsAppQuitting(true));
|
||||
}
|
||||
|
||||
@@ -1,79 +1,75 @@
|
||||
import {
|
||||
deleteDiskCache,
|
||||
getCacheDirectory,
|
||||
openDiskCache,
|
||||
setCustomCacheDirectory,
|
||||
} from "./api/cache";
|
||||
import { computeImageEmbedding, computeTextEmbedding } from "./api/clip";
|
||||
import {
|
||||
getAppVersion,
|
||||
getPlatform,
|
||||
logToDisk,
|
||||
openDirectory,
|
||||
openLogDirectory,
|
||||
selectDirectory,
|
||||
} from "./api/common";
|
||||
import { clearElectronStore } from "./api/electronStore";
|
||||
import {
|
||||
checkExistsAndCreateDir,
|
||||
exists,
|
||||
saveFileToDisk,
|
||||
saveStreamToDisk,
|
||||
} from "./api/export";
|
||||
import { runFFmpegCmd } from "./api/ffmpeg";
|
||||
import {
|
||||
deleteFile,
|
||||
deleteFolder,
|
||||
getDirFiles,
|
||||
isFolder,
|
||||
moveFile,
|
||||
readTextFile,
|
||||
rename,
|
||||
} from "./api/fs";
|
||||
import { convertToJPEG, generateImageThumbnail } from "./api/imageProcessor";
|
||||
import { getEncryptionKey, setEncryptionKey } from "./api/safeStorage";
|
||||
import {
|
||||
muteUpdateNotification,
|
||||
registerForegroundEventListener,
|
||||
registerUpdateEventListener,
|
||||
reloadWindow,
|
||||
sendNotification,
|
||||
updateAndRestart,
|
||||
skipAppUpdate,
|
||||
muteUpdateNotification,
|
||||
registerForegroundEventListener,
|
||||
} from './api/system';
|
||||
updateAndRestart,
|
||||
} from "./api/system";
|
||||
import {
|
||||
getElectronFilesFromGoogleZip,
|
||||
getPendingUploads,
|
||||
setToUploadCollection,
|
||||
setToUploadFiles,
|
||||
showUploadDirsDialog,
|
||||
showUploadFilesDialog,
|
||||
showUploadZipDialog,
|
||||
getPendingUploads,
|
||||
setToUploadFiles,
|
||||
getElectronFilesFromGoogleZip,
|
||||
setToUploadCollection,
|
||||
} from './api/upload';
|
||||
} from "./api/upload";
|
||||
import {
|
||||
registerWatcherFunctions,
|
||||
addWatchMapping,
|
||||
removeWatchMapping,
|
||||
updateWatchMappingSyncedFiles,
|
||||
updateWatchMappingIgnoredFiles,
|
||||
getWatchMappings,
|
||||
} from './api/watch';
|
||||
import { getEncryptionKey, setEncryptionKey } from './api/safeStorage';
|
||||
import { clearElectronStore } from './api/electronStore';
|
||||
registerWatcherFunctions,
|
||||
removeWatchMapping,
|
||||
updateWatchMappingIgnoredFiles,
|
||||
updateWatchMappingSyncedFiles,
|
||||
} from "./api/watch";
|
||||
import { setupLogging } from "./utils/logging";
|
||||
import {
|
||||
openDiskCache,
|
||||
deleteDiskCache,
|
||||
getCacheDirectory,
|
||||
setCustomCacheDirectory,
|
||||
} from './api/cache';
|
||||
import {
|
||||
checkExistsAndCreateDir,
|
||||
saveStreamToDisk,
|
||||
saveFileToDisk,
|
||||
exists,
|
||||
} from './api/export';
|
||||
import {
|
||||
selectDirectory,
|
||||
logToDisk,
|
||||
openLogDirectory,
|
||||
getSentryUserID,
|
||||
getAppVersion,
|
||||
openDirectory,
|
||||
updateOptOutOfCrashReports,
|
||||
getPlatform,
|
||||
} from './api/common';
|
||||
import { fixHotReloadNext12 } from './utils/preload';
|
||||
import {
|
||||
isFolder,
|
||||
getDirFiles,
|
||||
moveFile,
|
||||
deleteFolder,
|
||||
rename,
|
||||
readTextFile,
|
||||
deleteFile,
|
||||
} from './api/fs';
|
||||
import { convertToJPEG, generateImageThumbnail } from './api/imageProcessor';
|
||||
import { setupLogging } from './utils/logging';
|
||||
import {
|
||||
setupRendererProcessStatsLogger,
|
||||
logRendererProcessMemoryUsage,
|
||||
} from './utils/processStats';
|
||||
import { runFFmpegCmd } from './api/ffmpeg';
|
||||
import { computeImageEmbedding, computeTextEmbedding } from './api/clip';
|
||||
setupRendererProcessStatsLogger,
|
||||
} from "./utils/processStats";
|
||||
|
||||
fixHotReloadNext12();
|
||||
setupLogging();
|
||||
setupRendererProcessStatsLogger();
|
||||
|
||||
const windowObject: any = window;
|
||||
|
||||
windowObject['ElectronAPIs'] = {
|
||||
windowObject["ElectronAPIs"] = {
|
||||
exists,
|
||||
checkExistsAndCreateDir,
|
||||
saveStreamToDisk,
|
||||
@@ -108,7 +104,6 @@ windowObject['ElectronAPIs'] = {
|
||||
registerUpdateEventListener,
|
||||
updateAndRestart,
|
||||
skipAppUpdate,
|
||||
getSentryUserID,
|
||||
getAppVersion,
|
||||
runFFmpegCmd,
|
||||
muteUpdateNotification,
|
||||
@@ -120,7 +115,6 @@ windowObject['ElectronAPIs'] = {
|
||||
deleteFolder,
|
||||
rename,
|
||||
deleteFile,
|
||||
updateOptOutOfCrashReports,
|
||||
computeImageEmbedding,
|
||||
computeTextEmbedding,
|
||||
getPlatform,
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { app, BrowserWindow } from 'electron';
|
||||
import { autoUpdater } from 'electron-updater';
|
||||
import log from 'electron-log';
|
||||
import { setIsAppQuitting, setIsUpdateAvailable } from '../main';
|
||||
import { compareVersions } from 'compare-versions';
|
||||
import { AppUpdateInfo, GetFeatureFlagResponse } from '../types';
|
||||
import { compareVersions } from "compare-versions";
|
||||
import { app, BrowserWindow } from "electron";
|
||||
import { default as ElectronLog, default as log } from "electron-log";
|
||||
import { autoUpdater } from "electron-updater";
|
||||
import fetch from "node-fetch";
|
||||
import { setIsAppQuitting, setIsUpdateAvailable } from "../main";
|
||||
import { AppUpdateInfo, GetFeatureFlagResponse } from "../types";
|
||||
import { isPlatform } from "../utils/common/platform";
|
||||
import { logErrorSentry } from "./sentry";
|
||||
import {
|
||||
clearMuteUpdateNotificationVersion,
|
||||
clearSkipAppVersion,
|
||||
@@ -11,11 +14,7 @@ import {
|
||||
getSkipAppVersion,
|
||||
setMuteUpdateNotificationVersion,
|
||||
setSkipAppVersion,
|
||||
} from './userPreference';
|
||||
import fetch from 'node-fetch';
|
||||
import { logErrorSentry } from './sentry';
|
||||
import ElectronLog from 'electron-log';
|
||||
import { isPlatform } from '../utils/common/platform';
|
||||
} from "./userPreference";
|
||||
|
||||
const FIVE_MIN_IN_MICROSECOND = 5 * 60 * 1000;
|
||||
const ONE_DAY_IN_MICROSECOND = 1 * 24 * 60 * 60 * 1000;
|
||||
@@ -26,7 +25,7 @@ export function setupAutoUpdater(mainWindow: BrowserWindow) {
|
||||
checkForUpdateAndNotify(mainWindow);
|
||||
setInterval(
|
||||
() => checkForUpdateAndNotify(mainWindow),
|
||||
ONE_DAY_IN_MICROSECOND
|
||||
ONE_DAY_IN_MICROSECOND,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,22 +35,22 @@ export function forceCheckForUpdateAndNotify(mainWindow: BrowserWindow) {
|
||||
clearMuteUpdateNotificationVersion();
|
||||
checkForUpdateAndNotify(mainWindow);
|
||||
} catch (e) {
|
||||
logErrorSentry(e, 'forceCheckForUpdateAndNotify failed');
|
||||
logErrorSentry(e, "forceCheckForUpdateAndNotify failed");
|
||||
}
|
||||
}
|
||||
|
||||
async function checkForUpdateAndNotify(mainWindow: BrowserWindow) {
|
||||
try {
|
||||
log.debug('checkForUpdateAndNotify called');
|
||||
log.debug("checkForUpdateAndNotify called");
|
||||
const updateCheckResult = await autoUpdater.checkForUpdates();
|
||||
log.debug('update version', updateCheckResult.updateInfo.version);
|
||||
log.debug("update version", updateCheckResult.updateInfo.version);
|
||||
if (
|
||||
compareVersions(
|
||||
updateCheckResult.updateInfo.version,
|
||||
app.getVersion()
|
||||
app.getVersion(),
|
||||
) <= 0
|
||||
) {
|
||||
log.debug('already at latest version');
|
||||
log.debug("already at latest version");
|
||||
return;
|
||||
}
|
||||
const skipAppVersion = getSkipAppVersion();
|
||||
@@ -60,28 +59,28 @@ async function checkForUpdateAndNotify(mainWindow: BrowserWindow) {
|
||||
updateCheckResult.updateInfo.version === skipAppVersion
|
||||
) {
|
||||
log.info(
|
||||
'user chose to skip version ',
|
||||
updateCheckResult.updateInfo.version
|
||||
"user chose to skip version ",
|
||||
updateCheckResult.updateInfo.version,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const desktopCutoffVersion = await getDesktopCutoffVersion();
|
||||
if (
|
||||
desktopCutoffVersion &&
|
||||
isPlatform('mac') &&
|
||||
isPlatform("mac") &&
|
||||
compareVersions(
|
||||
updateCheckResult.updateInfo.version,
|
||||
desktopCutoffVersion
|
||||
desktopCutoffVersion,
|
||||
) > 0
|
||||
) {
|
||||
log.debug('auto update not possible due to key change');
|
||||
log.debug("auto update not possible due to key change");
|
||||
showUpdateDialog(mainWindow, {
|
||||
autoUpdatable: false,
|
||||
version: updateCheckResult.updateInfo.version,
|
||||
});
|
||||
} else {
|
||||
let timeout: NodeJS.Timeout;
|
||||
log.debug('attempting auto update');
|
||||
log.debug("attempting auto update");
|
||||
autoUpdater.downloadUpdate();
|
||||
const muteUpdateNotificationVersion =
|
||||
getMuteUpdateNotificationVersion();
|
||||
@@ -91,24 +90,24 @@ async function checkForUpdateAndNotify(mainWindow: BrowserWindow) {
|
||||
muteUpdateNotificationVersion
|
||||
) {
|
||||
log.info(
|
||||
'user chose to mute update notification for version ',
|
||||
updateCheckResult.updateInfo.version
|
||||
"user chose to mute update notification for version ",
|
||||
updateCheckResult.updateInfo.version,
|
||||
);
|
||||
return;
|
||||
}
|
||||
autoUpdater.on('update-downloaded', () => {
|
||||
autoUpdater.on("update-downloaded", () => {
|
||||
timeout = setTimeout(
|
||||
() =>
|
||||
showUpdateDialog(mainWindow, {
|
||||
autoUpdatable: true,
|
||||
version: updateCheckResult.updateInfo.version,
|
||||
}),
|
||||
FIVE_MIN_IN_MICROSECOND
|
||||
FIVE_MIN_IN_MICROSECOND,
|
||||
);
|
||||
});
|
||||
autoUpdater.on('error', (error) => {
|
||||
autoUpdater.on("error", (error) => {
|
||||
clearTimeout(timeout);
|
||||
logErrorSentry(error, 'auto update failed');
|
||||
logErrorSentry(error, "auto update failed");
|
||||
showUpdateDialog(mainWindow, {
|
||||
autoUpdatable: false,
|
||||
version: updateCheckResult.updateInfo.version,
|
||||
@@ -117,12 +116,12 @@ async function checkForUpdateAndNotify(mainWindow: BrowserWindow) {
|
||||
}
|
||||
setIsUpdateAvailable(true);
|
||||
} catch (e) {
|
||||
logErrorSentry(e, 'checkForUpdateAndNotify failed');
|
||||
logErrorSentry(e, "checkForUpdateAndNotify failed");
|
||||
}
|
||||
}
|
||||
|
||||
export function updateAndRestart() {
|
||||
ElectronLog.log('user quit the app');
|
||||
ElectronLog.log("user quit the app");
|
||||
setIsAppQuitting(true);
|
||||
autoUpdater.quitAndInstall();
|
||||
}
|
||||
@@ -142,18 +141,18 @@ export function muteUpdateNotification(version: string) {
|
||||
async function getDesktopCutoffVersion() {
|
||||
try {
|
||||
const featureFlags = (
|
||||
await fetch('https://static.ente.io/feature_flags.json')
|
||||
await fetch("https://static.ente.io/feature_flags.json")
|
||||
).json() as GetFeatureFlagResponse;
|
||||
return featureFlags.desktopCutoffVersion;
|
||||
} catch (e) {
|
||||
logErrorSentry(e, 'failed to get feature flags');
|
||||
logErrorSentry(e, "failed to get feature flags");
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function showUpdateDialog(
|
||||
mainWindow: BrowserWindow,
|
||||
updateInfo: AppUpdateInfo
|
||||
updateInfo: AppUpdateInfo,
|
||||
) {
|
||||
mainWindow.webContents.send('show-update-dialog', updateInfo);
|
||||
mainWindow.webContents.send("show-update-dialog", updateInfo);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { isPlatform } from '../utils/common/platform';
|
||||
import { AutoLauncherClient } from '../types/autoLauncher';
|
||||
import linuxAndWinAutoLauncher from './autoLauncherClients/linuxAndWinAutoLauncher';
|
||||
import macAutoLauncher from './autoLauncherClients/macAutoLauncher';
|
||||
import { AutoLauncherClient } from "../types/autoLauncher";
|
||||
import { isPlatform } from "../utils/common/platform";
|
||||
import linuxAndWinAutoLauncher from "./autoLauncherClients/linuxAndWinAutoLauncher";
|
||||
import macAutoLauncher from "./autoLauncherClients/macAutoLauncher";
|
||||
|
||||
class AutoLauncher {
|
||||
private client: AutoLauncherClient;
|
||||
async init() {
|
||||
if (isPlatform('linux') || isPlatform('windows')) {
|
||||
if (isPlatform("linux") || isPlatform("windows")) {
|
||||
this.client = linuxAndWinAutoLauncher;
|
||||
} else {
|
||||
this.client = macAutoLauncher;
|
||||
}
|
||||
// migrate old auto launch settings for windows from mac auto launcher to linux and windows auto launcher
|
||||
if (isPlatform('windows') && (await macAutoLauncher.isEnabled())) {
|
||||
if (isPlatform("windows") && (await macAutoLauncher.isEnabled())) {
|
||||
await macAutoLauncher.toggleAutoLaunch();
|
||||
await linuxAndWinAutoLauncher.toggleAutoLaunch();
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import AutoLaunch from 'auto-launch';
|
||||
import { AutoLauncherClient } from '../../types/autoLauncher';
|
||||
import { app } from 'electron';
|
||||
import AutoLaunch from "auto-launch";
|
||||
import { app } from "electron";
|
||||
import { AutoLauncherClient } from "../../types/autoLauncher";
|
||||
|
||||
const LAUNCHED_AS_HIDDEN_FLAG = 'hidden';
|
||||
const LAUNCHED_AS_HIDDEN_FLAG = "hidden";
|
||||
|
||||
class LinuxAndWinAutoLauncher implements AutoLauncherClient {
|
||||
private instance: AutoLaunch;
|
||||
constructor() {
|
||||
const autoLauncher = new AutoLaunch({
|
||||
name: 'ente',
|
||||
name: "ente",
|
||||
isHidden: true,
|
||||
});
|
||||
this.instance = autoLauncher;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { app } from 'electron';
|
||||
import { AutoLauncherClient } from '../../types/autoLauncher';
|
||||
import { app } from "electron";
|
||||
import { AutoLauncherClient } from "../../types/autoLauncher";
|
||||
|
||||
class MacAutoLauncher implements AutoLauncherClient {
|
||||
async isEnabled() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import chokidar from 'chokidar';
|
||||
import { BrowserWindow } from 'electron';
|
||||
import { logError } from '../services/logging';
|
||||
import { getWatchMappings } from '../api/watch';
|
||||
import chokidar from "chokidar";
|
||||
import { BrowserWindow } from "electron";
|
||||
import { getWatchMappings } from "../api/watch";
|
||||
import { logError } from "../services/logging";
|
||||
|
||||
export function initWatcher(mainWindow: BrowserWindow) {
|
||||
const mappings = getWatchMappings();
|
||||
@@ -13,20 +13,20 @@ export function initWatcher(mainWindow: BrowserWindow) {
|
||||
awaitWriteFinish: true,
|
||||
});
|
||||
watcher
|
||||
.on('add', (path) => {
|
||||
mainWindow.webContents.send('watch-add', path);
|
||||
.on("add", (path) => {
|
||||
mainWindow.webContents.send("watch-add", path);
|
||||
})
|
||||
.on('change', (path) => {
|
||||
mainWindow.webContents.send('watch-change', path);
|
||||
.on("change", (path) => {
|
||||
mainWindow.webContents.send("watch-change", path);
|
||||
})
|
||||
.on('unlink', (path) => {
|
||||
mainWindow.webContents.send('watch-unlink', path);
|
||||
.on("unlink", (path) => {
|
||||
mainWindow.webContents.send("watch-unlink", path);
|
||||
})
|
||||
.on('unlinkDir', (path) => {
|
||||
mainWindow.webContents.send('watch-unlink-dir', path);
|
||||
.on("unlinkDir", (path) => {
|
||||
mainWindow.webContents.send("watch-unlink-dir", path);
|
||||
})
|
||||
.on('error', (error) => {
|
||||
logError(error, 'error while watching files');
|
||||
.on("error", (error) => {
|
||||
logError(error, "error while watching files");
|
||||
});
|
||||
|
||||
return watcher;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user