Compare commits
10 Commits
selection-
...
ios-workfl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
989b1c6439 | ||
|
|
06c3541888 | ||
|
|
aca698e035 | ||
|
|
46a3e609bd | ||
|
|
72121b6181 | ||
|
|
f8d9aa230e | ||
|
|
ebd37ca22e | ||
|
|
ce36037f9e | ||
|
|
4cbb4c69f8 | ||
|
|
7f4c87068d |
207
.github/workflows/auth-internal-release.yml
vendored
@@ -1,68 +1,173 @@
|
||||
name: "Internal release (auth mobile)"
|
||||
|
||||
on:
|
||||
workflow_dispatch: # Allow manually running the action
|
||||
workflow_dispatch: # Allow manually running the action
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.24.3"
|
||||
FLUTTER_VERSION: "3.24.3"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: mobile/apps/auth
|
||||
steps:
|
||||
- name: Checkout code and submodules
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: mobile/apps/auth
|
||||
- name: Setup JDK 17
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 17
|
||||
|
||||
steps:
|
||||
- name: Checkout code and submodules
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install Flutter ${{ env.FLUTTER_VERSION }}
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
cache: true
|
||||
|
||||
- name: Setup JDK 17
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 17
|
||||
- name: Setup keys
|
||||
uses: timheuer/base64-to-file@v1
|
||||
with:
|
||||
fileName: "keystore/ente_auth_key.jks"
|
||||
encodedString: ${{ secrets.SIGNING_KEY }}
|
||||
|
||||
- name: Install Flutter ${{ env.FLUTTER_VERSION }}
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
cache: true
|
||||
- name: Build PlayStore AAB
|
||||
run: |
|
||||
flutter build appbundle --dart-define=cronetHttpNoPlay=true --release --flavor playstore
|
||||
env:
|
||||
SIGNING_KEY_PATH: "/home/runner/work/_temp/keystore/ente_auth_key.jks"
|
||||
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
|
||||
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
||||
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
|
||||
|
||||
- name: Setup keys
|
||||
uses: timheuer/base64-to-file@v1
|
||||
with:
|
||||
fileName: "keystore/ente_auth_key.jks"
|
||||
encodedString: ${{ secrets.SIGNING_KEY }}
|
||||
- name: Upload AAB to PlayStore
|
||||
uses: r0adkll/upload-google-play@v1
|
||||
with:
|
||||
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
|
||||
packageName: io.ente.auth
|
||||
releaseFiles: mobile/apps/auth/build/app/outputs/bundle/playstoreRelease/app-playstore-release.aab
|
||||
track: internal
|
||||
|
||||
- name: Build PlayStore AAB
|
||||
run: |
|
||||
flutter build appbundle --dart-define=cronetHttpNoPlay=true --release --flavor playstore
|
||||
env:
|
||||
SIGNING_KEY_PATH: "/home/runner/work/_temp/keystore/ente_auth_key.jks"
|
||||
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
|
||||
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
||||
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
|
||||
- name: Notify Discord
|
||||
uses: sarisia/actions-status-discord@v1
|
||||
with:
|
||||
webhook: ${{ secrets.DISCORD_INTERNAL_RELEASE_WEBHOOK }}
|
||||
nodetail: true
|
||||
title: "🏆 Internal release available for Auth"
|
||||
description: "[Download](https://play.google.com/store/apps/details?id=io.ente.auth)"
|
||||
color: 0x800080
|
||||
|
||||
- name: Upload AAB to PlayStore
|
||||
uses: r0adkll/upload-google-play@v1
|
||||
with:
|
||||
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
|
||||
packageName: io.ente.auth
|
||||
releaseFiles: mobile/apps/auth/build/app/outputs/bundle/playstoreRelease/app-playstore-release.aab
|
||||
track: internal
|
||||
build-ios:
|
||||
runs-on: macos-15
|
||||
environment: "ios-build"
|
||||
|
||||
- name: Notify Discord
|
||||
uses: sarisia/actions-status-discord@v1
|
||||
with:
|
||||
webhook: ${{ secrets.DISCORD_INTERNAL_RELEASE_WEBHOOK }}
|
||||
nodetail: true
|
||||
title: "🏆 Internal release available for Auth"
|
||||
description: "[Download](https://play.google.com/store/apps/details?id=io.ente.auth)"
|
||||
color: 0x800080
|
||||
env:
|
||||
FLUTTER_VERSION: "3.24.3"
|
||||
APP_STORE_CONNECT_PRIVATE_KEY: ${{ secrets.IOS_API_KEY }}
|
||||
APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.IOS_ISSUER_ID }}
|
||||
APP_STORE_CONNECT_KEY_IDENTIFIER: ${{ secrets.IOS_API_KEY_ID }}
|
||||
PROVISIONING_PROFILE: ${{ secrets.IOS_AUTHGITHUBDISTRIBUTION_PROFILE }}
|
||||
DIST_CERTIFICATE: ${{ secrets.IOS_DISTRIBUTION_P12_CERT }}
|
||||
DIST_CERTIFICATE_PASSWORD: ${{ secrets.IOS_DISTRIBUTION_P12_CERT_PWD }}
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: mobile/apps/auth
|
||||
|
||||
steps:
|
||||
- name: Checkout code and submodules
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install Flutter ${{ env.FLUTTER_VERSION }}
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
cache: true
|
||||
|
||||
- name: Install codemagic tools (optional)
|
||||
run: pip3 install --break-system-packages codemagic-cli-tools
|
||||
|
||||
- name: Install provisioning profile
|
||||
run: |
|
||||
PROFILES_HOME="$HOME/Library/MobileDevice/Provisioning Profiles"
|
||||
mkdir -p "$PROFILES_HOME"
|
||||
PROFILE_PATH="$PROFILES_HOME/$(uuidgen).mobileprovision"
|
||||
echo "$PROVISIONING_PROFILE" | base64 --decode > "$PROFILE_PATH"
|
||||
echo "Saved provisioning profile to $PROFILE_PATH"
|
||||
|
||||
- name: Import iOS Distribution Certificate
|
||||
run: |
|
||||
# Create keychain
|
||||
security create-keychain -p "apple123" build.keychain
|
||||
security set-keychain-settings -lut 21600 build.keychain
|
||||
security unlock-keychain -p "apple123" build.keychain
|
||||
security list-keychains -s build.keychain
|
||||
|
||||
# Decode and import certificate
|
||||
CERT_PATH=$RUNNER_TEMP/dist_cert.p12
|
||||
echo "$DIST_CERTIFICATE" | base64 --decode > "$CERT_PATH"
|
||||
security import "$CERT_PATH" -k build.keychain -P "$DIST_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
|
||||
|
||||
# Allow codesign to use this identity
|
||||
security set-key-partition-list -S apple-tool:,apple: -s -k "apple123" build.keychain
|
||||
|
||||
# Debug: show available code signing identities
|
||||
security find-identity -v -p codesigning
|
||||
|
||||
- name: Create export options plist
|
||||
run: |
|
||||
cat <<EOF > $HOME/export_options.plist
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>destination</key>
|
||||
<string>export</string>
|
||||
<key>generateAppStoreInformation</key>
|
||||
<false/>
|
||||
<key>manageAppVersionAndBuildNumber</key>
|
||||
<true/>
|
||||
<key>method</key>
|
||||
<string>app-store-connect</string>
|
||||
<key>signingStyle</key>
|
||||
<string>automatic</string>
|
||||
<key>stripSwiftSymbols</key>
|
||||
<true/>
|
||||
<key>teamID</key>
|
||||
<string>6Z68YJY9Q2</string>
|
||||
<key>testFlightInternalTestingOnly</key>
|
||||
<false/>
|
||||
<key>uploadSymbols</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
|
||||
- name: Build iOS IPA
|
||||
run: |
|
||||
flutter build ipa \
|
||||
--release \
|
||||
--export-options-plist=$HOME/export_options.plist \
|
||||
--dart-define=cronetHttpNoPlay=true
|
||||
|
||||
- name: Upload IPA to App Store Connect
|
||||
run: |
|
||||
IPA_PATH=$(find build/ios/archive/ -name "*.ipa" | head -n 1)
|
||||
if [[ -f "$IPA_PATH" ]]; then
|
||||
app-store-connect publish --path "$IPA_PATH"
|
||||
else
|
||||
echo "❌ IPA not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -48,7 +48,7 @@ See [docs/](docs/README.md) for how to edit these documents.
|
||||
|
||||
## Code contributions
|
||||
|
||||
If you'd like to contribute code, it is best to start small. Consider some well-scoped changes, say like adding more [custom icons to auth](mobile/apps/auth/docs/adding-icons.md), or fixing a specific bug.
|
||||
If you'd like to contribute code, it is best to start small. Consider some well-scoped changes, say like adding more [custom icons to auth](auth/docs/adding-icons.md), or fixing a specific bug.
|
||||
|
||||
Code that changes the behaviour of the product might not get merged, at least not initially. The PR can serve as a discussion bed, but you might find it easier to just start a discussion instead, or post your perspective in the (likely) existing thread about the behaviour change or new feature you wish for.
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ more, see [docs/adding-icons](docs/adding-icons.md).
|
||||
The best way to support this project is by checking out [Ente
|
||||
Photos](../mobile/README.md) or spreading the word.
|
||||
|
||||
For more ways to contribute, see [../../../CONTRIBUTING.md](../../../CONTRIBUTING.md).
|
||||
For more ways to contribute, see [../CONTRIBUTING.md](../../../CONTRIBUTING.md).
|
||||
|
||||
## Certificate Fingerprints
|
||||
|
||||
|
||||
@@ -60,15 +60,6 @@
|
||||
"slug": "amtrak",
|
||||
"hex": "003A5D"
|
||||
},
|
||||
{
|
||||
"title": "Animal Crossing",
|
||||
"slug:": "animal_crossing",
|
||||
"altNames": [
|
||||
"AnimalCrossing",
|
||||
"Bell Tree Forums",
|
||||
"BellTree Forums"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Ankama",
|
||||
"slug": "ankama"
|
||||
@@ -90,13 +81,6 @@
|
||||
"Docaposte AR24"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Art Fight",
|
||||
"slug": "art_fight",
|
||||
"altNames": [
|
||||
"ArtFight"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Aruba",
|
||||
"slug": "aruba",
|
||||
@@ -357,9 +341,6 @@
|
||||
"slug": "cih",
|
||||
"hex": "D14633"
|
||||
},
|
||||
{
|
||||
"title": "Chucklefish"
|
||||
},
|
||||
{
|
||||
"title": "Clipper",
|
||||
"slug": "clippercard",
|
||||
@@ -1524,9 +1505,6 @@
|
||||
{
|
||||
"title": "Skinport"
|
||||
},
|
||||
{
|
||||
"title": "Smogon"
|
||||
},
|
||||
{
|
||||
"title": "SMSPool",
|
||||
"slug": "sms_pool_net",
|
||||
@@ -1686,12 +1664,6 @@
|
||||
{
|
||||
"title": "TorGuard"
|
||||
},
|
||||
{
|
||||
"title": "Toyhouse",
|
||||
"altNames": [
|
||||
"Toyhou.se"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Trading 212"
|
||||
},
|
||||
@@ -1727,12 +1699,6 @@
|
||||
"Twitch tv"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Twitter",
|
||||
"altNames": [
|
||||
"X",
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Ubiquiti",
|
||||
"slug": "ubiquiti",
|
||||
@@ -1942,4 +1908,4 @@
|
||||
"slug": "cowheels"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 326.24 311.07"><path fill="#1aae5e" d="M266.38 1.26c-.93 9.12-7.71 47.21-27.28 83.44a1 1 0 0 1-1.7-.06c-4-8.24-24.86-44.58-79.29-44.91C96.71 39.35 66.67 87 66.67 87s-23.45 32.93-33.49 94.89-22.61 79-22.61 79L.39 277a2.54 2.54 0 0 0 1.11 3.67c13 5.81 72.68 30.38 151.68 30.38 53.8 0 70-7.58 74.25-10.55a.61.61 0 0 0-.35-1.12c-10.55 1.16-51.2-5.26-51.2-44 0-34.79 25.3-51.62 51.81-52.83 12.56-.58 39.35 4.74 47.72 29.3 7 20.66-2.31 39.15-5.45 44.5a.62.62 0 0 0 .77.89c9.52-3.84 53.71-25.77 55.47-93.95 1.55-59.91-47.64-85.62-70.8-86.71a1.18 1.18 0 0 1-1-1.62c6.23-15.94 45-57.27 51.84-64.46a1.57 1.57 0 0 0 0-2.17A159.05 159.05 0 0 0 268.46.17a1.42 1.42 0 0 0-2.08 1.09Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 730 B |
|
Before Width: | Height: | Size: 35 KiB |
@@ -1,13 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="584.48486" >
|
||||
<g transform="translate(-137.5,-22.390163)">
|
||||
<path style="fill:#1c252c" d="m 137.5,29.631487 0,252.320873 132.1114,0 c 51.99819,0 100.05059,-18.25003 100.05059,-71.85789 0,-36.24066 -26.10767,-52.7841 -54.82326,-61.4437 17.4955,-7.01771 37.3423,-21.76056 37.3423,-52.666034 0,-39.55613 -39.24823,-66.353249 -92.09117,-66.353249 l -122.58986,0 z m 80.93311,58.76577 29.75482,0 c 12.11271,0 21.34908,9.487931 21.34908,19.117473 0,8.94677 -9.59745,18.52237 -21.34908,18.52237 l -29.75482,0 0,-37.639843 z m 0,90.008333 39.27636,0 c 13.71131,0 25.21721,10.41366 25.21721,22.31611 0,12.63921 -10.76115,23.13438 -27.59759,23.13438 l -36.89598,0 0,-45.45049 z" id="path2998" />
|
||||
<path id="path3005" d="m 202.9606,309.47481 0,252.32088 132.11141,0 c 51.99819,0 100.05058,-18.25004 100.05058,-71.8579 0,-36.24065 -26.10766,-52.78409 -54.82326,-61.4437 17.4955,-7.01771 37.3423,-21.76056 37.3423,-52.66603 0,-39.55613 -39.24822,-66.35325 -92.09117,-66.35325 l -122.58986,0 z m 80.93312,58.76577 29.75482,0 c 12.11271,0 21.34908,9.48793 21.34908,19.11747 0,8.94678 -9.59745,18.52238 -21.34908,18.52238 l -29.75482,0 0,-37.63985 z m 0,90.00833 39.27636,0 c 13.71131,0 25.21721,10.41366 25.21721,22.31612 0,12.6392 -10.76115,23.13437 -27.5976,23.13437 l -36.89597,0 0,-45.45049 z" style="fill:#1c252c" />
|
||||
<path style="fill:#1c252c" d="m 376.01464,281.92185 0,-252.32088 203.52297,0 0,60.937872 -122.8279,0 0,33.087358 99.73816,0 0,57.12926 -99.73816,0 0,40.46655 122.8279,0 0,60.69984 z" id="path3007"/>
|
||||
<path style="fill:#1c252c" d="m 689.78812,289.1442 c 57.28767,0 103.11071,-32.67744 103.11071,-85.63169 0,-85.46143 -111.17753,-72.31037 -111.17753,-98.91916 0,-10.278288 10.80934,-15.732656 21.89462,-15.732656 19.10438,0 32.9075,12.586126 32.9075,12.586126 L 784.35446,55.903565 C 765.2233,37.697019 735.00886,22.390163 694.30743,22.390163 c -61.12759,0 -101.11859,36.280243 -101.11859,80.044867 0,86.54828 109.57491,73.98694 109.57491,101.14304 0,9.51935 -9.15835,19.09619 -25.76901,19.09619 -18.85922,0 -33.79879,-11.38479 -45.42578,-21.04418 l -48.11128,45.86837 c 19.37303,18.87022 50.47517,41.64575 106.33044,41.64575 z" id="path3009" />
|
||||
<path style="fill:#1c252c" d="m 855.18627,281.92185 0,-191.621047 -66.6508,0 0,-60.699833 214.23473,0 0,60.699833 -66.65082,0 0,191.621047 z" id="path3011" />
|
||||
<path style="fill:#1c252c" d="m 437.90467,309.29628 80.69507,0 0,151.15449 c 0,15.34925 15.27608,29.49386 31.20285,29.49386 15.02419,0 30.2111,-12.77313 30.2111,-30.30098 l 0,-150.34737 80.45703,0 0,149.31902 c 0,59.25051 -49.19276,106.51359 -112.80036,106.51359 -63.96969,0 -109.76569,-51.43651 -109.76569,-109.74208 z" id="path3013" />
|
||||
<path style="fill:#1c252c;fill-opacity:1;stroke:none" d="m 759.97084,561.61716 0,-90.16156 -94.84461,-162.15932 81.6448,0 53.66637,86.88408 53.85177,-86.88408 81.83021,0 -95.21543,163.08638 0,89.2345 z" id="path3015" />
|
||||
<path style="fill:#ffed31" d="m 936.11938,447.38916 -47.60772,47.60772 0,64.27041 47.60772,47.60771 201.38062,0 0,-159.48584 z" id="path3017" />
|
||||
<path style="fill:#1c252c" id="path3024" d="m 469,573.36218 c 0,2.20914 -1.79086,4 -4,4 -2.20914,0 -4,-1.79086 -4,-4 0,-2.20914 1.79086,-4 4,-4 2.20914,0 4,1.79086 4,4 z" transform="matrix(2.6779338,0,0,2.6779338,-327.65077,-1008.3244)" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 16 KiB |
@@ -1,170 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" version="1.1" width="451.00259" height="507.332" viewBox="0 0 451.00259 507.332" id="svg2">
|
||||
<defs id="defs4"/>
|
||||
<metadata id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
||||
<dc:title/>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g transform="translate(-197.35585,-58.696183)" id="layer1">
|
||||
<g transform="translate(28.601602,6.2831826)" id="g2986">
|
||||
<g id="g2988">
|
||||
<g id="g2990">
|
||||
<path d="m 284.953,425.625 c -41.441,-0.008 -74.219,-15.391 -94.77,-44.48 -23.637,-33.461 -28.066,-83.176 -11.559,-129.75 21.262,-60.016 68.266,-89.355 68.742,-89.641 l 2.801,-1.703 4.555,-14.98 21.422,4.02 14.844,29.734 -0.773,5.273 c -0.738,4.98 -1.48,9.875 -2.215,14.695 l -0.191,1.238 c -5.277,34.695 -9.832,64.66 -6.102,82.488 2.242,10.711 7.199,17.898 15.895,22.906 2.984,-3.594 5.02,-5.305 7.535,-5.305 h 0.812 l 0.98,0.34 c 3.582,1.434 4.047,5.508 3.922,10.246 0.348,0.098 0.703,0.199 1.07,0.289 6.363,1.621 11.062,5.375 13.602,10.852 4.191,9.082 0.234,17.125 -3.254,24.215 -2.578,5.238 -6.379,12.953 -8.754,23.418 3.586,6.008 4.141,11.75 3.934,15.641 -0.297,5.516 -2.406,11.066 -5.996,15.848 1.102,4.875 0.484,9.473 -1.844,13.688 -5.285,9.566 -15.68,10.465 -19.094,10.754 -1.574,0.137 -3.34,0.215 -5.262,0.215 h -0.3 z" id="path2992" style="fill:#260859"/>
|
||||
</g>
|
||||
<g id="g2994">
|
||||
<path d="m 506.766,425.695 c -6.77,0 -12.57,-2.73 -16.773,-7.902 -8.246,-10.133 -6.812,-26.621 -3.918,-41.938 -3.145,-2.297 -7.34,-5.594 -9.633,-10.934 l -5.355,-12.438 11.875,-6.527 c 24.914,-13.723 34.227,-39.723 27.668,-77.289 -2.957,-4.148 -7.086,-11.844 -10.824,-25.562 -2.727,-10.008 -13.16,-72.691 -7.508,-83.227 1.012,-1.898 5.035,-8.109 14.039,-8.109 5.695,0 19.23,3.664 28.004,12.398 0.246,-0.012 0.488,-0.016 0.734,-0.016 5.488,0 9.648,2.668 13.672,5.242 l 0.477,0.301 c 1.07,0.68 2.219,1.41 3.492,2.164 2.246,1.336 6.668,3.961 10.598,9.719 6.301,1.035 11.359,5.949 20.273,15.086 0.207,0.219 0.996,1.027 1.203,1.227 2.219,2.195 3.977,5.051 5.602,9.125 11.438,4.383 18.34,21.609 19.629,27.852 l 0.508,1.938 c 15.383,58.23 11.477,106.727 -11.297,140.25 -18.512,27.266 -48.637,43.574 -89.535,48.465 -1.045,0.117 -1.994,0.175 -2.931,0.175 l 0,0 z" id="path2996" style="fill:#260859"/>
|
||||
</g>
|
||||
</g>
|
||||
<path d="M 382.699,555.371 C 362.824,548.375 188.613,484.344 188.613,390.375 l 0.012,-281.641 c 0,-25.367 21.531,-46.004 47.996,-46.004 16.855,0 32.289,8.32 40.957,21.902 31.453,-3.801 73.332,-11.934 99.66,-23.809 l 2.559,-1.129 15.438,-7.281 16.172,7.629 c 26.121,12.301 69,20.699 101.461,24.602 8.672,-13.59 24.102,-21.914 40.949,-21.914 26.473,0 48.008,20.551 48.008,45.812 l -0.027,281.832 c 0,94.301 -174.176,158.012 -194.039,164.965 l -12.523,4.406 -12.537,-4.374 z" id="path2998" style="fill:#260859"/>
|
||||
<path d="M 389.637,535.469 C 371.207,528.989 209.688,469.871 209.688,390.375 L 209.7,108.734 c 0,-13.93 12.086,-24.93 26.922,-24.93 14.336,0 26.137,10.309 26.867,23.469 l 0.004,0.012 c 35.363,-3.055 88.648,-12.023 122.41,-27.25 l 2.426,-1.07 6.906,-3.254 7.176,3.387 c 33.441,15.754 87.988,25.031 124.539,28.188 h 0.004 c 0.73,-13.168 12.527,-23.48 26.863,-23.48 14.852,0 26.934,11.098 26.934,24.738 l -0.027,281.832 c 0,79.676 -161.52,138.625 -179.938,145.082 l -5.57,1.961 -5.579,-1.95 z" id="path3000" style="fill:#7c3a00"/>
|
||||
<path d="m 395.156,112.82 -1.527,0.621 c -37.711,15.406 -92.113,24.406 -127.242,27.43 l -22.969,1.984 -0.008,130.859 H 395.156 V 112.82 z" id="path3002" style="fill:#ffef6f"/>
|
||||
<path d="m 395.223,75.715 -6.895,3.25 -2.426,1.07 c -33.762,15.227 -87.047,24.195 -122.41,27.25 l -0.004,-0.012 c -0.73,-13.16 -12.531,-23.469 -26.867,-23.469 -14.836,0 -26.922,11 -26.922,24.93 l -0.012,281.641 c 0,79.496 161.52,138.613 179.949,145.094 l 5.578,1.949 0.008,-0.004 V 75.715 z" id="path3004" style="fill:#e8941a"/>
|
||||
<g id="g3006">
|
||||
<path d="m 553.816,100.664 c -5.395,0 -9.801,3.312 -10.035,7.539 l -0.941,17.375 -17.34,-1.5 c -38.246,-3.301 -94.871,-13.051 -130.277,-29.734 -0.773,0.363 -1.609,0.707 -2.402,1.062 0.016,-0.004 0.031,-0.012 0.043,-0.016 v 307.098 l 171.008,-126.707 0.02,-167.238 c -0.001,-4.27 -4.615,-7.879 -10.076,-7.879 z" id="path3008" style="fill:#e8941a"/>
|
||||
</g>
|
||||
<path d="M 392.863,397.402 V 95.391 c -35.527,16.031 -90.5,25.457 -127.926,28.688 l -17.336,1.5 -0.941,-17.367 c -0.234,-4.234 -4.645,-7.547 -10.039,-7.547 -5.766,0 -10.082,3.891 -10.09,7.219 l 0.027,0.852 -0.004,152.738 166.309,135.928 z" id="path3010" style="fill:#ffef6f"/>
|
||||
<path d="m 525.5,124.078 c -38.246,-3.301 -94.871,-13.051 -130.277,-29.734 -0.492,0.23 -1.031,0.449 -1.527,0.676 v 41.531 l 30.055,-1.555 113.195,21.496 10.082,-13.633 -4.188,-17.281 -17.34,-1.5 z" id="path3012" style="fill:#e8941a"/>
|
||||
<path d="m 393.695,95.02 c -35.492,16.262 -91.043,25.805 -128.758,29.059 l -17.336,1.5 -4.184,17.277 19.438,6.918 102.445,-11.754 28.395,-1.469 V 95.02 z" id="path3014" style="fill:#ffffff"/>
|
||||
<path d="m 226.555,243.211 -0.008,147.652 c 0,55.215 105.336,106.918 168.656,129.176 63.324,-22.238 168.66,-73.918 168.66,-129.176 l 0.016,-147.652 H 226.555 z" id="path3016" style="fill:#ffd26c"/>
|
||||
<path d="M 393.746,508.047 C 318.914,480.473 243.406,435.543 243.406,397.313 l 0.012,-254.457 22.969,-1.984 c 35.129,-3.023 89.531,-12.023 127.242,-27.43 l 1.594,-0.648 1.59,0.648 c 37.711,15.406 92.109,24.406 127.242,27.43 l 22.973,1.988 -0.023,254.453 c 0,38.266 -75.508,83.188 -150.344,110.734 l -1.457,0.535 -1.458,-0.535 z" id="path3018" style="fill:#ffffff"/>
|
||||
<path d="m 524.055,140.871 c -35.133,-3.023 -89.531,-12.023 -127.242,-27.43 l -1.59,-0.648 -1.594,0.648 1.527,-0.621 v 160.895 h 151.859 l 0.012,-130.855 -22.972,-1.989 z" id="path3020" style="fill:#ffd26c"/>
|
||||
<path d="m 395.156,112.82 -1.527,0.621 c -37.711,15.406 -92.113,24.406 -127.242,27.43 l -22.969,1.984 -0.008,130.859 H 395.156 V 112.82 z" id="path3022" style="fill:#ffef6f"/>
|
||||
<linearGradient x1="395.21631" y1="132.92529" x2="395.21631" y2="491.23761" id="SVGID_1_" gradientUnits="userSpaceOnUse">
|
||||
<stop id="stop3025" style="stop-color:#e8941a;stop-opacity:1" offset="0"/>
|
||||
<stop id="stop3027" style="stop-color:#ffef6f;stop-opacity:1" offset="1"/>
|
||||
</linearGradient>
|
||||
<path d="M 395.203,494.371 C 315.765,464.609 256.051,422.691 256.051,396.586 l 0.012,-242.137 11.41,-0.984 c 34.82,-2.996 88.879,-11.863 127.75,-27.043 38.859,15.18 92.914,24.047 127.742,27.043 l 11.418,0.988 -0.023,242.133 c -10e-4,26.133 -59.715,68.051 -139.157,97.785 l 0,0 z" id="path3029" style="fill:url(#SVGID_1_)"/>
|
||||
<path d="m 534.359,377.328 c 0,26.133 -59.715,68.051 -139.156,97.785 C 315.777,445.355 256.07,403.449 256.051,377.34 v 23.805 c 0,26.102 59.715,68.02 139.152,97.785 79.441,-29.734 139.156,-71.652 139.156,-97.785 l 0.023,-246.691 0,0 -0.023,222.874 z" id="path3031" style="fill:#260859"/>
|
||||
<path d="m 395.156,126.445 c -38.867,15.164 -92.883,24.023 -127.684,27.02 l -11.41,0.984 v 16.746 l 10.133,-0.867 c 35.152,-2.996 89.707,-11.855 128.961,-27.02 v -16.863 z" id="path3033" style="fill:#a84d10"/>
|
||||
<path d="m 522.965,153.465 c -34.828,-2.996 -88.883,-11.863 -127.742,-27.043 -0.223,0.086 -0.457,0.164 -0.68,0.25 0.199,-0.074 0.41,-0.148 0.613,-0.227 v 16.863 c 0.02,-0.008 0.043,-0.016 0.066,-0.023 39.25,15.18 93.844,24.047 129.023,27.043 l 10.137,0.871 v -16.746 l -11.417,-0.988 z" id="path3035" style="fill:#a84d10"/>
|
||||
<path d="m 256.062,158.086 -0.012,238.5 c 0,7.613 5.105,16.57 14.262,26.176 1.164,0.109 2.312,0.242 3.492,0.328 0.074,1.215 0.168,2.344 0.277,3.438 23.133,22.137 66.586,47.148 118.852,66.977 l -0.059,-1.766 c -0.59,-18.352 -2.348,-35.594 -4.691,-46.121 -5.438,-24.406 -22.613,-48.82 -37.359,-62.367 -6.184,-15.457 -1.742,-25.371 4.391,-39.07 1.871,-4.168 3.992,-8.887 5.699,-13.855 5.145,-14.93 4.266,-38.211 -1.984,-53.98 -2.898,-9.637 -9.184,-16.973 -16.832,-19.625 -2.328,-0.777 -5.027,-1.312 -8.23,-1.625 -3.215,-10.938 -1.609,-22.059 -1.598,-22.148 l 1.754,-10.398 -9.758,-3.961 c -0.977,-0.398 -9.941,-3.902 -23.02,-3.902 -6.027,0 -12.105,0.785 -18.117,2.34 0.82,-5.617 1.695,-11.391 2.598,-17.309 0,0 1.664,-10.949 2.402,-15.922 l 0.676,-4.617 -15.496,-26.328 c 0,0 -3.242,0.398 -5.836,0.617 l -11.411,4.618 z" id="path3037" style="fill:#260859"/>
|
||||
<path d="m 514.695,428.105 c 0.016,-1.758 0.035,-3.516 -0.082,-5.402 2.242,-0.328 4.438,-0.699 6.609,-1.102 8.461,-9.164 13.137,-17.707 13.137,-25.016 l -1.562,-226.52 c 0,0 2.516,-0.133 2.234,-0.102 -3.75,-10.566 -23.34,-16.086 -28.695,-16.086 -7.617,0 -11.059,4.895 -12.184,6.996 -5.09,9.496 4.488,69.965 7.688,81.676 0.309,1.141 0.621,2.191 0.938,3.234 l -1.672,8.418 c -14.316,-13.652 -32.395,-28.645 -42.766,-28.645 -0.656,0 -1.289,0.055 -1.898,0.156 -2.648,0.461 -4.789,1.961 -6.016,4.215 -1.777,3.258 -1.703,7.801 0.27,14.133 -5.043,-1.027 -11.129,-1.98 -16.34,-1.98 -6.387,0 -10.586,1.453 -12.836,4.434 -4.473,5.934 -2.328,15.238 6.367,27.66 2.562,3.668 5.789,7.285 8.969,10.848 0.66,0.746 1.328,1.492 1.992,2.242 -9.152,14.402 -11.535,36.027 -6.352,58.285 2.27,9.738 5.41,16.656 8.188,22.77 1.766,3.883 3.203,7.047 4.09,10.273 -21.77,18.578 -37.285,42.938 -42.648,67.016 -2.184,9.785 -3.836,25.16 -4.547,42.199 l -0.23,5.742 c 51.096,-19.35 93.811,-43.674 117.346,-65.444 z" id="path3039" style="fill:#260859"/>
|
||||
<g id="g3041">
|
||||
<path d="m 321.699,323.621 c -1.984,-4.289 -5.723,-7.242 -10.809,-8.539 -19.477,-4.91 -29.746,-14.688 -33.309,-31.699 -3.887,-18.574 0.723,-48.887 6.062,-83.988 l 0.188,-1.234 c 0.734,-4.816 1.477,-9.707 2.211,-14.672 l 0.582,-3.965 -13.316,-26.676 -15.652,-2.938 -3.922,12.906 -4.168,2.527 c -0.457,0.281 -46.105,28.566 -66.969,87.457 -16.059,45.312 -11.832,93.551 11.023,125.91 19.73,27.922 51.32,42.691 91.348,42.699 1.902,0.008 3.645,-0.066 5.184,-0.195 4.969,-0.43 12.004,-1.789 15.77,-8.598 2.121,-3.844 2.418,-7.98 0.891,-12.609 3.871,-4.461 6.141,-9.797 6.426,-15.113 0.195,-3.613 -0.395,-9.031 -4.211,-14.613 2.449,-11.852 6.652,-20.379 9.449,-26.062 3.375,-6.86 6.57,-13.344 3.222,-20.598 z" id="path3043" style="fill:#260859"/>
|
||||
<g id="g3045">
|
||||
<path d="m 199.93,259.562 c 14.34,-66.531 55.953,-77.508 55.953,-77.508 l -1.012,36.066 0.816,76.336 c 0,0 -2.602,60.848 30.262,73.891 32.863,13.039 6.941,42.215 -36.23,31.109 C 208.5,389.602 184.465,330.16 199.93,259.562 z" id="path3047" style="fill:#ffffff"/>
|
||||
<g id="g3049">
|
||||
<path d="m 228.879,317.406 c -10.066,-45.742 11.844,-91.457 22.941,-105.285 l 13.945,-45.91 7.914,15.453 c -4.523,30.586 -9.375,58.598 -9.957,80.766 -0.121,4.418 0.922,20.125 1.629,23.516 3.336,15.93 11.039,27.066 24.359,34.395 2.242,1.234 13.094,-16.984 15.656,-15.965 3.352,1.344 -1.461,21.836 2.469,22.828 9.133,2.305 -6.898,13.684 -11.938,45.445 -21.198,1.91 -56.975,-9.61 -67.018,-55.243 z" id="path3051" style="fill:#f26531"/>
|
||||
</g>
|
||||
<path d="m 194.371,256.984 c 19.648,-55.469 61.68,-80.957 61.68,-80.957 l -0.434,15.422 c -15.742,12.766 -62.27,71.184 -44.375,141.27 13.469,48.656 52.797,63.223 80.523,57.949 -4.102,9.059 14.18,18.254 -6.789,18.254 -91.675,-0.02 -114.8,-83.645 -90.605,-151.938 z" id="path3053" style="fill:#7ac143"/>
|
||||
</g>
|
||||
<polygon points="213.434,210.594 205.309,222.129 239.594,232.855 " id="polygon3055" style="fill:#260859"/>
|
||||
<polygon points="191.312,240.535 186.168,257.379 230.988,256.758 " id="polygon3057" style="fill:#260859"/>
|
||||
<polygon points="183.648,279.039 183.082,296.645 226.18,284.332 " id="polygon3059" style="fill:#260859"/>
|
||||
<polygon points="180.789,326.434 186,346.004 229.383,318.148 " id="polygon3061" style="fill:#260859"/>
|
||||
<polygon points="201.324,373.969 214.367,389.453 241.773,345.785 " id="polygon3063" style="fill:#260859"/>
|
||||
<polygon points="241.258,405.695 259.137,409.742 267.953,367.039 " id="polygon3065" style="fill:#260859"/>
|
||||
</g>
|
||||
<path d="m 324.59,268.086 c 0.148,0.016 0.281,0.008 0.438,0.023 -9.18,-17.043 -5.715,-37.352 -5.715,-37.352 0,0 -16.691,-6.77 -37.332,0.254 -24.391,8.301 -27.098,26.691 -24.246,32.094 2.688,5.09 9.637,6.027 15.934,2.34 3.547,9.219 8,15.848 18.32,24.191 l 34.66,-20.137 c -10e-4,10e-4 -0.837,-0.394 -2.059,-1.413 z" id="path3067" style="fill:#fff200"/>
|
||||
<path d="m 274.98,347.574 c -5.398,0 -8.523,-1.82 -9.355,-2.379 l -9.891,-6.449 12.168,-6.41 c 5.203,-1.285 10.617,-7.621 15.832,-14.719 -1.098,0.18 -2.164,0.363 -3.195,0.535 -6.023,1.031 -11.086,1.895 -14.895,1.895 -0.688,0 -1.328,-0.031 -1.918,-0.082 -0.203,-0.02 -0.395,-0.027 -0.59,-0.027 -4.445,0 -10.258,3.727 -12.125,5.164 l -6.609,5.445 -4.531,-8.77 c -0.234,-0.688 -2.207,-6.969 1.93,-14.664 3.105,-5.758 12.41,-13.875 23.41,-13.875 1.82,0 3.633,0.219 5.387,0.652 0.594,0.148 1.184,0.223 1.734,0.223 2.828,0 5.781,-1.98 8.781,-5.883 0.738,-1.004 1.242,-1.891 1.648,-2.598 0.473,-0.82 0.875,-1.5 1.395,-2.191 1.762,-2.109 4.008,-3.098 6.93,-3.098 5.004,0 11.262,2.648 13.82,10.074 l 0,0 c 0.008,0 0.719,1.465 4.141,4.703 3.637,0.766 6.613,2.043 8.863,3.809 3.297,2.598 5.059,6.297 4.953,10.43 -0.207,8.285 -7.871,17.668 -23.434,28.684 -11.148,7.875 -19.449,9.531 -24.449,9.531 l 0,0 z" id="path3069" style="fill:#260859"/>
|
||||
<path d="m 285.051,298.215 c -8.113,-6.562 -13.383,-12.48 -17.387,-19.559 -0.559,0.035 -1.121,0.059 -1.676,0.059 -7.941,0 -14.508,-3.812 -18.008,-10.461 -2.953,-5.582 -2.887,-13.828 0.176,-21.527 2.715,-6.844 10.266,-19.348 30.266,-26.16 7.363,-2.504 15.043,-3.773 22.824,-3.773 12.699,0 21.285,3.363 22.227,3.746 l 8.191,3.324 -1.473,8.734 c -0.02,0.137 -2.246,15.293 3.441,28.035 l 16.324,8.004 -17.734,10.391 -41.266,23.961 -5.905,-4.774 z" id="path3071" style="fill:#260859"/>
|
||||
<path d="m 313.297,327.484 c -4.66,0 -5.777,-4.406 -6.758,-8.293 -0.754,-2.98 -1.535,-6.062 -3.594,-8.117 -7.707,-7.703 -18.688,-12.191 -18.801,-12.238 l -1.008,-0.406 -0.25,-1.062 c -1.91,-8.031 -0.074,-15.875 5.312,-22.695 8.305,-10.508 25,-17.848 40.602,-17.848 4.613,0 8.863,0.637 12.629,1.895 11.438,3.969 18.797,19.078 16.785,34.398 -2.59,19.672 -19.195,32.516 -44.418,34.352 l -0.499,0.014 z" id="path3073" style="fill:#260859"/>
|
||||
<path d="m 390.77,491.805 c -0.586,-18.211 -2.32,-35.305 -4.645,-45.73 -5.391,-24.207 -22.441,-48.285 -37.074,-61.586 -6.785,-16.547 -1.922,-27.41 4.227,-41.141 1.859,-4.137 3.961,-8.82 5.645,-13.711 6.371,-18.496 3.301,-52.016 -9.367,-64.766 l -0.875,-0.883 -17.277,4.875 -0.285,1.207 c -3.629,15.34 -12.52,35.328 -19.969,40.625 l -5.223,3.711 0.672,6.355 c 1.371,13.16 -5.613,28.898 -23.094,64.328 -6.949,14.074 -9.164,28.023 -7.043,43.664 23.328,21.27 64.762,44.902 114.336,63.93 l -0.028,-0.878 z" id="path3075" style="fill:#260859"/>
|
||||
<path d="m 286.379,296.578 c -8.293,-6.707 -13.605,-12.801 -17.535,-20.16 -0.957,0.125 -1.914,0.188 -2.855,0.188 -7.129,0 -13.016,-3.398 -16.145,-9.336 -2.672,-5.051 -2.57,-12.621 0.27,-19.766 2.586,-6.512 9.793,-18.406 28.988,-24.941 7.148,-2.434 14.594,-3.664 22.145,-3.664 12.223,0 20.52,3.219 21.426,3.59 l 6.637,2.691 -1.195,7.066 c -0.023,0.141 -2.43,16.492 3.918,29.945 l 13.477,6.609 -14.352,8.406 -40,23.23 -4.779,-3.858 z" id="path3077" style="fill:#260859"/>
|
||||
<path d="m 324.59,268.086 c 0.148,0.016 0.281,0.008 0.438,0.023 -9.18,-17.043 -5.715,-37.352 -5.715,-37.352 0,0 -16.691,-6.77 -37.332,0.254 -24.391,8.301 -27.098,26.691 -24.246,32.094 2.688,5.09 9.637,6.027 15.934,2.34 3.547,9.219 8,15.848 18.32,24.191 l 34.66,-20.137 c -10e-4,10e-4 -0.837,-0.394 -2.059,-1.413 z" id="path3079" style="fill:#fff200"/>
|
||||
<path d="m 291.426,228.664 c -3.023,0.5 -6.176,1.234 -9.445,2.348 -1.793,0.609 -3.449,1.281 -5.012,1.988 0.133,0.762 0.27,1.52 0.402,2.305 2.273,13.105 5.395,30.973 23.766,49.016 l 3.77,-2.191 4.488,-4.691 c -0.477,-0.453 -0.945,-0.914 -1.398,-1.371 -15.052,-15.072 -17.841,-27.463 -16.571,-47.404 z" id="path3081" style="fill:#f26531"/>
|
||||
<path d="m 319.312,230.758 c 0,0 -4.73,-1.898 -12.234,-2.641 -2.945,19.641 0.68,32.223 5.871,43.66 2.867,-2.016 6.352,-4.246 12.078,-3.668 -9.179,-17.043 -5.715,-37.351 -5.715,-37.351 z" id="path3083" style="fill:#f26531"/>
|
||||
<path d="m 340.762,260.719 c -22.492,-7.508 -62.18,9.445 -55.824,36.164 0,0 11.375,4.582 19.496,12.699 5.488,5.477 3.191,16.223 9.211,15.781 55.078,-4.008 48.554,-57.203 27.117,-64.644 z" id="path3085" style="fill:#260859"/>
|
||||
<path d="m 384.066,446.531 c -5.324,-23.91 -22.367,-47.836 -36.781,-60.797 -7.402,-17.637 -2.094,-29.484 4.07,-43.25 1.84,-4.102 3.926,-8.746 5.574,-13.535 6.051,-17.562 3.086,-50.559 -8.871,-62.594 l -14.891,4.199 c -3.703,15.652 -12.715,36.105 -20.797,41.855 l -4.219,3 0.543,5.133 c 1.426,13.711 -5.629,29.66 -23.301,65.484 -7.051,14.281 -9.199,28.594 -6.531,44.879 23.355,20.395 62.828,42.727 109.797,60.965 -0.671,-20.983 -2.706,-36.886 -4.593,-45.339 z" id="path3087" style="fill:#260859"/>
|
||||
<path d="m 375.43,448.457 c -4.688,-21.027 -20.555,-44.578 -35.52,-57.488 -0.02,-0.051 -0.031,-0.094 -0.059,-0.145 -12.793,-28.352 1.895,-44.961 8.711,-64.754 5.188,-15.062 2.301,-44.332 -6.785,-53.473 -2.863,12.109 -12.199,38.438 -24.281,47.027 1.711,16.387 -6.383,34.281 -24.168,70.316 -7.707,15.625 -8.574,31.184 -3.348,49.906 22.02,16.477 53.324,33.613 89.688,48.441 -0.727,-17.475 -2.406,-31.607 -4.238,-39.83 z" id="path3089" style="fill:#f26531"/>
|
||||
<path d="m 336.758,268.195 c -10.891,-5.066 -39.801,0.785 -49.402,24.723 0,0 8.891,5.105 14.688,12.75 3.91,5.168 3.672,14.082 8.707,14.211 42.866,1.105 42.554,-43.981 26.007,-51.684 z" id="path3091" style="fill:#f26531"/>
|
||||
<path d="m 272.738,340.402 c -1.93,0 -3.652,-0.895 -4.738,-2.457 -1.965,-2.816 -0.754,-6.086 -0.109,-7.84 3.441,-9.297 12.891,-18.176 21.5,-20.219 l 16.016,-4.246 1.781,0.863 c 4.453,2.152 2.496,-3.094 5.336,-1.969 11.52,4.566 5.867,13.898 4.336,16.051 -3.465,4.875 -7.785,4.871 -10.066,5.266 -9.285,1.57 -14.492,4.105 -18.293,5.965 -1.504,0.734 -2.801,1.367 -4.184,1.898 -0.977,0.379 -3.066,2.02 -4.32,3 -2.829,2.224 -4.7,3.688 -7.259,3.688 l 0,0 z" id="path3093" style="fill:#260859"/>
|
||||
<path d="m 304.859,311.324 -14.234,3.773 c -6.875,1.625 -14.824,9.062 -17.707,16.859 -2.891,7.805 4.785,-1.43 9.469,-3.234 4.668,-1.812 10.121,-5.887 23.5,-8.156 13.379,-2.265 -1.028,-9.242 -1.028,-9.242 z" id="path3095" style="fill:#f26531"/>
|
||||
<path d="m 290.859,310.074 c 0,0 -11.062,22.945 -25.168,28.18 l 10.75,1.375 c 0,0 15.59,-9.383 18.441,-19.707 2.852,-10.312 2.551,-15.832 2.551,-15.832 l -6.574,5.984 z" id="path3097" style="fill:#260859"/>
|
||||
<path d="m 313.492,304.547 c -6.148,-4.84 -21.82,-2.699 -21.82,-2.699 l 8.758,5.172 c -8.391,9.762 -18.191,29.141 -30.828,32.25 0,0 8.082,5.398 25.703,-7.062 17.621,-12.46 24.347,-22.825 18.187,-27.661 z" id="path3099" style="fill:#fff200"/>
|
||||
<path d="m 289.879,287.703 c -0.773,1.035 -1.34,2.477 -3.102,4.871 -2.613,3.402 -8.828,10.496 -17.902,8.246 -9.074,-2.25 -18.012,4.523 -20.785,9.676 -2.77,5.152 -1.465,8.965 -1.465,8.965 0,0 9.391,-7.359 17.73,-6.609 8.348,0.758 32.059,-6.914 50.258,-4.016 0,0 -14.355,-10.16 -16.457,-16.086 -2.011,-5.848 -7.902,-5.488 -8.277,-5.047 z" id="path3101" style="fill:#fff200"/>
|
||||
<g id="g3103">
|
||||
<path d="m 318.129,275.305 c -8.238,10.934 -12.203,12.973 -19.973,17.445 18.227,33.57 52.637,-9.355 19.973,-17.445 z" id="path3105" style="fill:#260859"/>
|
||||
</g>
|
||||
<g id="g3107">
|
||||
<path d="m 315.082,300.051 c -3.988,0 -7.168,-2.73 -9.383,-5.508 5.203,-3.199 9.039,-6.363 14.363,-13.004 4.559,1.859 6.637,4.664 6.188,8.344 -0.574,4.703 -5.578,10.168 -11.168,10.168 l 0,0 z" id="path3109" style="fill:#ffffff"/>
|
||||
</g>
|
||||
<path d="m 317.875,282.395 c -2.621,1.168 -5.152,2.633 -7.699,3.727 -1.414,0.609 -3.875,2.562 -3.633,3.16 1.148,2.824 3.023,5.551 6.082,6.75 0.297,0.117 0.602,0.211 0.906,0.289 0.695,-1.246 1.336,-2.582 1.898,-4.008 1.118,-2.86 2.009,-6.751 2.446,-9.918 z" id="path3111" style="fill:#260859"/>
|
||||
<path d="m 313.707,296.379 c 0,-2.535 -0.289,-4.934 -0.781,-7.129 -2.305,2.059 -4.594,3.672 -7.227,5.293 1.91,2.398 4.551,4.75 7.801,5.352 0.125,-1.137 0.207,-2.305 0.207,-3.516 z" id="path3113" style="fill:#f37344"/>
|
||||
<path d="m 280.828,251.062 c 0.438,9.141 4.355,19.59 12.043,29.02 l 4.793,-4.211 c -11.609,-7.101 -16.836,-24.809 -16.836,-24.809 z" id="path3115" style="fill:#260859"/>
|
||||
<path d="m 291.141,238.789 c -1.156,10.66 1.484,23.461 8.672,35.766 l 6.305,-4 c -12.141,-10.336 -14.977,-31.766 -14.977,-31.766 z" id="path3117" style="fill:#260859"/>
|
||||
<path d="m 306.738,231.996 c -2.785,10.348 -2.152,23.402 3.047,36.672 l 6.844,-2.973 c -10.399,-12.082 -9.891,-33.699 -9.891,-33.699 z" id="path3119" style="fill:#260859"/>
|
||||
<path d="m 334.832,367.363 c 0.266,-6.996 2.133,-13.312 4.527,-19.387 0.395,-1.004 -7.34,5.734 -15.191,7.973 -7.262,2.07 -14.66,-0.41 -14.953,0.293 -2.434,5.727 -5.387,12.094 -8.852,19.297 19.012,1.656 27.176,-2.223 34.469,-8.176 z" id="path3121" style="fill:#7ac143"/>
|
||||
<path d="m 337.445,353.391 c 1.25,-3.918 2.812,-7.699 4.465,-11.438 -12.93,7.711 -20.523,10.559 -30.961,10.07 -1.262,3.164 -2.719,6.547 -4.34,10.152 13.114,1.403 24.301,-3.511 30.836,-8.784 z" id="path3123" style="fill:#ffffff"/>
|
||||
<path d="m 293.305,389.996 c -7.684,15.605 -8.547,31.148 -3.324,49.852 12.047,9.012 26.852,18.227 43.773,27.152 12.125,-36.82 -2.57,-65.309 -8.16,-75.688 -8.168,2.25 -33.199,11.153 -32.289,-1.316 z" id="path3125" style="fill:#ffffff"/>
|
||||
<g id="g3127">
|
||||
<path d="m 426.086,226.523 c -3.566,0 -6.266,-1.336 -7.719,-2.051 -0.879,-0.445 -2,-1.012 -7.145,-4.84 -5.227,1.496 -10.617,2.254 -16.043,2.254 -5.52,0 -10.984,-0.777 -16.266,-2.309 -5.227,3.891 -6.375,4.465 -7.148,4.855 -1.371,0.684 -4.199,2.09 -7.797,2.09 -4.008,0 -7.691,-1.621 -10.949,-4.805 -4.895,-4.824 -6.535,-9.34 -5.664,-15.602 0.137,-0.996 0.441,-2.527 1.691,-6.785 -1.613,-2.074 -3.102,-4.285 -4.441,-6.59 -9.844,-3.238 -12.664,-4.578 -14.113,-5.461 -4.582,-2.82 -6.859,-8.195 -6.98,-16.441 -0.117,-8.219 1.965,-13.617 6.363,-16.496 0.707,-0.461 1.859,-1.211 8.289,-4.105 0.246,-1.074 0.527,-2.141 0.832,-3.195 -5.164,-7.422 -5.906,-9.02 -6.309,-9.887 -1.41,-3.07 -2.191,-8.109 2.059,-14.746 1.551,-2.414 7.227,-10.281 15.102,-10.281 h 0.797 l 1.406,0.211 c 0.941,0.184 2.262,0.449 9.582,3.125 4.242,-2.965 8.844,-5.352 13.707,-7.109 2.277,-5.695 3.047,-6.969 3.422,-7.594 2.812,-4.617 8.301,-6.957 16.328,-6.957 8.25,0 13.824,2.352 16.57,6.988 0.398,0.676 1.055,1.781 3.363,7.566 4.855,1.758 9.449,4.141 13.676,7.098 7.227,-2.648 8.523,-2.91 9.391,-3.09 l 0.734,-0.148 1.395,-0.086 c 7.91,0 13.363,7.188 15.348,10.277 4.25,6.629 3.473,11.66 2.07,14.715 -0.418,0.906 -1.176,2.527 -6.289,9.871 0.312,1.074 0.594,2.156 0.844,3.246 6.473,2.906 7.566,3.625 8.297,4.105 4.398,2.875 6.48,8.273 6.359,16.496 -0.113,8.125 -2.453,13.648 -6.957,16.414 -1.016,0.625 -2.891,1.785 -14.141,5.484 -1.391,2.406 -2.957,4.711 -4.656,6.879 1.348,4.625 1.508,5.785 1.605,6.496 0.871,6.266 -0.762,10.777 -5.645,15.582 -3.269,3.201 -6.956,4.826 -10.968,4.826 l 0,0 z" id="path3129" style="fill:#260859"/>
|
||||
<path d="m 426.086,224.414 c -3.094,0 -5.512,-1.203 -6.812,-1.844 -0.746,-0.379 -1.895,-0.953 -7.633,-5.262 -5.348,1.641 -10.875,2.473 -16.461,2.473 -5.676,0 -11.273,-0.848 -16.676,-2.527 -5.824,4.367 -6.977,4.941 -7.691,5.301 -1.324,0.656 -3.75,1.859 -6.844,1.859 -3.438,0 -6.621,-1.41 -9.477,-4.203 -4.41,-4.344 -5.828,-8.211 -5.051,-13.801 0.137,-1 0.48,-2.645 1.938,-7.531 -1.992,-2.461 -3.781,-5.105 -5.344,-7.887 -10.289,-3.363 -13.133,-4.711 -14.441,-5.512 -3.922,-2.41 -5.867,-7.207 -5.973,-14.668 -0.109,-7.441 1.664,-12.254 5.41,-14.707 0.672,-0.438 1.848,-1.199 8.965,-4.375 0.359,-1.711 0.801,-3.402 1.309,-5.059 -5.598,-8.008 -6.336,-9.613 -6.707,-10.41 -1.188,-2.582 -1.816,-6.879 1.922,-12.715 1.41,-2.195 6.527,-9.309 13.328,-9.309 h 0.797 l 1.051,0.18 c 0.832,0.164 2.242,0.457 10.258,3.406 4.551,-3.312 9.668,-5.977 14.992,-7.801 2.492,-6.301 3.273,-7.594 3.621,-8.176 2.402,-3.941 7.281,-5.934 14.523,-5.934 7.453,0 12.418,2.004 14.754,5.953 0.371,0.625 1.039,1.754 3.574,8.16 5.312,1.828 10.418,4.48 14.965,7.789 8.012,-2.953 9.32,-3.219 10.129,-3.383 l 0.738,-0.152 1.047,-0.039 c 7.012,0 12.098,7.125 13.496,9.305 3.738,5.828 3.109,10.125 1.93,12.703 -0.387,0.828 -1.137,2.441 -6.684,10.375 0.52,1.68 0.957,3.387 1.32,5.109 7.172,3.195 8.281,3.922 8.969,4.375 3.746,2.449 5.52,7.262 5.414,14.707 -0.105,7.359 -2.109,12.285 -5.953,14.648 -0.922,0.566 -2.789,1.711 -14.469,5.527 -1.621,2.895 -3.48,5.625 -5.559,8.168 1.59,5.371 1.758,6.566 1.852,7.25 0.777,5.59 -0.633,9.457 -5.035,13.785 -2.863,2.808 -6.055,4.222 -9.492,4.222 l 0,0 z" id="path3131" style="fill:#260859"/>
|
||||
<path d="m 409.34,122.93 c 0,0 -5.383,-14.453 -6.75,-16.773 -1.375,-2.324 -13.246,-2.52 -14.793,0.031 -1.535,2.566 -6.75,16.742 -6.75,16.742 h 28.293 z" id="path3133" style="fill:#7f3f98"/>
|
||||
<path d="m 380.41,205.176 c 0,0 -11.297,8.801 -13.367,9.836 -2.066,1.031 -3.934,1.836 -6.656,-0.824 -2.676,-2.637 -2.969,-3.941 -2.598,-6.621 0.371,-2.668 4.875,-16.578 4.875,-16.578 l 17.746,14.187 z" id="path3135" style="fill:#7f3f98"/>
|
||||
<path d="m 409.645,205.176 c 0,0 11.297,8.801 13.367,9.836 2.066,1.031 3.938,1.836 6.656,-0.824 2.676,-2.637 2.969,-3.941 2.598,-6.621 -0.371,-2.668 -4.875,-16.578 -4.875,-16.578 l -17.746,14.187 z" id="path3137" style="fill:#7f3f98"/>
|
||||
<path d="m 352.539,155.469 c 0,0 -14.668,6.238 -16.891,7.691 -2.211,1.449 -2.164,13.59 0.336,15.125 2.5,1.531 17.105,6.121 17.105,6.121 l -0.55,-28.937 z" id="path3139" style="fill:#7f3f98"/>
|
||||
<path d="m 437.82,155.469 c 0,0 14.668,6.238 16.891,7.691 2.215,1.449 2.164,13.59 -0.336,15.125 -2.5,1.531 -17.105,6.121 -17.105,6.121 l 0.55,-28.937 z" id="path3141" style="fill:#7f3f98"/>
|
||||
<path d="m 348.719,153.543 -7.418,3.023 c -0.465,4.172 0.828,11.812 6.457,16.184 5.629,4.359 0.961,-19.207 0.961,-19.207 z" id="path3143" style="fill:#260859"/>
|
||||
<path d="m 441.246,153.543 7.418,3.023 c 0.465,4.172 -0.828,11.812 -6.457,16.184 -5.629,4.359 -0.961,-19.207 -0.961,-19.207 z" id="path3145" style="fill:#260859"/>
|
||||
<path d="m 367.875,129.066 c 0,0 -15.164,-5.836 -17.758,-6.371 -2.598,-0.547 -9.082,7.383 -7.863,10.039 1.223,2.66 10.508,15.508 10.508,15.508 l 15.113,-19.176 z" id="path3147" style="fill:#7f3f98"/>
|
||||
<path d="m 422.438,129.066 c 0,0 15.164,-5.836 17.754,-6.371 2.598,-0.547 9.086,7.383 7.867,10.039 -1.223,2.66 -10.508,15.508 -10.508,15.508 l -15.113,-19.176 z" id="path3149" style="fill:#7f3f98"/>
|
||||
<path d="m 359.793,121.949 c -2.27,3.754 -6.828,9.09 -3.359,17.332 2.875,-3.961 11.133,-15.062 11.43,-16.781 l -8.071,-0.551 z" id="path3151" style="fill:#260859"/>
|
||||
<path d="m 423.164,198.848 10.629,-4.398 3.391,4.844 c -4.156,3.188 -7.762,6.547 -14.637,6.121 -8.692,-0.552 0.617,-6.567 0.617,-6.567 z" id="path3153" style="fill:#260859"/>
|
||||
<path d="m 366.891,198.848 -10.629,-4.398 -3.391,4.844 c 4.156,3.188 7.762,6.547 14.637,6.121 8.695,-0.552 -0.617,-6.567 -0.617,-6.567 z" id="path3155" style="fill:#260859"/>
|
||||
<path d="m 430.566,121.949 c 2.266,3.754 6.824,9.09 3.352,17.332 -2.867,-3.961 -11.133,-15.062 -11.43,-16.781 l 8.078,-0.551 z" id="path3157" style="fill:#260859"/>
|
||||
<path d="m 443.168,163.367 c 0,26.504 -21.488,47.984 -47.988,47.984 -26.508,0 -47.992,-21.48 -47.992,-47.984 0,-26.504 21.484,-47.996 47.992,-47.996 26.5,0 47.988,21.492 47.988,47.996 z" id="path3159" style="fill:#7f3f98"/>
|
||||
</g>
|
||||
<path d="m 499.035,379.352 c -24.148,-14.574 -50.754,-40.875 -46.246,-67.043 1.824,-10.57 -6.406,-19.781 -14.363,-28.688 -3.098,-3.477 -6.305,-7.066 -8.812,-10.648 -5.574,-7.969 -11.219,-18.809 -6.41,-25.188 1.828,-2.426 5.477,-3.598 11.152,-3.598 6.438,0 14.301,1.52 19.57,2.734 -2.094,-5.496 -3.824,-12.004 -1.652,-15.984 0.93,-1.707 2.5,-2.793 4.531,-3.148 0.484,-0.082 1,-0.121 1.535,-0.121 10.691,0 31.27,18.254 44.082,30.727 l 2.691,-13.578 1.098,2.488 c 0.855,0.215 21,5.586 31.73,37.02 15.328,14.332 30.715,37.719 28.375,60.863 l -1.391,13.645 -24.707,-4.203 -10.504,-1.258 10.469,12.742 -6.188,10.285 -10.91,2.156 c -4.785,0.953 -10.098,1.809 -15.793,2.543 l -4.441,0.562 -3.816,-2.308 z" id="path3161" style="fill:#260859"/>
|
||||
<path d="m 499.883,380.711 c -8.453,-18.031 -15.75,-33.605 -11.617,-49.598 5.477,-21.191 4.926,-37.324 -1.633,-47.938 -5.246,-8.492 -12.496,-10.414 -13.895,-10.715 l -2.395,-0.512 -14.07,3.293 -0.402,0.141 c -17.102,5.828 -29.754,33.488 -21.32,69.688 2.219,9.543 5.305,16.332 8.051,22.375 2.02,4.445 3.656,8.047 4.535,11.91 -21.969,18.406 -37.598,42.656 -42.953,66.711 -2.16,9.668 -3.797,24.91 -4.496,41.824 l -0.195,4.844 c 48.699,-18.664 89.555,-41.785 113.125,-62.766 0.469,-12.918 -2.551,-27.004 -8.867,-40.922 -1.278,-2.812 -2.579,-5.585 -3.868,-8.335 z" id="path3163" style="fill:#260859"/>
|
||||
<path d="m 506.766,421.48 c -5.457,0 -10.125,-2.191 -13.5,-6.348 -7.531,-9.25 -5.598,-25.848 -2.543,-41.09 -0.52,-0.402 -1.082,-0.809 -1.566,-1.16 -2.887,-2.09 -6.832,-4.945 -8.84,-9.621 l -3.844,-8.93 8.52,-4.684 c 26.812,-14.766 36.797,-42.465 29.68,-82.332 -0.055,-0.164 -0.105,-0.328 -0.164,-0.488 -2.809,-3.723 -6.871,-11.012 -10.637,-24.828 -3.523,-12.914 -12.309,-71.832 -7.859,-80.129 0.945,-1.77 3.852,-5.887 10.324,-5.887 4.68,0 18.578,3.645 26.375,12.609 0.766,-0.152 1.562,-0.227 2.363,-0.227 4.25,0 7.562,2.121 11.398,4.578 1.246,0.789 2.582,1.645 4.094,2.543 2.199,1.305 6.66,3.945 10.301,10.102 5.594,0.066 9.668,3.734 19.703,14.016 0.09,0.102 1.055,1.09 1.293,1.32 2.059,2.035 3.676,4.914 5.316,9.516 10.035,2.156 17.312,18.496 18.711,25.281 0.051,0.262 0.152,0.633 0.289,1.145 l 0.273,1.02 c 15.062,57.008 11.363,104.316 -10.707,136.801 -17.801,26.219 -46.922,41.906 -86.551,46.645 -0.859,0.102 -1.648,0.148 -2.429,0.148 l 0,0 z" id="path3165" style="fill:#260859"/>
|
||||
<path d="m 595.402,237.887 c -1.785,-8.676 -10.664,-20.074 -11.051,-16.191 -0.199,2.031 1.609,13.535 2.16,20.777 -0.961,-0.691 -2.066,-1.449 -2.703,-1.969 -2.98,-14.074 -6.547,-29.066 -9.473,-31.961 -2.211,-2.176 -14.605,-15.398 -13.977,-11.551 2.879,17.59 7.48,30.176 11.324,39.035 -3.078,-2.262 -4.473,-3.328 -6.055,-4.422 -6.062,-4.195 -8.738,-30.359 -13.359,-39.422 -2.363,-4.652 -5.629,-6.57 -7.172,-7.484 -9.922,-5.895 -12.629,-9.863 -10.863,2.387 1.348,9.336 2.344,21.816 14.742,47.855 l -8.207,-6.227 c -6.785,-5.465 -11.07,-36.008 -13.523,-48.082 -2.031,-10.043 -21.027,-15.141 -21.797,-13.699 -1.18,2.203 3.742,53.883 8.754,72.25 5.008,18.359 10.426,24.254 10.836,25.301 3.816,20.711 5.387,47.844 -8.359,70.012 -2.25,3.625 -31.09,-18.539 -34.219,-15.227 -3.129,3.312 19.469,32.105 15.348,34.992 -2.375,1.672 -4.922,3.262 -7.656,4.766 1.898,4.414 10.406,6.801 11.91,12.598 0.566,0 -11.004,41.09 5.863,39.07 136.755,-16.336 89.27,-164.133 87.477,-172.808 z" id="path3167" style="fill:#ffffff"/>
|
||||
<path d="m 519.477,379.414 c -7.062,-21.422 -20.711,-50.16 -20.711,-50.16 l -28.637,-11.207 19.031,63.051 -6.086,-5.641 -1.121,4.543 10.562,19.551 c 0,0 27.895,4.254 31.422,-6.102 -1.546,-5.48 -2.132,-6.969 -4.46,-14.035 z" id="path3169" style="fill:#aaa4c4"/>
|
||||
<path d="m 506.004,389.176 c -3.055,0.156 -3.531,-6.988 -5.977,-6.602 -1.98,12.016 -3.234,29.453 7.898,28.121 105.012,-12.539 101.387,-102.594 93.238,-147.656 -1.339,28.816 1.314,121.039 -95.159,126.137 z" id="path3171" style="fill:#aaa4c4"/>
|
||||
<path d="m 499.363,403.242 c 1.051,4.82 3.543,8.055 8.562,7.453 48.184,-5.754 73.453,-27.84 85.957,-54.395 -20.577,34.462 -64.964,42.055 -94.519,46.942 z" id="path3173" style="fill:#f2f1f8"/>
|
||||
<path d="m 536.113,285.504 c -10.438,-31.156 -30.414,-36.156 -30.414,-36.156 l -1.723,13.531 c -1.992,-1.926 -34.645,-35.117 -46.812,-33.012 -8.293,1.434 -0.457,18.242 0.266,20.086 -6.105,-1.598 -28.066,-6.832 -32.543,-0.898 -4.473,5.93 2.891,17.617 6.457,22.711 8.84,12.648 26.277,24.93 23.523,40.898 -4.41,25.586 22.328,51.047 45.258,64.883 l 3.191,1.93 3.703,-0.469 c 5.539,-0.715 10.805,-1.555 15.66,-2.52 l 9.992,-1.977 4.938,-8.211 -12.848,-15.648 15.812,1.898 22.48,3.824 1.168,-11.398 c 2.088,-20.64 -11.045,-43.691 -28.108,-59.472 z" id="path3175" style="fill:#260859"/>
|
||||
<path d="m 511.441,262.371 0.492,25.715 20.984,8.66 C 522.91,277.422 511.441,262.371 511.441,262.371 z" id="path3177" style="fill:#6b5f91"/>
|
||||
<path d="m 497.977,381.613 c -8.625,-18.406 -16.074,-34.297 -11.754,-51.027 5.328,-20.621 4.859,-36.199 -1.383,-46.305 -4.797,-7.758 -11.289,-9.492 -12.543,-9.758 l -1.934,-0.414 -13.398,3.125 -0.414,0.145 c -16.215,5.523 -28.109,32.18 -19.945,67.215 2.172,9.352 5.219,16.047 7.902,21.953 2.297,5.055 4.102,9.027 4.949,13.617 -21.887,18.016 -37.902,42.508 -43.219,66.363 -2.129,9.539 -3.754,24.648 -4.445,41.449 l -0.16,3.938 c 46.297,-17.949 85.312,-39.887 108.82,-60.047 0.855,-12.961 -2.055,-27.484 -8.621,-41.949 -1.273,-2.801 -2.57,-5.566 -3.855,-8.305 z" id="path3179" style="fill:#260859"/>
|
||||
<path d="m 414.879,448.457 c -1.84,8.227 -3.52,22.379 -4.238,39.879 36.367,-14.809 67.703,-31.938 89.738,-48.418 3.441,-14 0.355,-31.008 -6.609,-46.34 -10.18,-22.418 -21.957,-42.605 -16.117,-65.207 10.691,-41.363 -7.207,-45.191 -7.207,-45.191 l -11.039,2.574 c -11.812,4.027 -21.172,26.828 -14.184,56.836 4.383,18.836 12.867,26.305 13.566,41.469 -24.969,19.054 -39.453,44.386 -43.91,64.398 z" id="path3181" style="fill:#ffffff"/>
|
||||
<polygon points="524.949,339.023 553.559,343.895 540.949,327.73 497.863,316.172 478.828,322.18 505.211,341.258 515.43,341.129 518.512,346.129 " id="polygon3183" style="fill:#42316f"/>
|
||||
<path d="m 512.535,370.281 c -0.336,-0.113 -1.629,-0.211 -2.324,-0.262 -3.934,-0.297 -11.246,-0.84 -16.621,-8.176 -9.293,-12.676 -18.852,-30.539 -18.945,-30.719 l -1.918,-3.586 16.203,-15.387 3.598,2.605 c 10.914,7.887 31.23,22.578 27.18,51.25 l -0.93,6.586 -6.243,-2.311 z" id="path3185" style="fill:#260859"/>
|
||||
<path d="m 507.996,273.875 c -8.719,-4.91 -1.633,8.113 -1.633,8.113 l 17.391,8.082 c 0,0 -7.035,-11.285 -15.758,-16.195 z" id="path3187" style="fill:#260859"/>
|
||||
<path d="m 497.902,358.684 c 5.445,7.418 12.918,5.246 16.504,6.578 3.41,-24.145 -12.492,-37.109 -25.016,-46.168 l -10.02,9.52 c 10e-4,-10e-4 9.341,17.515 18.532,30.07 z" id="path3189" style="fill:#f6a0a6"/>
|
||||
<polygon points="494.504,330.309 504.152,329.352 500.211,320.141 490.449,320.98 " id="polygon3191" style="fill:#260859"/>
|
||||
<path d="m 477.652,328.371 c 0.344,-1.586 0.645,-3.094 0.914,-4.574 l -0.824,-32.625 -7.129,-5.695 -4.527,-1.277 -6.68,1.555 c -3.07,1.051 -5.98,3.387 -8.48,6.777 4.602,7.199 9.676,10.191 8.43,22.199 -1.32,12.793 10.867,25.336 16.84,28.691 -0.165,-4.742 0.284,-9.649 1.456,-15.051 z" id="path3193" style="fill:#aaa4c4"/>
|
||||
<path d="m 515.441,362.789 c 2.875,-0.449 5.562,-0.922 8.031,-1.41 -11.984,-10.055 -24.457,-25.418 -30.348,-40.766 1.129,-0.16 2.285,-0.285 3.461,-0.387 l -12.074,-9.066 -1.441,17.137 20.973,26.426 11.398,8.066 z" id="path3195" style="fill:#260859"/>
|
||||
<g id="g3197">
|
||||
<path d="m 520.594,365.984 c -11.988,-10.059 -24.457,-25.414 -30.348,-40.773 14.125,-2 31.789,-0.09 63.312,18.684 2.402,-23.664 -8.188,-64.223 -120.211,-88.75 19.531,31.617 35.305,33.953 32.133,58.875 -2.477,19.504 17.742,40.82 40.18,54.359 5.613,-0.715 10.582,-1.535 14.934,-2.395 z" id="path3199" style="fill:#ffffff"/>
|
||||
</g>
|
||||
<polygon points="502.367,321.293 514.109,336.367 528.445,325.293 " id="polygon3201" style="fill:#ffffff"/>
|
||||
<polygon points="474.906,256.566 459.891,260.328 490.523,302.746 " id="polygon3203" style="fill:#260859"/>
|
||||
<polygon points="519.023,350.121 506.781,357.129 514.066,363.012 " id="polygon3205" style="fill:#ffffff"/>
|
||||
<polygon points="515.023,361.746 520.594,365.984 523.348,358.102 " id="polygon3207" style="fill:#ffffff"/>
|
||||
<g id="g3209">
|
||||
<path d="m 462.426,239.809 c 11.012,28.695 21.609,50.254 24.316,56.68 3.309,7.875 6.352,11.246 16.789,16.836 6.863,3.676 19.824,10.402 31.555,10.422 -8.766,-21.824 -16.195,-32.91 -38.211,-53.188 -11.328,-10.43 -24.145,-21.758 -34.449,-30.75 z" id="path3211" style="fill:#594c82"/>
|
||||
</g>
|
||||
<g id="g3213">
|
||||
<path d="M 521.926,322.051 C 505.781,318.36 496.391,306.5 495.473,288.668 l -0.387,-7.441 6.039,4.363 c 20.16,14.57 24.406,26.645 25.137,32.543 l 0.633,5.062 -4.969,-1.144 z" id="path3215" style="fill:#260859"/>
|
||||
</g>
|
||||
<g id="g3217">
|
||||
<path d="m 504.73,294.418 c 0.742,14.297 7.789,26.453 23.688,30.094 -0.844,-6.774 -6.383,-17.582 -23.688,-30.094 z" id="path3219" style="fill:#ffffff"/>
|
||||
</g>
|
||||
<path d="m 521.02,307.242 -1.926,-0.84 c -0.336,0.574 -0.645,1.184 -0.918,1.844 -1.785,4.355 -1.414,8.625 0.836,9.559 1.629,0.664 3.961,-0.762 5.711,-3.18 l -3.703,-7.383 z" id="path3221" style="fill:#260859"/>
|
||||
<path d="m 493.77,393.578 c -0.039,-0.094 -0.086,-0.184 -0.125,-0.273 -1.496,2.672 -2.945,5.547 -2.945,5.547 0,0 -19.02,-6.207 -25.148,-7.895 -5.867,10.895 -20.238,38.785 -8.461,75.781 16.672,-8.82 31.375,-17.91 43.289,-26.82 3.44,-14 0.354,-31.008 -6.61,-46.34 z" id="path3223" style="fill:#6b5f91"/>
|
||||
<path d="m 282.871,437.34 c 22.973,18.836 59.043,39.004 101.59,55.91 v -12.582 c -45.699,-18.512 -81.504,-40.121 -101.59,-58.043 v 14.715 z" id="path3225" style="fill:#260859"/>
|
||||
<path d="m 406.188,493.156 c 42.293,-16.809 78.164,-36.848 101.102,-55.594 v -14.703 c -19.926,17.703 -55.094,38.953 -99.949,57.25 l -1.153,13.047 z" id="path3227" style="fill:#260859"/>
|
||||
<path d="m 403.402,148.789 c -0.047,-0.816 0.004,-1.645 0.133,-2.477 1.031,-5.996 6.719,-10.016 12.719,-8.984 5.887,1.008 9.855,6.508 9.039,12.367 -7.613,-1.144 -14.949,-1.629 -21.891,-0.906 z" id="path3229" style="fill:#ffffff"/>
|
||||
<path d="m 365.43,162.512 c 6.816,9.141 17.707,15.051 29.973,15.051 12.27,0 23.16,-5.91 29.973,-15.051 H 365.43 z" id="path3231" style="fill:#260859"/>
|
||||
<path d="m 416.684,170.902 c -4.809,-1.645 -12.762,-2.707 -21.766,-2.707 -8.594,0 -16.242,0.969 -21.102,2.484 6.102,4.32 13.543,6.883 21.586,6.883 7.907,0 15.239,-2.472 21.282,-6.66 z" id="path3233" style="fill:#b30838"/>
|
||||
<path d="m 359.141,162.512 0.438,1.328 c 3.145,9.539 4.848,12.016 8.293,12.043 h 0.027 c 3.363,0 5.168,-2.57 8.371,-11.938 l 0.496,-1.434 h -17.625 z" id="path3235" style="fill:#260859"/>
|
||||
<g id="g3237">
|
||||
<path d="m 363.828,162.43 c 0,0 2.953,8.965 4.055,8.973 1.102,0.012 4.156,-8.914 4.156,-8.914 l -8.211,-0.059 z" id="path3239" style="fill:#ffffff"/>
|
||||
</g>
|
||||
<path d="m 414.266,162.512 0.438,1.328 c 3.145,9.539 4.848,12.016 8.293,12.043 h 0.027 c 3.363,0 5.168,-2.57 8.375,-11.938 l 0.492,-1.434 h -17.625 z" id="path3241" style="fill:#260859"/>
|
||||
<g id="g3243">
|
||||
<path d="m 418.953,162.43 c 0,0 2.953,8.965 4.055,8.973 1.105,0.012 4.156,-8.914 4.156,-8.914 l -8.211,-0.059 z" id="path3245" style="fill:#ffffff"/>
|
||||
</g>
|
||||
<path d="m 411.148,196.828 c 0,4.668 -7.133,8.449 -15.926,8.449 -8.797,0 -15.93,-3.781 -15.93,-8.449 0,-4.668 7.133,-8.449 15.93,-8.449 8.794,0 15.926,3.781 15.926,8.449 z" id="path3247" style="fill:#ffef6f"/>
|
||||
<path d="m 400.66,196.828 c 0,1.594 -2.434,2.883 -5.438,2.883 -3,0 -5.438,-1.289 -5.438,-2.883 0,-1.598 2.438,-2.891 5.438,-2.891 3.005,10e-4 5.438,1.293 5.438,2.891 z" id="path3249" style="fill:#7f3f98"/>
|
||||
<path d="m 410.676,141.695 c 0.078,2.168 1.301,3.871 2.711,3.816 1.422,-0.066 2.496,-1.863 2.402,-4.031 -0.082,-2.176 -1.309,-3.895 -2.727,-3.832 -1.414,0.059 -2.492,1.868 -2.386,4.047 z" id="path3251" style="fill:#260859"/>
|
||||
<path d="m 386.906,148.789 c 0.051,-0.816 -0.004,-1.645 -0.133,-2.477 -1.027,-5.996 -6.715,-10.016 -12.715,-8.984 -5.887,1.008 -9.855,6.508 -9.039,12.367 7.614,-1.144 14.95,-1.629 21.887,-0.906 z" id="path3253" style="fill:#ffffff"/>
|
||||
<path d="m 379.633,141.695 c -0.082,2.168 -1.301,3.871 -2.707,3.816 -1.426,-0.066 -2.504,-1.863 -2.402,-4.031 0.082,-2.176 1.309,-3.895 2.723,-3.832 1.417,0.059 2.495,1.868 2.386,4.047 z" id="path3255" style="fill:#260859"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 39 KiB |
@@ -1,49 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="197.72693mm"
|
||||
height="190.30197mm"
|
||||
viewBox="0 0 197.72693 190.30197"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
xml:space="preserve"
|
||||
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
|
||||
sodipodi:docname="Toyhouse.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.5"
|
||||
inkscape:cx="969"
|
||||
inkscape:cy="450"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1009"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" /><defs
|
||||
id="defs2" /><g
|
||||
inkscape:label="Capa 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(6.2923729,-54.858845)"><g
|
||||
id="g1824"><path
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M -6.2923729,154.22213 92.377447,54.858845 121.69053,84.093271 V 69.076617 h 31.14565 v 46.273543 l 38.59837,38.59837 h -32.36923 v 91.21228 H 25.02776 v -91.10105 z"
|
||||
id="path428"
|
||||
sodipodi:nodetypes="cccccccccccc" /><path
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 54.338047,170.6337 v 8.39821 H 70.80075 v 53.94872 h 11.012215 v -54.00434 h 17.920872 c 0,0 2.72e-4,-0.23357 2.72e-4,2.12606 v 51.91208 h 8.966631 v -29.12162 h 26.03469 v 28.76768 h 9.28125 v -61.66526 h -8.96663 v 25.72008 h -26.42796 l 0.22247,-26.09986 z"
|
||||
id="path526"
|
||||
sodipodi:nodetypes="cccccccscccccccccccc" /></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.2 KiB |
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 248 204">
|
||||
<path fill="#1d9bf0" d="M221.95 51.29c.15 2.17.15 4.34.15 6.53 0 66.73-50.8 143.69-143.69 143.69v-.04c-27.44.04-54.31-7.82-77.41-22.64 3.99.48 8 .72 12.02.73 22.74.02 44.83-7.61 62.72-21.66-21.61-.41-40.56-14.5-47.18-35.07 7.57 1.46 15.37 1.16 22.8-.87-23.56-4.76-40.51-25.46-40.51-49.5v-.64c7.02 3.91 14.88 6.08 22.92 6.32C11.58 63.31 4.74 33.79 18.14 10.71c25.64 31.55 63.47 50.73 104.08 52.76-4.07-17.54 1.49-35.92 14.61-48.25 20.34-19.12 52.33-18.14 71.45 2.19 11.31-2.23 22.15-6.38 32.07-12.26-3.77 11.69-11.66 21.62-22.2 27.93 10.01-1.18 19.79-3.86 29-7.95-6.78 10.16-15.32 19.01-25.2 26.16z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 732 B |
@@ -1 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512"><path d="M494.2 0h-31.8v31.8h31.8zM383.1 222.4v-63.6h63.5v63.5h63.5c1.1 58.9-3.4 110.2-33.3 161.6-86.6 152.4-300.5 172.9-414 39.2C36.3 392.4 17.2 355 8.3 315c-4.5-21.7-6.5-49.2-6.5-72.5V4h127l.2 242c.6 31.3 6.3 63.5 25 88 53.9 73 167.9 66.3 212.1-13.1 15.9-26.6 17.3-68.7 17-98.5m15.8-174.8h47.6v47.6H510v63.5h-63.5V95.3h-47.6z" style="fill:#005ed9"/></svg>
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="Ubiquiti" role="img"
|
||||
viewBox="0 0 512 512"><rect
|
||||
width="512" height="512"
|
||||
rx="15"
|
||||
fill="#399cdb"/><path d="M112 94v18h18V94h-18zm288 0c-82 0-90 31-90 61v172a147 147 0 01-3 28c43-9 72-36 86-82l7-23V94zm-234 18v18h18v-18h-18zm-18 18v18h18v-18h-18zm36 9v18h18v-18h-18zm-72 4v147c0 73 53 128 144 128 0 0-54-30-54-91V197h-18v66h-18v-39h-18v17h-18v-98h-18zm54 18v18h18v-18h-18zm-18 27v18h18v-18h-18zm252 87c-19 64-65 92-131 89-24-1-43-7-57-16 10 42 46 63 48 64l10 6c82-5 130-59 130-128v-15z" fill="#ffffff"/></svg>
|
||||
|
Before Width: | Height: | Size: 440 B After Width: | Height: | Size: 679 B |
@@ -1,7 +1,7 @@
|
||||
|
||||
name: ente_auth
|
||||
description: ente two-factor authenticator
|
||||
version: 4.4.3+443
|
||||
version: 4.4.4+444
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
|
||||
@@ -10,12 +10,12 @@ commit](https://github.com/ente-io/ente/commit/a8cdc811fd20ca4289d8e779c97f08ef5
|
||||
|
||||
Hello world
|
||||
|
||||
To know more about Ente, see [our main README](../../../README.md) or visit
|
||||
To know more about Ente, see [our main README](../README.md) or visit
|
||||
[ente.io](https://ente.io).
|
||||
|
||||
To use Ente Photos on the web, see [../../../web](../../../web/README.md). To use Ente
|
||||
Photos on the desktop, see [../../../desktop](../../../desktop/README.md). There is a also a
|
||||
[CLI tool](../../../cli/README.md) for easy / automated exports.
|
||||
To use Ente Photos on the web, see [../web](../web/README.md). To use Ente
|
||||
Photos on the desktop, see [../desktop](../desktop/README.md). There is a also a
|
||||
[CLI tool](../cli/README.md) for easy / automated exports.
|
||||
|
||||
If you're looking for Ente Auth instead, see [../auth](../auth/README.md).
|
||||
|
||||
@@ -32,16 +32,16 @@ without relying on third party stores.
|
||||
You can alternatively install the build from PlayStore or F-Droid.
|
||||
|
||||
<a href="https://play.google.com/store/apps/details?id=io.ente.photos">
|
||||
<img height="59" src="../../../.github/assets/play-store-badge.png">
|
||||
<img height="59" src="../.github/assets/play-store-badge.png">
|
||||
</a>
|
||||
<a href="https://f-droid.org/packages/io.ente.photos.fdroid/">
|
||||
<img height="59" src="../../../.github/assets/f-droid-badge.png">
|
||||
<img height="59" src="../.github/assets/f-droid-badge.png">
|
||||
</a>
|
||||
|
||||
### iOS
|
||||
|
||||
<a href="https://apps.apple.com/in/app/ente-photos/id1542026904">
|
||||
<img height="59" src="../../../.github/assets/app-store-badge.svg">
|
||||
<img height="59" src="../.github/assets/app-store-badge.svg">
|
||||
</a>
|
||||
|
||||
## 🧑💻 Building from source
|
||||
@@ -99,4 +99,4 @@ apksigner verify --print-certs <path_to_apk>
|
||||
|
||||
## 💚 Contribute
|
||||
|
||||
For more ways to contribute, see [../../../CONTRIBUTING.md](../../../CONTRIBUTING.md).
|
||||
For more ways to contribute, see [../CONTRIBUTING.md](../CONTRIBUTING.md).
|
||||
|
||||
@@ -157,23 +157,6 @@ class UploadLocksDB {
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> getLockData(String id) async {
|
||||
final db = await instance.database;
|
||||
final rows = await db.query(
|
||||
_uploadLocksTable.table,
|
||||
where: '${_uploadLocksTable.columnID} = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
if (rows.isEmpty) {
|
||||
return "No lock found for $id";
|
||||
}
|
||||
final row = rows.first;
|
||||
final time = row[_uploadLocksTable.columnTime] as int;
|
||||
final owner = row[_uploadLocksTable.columnOwner] as String;
|
||||
final duration = DateTime.now().millisecondsSinceEpoch - time;
|
||||
return "Lock for $id acquired by $owner since ${Duration(milliseconds: duration)}";
|
||||
}
|
||||
|
||||
Future<bool> isLocked(String id, String owner) async {
|
||||
final db = await instance.database;
|
||||
final rows = await db.query(
|
||||
|
||||
@@ -5,7 +5,6 @@ import 'package:photos/models/selected_files.dart';
|
||||
import 'package:photos/theme/ente_theme.dart';
|
||||
|
||||
class ActionBarWidget extends StatefulWidget {
|
||||
final Color? backgroundColor;
|
||||
final SelectedFiles? selectedFiles;
|
||||
final VoidCallback? onCancel;
|
||||
|
||||
@@ -13,7 +12,6 @@ class ActionBarWidget extends StatefulWidget {
|
||||
required this.onCancel,
|
||||
this.selectedFiles,
|
||||
super.key,
|
||||
required this.backgroundColor,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -44,57 +42,53 @@ class _ActionBarWidgetState extends State<ActionBarWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = getEnteTextTheme(context);
|
||||
final colorScheme = getEnteColorScheme(context);
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: widget.backgroundColor ?? colorScheme.backgroundElevated2,
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, -1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ValueListenableBuilder(
|
||||
valueListenable: _selectedFilesNotifier,
|
||||
builder: (context, value, child) {
|
||||
return Text(
|
||||
_selectedOwnedFilesNotifier.value !=
|
||||
_selectedFilesNotifier.value
|
||||
? S.of(context).selectedPhotosWithYours(
|
||||
_selectedFilesNotifier.value,
|
||||
_selectedOwnedFilesNotifier.value,
|
||||
)
|
||||
: S.of(context).selectedPhotos(
|
||||
_selectedFilesNotifier.value,
|
||||
),
|
||||
style: textTheme.mini,
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
widget.onCancel?.call();
|
||||
},
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
size: 16,
|
||||
color: textTheme.mini.color,
|
||||
return SizedBox(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 8, 20, 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: _selectedFilesNotifier,
|
||||
builder: (context, value, child) {
|
||||
return Text(
|
||||
_selectedOwnedFilesNotifier.value !=
|
||||
_selectedFilesNotifier.value
|
||||
? S.of(context).selectedPhotosWithYours(
|
||||
_selectedFilesNotifier.value,
|
||||
_selectedOwnedFilesNotifier.value,
|
||||
)
|
||||
: S.of(context).selectedPhotos(
|
||||
_selectedFilesNotifier.value,
|
||||
),
|
||||
style: textTheme.miniMuted,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
widget.onCancel?.call();
|
||||
},
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text(
|
||||
S.of(context).cancel,
|
||||
style: textTheme.mini,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import "package:photos/models/gallery_type.dart";
|
||||
import "package:photos/models/ml/face/person.dart";
|
||||
import 'package:photos/models/selected_files.dart';
|
||||
import 'package:photos/theme/ente_theme.dart';
|
||||
import 'package:photos/ui/components/bottom_action_bar/action_bar_widget.dart';
|
||||
import "package:photos/ui/components/divider_widget.dart";
|
||||
import "package:photos/ui/viewer/actions/file_selection_actions_widget.dart";
|
||||
|
||||
class BottomActionBarWidget extends StatelessWidget {
|
||||
@@ -14,6 +16,7 @@ class BottomActionBarWidget extends StatelessWidget {
|
||||
final String? clusterID;
|
||||
final SelectedFiles selectedFiles;
|
||||
final VoidCallback? onCancel;
|
||||
final Color? backgroundColor;
|
||||
|
||||
const BottomActionBarWidget({
|
||||
required this.galleryType,
|
||||
@@ -22,6 +25,7 @@ class BottomActionBarWidget extends StatelessWidget {
|
||||
this.person,
|
||||
this.clusterID,
|
||||
this.onCancel,
|
||||
this.backgroundColor,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@@ -35,10 +39,10 @@ class BottomActionBarWidget extends StatelessWidget {
|
||||
: 0;
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.backgroundElevated,
|
||||
color: backgroundColor ?? colorScheme.backgroundElevated2,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(24),
|
||||
topRight: Radius.circular(24),
|
||||
topLeft: Radius.circular(8),
|
||||
topRight: Radius.circular(8),
|
||||
),
|
||||
),
|
||||
padding: EdgeInsets.only(
|
||||
@@ -58,7 +62,12 @@ class BottomActionBarWidget extends StatelessWidget {
|
||||
person: person,
|
||||
clusterID: clusterID,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
const DividerWidget(dividerType: DividerType.bottomBar),
|
||||
ActionBarWidget(
|
||||
selectedFiles: selectedFiles,
|
||||
onCancel: onCancel,
|
||||
),
|
||||
// const SizedBox(height: 2)
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import "package:auto_size_text/auto_size_text.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_svg/svg.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
@@ -107,7 +106,7 @@ class __BodyState extends State<_Body> {
|
||||
children: [
|
||||
if (widget.icon == Icons.navigation_rounded)
|
||||
Transform.rotate(
|
||||
angle: math.pi * 2,
|
||||
angle: math.pi / 2,
|
||||
child: Icon(
|
||||
widget.icon,
|
||||
size: 24,
|
||||
@@ -138,7 +137,7 @@ class __BodyState extends State<_Body> {
|
||||
SvgPicture.asset(
|
||||
widget.svgAssetPath!,
|
||||
colorFilter: ColorFilter.mode(
|
||||
getEnteColorScheme(context).textBase,
|
||||
getEnteColorScheme(context).textMuted,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
width: 24,
|
||||
@@ -150,16 +149,14 @@ class __BodyState extends State<_Body> {
|
||||
Icon(
|
||||
widget.icon,
|
||||
size: 24,
|
||||
color: getEnteColorScheme(context).textBase,
|
||||
color: getEnteColorScheme(context).textMuted,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Flexible(
|
||||
child: AutoSizeText(
|
||||
widget.labelText,
|
||||
textAlign: TextAlign.center,
|
||||
//textTheme in [getWidthOfLongestWord] should be same as this
|
||||
style: getEnteTextTheme(context).mini,
|
||||
),
|
||||
Text(
|
||||
widget.labelText,
|
||||
textAlign: TextAlign.center,
|
||||
//textTheme in [getWidthOfLongestWord] should be same as this
|
||||
style: getEnteTextTheme(context).miniMuted,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -183,7 +180,8 @@ class __BodyState extends State<_Body> {
|
||||
|
||||
double maxWidth = 0.0;
|
||||
for (String word in words) {
|
||||
final width = computeWidthOfWord(word, getEnteTextTheme(context).mini);
|
||||
final width =
|
||||
computeWidthOfWord(word, getEnteTextTheme(context).miniMuted);
|
||||
if (width > maxWidth) {
|
||||
maxWidth = width;
|
||||
}
|
||||
|
||||
@@ -50,7 +50,6 @@ import 'package:photos/utils/navigation_util.dart';
|
||||
import "package:photos/utils/share_util.dart";
|
||||
import "package:photos/utils/standalone/simple_task_queue.dart";
|
||||
import "package:screenshot/screenshot.dart";
|
||||
import "package:smooth_page_indicator/smooth_page_indicator.dart";
|
||||
|
||||
class FileSelectionActionsWidget extends StatefulWidget {
|
||||
final GalleryType type;
|
||||
@@ -90,7 +89,6 @@ class _FileSelectionActionsWidgetState
|
||||
Collection? _cachedCollectionForSharedLink;
|
||||
final GlobalKey shareButtonKey = GlobalKey();
|
||||
final GlobalKey sendLinkButtonKey = GlobalKey();
|
||||
final PageController _pageController = PageController();
|
||||
final StreamController<double> _progressController =
|
||||
StreamController<double>();
|
||||
|
||||
@@ -155,31 +153,6 @@ class _FileSelectionActionsWidgetState
|
||||
//for items that should be shown.
|
||||
final List<SelectionActionButton> items = [];
|
||||
|
||||
final showUploadIcon = widget.type == GalleryType.localFolder &&
|
||||
split.ownedByCurrentUser.isEmpty;
|
||||
|
||||
//add to album
|
||||
if (widget.type.showAddToAlbum()) {
|
||||
if (showUploadIcon) {
|
||||
items.add(
|
||||
SelectionActionButton(
|
||||
icon: Icons.cloud_upload_outlined,
|
||||
labelText: S.of(context).addToEnte,
|
||||
onTap: _addToAlbum,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
items.add(
|
||||
SelectionActionButton(
|
||||
icon: Icons.add_outlined,
|
||||
labelText: S.of(context).addToAlbum,
|
||||
onTap: _addToAlbum,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//share link
|
||||
if (widget.type.showCreateLink()) {
|
||||
if (_cachedCollectionForSharedLink != null && anyUploadedFiles) {
|
||||
items.add(
|
||||
@@ -193,7 +166,7 @@ class _FileSelectionActionsWidgetState
|
||||
items.add(
|
||||
SelectionActionButton(
|
||||
icon: Icons.navigation_rounded,
|
||||
labelText: S.of(context).share,
|
||||
labelText: S.of(context).sendLink,
|
||||
onTap: anyUploadedFiles ? _onSendLinkTapped : null,
|
||||
shouldShow: ownedFilesCount > 0,
|
||||
key: sendLinkButtonKey,
|
||||
@@ -201,150 +174,6 @@ class _FileSelectionActionsWidgetState
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//Favorite
|
||||
if (widget.type.showFavoriteOption()) {
|
||||
items.add(
|
||||
SelectionActionButton(
|
||||
icon: Icons.favorite_border_rounded,
|
||||
labelText: S.of(context).favorite,
|
||||
onTap: anyUploadedFiles ? _onFavoriteClick : null,
|
||||
shouldShow: ownedFilesCount > 0,
|
||||
),
|
||||
);
|
||||
} else if (widget.type.showUnFavoriteOption()) {
|
||||
items.add(
|
||||
SelectionActionButton(
|
||||
icon: Icons.favorite,
|
||||
labelText: S.of(context).removeFromFavorite,
|
||||
onTap: _onUnFavoriteClick,
|
||||
shouldShow: ownedFilesCount > 0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
//Download
|
||||
if (showDownloadOption) {
|
||||
items.add(
|
||||
SelectionActionButton(
|
||||
labelText: S.of(context).download,
|
||||
icon: Icons.cloud_download_outlined,
|
||||
onTap: () => _download(widget.selectedFiles.files.toList()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
//Delete (for owned photos only)
|
||||
if (widget.type.showDeleteOption()) {
|
||||
items.add(
|
||||
SelectionActionButton(
|
||||
icon: Icons.delete_outline,
|
||||
labelText: S.of(context).delete,
|
||||
onTap: anyOwnedFiles ? _onDeleteClick : null,
|
||||
shouldShow: allOwnedFiles,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
//Hide (for owned photos only)
|
||||
if (widget.type.showHideOption()) {
|
||||
items.add(
|
||||
SelectionActionButton(
|
||||
icon: Icons.visibility_off_outlined,
|
||||
labelText: S.of(context).hide,
|
||||
onTap: anyUploadedFiles ? _onHideClick : null,
|
||||
shouldShow: ownedFilesCount > 0,
|
||||
),
|
||||
);
|
||||
} else if (widget.type.showUnHideOption()) {
|
||||
items.add(
|
||||
SelectionActionButton(
|
||||
icon: Icons.visibility_outlined,
|
||||
labelText: S.of(context).unhide,
|
||||
onTap: _onUnhideClick,
|
||||
shouldShow: ownedFilesCount > 0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
//Archive (for owned photos only)
|
||||
if (widget.type.showArchiveOption()) {
|
||||
items.add(
|
||||
SelectionActionButton(
|
||||
icon: Icons.archive_outlined,
|
||||
labelText: S.of(context).archive,
|
||||
onTap: anyUploadedFiles ? _onArchiveClick : null,
|
||||
shouldShow: ownedFilesCount > 0,
|
||||
),
|
||||
);
|
||||
} else if (widget.type.showUnArchiveOption()) {
|
||||
items.add(
|
||||
SelectionActionButton(
|
||||
icon: Icons.unarchive,
|
||||
labelText: S.of(context).unarchive,
|
||||
onTap: _onUnArchiveClick,
|
||||
shouldShow: ownedFilesCount > 0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
//Guest view
|
||||
items.add(
|
||||
SelectionActionButton(
|
||||
svgAssetPath: "assets/icons/guest_view_icon.svg",
|
||||
labelText: S.of(context).guestView,
|
||||
onTap: _onGuestViewClick,
|
||||
),
|
||||
);
|
||||
|
||||
//Create collage
|
||||
if (widget.type != GalleryType.sharedPublicCollection) {
|
||||
items.add(
|
||||
SelectionActionButton(
|
||||
icon: Icons.grid_view_outlined,
|
||||
labelText: S.of(context).createCollage,
|
||||
onTap: _onCreateCollageClicked,
|
||||
shouldShow: showCollageOption,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
//Edit (only if single photo is selected)
|
||||
if (widget.type.showBulkEditTime()) {
|
||||
items.add(
|
||||
SelectionActionButton(
|
||||
shouldShow: widget.selectedFiles.files.every(
|
||||
(element) => (element.ownerID == currentUserID),
|
||||
),
|
||||
labelText: S.of(context).editTime,
|
||||
icon: Icons.edit_calendar_outlined,
|
||||
onTap: () async {
|
||||
final newDate = await showEditDateSheet(
|
||||
context,
|
||||
widget.selectedFiles.files,
|
||||
);
|
||||
if (newDate != null) {
|
||||
widget.selectedFiles.clearAll();
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
//Edit location (for owned photos only)
|
||||
if (widget.type.showEditLocation()) {
|
||||
items.add(
|
||||
SelectionActionButton(
|
||||
shouldShow: widget.selectedFiles.files.any(
|
||||
(element) => (element.ownerID == currentUserID),
|
||||
),
|
||||
labelText: S.of(context).editLocation,
|
||||
icon: Icons.edit_location_alt_outlined,
|
||||
onTap: _editLocation,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.type == GalleryType.peopleTag && widget.person != null) {
|
||||
items.add(
|
||||
SelectionActionButton(
|
||||
@@ -374,6 +203,28 @@ class _FileSelectionActionsWidgetState
|
||||
);
|
||||
}
|
||||
|
||||
final showUploadIcon = widget.type == GalleryType.localFolder &&
|
||||
split.ownedByCurrentUser.isEmpty;
|
||||
if (widget.type.showAddToAlbum()) {
|
||||
if (showUploadIcon) {
|
||||
items.add(
|
||||
SelectionActionButton(
|
||||
icon: Icons.cloud_upload_outlined,
|
||||
labelText: S.of(context).addToEnte,
|
||||
onTap: _addToAlbum,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
items.add(
|
||||
SelectionActionButton(
|
||||
icon: Icons.add_outlined,
|
||||
labelText: S.of(context).addToAlbum,
|
||||
onTap: _addToAlbum,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (widget.type.showAddtoHiddenAlbum()) {
|
||||
items.add(
|
||||
SelectionActionButton(
|
||||
@@ -405,6 +256,17 @@ class _FileSelectionActionsWidgetState
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.type.showDeleteOption()) {
|
||||
items.add(
|
||||
SelectionActionButton(
|
||||
icon: Icons.delete_outline,
|
||||
labelText: S.of(context).delete,
|
||||
onTap: anyOwnedFiles ? _onDeleteClick : null,
|
||||
shouldShow: allOwnedFiles,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.type.showRemoveFromAlbum()) {
|
||||
items.add(
|
||||
SelectionActionButton(
|
||||
@@ -426,6 +288,42 @@ class _FileSelectionActionsWidgetState
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.type.showFavoriteOption()) {
|
||||
items.add(
|
||||
SelectionActionButton(
|
||||
icon: Icons.favorite_border_rounded,
|
||||
labelText: S.of(context).favorite,
|
||||
onTap: anyUploadedFiles ? _onFavoriteClick : null,
|
||||
shouldShow: ownedFilesCount > 0,
|
||||
),
|
||||
);
|
||||
} else if (widget.type.showUnFavoriteOption()) {
|
||||
items.add(
|
||||
SelectionActionButton(
|
||||
icon: Icons.favorite,
|
||||
labelText: S.of(context).removeFromFavorite,
|
||||
onTap: _onUnFavoriteClick,
|
||||
shouldShow: ownedFilesCount > 0,
|
||||
),
|
||||
);
|
||||
}
|
||||
items.add(
|
||||
SelectionActionButton(
|
||||
svgAssetPath: "assets/icons/guest_view_icon.svg",
|
||||
labelText: S.of(context).guestView,
|
||||
onTap: _onGuestViewClick,
|
||||
),
|
||||
);
|
||||
if (widget.type != GalleryType.sharedPublicCollection) {
|
||||
items.add(
|
||||
SelectionActionButton(
|
||||
icon: Icons.grid_view_outlined,
|
||||
labelText: S.of(context).createCollage,
|
||||
onTap: _onCreateCollageClicked,
|
||||
shouldShow: showCollageOption,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (flagService.internalUser &&
|
||||
widget.type != GalleryType.sharedPublicCollection) {
|
||||
items.add(
|
||||
@@ -437,6 +335,45 @@ class _FileSelectionActionsWidgetState
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.type.showHideOption()) {
|
||||
items.add(
|
||||
SelectionActionButton(
|
||||
icon: Icons.visibility_off_outlined,
|
||||
labelText: S.of(context).hide,
|
||||
onTap: anyUploadedFiles ? _onHideClick : null,
|
||||
shouldShow: ownedFilesCount > 0,
|
||||
),
|
||||
);
|
||||
} else if (widget.type.showUnHideOption()) {
|
||||
items.add(
|
||||
SelectionActionButton(
|
||||
icon: Icons.visibility_outlined,
|
||||
labelText: S.of(context).unhide,
|
||||
onTap: _onUnhideClick,
|
||||
shouldShow: ownedFilesCount > 0,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (widget.type.showArchiveOption()) {
|
||||
items.add(
|
||||
SelectionActionButton(
|
||||
icon: Icons.archive_outlined,
|
||||
labelText: S.of(context).archive,
|
||||
onTap: anyUploadedFiles ? _onArchiveClick : null,
|
||||
shouldShow: ownedFilesCount > 0,
|
||||
),
|
||||
);
|
||||
} else if (widget.type.showUnArchiveOption()) {
|
||||
items.add(
|
||||
SelectionActionButton(
|
||||
icon: Icons.unarchive,
|
||||
labelText: S.of(context).unarchive,
|
||||
onTap: _onUnArchiveClick,
|
||||
shouldShow: ownedFilesCount > 0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.type.showRestoreOption()) {
|
||||
items.add(
|
||||
SelectionActionButton(
|
||||
@@ -457,6 +394,49 @@ class _FileSelectionActionsWidgetState
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.type.showBulkEditTime()) {
|
||||
items.add(
|
||||
SelectionActionButton(
|
||||
shouldShow: widget.selectedFiles.files.every(
|
||||
(element) => (element.ownerID == currentUserID),
|
||||
),
|
||||
labelText: S.of(context).editTime,
|
||||
icon: Icons.edit_calendar_outlined,
|
||||
onTap: () async {
|
||||
final newDate = await showEditDateSheet(
|
||||
context,
|
||||
widget.selectedFiles.files,
|
||||
);
|
||||
if (newDate != null) {
|
||||
widget.selectedFiles.clearAll();
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.type.showEditLocation()) {
|
||||
items.add(
|
||||
SelectionActionButton(
|
||||
shouldShow: widget.selectedFiles.files.any(
|
||||
(element) => (element.ownerID == currentUserID),
|
||||
),
|
||||
labelText: S.of(context).editLocation,
|
||||
icon: Icons.edit_location_alt_outlined,
|
||||
onTap: _editLocation,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (showDownloadOption) {
|
||||
items.add(
|
||||
SelectionActionButton(
|
||||
labelText: S.of(context).download,
|
||||
icon: Icons.cloud_download_outlined,
|
||||
onTap: () => _download(widget.selectedFiles.files.toList()),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (widget.type != GalleryType.sharedPublicCollection) {
|
||||
items.add(
|
||||
SelectionActionButton(
|
||||
@@ -468,147 +448,38 @@ class _FileSelectionActionsWidgetState
|
||||
);
|
||||
}
|
||||
|
||||
// Filter items that should be shown first
|
||||
final List<SelectionActionButton> visibleItems = items
|
||||
.where((item) => item.shouldShow == null || item.shouldShow == true)
|
||||
.toList();
|
||||
|
||||
final List<SelectionActionButton> firstThreeItems =
|
||||
visibleItems.length > 3 ? visibleItems.take(3).toList() : visibleItems;
|
||||
|
||||
final List<SelectionActionButton> otherItems =
|
||||
visibleItems.length > 3 ? visibleItems.sublist(3) : [];
|
||||
|
||||
final List<List<SelectionActionButton>> groupedOtherItems = [];
|
||||
for (int i = 0; i < otherItems.length; i += 4) {
|
||||
int end = (i + 4 < otherItems.length) ? i + 4 : otherItems.length;
|
||||
groupedOtherItems.add(otherItems.sublist(i, end));
|
||||
}
|
||||
|
||||
if (visibleItems.isNotEmpty) {
|
||||
if (items.isNotEmpty) {
|
||||
final scrollController = ScrollController();
|
||||
// h4ck: https://github.com/flutter/flutter/issues/57920#issuecomment-893970066
|
||||
return MediaQuery(
|
||||
data: MediaQuery.of(context).removePadding(removeBottom: true),
|
||||
child: SafeArea(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(bottom: 20),
|
||||
child: Column(
|
||||
children: [
|
||||
// First Row
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
child: Scrollbar(
|
||||
radius: const Radius.circular(1),
|
||||
thickness: 2,
|
||||
controller: scrollController,
|
||||
thumbVisibility: true,
|
||||
child: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(
|
||||
decelerationRate: ScrollDecelerationRate.fast,
|
||||
),
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(bottom: 24),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(width: 4),
|
||||
...items,
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
),
|
||||
child: Row(
|
||||
children: firstThreeItems
|
||||
.map(
|
||||
(item) => Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Container(
|
||||
height: MediaQuery.of(context).size.height *
|
||||
0.10,
|
||||
decoration: BoxDecoration(
|
||||
color: getEnteColorScheme(context)
|
||||
.backgroundElevated2,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
),
|
||||
item,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
|
||||
// Second Row
|
||||
if (groupedOtherItems.isNotEmpty) ...[
|
||||
const SizedBox(height: 24),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 74,
|
||||
decoration: BoxDecoration(
|
||||
color: getEnteColorScheme(context).backgroundElevated2,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: PageView.builder(
|
||||
controller: _pageController,
|
||||
itemCount: groupedOtherItems.length,
|
||||
onPageChanged: (index) {
|
||||
if (index >= groupedOtherItems.length &&
|
||||
groupedOtherItems.isNotEmpty) {
|
||||
_pageController.animateToPage(
|
||||
groupedOtherItems.length - 1,
|
||||
duration: const Duration(seconds: 5),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}
|
||||
},
|
||||
itemBuilder: (context, pageIndex) {
|
||||
if (pageIndex >= groupedOtherItems.length) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
final currentGroup = groupedOtherItems[pageIndex];
|
||||
|
||||
return Row(
|
||||
children: currentGroup.map((item) {
|
||||
return Expanded(
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(seconds: 5),
|
||||
transitionBuilder: (
|
||||
Widget child,
|
||||
Animation<double> animation,
|
||||
) {
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: item is Widget
|
||||
? KeyedSubtree(
|
||||
key: ValueKey(item.hashCode),
|
||||
child: item,
|
||||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (groupedOtherItems.length > 1)
|
||||
SmoothPageIndicator(
|
||||
controller: _pageController,
|
||||
count: groupedOtherItems.length,
|
||||
effect: const WormEffect(
|
||||
dotHeight: 6,
|
||||
dotWidth: 6,
|
||||
spacing: 6,
|
||||
activeDotColor: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
@@ -624,6 +495,7 @@ class _FileSelectionActionsWidgetState
|
||||
topControl: Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
// This container is for increasing the tap area
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 36,
|
||||
|
||||
@@ -11,7 +11,6 @@ import "package:photos/models/search/hierarchical/only_them_filter.dart";
|
||||
import 'package:photos/models/selected_files.dart';
|
||||
import "package:photos/theme/effects.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
import "package:photos/ui/components/bottom_action_bar/action_bar_widget.dart";
|
||||
import 'package:photos/ui/components/bottom_action_bar/bottom_action_bar_widget.dart';
|
||||
import "package:photos/ui/viewer/gallery/state/gallery_files_inherited_widget.dart";
|
||||
import "package:photos/ui/viewer/gallery/state/inherited_search_filter_data.dart";
|
||||
@@ -26,7 +25,6 @@ class FileSelectionOverlayBar extends StatefulWidget {
|
||||
final Color? backgroundColor;
|
||||
final PersonEntity? person;
|
||||
final String? clusterID;
|
||||
final VoidCallback? onCancel;
|
||||
|
||||
const FileSelectionOverlayBar(
|
||||
this.galleryType,
|
||||
@@ -35,7 +33,6 @@ class FileSelectionOverlayBar extends StatefulWidget {
|
||||
this.backgroundColor,
|
||||
this.person,
|
||||
this.clusterID,
|
||||
this.onCancel,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@@ -130,30 +127,13 @@ class _FileSelectionOverlayBarState extends State<FileSelectionOverlayBar> {
|
||||
duration: const Duration(milliseconds: 400),
|
||||
firstChild: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 12),
|
||||
child: SelectAllButton(
|
||||
backgroundColor: widget.backgroundColor,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 12),
|
||||
child: ActionBarWidget(
|
||||
selectedFiles: widget.selectedFiles,
|
||||
onCancel: () {
|
||||
if (widget.selectedFiles.files.isNotEmpty) {
|
||||
widget.selectedFiles.clearAll();
|
||||
}
|
||||
},
|
||||
backgroundColor: widget.backgroundColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 4),
|
||||
child: SelectAllButton(
|
||||
backgroundColor: widget.backgroundColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
@@ -171,6 +151,7 @@ class _FileSelectionOverlayBarState extends State<FileSelectionOverlayBar> {
|
||||
widget.selectedFiles.clearAll();
|
||||
}
|
||||
},
|
||||
backgroundColor: widget.backgroundColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -266,47 +247,50 @@ class _SelectAllButtonState extends State<SelectAllButton> {
|
||||
_allSelected = !_allSelected;
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: widget.backgroundColor ?? colorScheme.backgroundElevated2,
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, -1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).selectAll,
|
||||
style: getEnteTextTheme(context).mini,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
ListenableBuilder(
|
||||
listenable: selectionState!.selectedFiles,
|
||||
builder: (context, _) {
|
||||
if (selectionState.selectedFiles.files.length ==
|
||||
allGalleryFiles.length) {
|
||||
_allSelected = true;
|
||||
} else {
|
||||
_allSelected = false;
|
||||
}
|
||||
return Icon(
|
||||
_allSelected
|
||||
? Icons.check_circle
|
||||
: Icons.check_circle_outline,
|
||||
color: _allSelected ? null : colorScheme.strokeBase,
|
||||
size: 16,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: widget.backgroundColor ?? colorScheme.backgroundElevated2,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, -1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).selectAllShort,
|
||||
style: getEnteTextTheme(context).miniMuted,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
ListenableBuilder(
|
||||
listenable: selectionState!.selectedFiles,
|
||||
builder: (context, _) {
|
||||
if (selectionState.selectedFiles.files.length ==
|
||||
allGalleryFiles.length) {
|
||||
_allSelected = true;
|
||||
} else {
|
||||
_allSelected = false;
|
||||
}
|
||||
return Icon(
|
||||
_allSelected
|
||||
? Icons.check_circle
|
||||
: Icons.check_circle_outline,
|
||||
color: _allSelected ? null : colorScheme.strokeMuted,
|
||||
size: 18,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -519,8 +519,7 @@ class FileUploader {
|
||||
DateTime.now().microsecondsSinceEpoch,
|
||||
);
|
||||
} catch (e) {
|
||||
final lockInfo = await _uploadLocks.getLockData(lockKey);
|
||||
_logger.warning("Lock was already taken ($lockInfo) for " + file.tag);
|
||||
_logger.warning("Lock was already taken for " + file.toString());
|
||||
throw LockAlreadyAcquiredError();
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834
|
||||
sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "72.0.0"
|
||||
version: "76.0.0"
|
||||
_flutterfire_internals:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -21,7 +21,7 @@ packages:
|
||||
dependency: transitive
|
||||
description: dart
|
||||
source: sdk
|
||||
version: "0.3.2"
|
||||
version: "0.3.3"
|
||||
adaptive_theme:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -34,10 +34,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139
|
||||
sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.7.0"
|
||||
version: "6.11.0"
|
||||
android_intent_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -134,14 +134,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.11.0"
|
||||
auto_size_text:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: auto_size_text
|
||||
sha256: "3f5261cd3fb5f2a9ab4e2fc3fba84fd9fcaac8821f20a1d4e71f557521b22599"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
battery_info:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -325,10 +317,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: collection
|
||||
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.18.0"
|
||||
version: "1.19.0"
|
||||
computer:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1400,18 +1392,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
|
||||
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.5"
|
||||
version: "10.0.7"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
|
||||
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.5"
|
||||
version: "3.0.8"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1520,10 +1512,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: macros
|
||||
sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
|
||||
sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.2-main.4"
|
||||
version: "0.1.3-main.0"
|
||||
maps_launcher:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -2301,15 +2293,7 @@ packages:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.99"
|
||||
smooth_page_indicator:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: smooth_page_indicator
|
||||
sha256: b21ebb8bc39cf72d11c7cfd809162a48c3800668ced1c9da3aade13a32cf6c1c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
version: "0.0.0"
|
||||
source_gen:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2434,10 +2418,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
|
||||
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.11.1"
|
||||
version: "1.12.0"
|
||||
step_progress_indicator:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -2466,10 +2450,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
|
||||
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.3.0"
|
||||
styled_text:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -2530,26 +2514,26 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: test
|
||||
sha256: "7ee44229615f8f642b68120165ae4c2a75fe77ae2065b1e55ae4711f6cf0899e"
|
||||
sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.25.7"
|
||||
version: "1.25.8"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
|
||||
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.2"
|
||||
version: "0.7.3"
|
||||
test_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_core
|
||||
sha256: "55ea5a652e38a1dfb32943a7973f3681a60f872f8c3a05a14664ad54ef9c6696"
|
||||
sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.4"
|
||||
version: "0.6.5"
|
||||
thermal:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -2829,10 +2813,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
|
||||
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.2.5"
|
||||
version: "14.3.0"
|
||||
volume_controller:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2893,10 +2877,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webdriver
|
||||
sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e"
|
||||
sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
version: "3.0.4"
|
||||
webkit_inspection_protocol:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -25,7 +25,6 @@ dependencies:
|
||||
app_links: ^6.4.0
|
||||
archive: ^3.6.1
|
||||
async: ^2.11.0
|
||||
auto_size_text: ^3.0.0
|
||||
battery_info: # replace with battery_plus
|
||||
git:
|
||||
url: https://github.com/ente-io/battery_info
|
||||
@@ -185,7 +184,6 @@ dependencies:
|
||||
sentry_flutter: ^8.14.1
|
||||
share_plus: ^10.0.2
|
||||
shared_preferences: ^2.0.5
|
||||
smooth_page_indicator: ^1.2.1
|
||||
sqflite: ^2.3.0
|
||||
sqflite_migration: ^0.3.0
|
||||
sqlite3_flutter_libs: ^0.5.20
|
||||
|
||||
@@ -5,9 +5,6 @@ import (
|
||||
"database/sql"
|
||||
b64 "encoding/base64"
|
||||
"fmt"
|
||||
"github.com/ente-io/museum/pkg/controller/collections"
|
||||
publicCtrl "github.com/ente-io/museum/pkg/controller/public"
|
||||
"github.com/ente-io/museum/pkg/repo/public"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
@@ -17,6 +14,8 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/ente-io/museum/pkg/controller/collections"
|
||||
|
||||
"github.com/ente-io/museum/ente/base"
|
||||
"github.com/ente-io/museum/pkg/controller/emergency"
|
||||
"github.com/ente-io/museum/pkg/controller/file_copy"
|
||||
@@ -98,7 +97,6 @@ func main() {
|
||||
}
|
||||
|
||||
viper.SetDefault("apps.public-albums", "https://albums.ente.io")
|
||||
viper.SetDefault("apps.public-locker", "https://locker.ente.io")
|
||||
viper.SetDefault("apps.accounts", "https://accounts.ente.io")
|
||||
viper.SetDefault("apps.cast", "https://cast.ente.io")
|
||||
viper.SetDefault("apps.family", "https://family.ente.io")
|
||||
@@ -176,13 +174,11 @@ func main() {
|
||||
fileRepo := &repo.FileRepository{DB: db, S3Config: s3Config, QueueRepo: queueRepo,
|
||||
ObjectRepo: objectRepo, ObjectCleanupRepo: objectCleanupRepo,
|
||||
ObjectCopiesRepo: objectCopiesRepo, UsageRepo: usageRepo}
|
||||
fileLinkRepo := public.NewFileLinkRepo(db)
|
||||
fileDataRepo := &fileDataRepo.Repository{DB: db, ObjectCleanupRepo: objectCleanupRepo}
|
||||
familyRepo := &repo.FamilyRepository{DB: db}
|
||||
trashRepo := &repo.TrashRepository{DB: db, ObjectRepo: objectRepo, FileRepo: fileRepo, QueueRepo: queueRepo, FileLinkRepo: fileLinkRepo}
|
||||
collectionLinkRepo := public.NewCollectionLinkRepository(db, viper.GetString("apps.public-albums"))
|
||||
|
||||
collectionRepo := &repo.CollectionRepository{DB: db, FileRepo: fileRepo, CollectionLinkRepo: collectionLinkRepo,
|
||||
trashRepo := &repo.TrashRepository{DB: db, ObjectRepo: objectRepo, FileRepo: fileRepo, QueueRepo: queueRepo}
|
||||
publicCollectionRepo := repo.NewPublicCollectionRepository(db, viper.GetString("apps.public-albums"))
|
||||
collectionRepo := &repo.CollectionRepository{DB: db, FileRepo: fileRepo, PublicCollectionRepo: publicCollectionRepo,
|
||||
TrashRepo: trashRepo, SecretEncryptionKey: secretEncryptionKeyBytes, QueueRepo: queueRepo, LatencyLogger: latencyLogger}
|
||||
pushRepo := &repo.PushTokenRepository{DB: db}
|
||||
kexRepo := &kex.Repository{
|
||||
@@ -304,27 +300,26 @@ func main() {
|
||||
UsageRepo: usageRepo,
|
||||
}
|
||||
|
||||
collectionLinkCtrl := &publicCtrl.CollectionLinkController{
|
||||
publicCollectionCtrl := &controller.PublicCollectionController{
|
||||
FileController: fileController,
|
||||
EmailNotificationCtrl: emailNotificationCtrl,
|
||||
CollectionLinkRepo: collectionLinkRepo,
|
||||
FileLinkRepo: fileLinkRepo,
|
||||
PublicCollectionRepo: publicCollectionRepo,
|
||||
CollectionRepo: collectionRepo,
|
||||
UserRepo: userRepo,
|
||||
JwtSecret: jwtSecretBytes,
|
||||
}
|
||||
|
||||
collectionController := &collections.CollectionController{
|
||||
CollectionRepo: collectionRepo,
|
||||
EmailCtrl: emailNotificationCtrl,
|
||||
AccessCtrl: accessCtrl,
|
||||
CollectionLinkCtrl: collectionLinkCtrl,
|
||||
UserRepo: userRepo,
|
||||
FileRepo: fileRepo,
|
||||
CastRepo: &castDb,
|
||||
BillingCtrl: billingController,
|
||||
QueueRepo: queueRepo,
|
||||
TaskRepo: taskLockingRepo,
|
||||
CollectionRepo: collectionRepo,
|
||||
EmailCtrl: emailNotificationCtrl,
|
||||
AccessCtrl: accessCtrl,
|
||||
PublicCollectionCtrl: publicCollectionCtrl,
|
||||
UserRepo: userRepo,
|
||||
FileRepo: fileRepo,
|
||||
CastRepo: &castDb,
|
||||
BillingCtrl: billingController,
|
||||
QueueRepo: queueRepo,
|
||||
TaskRepo: taskLockingRepo,
|
||||
}
|
||||
|
||||
kexCtrl := &kexCtrl.Controller{
|
||||
@@ -356,12 +351,6 @@ func main() {
|
||||
userCache,
|
||||
userCacheCtrl,
|
||||
)
|
||||
fileLinkCtrl := &publicCtrl.FileLinkController{
|
||||
FileController: fileController,
|
||||
FileLinkRepo: fileLinkRepo,
|
||||
FileRepo: fileRepo,
|
||||
JwtSecret: jwtSecretBytes,
|
||||
}
|
||||
|
||||
passkeyCtrl := &controller.PasskeyController{
|
||||
Repo: passkeysRepo,
|
||||
@@ -369,21 +358,14 @@ func main() {
|
||||
}
|
||||
|
||||
authMiddleware := middleware.AuthMiddleware{UserAuthRepo: userAuthRepo, Cache: authCache, UserController: userController}
|
||||
collectionLinkMiddleware := middleware.CollectionLinkMiddleware{
|
||||
CollectionLinkRepo: collectionLinkRepo,
|
||||
PublicCollectionCtrl: collectionLinkCtrl,
|
||||
accessTokenMiddleware := middleware.AccessTokenMiddleware{
|
||||
PublicCollectionRepo: publicCollectionRepo,
|
||||
PublicCollectionCtrl: publicCollectionCtrl,
|
||||
CollectionRepo: collectionRepo,
|
||||
Cache: accessTokenCache,
|
||||
BillingCtrl: billingController,
|
||||
DiscordController: discordController,
|
||||
}
|
||||
fileLinkMiddleware := &middleware.FileLinkMiddleware{
|
||||
FileLinkRepo: fileLinkRepo,
|
||||
FileLinkCtrl: fileLinkCtrl,
|
||||
Cache: accessTokenCache,
|
||||
BillingCtrl: billingController,
|
||||
DiscordController: discordController,
|
||||
}
|
||||
|
||||
if environment != "local" {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
@@ -422,9 +404,7 @@ func main() {
|
||||
familiesJwtAuthAPI.Use(rateLimiter.GlobalRateLimiter(), authMiddleware.TokenAuthMiddleware(jwt.FAMILIES.Ptr()), rateLimiter.APIRateLimitForUserMiddleware(urlSanitizer))
|
||||
|
||||
publicCollectionAPI := server.Group("/public-collection")
|
||||
publicCollectionAPI.Use(rateLimiter.GlobalRateLimiter(), collectionLinkMiddleware.Authenticate(urlSanitizer))
|
||||
fileLinkApi := server.GET("/file-link")
|
||||
fileLinkApi.Use(rateLimiter.GlobalRateLimiter(), fileLinkMiddleware.Authenticate(urlSanitizer))
|
||||
publicCollectionAPI.Use(rateLimiter.GlobalRateLimiter(), accessTokenMiddleware.AccessTokenAuthMiddleware(urlSanitizer))
|
||||
|
||||
healthCheckHandler := &api.HealthCheckHandler{
|
||||
DB: db,
|
||||
@@ -452,7 +432,6 @@ func main() {
|
||||
Controller: fileController,
|
||||
FileCopyCtrl: fileCopyCtrl,
|
||||
FileDataCtrl: fileDataCtrl,
|
||||
FileUrlCtrl: fileLinkCtrl,
|
||||
}
|
||||
privateAPI.GET("/files/upload-urls", fileHandler.GetUploadURLs)
|
||||
privateAPI.GET("/files/multipart-upload-urls", fileHandler.GetMultipartUploadURLs)
|
||||
@@ -461,11 +440,6 @@ func main() {
|
||||
privateAPI.GET("/files/preview/:fileID", fileHandler.GetThumbnail)
|
||||
privateAPI.GET("/files/preview/v2/:fileID", fileHandler.GetThumbnail)
|
||||
|
||||
privateAPI.POST("/files/share-url", fileHandler.ShareUrl)
|
||||
privateAPI.PUT("/files/share-url", fileHandler.UpdateFileURL)
|
||||
privateAPI.DELETE("/files/share-url/:fileID", fileHandler.DisableUrl)
|
||||
privateAPI.GET("/files/share-urls/", fileHandler.GetUrls)
|
||||
|
||||
privateAPI.PUT("/files/data", fileHandler.PutFileData)
|
||||
privateAPI.PUT("/files/video-data", fileHandler.PutVideoData)
|
||||
privateAPI.POST("/files/data/status-diff", fileHandler.FileDataStatusDiff)
|
||||
@@ -592,19 +566,13 @@ func main() {
|
||||
privateAPI.PUT("/collections/sharee-magic-metadata", collectionHandler.ShareeMagicMetadataUpdate)
|
||||
|
||||
publicCollectionHandler := &api.PublicCollectionHandler{
|
||||
Controller: collectionLinkCtrl,
|
||||
Controller: publicCollectionCtrl,
|
||||
FileCtrl: fileController,
|
||||
CollectionCtrl: collectionController,
|
||||
FileDataCtrl: fileDataCtrl,
|
||||
StorageBonusController: storageBonusCtrl,
|
||||
}
|
||||
|
||||
fileLinkApi.GET("/info", fileHandler.LinkInfo)
|
||||
fileLinkApi.GET("/pass-info", fileHandler.PasswordInfo)
|
||||
fileLinkApi.GET("/thumbnail", fileHandler.LinkThumbnail)
|
||||
fileLinkApi.GET("/file", fileHandler.LinkFile)
|
||||
fileLinkApi.POST("/verify-password", fileHandler.VerifyPassword)
|
||||
|
||||
publicCollectionAPI.GET("/files/preview/:fileID", publicCollectionHandler.GetThumbnail)
|
||||
publicCollectionAPI.GET("/files/download/:fileID", publicCollectionHandler.GetFile)
|
||||
publicCollectionAPI.GET("/files/data/fetch", publicCollectionHandler.GetFileData)
|
||||
@@ -802,7 +770,7 @@ func main() {
|
||||
setKnownAPIs(server.Routes())
|
||||
setupAndStartBackgroundJobs(objectCleanupController, replicationController3, fileDataCtrl)
|
||||
setupAndStartCrons(
|
||||
userAuthRepo, collectionLinkRepo, fileLinkRepo, twoFactorRepo, passkeysRepo, fileController, taskLockingRepo, emailNotificationCtrl,
|
||||
userAuthRepo, publicCollectionRepo, twoFactorRepo, passkeysRepo, fileController, taskLockingRepo, emailNotificationCtrl,
|
||||
trashController, pushController, objectController, dataCleanupController, storageBonusCtrl, emergencyCtrl,
|
||||
embeddingController, healthCheckHandler, kexCtrl, castDb)
|
||||
|
||||
@@ -931,8 +899,7 @@ func setupAndStartBackgroundJobs(
|
||||
objectCleanupController.StartClearingOrphanObjects()
|
||||
}
|
||||
|
||||
func setupAndStartCrons(userAuthRepo *repo.UserAuthRepository, collectionLinkRepo *public.CollectionLinkRepo,
|
||||
fileLinkRepo *public.FileLinkRepository,
|
||||
func setupAndStartCrons(userAuthRepo *repo.UserAuthRepository, publicCollectionRepo *repo.PublicCollectionRepository,
|
||||
twoFactorRepo *repo.TwoFactorRepository, passkeysRepo *passkey.Repository, fileController *controller.FileController,
|
||||
taskRepo *repo.TaskLockRepository, emailNotificationCtrl *email.EmailNotificationController,
|
||||
trashController *controller.TrashController, pushController *controller.PushController,
|
||||
@@ -958,8 +925,7 @@ func setupAndStartCrons(userAuthRepo *repo.UserAuthRepository, collectionLinkRep
|
||||
schedule(c, "@every 24h", func() {
|
||||
_ = userAuthRepo.RemoveDeletedTokens(timeUtil.MicrosecondsBeforeDays(30))
|
||||
_ = castDb.DeleteOldSessions(context.Background(), timeUtil.MicrosecondsBeforeDays(7))
|
||||
_ = collectionLinkRepo.CleanupAccessHistory(context.Background())
|
||||
_ = fileLinkRepo.CleanupAccessHistory(context.Background())
|
||||
_ = publicCollectionRepo.CleanupAccessHistory(context.Background())
|
||||
})
|
||||
|
||||
schedule(c, "@every 1m", func() {
|
||||
|
||||
@@ -79,14 +79,9 @@ http:
|
||||
apps:
|
||||
# Default is https://albums.ente.io
|
||||
#
|
||||
# If you're running a self hosted instance and wish to serve public links for photos,
|
||||
# If you're running a self hosted instance and wish to serve public links,
|
||||
# set this to the URL where your albums web app is running.
|
||||
public-albums:
|
||||
# Default is https://locker.ente.io
|
||||
#
|
||||
# If you're running a self-hosted instance and wish to serve public links for locker,
|
||||
# set this to the URL where your albums web app is running.
|
||||
public-locker:
|
||||
# Default is https://cast.ente.io
|
||||
cast:
|
||||
# Default is https://accounts.ente.io
|
||||
|
||||
@@ -97,8 +97,8 @@ var ErrUserDeleted = errors.New("user account has been deleted")
|
||||
// ErrLockUnavailable is thrown when a lock could not be acquired
|
||||
var ErrLockUnavailable = errors.New("could not acquire lock")
|
||||
|
||||
// ErrActiveLinkAlreadyExists is thrown when an active link already exists for entity
|
||||
var ErrActiveLinkAlreadyExists = errors.New("link already exists for this entity")
|
||||
// ErrActiveLinkAlreadyExists is thrown when the collection already has active public link
|
||||
var ErrActiveLinkAlreadyExists = errors.New("Collection already has active public link")
|
||||
|
||||
// ErrNotImplemented indicates that the action that we tried to perform is not
|
||||
// available at this museum instance. e.g. this could be something that is not
|
||||
@@ -176,11 +176,6 @@ var ErrMaxPasskeysReached = ApiError{
|
||||
Message: "Max passkeys limit reached",
|
||||
HttpStatusCode: http.StatusConflict,
|
||||
}
|
||||
var ErrPassProtectedResource = ApiError{
|
||||
Code: "PASS_PROTECTED_RESOURCE",
|
||||
Message: "This resource is password protected",
|
||||
HttpStatusCode: http.StatusForbidden,
|
||||
}
|
||||
|
||||
var ErrCastPermissionDenied = ApiError{
|
||||
Code: "CAST_PERMISSION_DENIED",
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
package ente
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/ente-io/museum/pkg/utils/time"
|
||||
)
|
||||
|
||||
// CreateFileUrl represents an encrypted file in the system
|
||||
type CreateFileUrl struct {
|
||||
FileID int64 `json:"fileID" binding:"required"`
|
||||
App App `json:"app" binding:"required"`
|
||||
}
|
||||
|
||||
// UpdateFileUrl ..
|
||||
type UpdateFileUrl struct {
|
||||
LinkID string `json:"linkID" binding:"required"`
|
||||
FileID int64 `json:"fileID" binding:"required"`
|
||||
ValidTill *int64 `json:"validTill"`
|
||||
DeviceLimit *int `json:"deviceLimit"`
|
||||
PassHash *string
|
||||
Nonce *string
|
||||
MemLimit *int64
|
||||
OpsLimit *int64
|
||||
EnableDownload *bool `json:"enableDownload"`
|
||||
DisablePassword *bool `json:"disablePassword"`
|
||||
}
|
||||
|
||||
func (ut *UpdateFileUrl) Validate() error {
|
||||
if ut.DeviceLimit == nil && ut.ValidTill == nil && ut.DisablePassword == nil &&
|
||||
ut.Nonce == nil && ut.PassHash == nil && ut.EnableDownload == nil {
|
||||
return NewBadRequestWithMessage("all parameters are missing")
|
||||
}
|
||||
|
||||
if ut.DeviceLimit != nil && (*ut.DeviceLimit < 0 || *ut.DeviceLimit > 50) {
|
||||
return NewBadRequestWithMessage(fmt.Sprintf("device limit: %d out of range [0-50]", *ut.DeviceLimit))
|
||||
}
|
||||
|
||||
if ut.ValidTill != nil && *ut.ValidTill != 0 && *ut.ValidTill < time.Microseconds() {
|
||||
return NewBadRequestWithMessage("valid till should be greater than current timestamp")
|
||||
}
|
||||
|
||||
var allPassParamsMissing = ut.Nonce == nil && ut.PassHash == nil && ut.MemLimit == nil && ut.OpsLimit == nil
|
||||
var allPassParamsPresent = ut.Nonce != nil && ut.PassHash != nil && ut.MemLimit != nil && ut.OpsLimit != nil
|
||||
|
||||
if !(allPassParamsMissing || allPassParamsPresent) {
|
||||
return NewBadRequestWithMessage("all password params should be either present or missing")
|
||||
}
|
||||
|
||||
if allPassParamsPresent && ut.DisablePassword != nil && *ut.DisablePassword {
|
||||
return NewBadRequestWithMessage("can not set and disable password in same request")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type FileLinkRow struct {
|
||||
LinkID string
|
||||
OwnerID int64
|
||||
FileID int64
|
||||
Token string
|
||||
DeviceLimit int
|
||||
ValidTill int64
|
||||
IsDisabled bool
|
||||
PassHash *string
|
||||
Nonce *string
|
||||
MemLimit *int64
|
||||
OpsLimit *int64
|
||||
EnableDownload bool
|
||||
CreatedAt int64
|
||||
UpdatedAt int64
|
||||
}
|
||||
|
||||
type FileUrl struct {
|
||||
LinkID string `json:"linkID" binding:"required"`
|
||||
URL string `json:"url" binding:"required"`
|
||||
OwnerID int64 `json:"ownerID" binding:"required"`
|
||||
FileID int64 `json:"fileID" binding:"required"`
|
||||
ValidTill int64 `json:"validTill"`
|
||||
DeviceLimit int `json:"deviceLimit"`
|
||||
PasswordEnabled bool `json:"passwordEnabled"`
|
||||
// Nonce contains the nonce value for the password if the link is password protected.
|
||||
Nonce *string `json:"nonce,omitempty"`
|
||||
MemLimit *int64 `json:"memLimit,omitempty"`
|
||||
OpsLimit *int64 `json:"opsLimit,omitempty"`
|
||||
EnableDownload bool `json:"enableDownload"`
|
||||
CreatedAt int64 `json:"createdAt"`
|
||||
}
|
||||
|
||||
type FileLinkAccessContext struct {
|
||||
LinkID string
|
||||
IP string
|
||||
UserAgent string
|
||||
FileID int64
|
||||
OwnerID int64
|
||||
}
|
||||
@@ -40,13 +40,13 @@ func (w WebCommonJWTClaim) Valid() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// LinkPasswordClaim refer to token granted post link password verification
|
||||
type LinkPasswordClaim struct {
|
||||
// PublicAlbumPasswordClaim refer to token granted post public album password verification
|
||||
type PublicAlbumPasswordClaim struct {
|
||||
PassHash string `json:"passKey"`
|
||||
ExpiryTime int64 `json:"expiryTime"`
|
||||
}
|
||||
|
||||
func (c LinkPasswordClaim) Valid() error {
|
||||
func (c PublicAlbumPasswordClaim) Valid() error {
|
||||
if c.ExpiryTime < time.Microseconds() {
|
||||
return errors.New("token expired")
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package ente
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/ente-io/museum/pkg/utils/time"
|
||||
"github.com/ente-io/stacktrace"
|
||||
)
|
||||
@@ -32,33 +32,6 @@ type UpdatePublicAccessTokenRequest struct {
|
||||
EnableJoin *bool `json:"enableJoin"`
|
||||
}
|
||||
|
||||
func (ut *UpdatePublicAccessTokenRequest) Validate() error {
|
||||
if ut.DeviceLimit == nil && ut.ValidTill == nil && ut.DisablePassword == nil &&
|
||||
ut.Nonce == nil && ut.PassHash == nil && ut.EnableDownload == nil && ut.EnableCollect == nil {
|
||||
return NewBadRequestWithMessage("all parameters are missing")
|
||||
}
|
||||
|
||||
if ut.DeviceLimit != nil && (*ut.DeviceLimit < 0 || *ut.DeviceLimit > 50) {
|
||||
return NewBadRequestWithMessage(fmt.Sprintf("device limit: %d out of range [0-50]", *ut.DeviceLimit))
|
||||
}
|
||||
|
||||
if ut.ValidTill != nil && *ut.ValidTill != 0 && *ut.ValidTill < time.Microseconds() {
|
||||
return NewBadRequestWithMessage("valid till should be greater than current timestamp")
|
||||
}
|
||||
|
||||
var allPassParamsMissing = ut.Nonce == nil && ut.PassHash == nil && ut.MemLimit == nil && ut.OpsLimit == nil
|
||||
var allPassParamsPresent = ut.Nonce != nil && ut.PassHash != nil && ut.MemLimit != nil && ut.OpsLimit != nil
|
||||
|
||||
if !(allPassParamsMissing || allPassParamsPresent) {
|
||||
return NewBadRequestWithMessage("all password params should be either present or missing")
|
||||
}
|
||||
|
||||
if allPassParamsPresent && ut.DisablePassword != nil && *ut.DisablePassword {
|
||||
return NewBadRequestWithMessage("can not set and disable password in same request")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type VerifyPasswordRequest struct {
|
||||
PassHash string `json:"passHash" binding:"required"`
|
||||
}
|
||||
@@ -67,8 +40,8 @@ type VerifyPasswordResponse struct {
|
||||
JWTToken string `json:"jwtToken"`
|
||||
}
|
||||
|
||||
// CollectionLinkRow represents row entity for public_collection_token table
|
||||
type CollectionLinkRow struct {
|
||||
// PublicCollectionToken represents row entity for public_collection_token table
|
||||
type PublicCollectionToken struct {
|
||||
ID int64
|
||||
CollectionID int64
|
||||
Token string
|
||||
@@ -84,7 +57,7 @@ type CollectionLinkRow struct {
|
||||
EnableJoin bool
|
||||
}
|
||||
|
||||
func (p CollectionLinkRow) CanJoin() error {
|
||||
func (p PublicCollectionToken) CanJoin() error {
|
||||
if p.IsDisabled {
|
||||
return NewBadRequestWithMessage("link disabled")
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
|
||||
DROP TABLE IF EXISTS public_file_tokens_access_history;
|
||||
DROP TABLE IF EXISTS public_file_tokens;
|
||||
@@ -1,46 +0,0 @@
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public_file_tokens
|
||||
(
|
||||
id text primary key,
|
||||
file_id bigint NOT NULL,
|
||||
owner_id bigint NOT NULL,
|
||||
app text NOT NULL,
|
||||
access_token text not null,
|
||||
valid_till bigint not null DEFAULT 0,
|
||||
device_limit int not null DEFAULT 0,
|
||||
is_disabled bool not null DEFAULT FALSE,
|
||||
enable_download bool not null DEFAULT TRUE,
|
||||
pw_hash TEXT,
|
||||
pw_nonce TEXT,
|
||||
mem_limit BIGINT,
|
||||
ops_limit BIGINT,
|
||||
created_at bigint NOT NULL DEFAULT now_utc_micro_seconds(),
|
||||
updated_at bigint NOT NULL DEFAULT now_utc_micro_seconds()
|
||||
);
|
||||
|
||||
|
||||
CREATE OR REPLACE TRIGGER update_public_file_tokens_updated_at
|
||||
BEFORE UPDATE
|
||||
ON public_file_tokens
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE
|
||||
trigger_updated_at_microseconds_column();
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public_file_tokens_access_history
|
||||
(
|
||||
id text NOT NULL,
|
||||
ip text not null,
|
||||
user_agent text not null,
|
||||
created_at bigint NOT NULL DEFAULT now_utc_micro_seconds(),
|
||||
CONSTRAINT unique_access_id_ip_ua UNIQUE (id, ip, user_agent),
|
||||
CONSTRAINT fk_public_file_history_token_id
|
||||
FOREIGN KEY (id)
|
||||
REFERENCES public_file_tokens (id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS public_file_token_unique_idx ON public_file_tokens (access_token) WHERE is_disabled = FALSE;
|
||||
CREATE INDEX IF NOT EXISTS public_file_tokens_owner_id_updated_at_idx ON public_file_tokens (owner_id, updated_at);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS public_active_file_link_unique_idx ON public_file_tokens (file_id, is_disabled) WHERE is_disabled = FALSE;
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/ente-io/museum/ente"
|
||||
"github.com/ente-io/museum/pkg/controller"
|
||||
"github.com/ente-io/museum/pkg/utils/auth"
|
||||
"github.com/ente-io/museum/pkg/utils/handler"
|
||||
"github.com/ente-io/museum/pkg/utils/time"
|
||||
@@ -171,6 +172,35 @@ func (h *CollectionHandler) UpdateShareURL(c *gin.Context) {
|
||||
handler.Error(c, stacktrace.Propagate(err, ""))
|
||||
return
|
||||
}
|
||||
if req.DeviceLimit == nil && req.ValidTill == nil && req.DisablePassword == nil &&
|
||||
req.Nonce == nil && req.PassHash == nil && req.EnableDownload == nil && req.EnableCollect == nil {
|
||||
handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, "all parameters are missing"))
|
||||
return
|
||||
}
|
||||
|
||||
if req.DeviceLimit != nil && (*req.DeviceLimit < 0 || *req.DeviceLimit > controller.DeviceLimitThreshold) {
|
||||
handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("device limit: %d out of range", *req.DeviceLimit)))
|
||||
return
|
||||
}
|
||||
|
||||
if req.ValidTill != nil && *req.ValidTill != 0 && *req.ValidTill < time.Microseconds() {
|
||||
handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, "valid till should be greater than current timestamp"))
|
||||
return
|
||||
}
|
||||
|
||||
var allPassParamsMissing = req.Nonce == nil && req.PassHash == nil && req.MemLimit == nil && req.OpsLimit == nil
|
||||
var allPassParamsPresent = req.Nonce != nil && req.PassHash != nil && req.MemLimit != nil && req.OpsLimit != nil
|
||||
|
||||
if !(allPassParamsMissing || allPassParamsPresent) {
|
||||
handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, "all password params should be either present or missing"))
|
||||
return
|
||||
}
|
||||
|
||||
if allPassParamsPresent && req.DisablePassword != nil && *req.DisablePassword {
|
||||
handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, "can not set and disable password in same request"))
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.Controller.UpdateShareURL(c, auth.GetUserID(c.Request.Header), req)
|
||||
if err != nil {
|
||||
handler.Error(c, stacktrace.Propagate(err, ""))
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
"github.com/ente-io/museum/pkg/controller/file_copy"
|
||||
"github.com/ente-io/museum/pkg/controller/filedata"
|
||||
"github.com/ente-io/museum/pkg/controller/public"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
@@ -25,7 +24,6 @@ import (
|
||||
// FileHandler exposes request handlers for all encrypted file related requests
|
||||
type FileHandler struct {
|
||||
Controller *controller.FileController
|
||||
FileUrlCtrl *public.FileLinkController
|
||||
FileCopyCtrl *file_copy.FileCopyController
|
||||
FileDataCtrl *filedata.Controller
|
||||
}
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/ente-io/museum/ente"
|
||||
"github.com/ente-io/museum/pkg/utils/auth"
|
||||
"github.com/ente-io/museum/pkg/utils/handler"
|
||||
"github.com/ente-io/stacktrace"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// ShareUrl a sharable url for the file
|
||||
func (h *FileHandler) ShareUrl(c *gin.Context) {
|
||||
var file ente.CreateFileUrl
|
||||
if err := c.ShouldBindJSON(&file); err != nil {
|
||||
handler.Error(c, stacktrace.Propagate(err, ""))
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.FileUrlCtrl.CreateLink(c, file)
|
||||
if err != nil {
|
||||
handler.Error(c, stacktrace.Propagate(err, ""))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
func (h *FileHandler) LinkInfo(c *gin.Context) {
|
||||
resp, err := h.FileUrlCtrl.Info(c)
|
||||
if err != nil {
|
||||
handler.Error(c, stacktrace.Propagate(err, ""))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"file": resp,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *FileHandler) PasswordInfo(c *gin.Context) {
|
||||
resp, err := h.FileUrlCtrl.PassInfo(c)
|
||||
if err != nil {
|
||||
handler.Error(c, stacktrace.Propagate(err, ""))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"nonce": resp.Nonce,
|
||||
"opsLimit": resp.OpsLimit,
|
||||
"memLimit": resp.MemLimit,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *FileHandler) LinkThumbnail(c *gin.Context) {
|
||||
linkCtx := auth.MustGetFileLinkAccessContext(c)
|
||||
url, err := h.Controller.GetThumbnailURL(c, linkCtx.OwnerID, linkCtx.FileID)
|
||||
if err != nil {
|
||||
handler.Error(c, stacktrace.Propagate(err, ""))
|
||||
return
|
||||
}
|
||||
c.Redirect(http.StatusTemporaryRedirect, url)
|
||||
}
|
||||
|
||||
func (h *FileHandler) LinkFile(c *gin.Context) {
|
||||
linkCtx := auth.MustGetFileLinkAccessContext(c)
|
||||
url, err := h.Controller.GetFileURL(c, linkCtx.OwnerID, linkCtx.FileID)
|
||||
if err != nil {
|
||||
handler.Error(c, stacktrace.Propagate(err, ""))
|
||||
return
|
||||
}
|
||||
c.Redirect(http.StatusTemporaryRedirect, url)
|
||||
}
|
||||
|
||||
func (h *FileHandler) DisableUrl(c *gin.Context) {
|
||||
cID, err := strconv.ParseInt(c.Param("fileID"), 10, 64)
|
||||
if err != nil {
|
||||
handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, ""))
|
||||
return
|
||||
}
|
||||
err = h.FileUrlCtrl.Disable(c, cID)
|
||||
if err != nil {
|
||||
handler.Error(c, stacktrace.Propagate(err, ""))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{})
|
||||
}
|
||||
|
||||
func (h *FileHandler) GetUrls(c *gin.Context) {
|
||||
sinceTime, err := strconv.ParseInt(c.Query("sinceTime"), 10, 64)
|
||||
if err != nil {
|
||||
handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, "sinceTime parsing failed"))
|
||||
return
|
||||
}
|
||||
limit := 500
|
||||
if c.Query("limit") != "" {
|
||||
limit, err = strconv.Atoi(c.Query("limit"))
|
||||
if err != nil || limit < 1 {
|
||||
handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, ""))
|
||||
return
|
||||
}
|
||||
}
|
||||
response, err := h.FileUrlCtrl.GetUrls(c, sinceTime, int64(limit))
|
||||
if err != nil {
|
||||
handler.Error(c, stacktrace.Propagate(err, ""))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"diff": response,
|
||||
})
|
||||
}
|
||||
|
||||
// VerifyPassword verifies the password for given link access token and return signed jwt token if it's valid
|
||||
func (h *FileHandler) VerifyPassword(c *gin.Context) {
|
||||
var req ente.VerifyPasswordRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
handler.Error(c, stacktrace.Propagate(err, ""))
|
||||
return
|
||||
}
|
||||
resp, err := h.FileUrlCtrl.VerifyPassword(c, req)
|
||||
if err != nil {
|
||||
handler.Error(c, stacktrace.Propagate(err, ""))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// UpdateFileURL updates the share URL for a file
|
||||
func (h *FileHandler) UpdateFileURL(c *gin.Context) {
|
||||
var req ente.UpdateFileUrl
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
handler.Error(c, stacktrace.Propagate(err, ""))
|
||||
return
|
||||
}
|
||||
response, err := h.FileUrlCtrl.UpdateSharedUrl(c, req)
|
||||
if err != nil {
|
||||
handler.Error(c, stacktrace.Propagate(err, ""))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"result": response,
|
||||
})
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
fileData "github.com/ente-io/museum/ente/filedata"
|
||||
"github.com/ente-io/museum/pkg/controller/collections"
|
||||
"github.com/ente-io/museum/pkg/controller/filedata"
|
||||
"github.com/ente-io/museum/pkg/controller/public"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
@@ -21,7 +20,7 @@ import (
|
||||
|
||||
// PublicCollectionHandler exposes request handlers for publicly accessible collections
|
||||
type PublicCollectionHandler struct {
|
||||
Controller *public.CollectionLinkController
|
||||
Controller *controller.PublicCollectionController
|
||||
FileCtrl *controller.FileController
|
||||
CollectionCtrl *collections.CollectionController
|
||||
FileDataCtrl *filedata.Controller
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"github.com/ente-io/museum/pkg/controller"
|
||||
"github.com/ente-io/museum/pkg/controller/access"
|
||||
"github.com/ente-io/museum/pkg/controller/email"
|
||||
"github.com/ente-io/museum/pkg/controller/public"
|
||||
"github.com/ente-io/museum/pkg/repo/cast"
|
||||
"github.com/ente-io/museum/pkg/utils/array"
|
||||
"github.com/ente-io/museum/pkg/utils/auth"
|
||||
@@ -25,16 +24,16 @@ const (
|
||||
|
||||
// CollectionController encapsulates logic that deals with collections
|
||||
type CollectionController struct {
|
||||
CollectionLinkCtrl *public.CollectionLinkController
|
||||
EmailCtrl *email.EmailNotificationController
|
||||
AccessCtrl access.Controller
|
||||
BillingCtrl *controller.BillingController
|
||||
CollectionRepo *repo.CollectionRepository
|
||||
UserRepo *repo.UserRepository
|
||||
FileRepo *repo.FileRepository
|
||||
QueueRepo *repo.QueueRepository
|
||||
CastRepo *cast.Repository
|
||||
TaskRepo *repo.TaskLockRepository
|
||||
PublicCollectionCtrl *controller.PublicCollectionController
|
||||
EmailCtrl *email.EmailNotificationController
|
||||
AccessCtrl access.Controller
|
||||
BillingCtrl *controller.BillingController
|
||||
CollectionRepo *repo.CollectionRepository
|
||||
UserRepo *repo.UserRepository
|
||||
FileRepo *repo.FileRepository
|
||||
QueueRepo *repo.QueueRepository
|
||||
CastRepo *cast.Repository
|
||||
TaskRepo *repo.TaskLockRepository
|
||||
}
|
||||
|
||||
// Create creates a collection
|
||||
@@ -149,7 +148,7 @@ func (c *CollectionController) TrashV3(ctx *gin.Context, req ente.TrashCollectio
|
||||
}
|
||||
|
||||
}
|
||||
err = c.CollectionLinkCtrl.Disable(ctx, cID)
|
||||
err = c.PublicCollectionCtrl.Disable(ctx, cID)
|
||||
if err != nil {
|
||||
return stacktrace.Propagate(err, "failed to disabled public share url")
|
||||
}
|
||||
@@ -210,7 +209,7 @@ func (c *CollectionController) HandleAccountDeletion(ctx context.Context, userID
|
||||
if err != nil {
|
||||
return stacktrace.Propagate(err, "failed to revoke cast token for user")
|
||||
}
|
||||
err = c.CollectionLinkCtrl.HandleAccountDeletion(ctx, userID, logger)
|
||||
err = c.PublicCollectionCtrl.HandleAccountDeletion(ctx, userID, logger)
|
||||
return stacktrace.Propagate(err, "")
|
||||
}
|
||||
|
||||
|
||||
@@ -70,21 +70,21 @@ func (c *CollectionController) JoinViaLink(ctx *gin.Context, req ente.JoinCollec
|
||||
if !collection.AllowSharing() {
|
||||
return stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("joining %s is not allowed", collection.Type))
|
||||
}
|
||||
collectionLinkToken, err := c.CollectionLinkCtrl.GetActiveCollectionLinkToken(ctx, req.CollectionID)
|
||||
publicCollectionToken, err := c.PublicCollectionCtrl.GetActivePublicCollectionToken(ctx, req.CollectionID)
|
||||
if err != nil {
|
||||
return stacktrace.Propagate(err, "")
|
||||
}
|
||||
|
||||
if canJoin := collectionLinkToken.CanJoin(); canJoin != nil {
|
||||
if canJoin := publicCollectionToken.CanJoin(); canJoin != nil {
|
||||
return stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("can not join collection: %s", canJoin.Error()))
|
||||
}
|
||||
accessToken := auth.GetAccessToken(ctx)
|
||||
if collectionLinkToken.Token != accessToken {
|
||||
if publicCollectionToken.Token != accessToken {
|
||||
return stacktrace.Propagate(ente.ErrPermissionDenied, "token doesn't match collection")
|
||||
}
|
||||
if collectionLinkToken.PassHash != nil && *collectionLinkToken.PassHash != "" {
|
||||
if publicCollectionToken.PassHash != nil && *publicCollectionToken.PassHash != "" {
|
||||
accessTokenJWT := auth.GetAccessTokenJWT(ctx)
|
||||
if passCheckErr := c.CollectionLinkCtrl.ValidateJWTToken(ctx, accessTokenJWT, *collectionLinkToken.PassHash); passCheckErr != nil {
|
||||
if passCheckErr := c.PublicCollectionCtrl.ValidateJWTToken(ctx, accessTokenJWT, *publicCollectionToken.PassHash); passCheckErr != nil {
|
||||
return stacktrace.Propagate(passCheckErr, "")
|
||||
}
|
||||
}
|
||||
@@ -93,7 +93,7 @@ func (c *CollectionController) JoinViaLink(ctx *gin.Context, req ente.JoinCollec
|
||||
return stacktrace.Propagate(err, "")
|
||||
}
|
||||
role := ente.VIEWER
|
||||
if collectionLinkToken.EnableCollect {
|
||||
if publicCollectionToken.EnableCollect {
|
||||
role = ente.COLLABORATOR
|
||||
}
|
||||
joinErr := c.CollectionRepo.Share(req.CollectionID, collection.Owner.ID, userID, req.EncryptedKey, role, time.Microseconds())
|
||||
@@ -197,7 +197,7 @@ func (c *CollectionController) ShareURL(ctx context.Context, userID int64, req e
|
||||
if err != nil {
|
||||
return ente.PublicURL{}, stacktrace.Propagate(err, "")
|
||||
}
|
||||
response, err := c.CollectionLinkCtrl.CreateLink(ctx, req)
|
||||
response, err := c.PublicCollectionCtrl.CreateAccessToken(ctx, req)
|
||||
if err != nil {
|
||||
return ente.PublicURL{}, stacktrace.Propagate(err, "")
|
||||
}
|
||||
@@ -205,26 +205,20 @@ func (c *CollectionController) ShareURL(ctx context.Context, userID int64, req e
|
||||
}
|
||||
|
||||
// UpdateShareURL updates the shared url configuration
|
||||
func (c *CollectionController) UpdateShareURL(
|
||||
ctx context.Context,
|
||||
userID int64,
|
||||
req ente.UpdatePublicAccessTokenRequest,
|
||||
) (*ente.PublicURL, error) {
|
||||
if err := req.Validate(); err != nil {
|
||||
return nil, stacktrace.Propagate(err, "")
|
||||
}
|
||||
func (c *CollectionController) UpdateShareURL(ctx context.Context, userID int64, req ente.UpdatePublicAccessTokenRequest) (
|
||||
ente.PublicURL, error) {
|
||||
if err := c.verifyOwnership(req.CollectionID, userID); err != nil {
|
||||
return nil, stacktrace.Propagate(err, "")
|
||||
return ente.PublicURL{}, stacktrace.Propagate(err, "")
|
||||
}
|
||||
err := c.BillingCtrl.HasActiveSelfOrFamilySubscription(userID, true)
|
||||
if err != nil {
|
||||
return nil, stacktrace.Propagate(err, "")
|
||||
return ente.PublicURL{}, stacktrace.Propagate(err, "")
|
||||
}
|
||||
response, err := c.CollectionLinkCtrl.UpdateSharedUrl(ctx, req)
|
||||
response, err := c.PublicCollectionCtrl.UpdateSharedUrl(ctx, req)
|
||||
if err != nil {
|
||||
return nil, stacktrace.Propagate(err, "")
|
||||
return ente.PublicURL{}, stacktrace.Propagate(err, "")
|
||||
}
|
||||
return &response, nil
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// DisableSharedURL disable a public auth-token for the given collectionID
|
||||
@@ -232,7 +226,7 @@ func (c *CollectionController) DisableSharedURL(ctx context.Context, userID int6
|
||||
if err := c.verifyOwnership(cID, userID); err != nil {
|
||||
return stacktrace.Propagate(err, "")
|
||||
}
|
||||
err := c.CollectionLinkCtrl.Disable(ctx, cID)
|
||||
err := c.PublicCollectionCtrl.Disable(ctx, cID)
|
||||
return stacktrace.Propagate(err, "")
|
||||
}
|
||||
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
package public
|
||||
|
||||
import (
|
||||
"github.com/ente-io/museum/ente"
|
||||
"github.com/ente-io/museum/pkg/controller"
|
||||
"github.com/ente-io/museum/pkg/repo"
|
||||
"github.com/ente-io/museum/pkg/repo/public"
|
||||
"github.com/ente-io/museum/pkg/utils/auth"
|
||||
"github.com/ente-io/stacktrace"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/lithammer/shortuuid/v3"
|
||||
)
|
||||
|
||||
// FileLinkController controls share collection operations
|
||||
type FileLinkController struct {
|
||||
FileController *controller.FileController
|
||||
FileLinkRepo *public.FileLinkRepository
|
||||
FileRepo *repo.FileRepository
|
||||
JwtSecret []byte
|
||||
}
|
||||
|
||||
func (c *FileLinkController) CreateLink(ctx *gin.Context, req ente.CreateFileUrl) (*ente.FileUrl, error) {
|
||||
actorUserID := auth.GetUserID(ctx.Request.Header)
|
||||
app := auth.GetApp(ctx)
|
||||
if req.App != app {
|
||||
return nil, stacktrace.Propagate(ente.NewBadRequestWithMessage("app mismatch"), "app mismatch")
|
||||
}
|
||||
file, err := c.FileRepo.GetFileAttributes(req.FileID)
|
||||
if err != nil {
|
||||
return nil, stacktrace.Propagate(err, "failed to get file attributes")
|
||||
}
|
||||
if actorUserID != file.OwnerID {
|
||||
return nil, stacktrace.Propagate(ente.NewPermissionDeniedError("not file owner"), "")
|
||||
}
|
||||
accessToken := shortuuid.New()[0:AccessTokenLength]
|
||||
_, err = c.FileLinkRepo.Insert(ctx, req.FileID, actorUserID, accessToken, app)
|
||||
if err == nil || err == ente.ErrActiveLinkAlreadyExists {
|
||||
row, rowErr := c.FileLinkRepo.GetFileUrlRowByFileID(ctx, req.FileID)
|
||||
if rowErr != nil {
|
||||
return nil, stacktrace.Propagate(rowErr, "failed to get active file url token")
|
||||
}
|
||||
return c.mapRowToFileUrl(ctx, row), nil
|
||||
}
|
||||
return nil, stacktrace.Propagate(err, "failed to create public file link")
|
||||
}
|
||||
|
||||
// Disable all public accessTokens generated for the given fileID till date.
|
||||
func (c *FileLinkController) Disable(ctx *gin.Context, fileID int64) error {
|
||||
userID := auth.GetUserID(ctx.Request.Header)
|
||||
file, err := c.FileRepo.GetFileAttributes(fileID)
|
||||
if err != nil {
|
||||
return stacktrace.Propagate(err, "failed to get file attributes")
|
||||
}
|
||||
if userID != file.OwnerID {
|
||||
return stacktrace.Propagate(ente.NewPermissionDeniedError("not file owner"), "")
|
||||
}
|
||||
return c.FileLinkRepo.DisableLinkForFiles(ctx, []int64{fileID})
|
||||
}
|
||||
|
||||
func (c *FileLinkController) GetUrls(ctx *gin.Context, sinceTime int64, limit int64) ([]*ente.FileUrl, error) {
|
||||
userID := auth.GetUserID(ctx.Request.Header)
|
||||
app := auth.GetApp(ctx)
|
||||
fileLinks, err := c.FileLinkRepo.GetFileUrls(ctx, userID, sinceTime, limit, app)
|
||||
if err != nil {
|
||||
return nil, stacktrace.Propagate(err, "failed to get file urls")
|
||||
}
|
||||
var fileUrls []*ente.FileUrl
|
||||
for _, row := range fileLinks {
|
||||
fileUrls = append(fileUrls, c.mapRowToFileUrl(ctx, row))
|
||||
}
|
||||
return fileUrls, nil
|
||||
}
|
||||
|
||||
func (c *FileLinkController) UpdateSharedUrl(ctx *gin.Context, req ente.UpdateFileUrl) (*ente.FileUrl, error) {
|
||||
if err := req.Validate(); err != nil {
|
||||
return nil, stacktrace.Propagate(err, "invalid request")
|
||||
}
|
||||
fileLinkRow, err := c.FileLinkRepo.GetActiveFileUrlToken(ctx, req.FileID)
|
||||
if err != nil {
|
||||
return nil, stacktrace.Propagate(err, "failed to get file link info")
|
||||
}
|
||||
if fileLinkRow.OwnerID != auth.GetUserID(ctx.Request.Header) {
|
||||
return nil, stacktrace.Propagate(ente.NewPermissionDeniedError("not file owner"), "")
|
||||
}
|
||||
if req.ValidTill != nil {
|
||||
fileLinkRow.ValidTill = *req.ValidTill
|
||||
}
|
||||
if req.DeviceLimit != nil {
|
||||
fileLinkRow.DeviceLimit = *req.DeviceLimit
|
||||
}
|
||||
if req.PassHash != nil && req.Nonce != nil && req.OpsLimit != nil && req.MemLimit != nil {
|
||||
fileLinkRow.PassHash = req.PassHash
|
||||
fileLinkRow.Nonce = req.Nonce
|
||||
fileLinkRow.OpsLimit = req.OpsLimit
|
||||
fileLinkRow.MemLimit = req.MemLimit
|
||||
} else if req.DisablePassword != nil && *req.DisablePassword {
|
||||
fileLinkRow.PassHash = nil
|
||||
fileLinkRow.Nonce = nil
|
||||
fileLinkRow.OpsLimit = nil
|
||||
fileLinkRow.MemLimit = nil
|
||||
}
|
||||
if req.EnableDownload != nil {
|
||||
fileLinkRow.EnableDownload = *req.EnableDownload
|
||||
}
|
||||
|
||||
err = c.FileLinkRepo.UpdateLink(ctx, *fileLinkRow)
|
||||
if err != nil {
|
||||
return nil, stacktrace.Propagate(err, "")
|
||||
}
|
||||
return c.mapRowToFileUrl(ctx, fileLinkRow), nil
|
||||
}
|
||||
|
||||
func (c *FileLinkController) Info(ctx *gin.Context) (*ente.File, error) {
|
||||
accessContext := auth.MustGetFileLinkAccessContext(ctx)
|
||||
return c.FileRepo.GetFileAttributes(accessContext.FileID)
|
||||
}
|
||||
|
||||
func (c *FileLinkController) PassInfo(ctx *gin.Context) (*ente.FileLinkRow, error) {
|
||||
accessContext := auth.MustGetFileLinkAccessContext(ctx)
|
||||
return c.FileLinkRepo.GetFileUrlRowByFileID(ctx, accessContext.FileID)
|
||||
}
|
||||
|
||||
// VerifyPassword verifies if the user has provided correct pw hash. If yes, it returns a signed jwt token which can be
|
||||
// used by the client to pass in other requests for public collection.
|
||||
// Having a separate endpoint for password validation allows us to easily rate-limit the attempts for brute-force
|
||||
// attack for guessing password.
|
||||
func (c *FileLinkController) VerifyPassword(ctx *gin.Context, req ente.VerifyPasswordRequest) (*ente.VerifyPasswordResponse, error) {
|
||||
accessContext := auth.MustGetFileLinkAccessContext(ctx)
|
||||
collectionLinkRow, err := c.FileLinkRepo.GetActiveFileUrlToken(ctx, accessContext.FileID)
|
||||
if err != nil {
|
||||
return nil, stacktrace.Propagate(err, "failed to get public collection info")
|
||||
}
|
||||
return verifyPassword(c.JwtSecret, collectionLinkRow.PassHash, req)
|
||||
}
|
||||
|
||||
func (c *FileLinkController) ValidateJWTToken(ctx *gin.Context, jwtToken string, passwordHash string) error {
|
||||
return validateJWTToken(c.JwtSecret, jwtToken, passwordHash)
|
||||
}
|
||||
|
||||
func (c *FileLinkController) mapRowToFileUrl(ctx *gin.Context, row *ente.FileLinkRow) *ente.FileUrl {
|
||||
app := auth.GetApp(ctx)
|
||||
var url string
|
||||
if app == ente.Locker {
|
||||
url = c.FileLinkRepo.LockerFileLink(row.Token)
|
||||
} else {
|
||||
url = c.FileLinkRepo.PhotoLink(row.Token)
|
||||
}
|
||||
return &ente.FileUrl{
|
||||
LinkID: row.LinkID,
|
||||
FileID: row.FileID,
|
||||
URL: url,
|
||||
OwnerID: row.OwnerID,
|
||||
ValidTill: row.ValidTill,
|
||||
DeviceLimit: row.DeviceLimit,
|
||||
PasswordEnabled: row.PassHash != nil,
|
||||
Nonce: row.Nonce,
|
||||
OpsLimit: row.OpsLimit,
|
||||
MemLimit: row.MemLimit,
|
||||
EnableDownload: row.EnableDownload,
|
||||
CreatedAt: row.CreatedAt,
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package public
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/ente-io/museum/ente"
|
||||
enteJWT "github.com/ente-io/museum/ente/jwt"
|
||||
"github.com/ente-io/museum/pkg/utils/time"
|
||||
"github.com/ente-io/stacktrace"
|
||||
"github.com/golang-jwt/jwt"
|
||||
)
|
||||
|
||||
func validateJWTToken(secret []byte, jwtToken string, passwordHash string) error {
|
||||
token, err := jwt.ParseWithClaims(jwtToken, &enteJWT.LinkPasswordClaim{}, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return stacktrace.Propagate(fmt.Errorf("unexpected signing method: %v", token.Header["alg"]), ""), nil
|
||||
}
|
||||
return secret, nil
|
||||
})
|
||||
if err != nil {
|
||||
return stacktrace.Propagate(err, "JWT parsed failed")
|
||||
}
|
||||
claims, ok := token.Claims.(*enteJWT.LinkPasswordClaim)
|
||||
|
||||
if !ok {
|
||||
return stacktrace.Propagate(errors.New("no claim in jwt token"), "")
|
||||
}
|
||||
if token.Valid && claims.PassHash == passwordHash {
|
||||
return nil
|
||||
}
|
||||
return ente.ErrInvalidPassword
|
||||
}
|
||||
|
||||
func verifyPassword(secret []byte, expectedPassHash *string, req ente.VerifyPasswordRequest) (*ente.VerifyPasswordResponse, error) {
|
||||
if expectedPassHash == nil || *expectedPassHash == "" {
|
||||
return nil, stacktrace.Propagate(ente.ErrBadRequest, "password is not configured for the link")
|
||||
}
|
||||
if req.PassHash != *expectedPassHash {
|
||||
return nil, stacktrace.Propagate(ente.ErrInvalidPassword, "incorrect password for link")
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, &enteJWT.LinkPasswordClaim{
|
||||
PassHash: req.PassHash,
|
||||
ExpiryTime: time.NDaysFromNow(365),
|
||||
})
|
||||
// Sign and get the complete encoded token as a string using the secret
|
||||
tokenString, err := token.SignedString(secret)
|
||||
|
||||
if err != nil {
|
||||
return nil, stacktrace.Propagate(err, "")
|
||||
}
|
||||
return &ente.VerifyPasswordResponse{
|
||||
JWTToken: tokenString,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
package public
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/ente-io/museum/pkg/controller"
|
||||
"github.com/ente-io/museum/pkg/repo/public"
|
||||
|
||||
"github.com/ente-io/museum/ente"
|
||||
enteJWT "github.com/ente-io/museum/ente/jwt"
|
||||
emailCtrl "github.com/ente-io/museum/pkg/controller/email"
|
||||
"github.com/ente-io/museum/pkg/repo"
|
||||
"github.com/ente-io/museum/pkg/utils/auth"
|
||||
@@ -15,6 +14,7 @@ import (
|
||||
"github.com/ente-io/museum/pkg/utils/time"
|
||||
"github.com/ente-io/stacktrace"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/lithammer/shortuuid/v3"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
@@ -49,24 +49,23 @@ const (
|
||||
AbuseLimitExceededTemplate = "report_limit_exceeded_alert.html"
|
||||
)
|
||||
|
||||
// CollectionLinkController controls share collection operations
|
||||
type CollectionLinkController struct {
|
||||
FileController *controller.FileController
|
||||
// PublicCollectionController controls share collection operations
|
||||
type PublicCollectionController struct {
|
||||
FileController *FileController
|
||||
EmailNotificationCtrl *emailCtrl.EmailNotificationController
|
||||
CollectionLinkRepo *public.CollectionLinkRepo
|
||||
FileLinkRepo *public.FileLinkRepository
|
||||
PublicCollectionRepo *repo.PublicCollectionRepository
|
||||
CollectionRepo *repo.CollectionRepository
|
||||
UserRepo *repo.UserRepository
|
||||
JwtSecret []byte
|
||||
}
|
||||
|
||||
func (c *CollectionLinkController) CreateLink(ctx context.Context, req ente.CreatePublicAccessTokenRequest) (ente.PublicURL, error) {
|
||||
func (c *PublicCollectionController) CreateAccessToken(ctx context.Context, req ente.CreatePublicAccessTokenRequest) (ente.PublicURL, error) {
|
||||
accessToken := shortuuid.New()[0:AccessTokenLength]
|
||||
err := c.CollectionLinkRepo.
|
||||
err := c.PublicCollectionRepo.
|
||||
Insert(ctx, req.CollectionID, accessToken, req.ValidTill, req.DeviceLimit, req.EnableCollect, req.EnableJoin)
|
||||
if err != nil {
|
||||
if errors.Is(err, ente.ErrActiveLinkAlreadyExists) {
|
||||
collectionToPubUrlMap, err2 := c.CollectionLinkRepo.GetCollectionToActivePublicURLMap(ctx, []int64{req.CollectionID})
|
||||
collectionToPubUrlMap, err2 := c.PublicCollectionRepo.GetCollectionToActivePublicURLMap(ctx, []int64{req.CollectionID})
|
||||
if err2 != nil {
|
||||
return ente.PublicURL{}, stacktrace.Propagate(err2, "")
|
||||
}
|
||||
@@ -82,7 +81,7 @@ func (c *CollectionLinkController) CreateLink(ctx context.Context, req ente.Crea
|
||||
}
|
||||
}
|
||||
response := ente.PublicURL{
|
||||
URL: c.CollectionLinkRepo.GetAlbumUrl(accessToken),
|
||||
URL: c.PublicCollectionRepo.GetAlbumUrl(accessToken),
|
||||
ValidTill: req.ValidTill,
|
||||
DeviceLimit: req.DeviceLimit,
|
||||
EnableDownload: true,
|
||||
@@ -92,11 +91,11 @@ func (c *CollectionLinkController) CreateLink(ctx context.Context, req ente.Crea
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (c *CollectionLinkController) GetActiveCollectionLinkToken(ctx context.Context, collectionID int64) (ente.CollectionLinkRow, error) {
|
||||
return c.CollectionLinkRepo.GetActiveCollectionLinkRow(ctx, collectionID)
|
||||
func (c *PublicCollectionController) GetActivePublicCollectionToken(ctx context.Context, collectionID int64) (ente.PublicCollectionToken, error) {
|
||||
return c.PublicCollectionRepo.GetActivePublicCollectionToken(ctx, collectionID)
|
||||
}
|
||||
|
||||
func (c *CollectionLinkController) CreateFile(ctx *gin.Context, file ente.File, app ente.App) (ente.File, error) {
|
||||
func (c *PublicCollectionController) CreateFile(ctx *gin.Context, file ente.File, app ente.App) (ente.File, error) {
|
||||
collection, err := c.GetPublicCollection(ctx, true)
|
||||
if err != nil {
|
||||
return ente.File{}, stacktrace.Propagate(err, "")
|
||||
@@ -119,13 +118,13 @@ func (c *CollectionLinkController) CreateFile(ctx *gin.Context, file ente.File,
|
||||
}
|
||||
|
||||
// Disable all public accessTokens generated for the given cID till date.
|
||||
func (c *CollectionLinkController) Disable(ctx context.Context, cID int64) error {
|
||||
err := c.CollectionLinkRepo.DisableSharing(ctx, cID)
|
||||
func (c *PublicCollectionController) Disable(ctx context.Context, cID int64) error {
|
||||
err := c.PublicCollectionRepo.DisableSharing(ctx, cID)
|
||||
return stacktrace.Propagate(err, "")
|
||||
}
|
||||
|
||||
func (c *CollectionLinkController) UpdateSharedUrl(ctx context.Context, req ente.UpdatePublicAccessTokenRequest) (ente.PublicURL, error) {
|
||||
publicCollectionToken, err := c.CollectionLinkRepo.GetActiveCollectionLinkRow(ctx, req.CollectionID)
|
||||
func (c *PublicCollectionController) UpdateSharedUrl(ctx context.Context, req ente.UpdatePublicAccessTokenRequest) (ente.PublicURL, error) {
|
||||
publicCollectionToken, err := c.PublicCollectionRepo.GetActivePublicCollectionToken(ctx, req.CollectionID)
|
||||
if err != nil {
|
||||
return ente.PublicURL{}, err
|
||||
}
|
||||
@@ -155,12 +154,12 @@ func (c *CollectionLinkController) UpdateSharedUrl(ctx context.Context, req ente
|
||||
if req.EnableJoin != nil {
|
||||
publicCollectionToken.EnableJoin = *req.EnableJoin
|
||||
}
|
||||
err = c.CollectionLinkRepo.UpdatePublicCollectionToken(ctx, publicCollectionToken)
|
||||
err = c.PublicCollectionRepo.UpdatePublicCollectionToken(ctx, publicCollectionToken)
|
||||
if err != nil {
|
||||
return ente.PublicURL{}, stacktrace.Propagate(err, "")
|
||||
}
|
||||
return ente.PublicURL{
|
||||
URL: c.CollectionLinkRepo.GetAlbumUrl(publicCollectionToken.Token),
|
||||
URL: c.PublicCollectionRepo.GetAlbumUrl(publicCollectionToken.Token),
|
||||
DeviceLimit: publicCollectionToken.DeviceLimit,
|
||||
ValidTill: publicCollectionToken.ValidTill,
|
||||
EnableDownload: publicCollectionToken.EnableDownload,
|
||||
@@ -177,23 +176,58 @@ func (c *CollectionLinkController) UpdateSharedUrl(ctx context.Context, req ente
|
||||
// used by the client to pass in other requests for public collection.
|
||||
// Having a separate endpoint for password validation allows us to easily rate-limit the attempts for brute-force
|
||||
// attack for guessing password.
|
||||
func (c *CollectionLinkController) VerifyPassword(ctx *gin.Context, req ente.VerifyPasswordRequest) (*ente.VerifyPasswordResponse, error) {
|
||||
func (c *PublicCollectionController) VerifyPassword(ctx *gin.Context, req ente.VerifyPasswordRequest) (*ente.VerifyPasswordResponse, error) {
|
||||
accessContext := auth.MustGetPublicAccessContext(ctx)
|
||||
collectionLinkRow, err := c.CollectionLinkRepo.GetActiveCollectionLinkRow(ctx, accessContext.CollectionID)
|
||||
publicCollectionToken, err := c.PublicCollectionRepo.GetActivePublicCollectionToken(ctx, accessContext.CollectionID)
|
||||
if err != nil {
|
||||
return nil, stacktrace.Propagate(err, "failed to get public collection info")
|
||||
}
|
||||
return verifyPassword(c.JwtSecret, collectionLinkRow.PassHash, req)
|
||||
if publicCollectionToken.PassHash == nil || *publicCollectionToken.PassHash == "" {
|
||||
return nil, stacktrace.Propagate(ente.ErrBadRequest, "password is not configured for the link")
|
||||
}
|
||||
if req.PassHash != *publicCollectionToken.PassHash {
|
||||
return nil, stacktrace.Propagate(ente.ErrInvalidPassword, "incorrect password for link")
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, &enteJWT.PublicAlbumPasswordClaim{
|
||||
PassHash: req.PassHash,
|
||||
ExpiryTime: time.NDaysFromNow(365),
|
||||
})
|
||||
// Sign and get the complete encoded token as a string using the secret
|
||||
tokenString, err := token.SignedString(c.JwtSecret)
|
||||
|
||||
if err != nil {
|
||||
return nil, stacktrace.Propagate(err, "")
|
||||
}
|
||||
return &ente.VerifyPasswordResponse{
|
||||
JWTToken: tokenString,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *CollectionLinkController) ValidateJWTToken(ctx *gin.Context, jwtToken string, passwordHash string) error {
|
||||
return validateJWTToken(c.JwtSecret, jwtToken, passwordHash)
|
||||
func (c *PublicCollectionController) ValidateJWTToken(ctx *gin.Context, jwtToken string, passwordHash string) error {
|
||||
token, err := jwt.ParseWithClaims(jwtToken, &enteJWT.PublicAlbumPasswordClaim{}, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return stacktrace.Propagate(fmt.Errorf("unexpected signing method: %v", token.Header["alg"]), ""), nil
|
||||
}
|
||||
return c.JwtSecret, nil
|
||||
})
|
||||
if err != nil {
|
||||
return stacktrace.Propagate(err, "JWT parsed failed")
|
||||
}
|
||||
claims, ok := token.Claims.(*enteJWT.PublicAlbumPasswordClaim)
|
||||
|
||||
if !ok {
|
||||
return stacktrace.Propagate(errors.New("no claim in jwt token"), "")
|
||||
}
|
||||
if token.Valid && claims.PassHash == passwordHash {
|
||||
return nil
|
||||
}
|
||||
return ente.ErrInvalidPassword
|
||||
}
|
||||
|
||||
// ReportAbuse captures abuse report for a publicly shared collection.
|
||||
// It will also disable the accessToken for the collection if total abuse reports for the said collection
|
||||
// reaches AutoDisableAbuseThreshold
|
||||
func (c *CollectionLinkController) ReportAbuse(ctx *gin.Context, req ente.AbuseReportRequest) error {
|
||||
func (c *PublicCollectionController) ReportAbuse(ctx *gin.Context, req ente.AbuseReportRequest) error {
|
||||
accessContext := auth.MustGetPublicAccessContext(ctx)
|
||||
readableReason, found := AllowedReasons[req.Reason]
|
||||
if !found {
|
||||
@@ -201,11 +235,11 @@ func (c *CollectionLinkController) ReportAbuse(ctx *gin.Context, req ente.AbuseR
|
||||
}
|
||||
logrus.WithField("collectionID", accessContext.CollectionID).Error("CRITICAL: received abuse report")
|
||||
|
||||
err := c.CollectionLinkRepo.RecordAbuseReport(ctx, accessContext, req.URL, req.Reason, req.Details)
|
||||
err := c.PublicCollectionRepo.RecordAbuseReport(ctx, accessContext, req.URL, req.Reason, req.Details)
|
||||
if err != nil {
|
||||
return stacktrace.Propagate(err, "")
|
||||
}
|
||||
count, err := c.CollectionLinkRepo.GetAbuseReportCount(ctx, accessContext)
|
||||
count, err := c.PublicCollectionRepo.GetAbuseReportCount(ctx, accessContext)
|
||||
if err != nil {
|
||||
return stacktrace.Propagate(err, "")
|
||||
}
|
||||
@@ -219,7 +253,7 @@ func (c *CollectionLinkController) ReportAbuse(ctx *gin.Context, req ente.AbuseR
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CollectionLinkController) onAbuseReportReceived(collectionID int64, report ente.AbuseReportRequest, readableReason string, abuseCount int64) {
|
||||
func (c *PublicCollectionController) onAbuseReportReceived(collectionID int64, report ente.AbuseReportRequest, readableReason string, abuseCount int64) {
|
||||
collection, err := c.CollectionRepo.Get(collectionID)
|
||||
if err != nil {
|
||||
logrus.Error("Could not get collection for abuse report")
|
||||
@@ -258,9 +292,9 @@ func (c *CollectionLinkController) onAbuseReportReceived(collectionID int64, rep
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CollectionLinkController) HandleAccountDeletion(ctx context.Context, userID int64, logger *logrus.Entry) error {
|
||||
func (c *PublicCollectionController) HandleAccountDeletion(ctx context.Context, userID int64, logger *logrus.Entry) error {
|
||||
logger.Info("updating public collection on account deletion")
|
||||
collectionIDs, err := c.CollectionLinkRepo.GetActivePublicTokenForUser(ctx, userID)
|
||||
collectionIDs, err := c.PublicCollectionRepo.GetActivePublicTokenForUser(ctx, userID)
|
||||
if err != nil {
|
||||
return stacktrace.Propagate(err, "")
|
||||
}
|
||||
@@ -271,12 +305,12 @@ func (c *CollectionLinkController) HandleAccountDeletion(ctx context.Context, us
|
||||
return stacktrace.Propagate(err, "")
|
||||
}
|
||||
}
|
||||
return c.FileLinkRepo.DisableLinksForUser(ctx, userID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPublicCollection will return collection info for a public url.
|
||||
// is mustAllowCollect is set to true but the underlying collection doesn't allow uploading
|
||||
func (c *CollectionLinkController) GetPublicCollection(ctx *gin.Context, mustAllowCollect bool) (ente.Collection, error) {
|
||||
func (c *PublicCollectionController) GetPublicCollection(ctx *gin.Context, mustAllowCollect bool) (ente.Collection, error) {
|
||||
accessContext := auth.MustGetPublicAccessContext(ctx)
|
||||
collection, err := c.CollectionRepo.Get(accessContext.CollectionID)
|
||||
if err != nil {
|
||||
@@ -5,8 +5,6 @@ import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
public2 "github.com/ente-io/museum/pkg/controller/public"
|
||||
"github.com/ente-io/museum/pkg/repo/public"
|
||||
"net/http"
|
||||
|
||||
"github.com/ente-io/museum/ente"
|
||||
@@ -26,20 +24,20 @@ import (
|
||||
var passwordWhiteListedURLs = []string{"/public-collection/info", "/public-collection/report-abuse", "/public-collection/verify-password"}
|
||||
var whitelistedCollectionShareIDs = []int64{111}
|
||||
|
||||
// CollectionLinkMiddleware intercepts and authenticates incoming requests
|
||||
type CollectionLinkMiddleware struct {
|
||||
CollectionLinkRepo *public.CollectionLinkRepo
|
||||
PublicCollectionCtrl *public2.CollectionLinkController
|
||||
// AccessTokenMiddleware intercepts and authenticates incoming requests
|
||||
type AccessTokenMiddleware struct {
|
||||
PublicCollectionRepo *repo.PublicCollectionRepository
|
||||
PublicCollectionCtrl *controller.PublicCollectionController
|
||||
CollectionRepo *repo.CollectionRepository
|
||||
Cache *cache.Cache
|
||||
BillingCtrl *controller.BillingController
|
||||
DiscordController *discord.DiscordController
|
||||
}
|
||||
|
||||
// Authenticate returns a middle ware that extracts the `X-Auth-Access-Token`
|
||||
// AccessTokenAuthMiddleware returns a middle ware that extracts the `X-Auth-Access-Token`
|
||||
// within the header of a request and uses it to validate the access token and set the
|
||||
// ente.PublicAccessContext with auth.PublicAccessKey as key
|
||||
func (m *CollectionLinkMiddleware) Authenticate(urlSanitizer func(_ *gin.Context) string) gin.HandlerFunc {
|
||||
func (m *AccessTokenMiddleware) AccessTokenAuthMiddleware(urlSanitizer func(_ *gin.Context) string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
accessToken := auth.GetAccessToken(c)
|
||||
if accessToken == "" {
|
||||
@@ -54,7 +52,7 @@ func (m *CollectionLinkMiddleware) Authenticate(urlSanitizer func(_ *gin.Context
|
||||
cacheKey := computeHashKeyForList([]string{accessToken, clientIP, userAgent}, ":")
|
||||
cachedValue, cacheHit := m.Cache.Get(cacheKey)
|
||||
if !cacheHit {
|
||||
publicCollectionSummary, err = m.CollectionLinkRepo.GetCollectionSummaryByToken(c, accessToken)
|
||||
publicCollectionSummary, err = m.PublicCollectionRepo.GetCollectionSummaryByToken(c, accessToken)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
|
||||
return
|
||||
@@ -114,7 +112,7 @@ func (m *CollectionLinkMiddleware) Authenticate(urlSanitizer func(_ *gin.Context
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
func (m *CollectionLinkMiddleware) validateOwnersSubscription(cID int64) error {
|
||||
func (m *AccessTokenMiddleware) validateOwnersSubscription(cID int64) error {
|
||||
userID, err := m.CollectionRepo.GetOwnerID(cID)
|
||||
if err != nil {
|
||||
return stacktrace.Propagate(err, "")
|
||||
@@ -122,7 +120,7 @@ func (m *CollectionLinkMiddleware) validateOwnersSubscription(cID int64) error {
|
||||
return m.BillingCtrl.HasActiveSelfOrFamilySubscription(userID, false)
|
||||
}
|
||||
|
||||
func (m *CollectionLinkMiddleware) isDeviceLimitReached(ctx context.Context,
|
||||
func (m *AccessTokenMiddleware) isDeviceLimitReached(ctx context.Context,
|
||||
collectionSummary ente.PublicCollectionSummary, ip string, ua string) (bool, error) {
|
||||
// skip deviceLimit check & record keeping for requests via CF worker
|
||||
if network.IsCFWorkerIP(ip) {
|
||||
@@ -130,7 +128,7 @@ func (m *CollectionLinkMiddleware) isDeviceLimitReached(ctx context.Context,
|
||||
}
|
||||
|
||||
sharedID := collectionSummary.ID
|
||||
hasAccessedInPast, err := m.CollectionLinkRepo.AccessedInPast(ctx, sharedID, ip, ua)
|
||||
hasAccessedInPast, err := m.PublicCollectionRepo.AccessedInPast(ctx, sharedID, ip, ua)
|
||||
if err != nil {
|
||||
return false, stacktrace.Propagate(err, "")
|
||||
}
|
||||
@@ -138,17 +136,17 @@ func (m *CollectionLinkMiddleware) isDeviceLimitReached(ctx context.Context,
|
||||
if hasAccessedInPast {
|
||||
return false, nil
|
||||
}
|
||||
count, err := m.CollectionLinkRepo.GetUniqueAccessCount(ctx, sharedID)
|
||||
count, err := m.PublicCollectionRepo.GetUniqueAccessCount(ctx, sharedID)
|
||||
if err != nil {
|
||||
return false, stacktrace.Propagate(err, "failed to get unique access count")
|
||||
}
|
||||
|
||||
deviceLimit := int64(collectionSummary.DeviceLimit)
|
||||
if deviceLimit == public2.DeviceLimitThreshold {
|
||||
deviceLimit = public2.DeviceLimitThresholdMultiplier * public2.DeviceLimitThreshold
|
||||
if deviceLimit == controller.DeviceLimitThreshold {
|
||||
deviceLimit = controller.DeviceLimitThresholdMultiplier * controller.DeviceLimitThreshold
|
||||
}
|
||||
|
||||
if count >= public2.DeviceLimitWarningThreshold {
|
||||
if count >= controller.DeviceLimitWarningThreshold {
|
||||
if !array.Int64InList(sharedID, whitelistedCollectionShareIDs) {
|
||||
m.DiscordController.NotifyPotentialAbuse(
|
||||
fmt.Sprintf("Album exceeds warning threshold: {CollectionID: %d, ShareID: %d}",
|
||||
@@ -159,12 +157,12 @@ func (m *CollectionLinkMiddleware) isDeviceLimitReached(ctx context.Context,
|
||||
if deviceLimit > 0 && count >= deviceLimit {
|
||||
return true, nil
|
||||
}
|
||||
err = m.CollectionLinkRepo.RecordAccessHistory(ctx, sharedID, ip, ua)
|
||||
err = m.PublicCollectionRepo.RecordAccessHistory(ctx, sharedID, ip, ua)
|
||||
return false, stacktrace.Propagate(err, "failed to record access history")
|
||||
}
|
||||
|
||||
// validatePassword will verify if the user is provided correct password for the public album
|
||||
func (m *CollectionLinkMiddleware) validatePassword(c *gin.Context, reqPath string,
|
||||
func (m *AccessTokenMiddleware) validatePassword(c *gin.Context, reqPath string,
|
||||
collectionSummary ente.PublicCollectionSummary) error {
|
||||
if array.StringInList(reqPath, passwordWhiteListedURLs) {
|
||||
return nil
|
||||
@@ -1,168 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
publicCtrl "github.com/ente-io/museum/pkg/controller/public"
|
||||
"github.com/ente-io/museum/pkg/repo/public"
|
||||
"github.com/ente-io/museum/pkg/utils/array"
|
||||
"net/http"
|
||||
|
||||
"github.com/ente-io/museum/ente"
|
||||
"github.com/ente-io/museum/pkg/controller"
|
||||
"github.com/ente-io/museum/pkg/controller/discord"
|
||||
"github.com/ente-io/museum/pkg/utils/auth"
|
||||
"github.com/ente-io/museum/pkg/utils/network"
|
||||
"github.com/ente-io/museum/pkg/utils/time"
|
||||
"github.com/ente-io/stacktrace"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/patrickmn/go-cache"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var filePasswordWhiteListedURLs = []string{"/file-link/pass-info", "/file-link/verify-password"}
|
||||
|
||||
// FileLinkMiddleware intercepts and authenticates incoming requests
|
||||
type FileLinkMiddleware struct {
|
||||
FileLinkRepo *public.FileLinkRepository
|
||||
FileLinkCtrl *publicCtrl.FileLinkController
|
||||
Cache *cache.Cache
|
||||
BillingCtrl *controller.BillingController
|
||||
DiscordController *discord.DiscordController
|
||||
}
|
||||
|
||||
// Authenticate returns a middle ware that extracts the `X-Auth-Access-Token`
|
||||
// within the header of a request and uses it to validate the access token and set the
|
||||
// ente.PublicAccessContext with auth.PublicAccessKey as key
|
||||
func (m *FileLinkMiddleware) Authenticate(urlSanitizer func(_ *gin.Context) string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
accessToken := auth.GetAccessToken(c)
|
||||
if accessToken == "" {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing accessToken"})
|
||||
return
|
||||
}
|
||||
clientIP := network.GetClientIP(c)
|
||||
userAgent := c.GetHeader("User-Agent")
|
||||
|
||||
cacheKey := computeHashKeyForList([]string{accessToken, clientIP, userAgent}, ":")
|
||||
cachedValue, cacheHit := m.Cache.Get(cacheKey)
|
||||
var fileLinkRow *ente.FileLinkRow
|
||||
var err error
|
||||
if !cacheHit {
|
||||
fileLinkRow, err = m.FileLinkRepo.GetFileUrlRowByToken(c, accessToken)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Info("failed to get file link row by token")
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
|
||||
return
|
||||
}
|
||||
if fileLinkRow.IsDisabled {
|
||||
c.AbortWithStatusJSON(http.StatusGone, gin.H{"error": "disabled token"})
|
||||
return
|
||||
}
|
||||
// validate if user still has active paid subscription
|
||||
if err = m.BillingCtrl.HasActiveSelfOrFamilySubscription(fileLinkRow.OwnerID, true); err != nil {
|
||||
logrus.WithError(err).Info("failed to verify active paid subscription")
|
||||
c.AbortWithStatusJSON(http.StatusGone, gin.H{"error": "no active subscription"})
|
||||
return
|
||||
}
|
||||
|
||||
// validate device limit
|
||||
reached, limitErr := m.isDeviceLimitReached(c, fileLinkRow, clientIP, userAgent)
|
||||
if limitErr != nil {
|
||||
logrus.WithError(limitErr).Error("failed to check device limit")
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "something went wrong"})
|
||||
return
|
||||
}
|
||||
if reached {
|
||||
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"error": "reached device limit"})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
fileLinkRow = cachedValue.(*ente.FileLinkRow)
|
||||
}
|
||||
|
||||
if fileLinkRow.ValidTill > 0 && // expiry time is defined, 0 indicates no expiry
|
||||
fileLinkRow.ValidTill < time.Microseconds() {
|
||||
c.AbortWithStatusJSON(http.StatusGone, gin.H{"error": "expired token"})
|
||||
return
|
||||
}
|
||||
|
||||
// checks password protected public collection
|
||||
if fileLinkRow.PassHash != nil && *fileLinkRow.PassHash != "" {
|
||||
reqPath := urlSanitizer(c)
|
||||
if err = m.validatePassword(c, reqPath, fileLinkRow); err != nil {
|
||||
logrus.WithError(err).Warn("password validation failed")
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": err})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !cacheHit {
|
||||
m.Cache.Set(cacheKey, fileLinkRow, cache.DefaultExpiration)
|
||||
}
|
||||
|
||||
c.Set(auth.FileLinkAccessKey, &ente.FileLinkAccessContext{
|
||||
LinkID: fileLinkRow.LinkID,
|
||||
IP: clientIP,
|
||||
UserAgent: userAgent,
|
||||
FileID: fileLinkRow.FileID,
|
||||
OwnerID: fileLinkRow.OwnerID,
|
||||
})
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *FileLinkMiddleware) isDeviceLimitReached(ctx context.Context,
|
||||
collectionSummary *ente.FileLinkRow, ip string, ua string) (bool, error) {
|
||||
// skip deviceLimit check & record keeping for requests via CF worker
|
||||
if network.IsCFWorkerIP(ip) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
sharedID := collectionSummary.LinkID
|
||||
hasAccessedInPast, err := m.FileLinkRepo.AccessedInPast(ctx, sharedID, ip, ua)
|
||||
if err != nil {
|
||||
return false, stacktrace.Propagate(err, "")
|
||||
}
|
||||
// if the device has accessed the url in the past, let it access it now as well, irrespective of device limit.
|
||||
if hasAccessedInPast {
|
||||
return false, nil
|
||||
}
|
||||
count, err := m.FileLinkRepo.GetUniqueAccessCount(ctx, sharedID)
|
||||
if err != nil {
|
||||
return false, stacktrace.Propagate(err, "failed to get unique access count")
|
||||
}
|
||||
|
||||
deviceLimit := int64(collectionSummary.DeviceLimit)
|
||||
if deviceLimit == publicCtrl.DeviceLimitThreshold {
|
||||
deviceLimit = publicCtrl.DeviceLimitThresholdMultiplier * publicCtrl.DeviceLimitThreshold
|
||||
}
|
||||
|
||||
if count >= publicCtrl.DeviceLimitWarningThreshold {
|
||||
m.DiscordController.NotifyPotentialAbuse(
|
||||
fmt.Sprintf("FileLink exceeds warning threshold: {FileID: %d, ShareID: %s}",
|
||||
collectionSummary.FileID, collectionSummary.LinkID))
|
||||
}
|
||||
|
||||
if deviceLimit > 0 && count >= deviceLimit {
|
||||
return true, nil
|
||||
}
|
||||
err = m.FileLinkRepo.RecordAccessHistory(ctx, sharedID, ip, ua)
|
||||
return false, stacktrace.Propagate(err, "failed to record access history")
|
||||
}
|
||||
|
||||
// validatePassword will verify if the user is provided correct password for the public album
|
||||
func (m *FileLinkMiddleware) validatePassword(
|
||||
c *gin.Context,
|
||||
reqPath string,
|
||||
fileLinkRow *ente.FileLinkRow,
|
||||
) error {
|
||||
accessTokenJWT := auth.GetAccessTokenJWT(c)
|
||||
if accessTokenJWT == "" {
|
||||
if array.StringInList(reqPath, filePasswordWhiteListedURLs) {
|
||||
return nil
|
||||
}
|
||||
return &ente.ErrPassProtectedResource
|
||||
}
|
||||
return m.FileLinkCtrl.ValidateJWTToken(c, accessTokenJWT, *fileLinkRow.PassHash)
|
||||
}
|
||||
@@ -140,7 +140,6 @@ func (r *RateLimitMiddleware) getLimiter(reqPath string, reqMethod string) *limi
|
||||
reqPath == "/users/verify-email" ||
|
||||
reqPath == "/user/change-email" ||
|
||||
reqPath == "/public-collection/verify-password" ||
|
||||
reqPath == "/file-link/verify-password" ||
|
||||
reqPath == "/family/accept-invite" ||
|
||||
reqPath == "/users/srp/attributes" ||
|
||||
(reqPath == "/cast/device-info" && reqMethod == "POST") ||
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"github.com/ente-io/museum/pkg/repo/public"
|
||||
"strconv"
|
||||
t "time"
|
||||
|
||||
@@ -23,13 +22,13 @@ import (
|
||||
// CollectionRepository defines the methods for inserting, updating and
|
||||
// retrieving collection entities from the underlying repository
|
||||
type CollectionRepository struct {
|
||||
DB *sql.DB
|
||||
FileRepo *FileRepository
|
||||
CollectionLinkRepo *public.CollectionLinkRepo
|
||||
TrashRepo *TrashRepository
|
||||
SecretEncryptionKey []byte
|
||||
QueueRepo *QueueRepository
|
||||
LatencyLogger *prometheus.HistogramVec
|
||||
DB *sql.DB
|
||||
FileRepo *FileRepository
|
||||
PublicCollectionRepo *PublicCollectionRepository
|
||||
TrashRepo *TrashRepository
|
||||
SecretEncryptionKey []byte
|
||||
QueueRepo *QueueRepository
|
||||
LatencyLogger *prometheus.HistogramVec
|
||||
}
|
||||
|
||||
type SharedCollection struct {
|
||||
@@ -75,7 +74,7 @@ func (repo *CollectionRepository) Get(collectionID int64) (ente.Collection, erro
|
||||
c.EncryptedName = encryptedName.String
|
||||
c.NameDecryptionNonce = nameDecryptionNonce.String
|
||||
}
|
||||
urlMap, err := repo.CollectionLinkRepo.GetCollectionToActivePublicURLMap(context.Background(), []int64{collectionID})
|
||||
urlMap, err := repo.PublicCollectionRepo.GetCollectionToActivePublicURLMap(context.Background(), []int64{collectionID})
|
||||
if err != nil {
|
||||
return ente.Collection{}, stacktrace.Propagate(err, "failed to get publicURL info")
|
||||
}
|
||||
@@ -175,7 +174,7 @@ pct.access_token, pct.valid_till, pct.device_limit, pct.created_at, pct.updated_
|
||||
if _, ok := addPublicUrlMap[pctToken.String]; !ok {
|
||||
addPublicUrlMap[pctToken.String] = true
|
||||
url := ente.PublicURL{
|
||||
URL: repo.CollectionLinkRepo.GetAlbumUrl(pctToken.String),
|
||||
URL: repo.PublicCollectionRepo.GetAlbumUrl(pctToken.String),
|
||||
DeviceLimit: int(pctDeviceLimit.Int32),
|
||||
ValidTill: pctValidTill.Int64,
|
||||
EnableDownload: pctEnableDownload.Bool,
|
||||
|
||||
@@ -638,16 +638,6 @@ func (repo *FileRepository) GetFileAttributesForCopy(fileIDs []int64) ([]ente.Fi
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (repo *FileRepository) GetFileAttributes(fileID int64) (*ente.File, error) {
|
||||
rows := repo.DB.QueryRow(`SELECT file_id, owner_id, file_decryption_header, thumbnail_decryption_header, metadata_decryption_header, encrypted_metadata, pub_magic_metadata FROM files WHERE file_id = $1`, fileID)
|
||||
var file ente.File
|
||||
err := rows.Scan(&file.ID, &file.OwnerID, &file.File.DecryptionHeader, &file.Thumbnail.DecryptionHeader, &file.Metadata.DecryptionHeader, &file.Metadata.EncryptedData, &file.PubicMagicMetadata)
|
||||
if err != nil {
|
||||
return nil, stacktrace.Propagate(err, "")
|
||||
}
|
||||
return &file, nil
|
||||
}
|
||||
|
||||
// GetUsage gets the Storage usage of a user
|
||||
// Deprecated: GetUsage is deprecated, use UsageRepository.GetUsage
|
||||
func (repo *FileRepository) GetUsage(userID int64) (int64, error) {
|
||||
|
||||
@@ -1,218 +0,0 @@
|
||||
package public
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/ente-io/museum/ente/base"
|
||||
"github.com/lib/pq"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/ente-io/museum/ente"
|
||||
"github.com/ente-io/stacktrace"
|
||||
)
|
||||
|
||||
// FileLinkRepository defines the methods for inserting, updating and
|
||||
// retrieving entities related to public file
|
||||
type FileLinkRepository struct {
|
||||
DB *sql.DB
|
||||
photoHost string
|
||||
lockerHost string
|
||||
}
|
||||
|
||||
// NewFileLinkRepo ..
|
||||
func NewFileLinkRepo(db *sql.DB) *FileLinkRepository {
|
||||
albumHost := viper.GetString("apps.public-albums")
|
||||
if albumHost == "" {
|
||||
albumHost = "https://albums.ente.io"
|
||||
}
|
||||
lockerHost := viper.GetString("apps.public-locker")
|
||||
if lockerHost == "" {
|
||||
lockerHost = "https://locker.ente.io"
|
||||
}
|
||||
return &FileLinkRepository{
|
||||
DB: db,
|
||||
photoHost: albumHost,
|
||||
lockerHost: lockerHost,
|
||||
}
|
||||
}
|
||||
|
||||
func (pcr *FileLinkRepository) PhotoLink(token string) string {
|
||||
return fmt.Sprintf("%s/?t=%s", pcr.photoHost, token)
|
||||
}
|
||||
|
||||
func (pcr *FileLinkRepository) LockerFileLink(token string) string {
|
||||
return fmt.Sprintf("%s/?t=%s", pcr.lockerHost, token)
|
||||
}
|
||||
|
||||
func (pcr *FileLinkRepository) Insert(
|
||||
ctx context.Context,
|
||||
fileID int64,
|
||||
ownerID int64,
|
||||
token string,
|
||||
app ente.App,
|
||||
) (*string, error) {
|
||||
id, err := base.NewID("pft")
|
||||
if err != nil {
|
||||
return nil, stacktrace.Propagate(err, "failed to generate new ID for public file token")
|
||||
}
|
||||
_, err = pcr.DB.ExecContext(ctx, `INSERT INTO public_file_tokens
|
||||
(id, file_id, owner_id, access_token, app) VALUES ($1, $2, $3, $4, $5)`,
|
||||
id, fileID, ownerID, token, string(app))
|
||||
if err != nil {
|
||||
if err.Error() == "pq: duplicate key value violates unique constraint \"public_active_file_link_unique_idx\"" {
|
||||
return nil, ente.ErrActiveLinkAlreadyExists
|
||||
}
|
||||
return nil, stacktrace.Propagate(err, "failed to insert")
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// GetActiveFileUrlToken will return ente.CollectionLinkRow for given collection ID
|
||||
// Note: The token could be expired or deviceLimit is already reached
|
||||
func (pcr *FileLinkRepository) GetActiveFileUrlToken(ctx context.Context, fileID int64) (*ente.FileLinkRow, error) {
|
||||
row := pcr.DB.QueryRowContext(ctx, `SELECT id, file_id, owner_id, access_token, valid_till, device_limit,
|
||||
is_disabled, pw_hash, pw_nonce, mem_limit, ops_limit, enable_download FROM
|
||||
public_file_tokens WHERE file_id = $1 and is_disabled = FALSE`,
|
||||
fileID)
|
||||
|
||||
ret := ente.FileLinkRow{}
|
||||
err := row.Scan(&ret.LinkID, &ret.FileID, ret.OwnerID, &ret.Token, &ret.ValidTill, &ret.DeviceLimit,
|
||||
&ret.IsDisabled, &ret.PassHash, &ret.Nonce, &ret.MemLimit, &ret.OpsLimit, &ret.EnableDownload)
|
||||
if err != nil {
|
||||
return nil, stacktrace.Propagate(err, "")
|
||||
}
|
||||
return &ret, nil
|
||||
}
|
||||
func (pcr *FileLinkRepository) GetFileUrls(ctx context.Context, userID int64, sinceTime int64, limit int64, app ente.App) ([]*ente.FileLinkRow, error) {
|
||||
if limit <= 0 {
|
||||
limit = 500
|
||||
}
|
||||
query := `SELECT id, file_id, owner_id, is_disabled, valid_till, device_limit, enable_download, pw_hash, pw_nonce, mem_limit, ops_limit,
|
||||
created_at, updated_at FROM public_file_tokens
|
||||
WHERE owner_id = $1 AND created_at > $2 AND app = $3 ORDER BY updated_at DESC LIMIT $4`
|
||||
rows, err := pcr.DB.QueryContext(ctx, query, userID, sinceTime, string(app), limit)
|
||||
if err != nil {
|
||||
return nil, stacktrace.Propagate(err, "failed to get public file urls")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var result []*ente.FileLinkRow
|
||||
for rows.Next() {
|
||||
var row ente.FileLinkRow
|
||||
err = rows.Scan(&row.LinkID, &row.FileID, &row.OwnerID, &row.IsDisabled,
|
||||
&row.ValidTill, &row.DeviceLimit, &row.EnableDownload,
|
||||
&row.PassHash, &row.Nonce, &row.MemLimit,
|
||||
&row.OpsLimit, &row.CreatedAt, &row.UpdatedAt)
|
||||
if err != nil {
|
||||
return nil, stacktrace.Propagate(err, "failed to scan public file url row")
|
||||
}
|
||||
result = append(result, &row)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (pcr *FileLinkRepository) DisableLinkForFiles(ctx context.Context, fileIDs []int64) error {
|
||||
if len(fileIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
query := `UPDATE public_file_tokens SET is_disabled = TRUE WHERE file_id = ANY($1)`
|
||||
_, err := pcr.DB.ExecContext(ctx, query, pq.Array(fileIDs))
|
||||
if err != nil {
|
||||
return stacktrace.Propagate(err, "failed to disable public file links")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DisableLinksForUser will disable all public file links for the given user
|
||||
func (pcr *FileLinkRepository) DisableLinksForUser(ctx context.Context, userID int64) error {
|
||||
_, err := pcr.DB.ExecContext(ctx, `UPDATE public_file_tokens SET is_disabled = TRUE WHERE owner_id = $1`, userID)
|
||||
if err != nil {
|
||||
return stacktrace.Propagate(err, "failed to disable public file link")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pcr *FileLinkRepository) GetFileUrlRowByToken(ctx context.Context, accessToken string) (*ente.FileLinkRow, error) {
|
||||
row := pcr.DB.QueryRowContext(ctx,
|
||||
`SELECT id, file_id, owner_id, is_disabled, valid_till, device_limit, enable_download, pw_hash, pw_nonce, mem_limit, ops_limit
|
||||
created_at, updated_at
|
||||
from public_file_tokens
|
||||
where access_token = $1
|
||||
`, accessToken)
|
||||
var result = ente.FileLinkRow{}
|
||||
err := row.Scan(&result.LinkID, &result.FileID, &result.OwnerID, &result.IsDisabled, &result.EnableDownload, &result.ValidTill, &result.DeviceLimit, &result.PassHash, &result.Nonce, &result.MemLimit, &result.OpsLimit, &result.CreatedAt, &result.UpdatedAt)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, ente.ErrNotFound
|
||||
}
|
||||
return nil, stacktrace.Propagate(err, "failed to get public file url summary by token")
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (pcr *FileLinkRepository) GetFileUrlRowByFileID(ctx context.Context, fileID int64) (*ente.FileLinkRow, error) {
|
||||
row := pcr.DB.QueryRowContext(ctx,
|
||||
`SELECT id, file_id, access_token, owner_id, is_disabled, enable_download, valid_till, device_limit, pw_hash, pw_nonce, mem_limit, ops_limit,
|
||||
created_at, updated_at
|
||||
from public_file_tokens
|
||||
where file_id = $1 and is_disabled = FALSE`, fileID)
|
||||
var result = ente.FileLinkRow{}
|
||||
err := row.Scan(&result.LinkID, &result.FileID, &result.Token, &result.OwnerID, &result.IsDisabled, &result.EnableDownload, &result.ValidTill, &result.DeviceLimit, &result.PassHash, &result.Nonce, &result.MemLimit, &result.OpsLimit, &result.CreatedAt, &result.UpdatedAt)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, ente.ErrNotFound
|
||||
}
|
||||
return nil, stacktrace.Propagate(err, "failed to get public file url summary by file ID")
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// UpdateLink will update the row for corresponding public file token
|
||||
func (pcr *FileLinkRepository) UpdateLink(ctx context.Context, pct ente.FileLinkRow) error {
|
||||
_, err := pcr.DB.ExecContext(ctx, `UPDATE public_file_tokens SET valid_till = $1, device_limit = $2,
|
||||
pw_hash = $3, pw_nonce = $4, mem_limit = $5, ops_limit = $6, enable_download = $7
|
||||
where id = $8`,
|
||||
pct.ValidTill, pct.DeviceLimit, pct.PassHash, pct.Nonce, pct.MemLimit, pct.OpsLimit, pct.EnableDownload, pct.LinkID)
|
||||
return stacktrace.Propagate(err, "failed to update public file token")
|
||||
}
|
||||
|
||||
func (pcr *FileLinkRepository) GetUniqueAccessCount(ctx context.Context, linkId string) (int64, error) {
|
||||
row := pcr.DB.QueryRowContext(ctx, `SELECT count(*) FROM public_file_tokens_access_history WHERE id = $1`, linkId)
|
||||
var count int64 = 0
|
||||
err := row.Scan(&count)
|
||||
if err != nil {
|
||||
return -1, stacktrace.Propagate(err, "")
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (pcr *FileLinkRepository) RecordAccessHistory(ctx context.Context, shareID string, ip string, ua string) error {
|
||||
_, err := pcr.DB.ExecContext(ctx, `INSERT INTO public_file_tokens_access_history
|
||||
(id, ip, user_agent) VALUES ($1, $2, $3)
|
||||
ON CONFLICT ON CONSTRAINT unique_access_id_ip_ua DO NOTHING;`,
|
||||
shareID, ip, ua)
|
||||
return stacktrace.Propagate(err, "failed to record access history")
|
||||
}
|
||||
|
||||
// AccessedInPast returns true if the given ip, ua agent combination has accessed the url in the past
|
||||
func (pcr *FileLinkRepository) AccessedInPast(ctx context.Context, shareID string, ip string, ua string) (bool, error) {
|
||||
row := pcr.DB.QueryRowContext(ctx, `select id from public_file_tokens_access_history where id =$1 and ip = $2 and user_agent = $3`,
|
||||
shareID, ip, ua)
|
||||
var tempID int64
|
||||
err := row.Scan(&tempID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return false, nil
|
||||
}
|
||||
return true, stacktrace.Propagate(err, "failed to record access history")
|
||||
}
|
||||
|
||||
// CleanupAccessHistory public_file_tokens_access_history where public_collection_tokens is disabled and the last updated time is older than 30 days
|
||||
func (pcr *FileLinkRepository) CleanupAccessHistory(ctx context.Context) error {
|
||||
_, err := pcr.DB.ExecContext(ctx, `DELETE FROM public_file_tokens_access_history WHERE id IN (SELECT id FROM public_file_tokens WHERE is_disabled = TRUE AND updated_at < (now_utc_micro_seconds() - (24::BIGINT * 30 * 60 * 60 * 1000 * 1000)))`)
|
||||
if err != nil {
|
||||
return stacktrace.Propagate(err, "failed to clean up public file access history")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package public
|
||||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -13,29 +13,29 @@ import (
|
||||
|
||||
const BaseShareURL = "https://albums.ente.io/?t=%s"
|
||||
|
||||
// CollectionLinkRepo defines the methods for inserting, updating and
|
||||
// PublicCollectionRepository defines the methods for inserting, updating and
|
||||
// retrieving entities related to public collections
|
||||
type CollectionLinkRepo struct {
|
||||
type PublicCollectionRepository struct {
|
||||
DB *sql.DB
|
||||
albumHost string
|
||||
}
|
||||
|
||||
// NewCollectionLinkRepository ..
|
||||
func NewCollectionLinkRepository(db *sql.DB, albumHost string) *CollectionLinkRepo {
|
||||
// NewPublicCollectionRepository ..
|
||||
func NewPublicCollectionRepository(db *sql.DB, albumHost string) *PublicCollectionRepository {
|
||||
if albumHost == "" {
|
||||
albumHost = "https://albums.ente.io"
|
||||
}
|
||||
return &CollectionLinkRepo{
|
||||
return &PublicCollectionRepository{
|
||||
DB: db,
|
||||
albumHost: albumHost,
|
||||
}
|
||||
}
|
||||
|
||||
func (pcr *CollectionLinkRepo) GetAlbumUrl(token string) string {
|
||||
func (pcr *PublicCollectionRepository) GetAlbumUrl(token string) string {
|
||||
return fmt.Sprintf("%s/?t=%s", pcr.albumHost, token)
|
||||
}
|
||||
|
||||
func (pcr *CollectionLinkRepo) Insert(ctx context.Context,
|
||||
func (pcr *PublicCollectionRepository) Insert(ctx context.Context,
|
||||
cID int64, token string, validTill int64, deviceLimit int, enableCollect bool, enableJoin *bool) error {
|
||||
// default value for enableJoin is true
|
||||
join := true
|
||||
@@ -51,7 +51,7 @@ func (pcr *CollectionLinkRepo) Insert(ctx context.Context,
|
||||
return stacktrace.Propagate(err, "failed to insert")
|
||||
}
|
||||
|
||||
func (pcr *CollectionLinkRepo) DisableSharing(ctx context.Context, cID int64) error {
|
||||
func (pcr *PublicCollectionRepository) DisableSharing(ctx context.Context, cID int64) error {
|
||||
_, err := pcr.DB.ExecContext(ctx, `UPDATE public_collection_tokens SET is_disabled = true where
|
||||
collection_id = $1 and is_disabled = false`, cID)
|
||||
return stacktrace.Propagate(err, "failed to disable sharing")
|
||||
@@ -59,7 +59,7 @@ func (pcr *CollectionLinkRepo) DisableSharing(ctx context.Context, cID int64) er
|
||||
|
||||
// GetCollectionToActivePublicURLMap will return map of collectionID to PublicURLs which are not disabled yet.
|
||||
// Note: The url could be expired or deviceLimit is already reached
|
||||
func (pcr *CollectionLinkRepo) GetCollectionToActivePublicURLMap(ctx context.Context, collectionIDs []int64) (map[int64][]ente.PublicURL, error) {
|
||||
func (pcr *PublicCollectionRepository) GetCollectionToActivePublicURLMap(ctx context.Context, collectionIDs []int64) (map[int64][]ente.PublicURL, error) {
|
||||
rows, err := pcr.DB.QueryContext(ctx, `SELECT collection_id, access_token, valid_till, device_limit, enable_download, enable_collect, enable_join, pw_nonce, mem_limit, ops_limit FROM
|
||||
public_collection_tokens WHERE collection_id = ANY($1) and is_disabled = FALSE`,
|
||||
pq.Array(collectionIDs))
|
||||
@@ -92,26 +92,26 @@ func (pcr *CollectionLinkRepo) GetCollectionToActivePublicURLMap(ctx context.Con
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetActiveCollectionLinkRow will return ente.CollectionLinkRow for given collection ID
|
||||
// GetActivePublicCollectionToken will return ente.PublicCollectionToken for given collection ID
|
||||
// Note: The token could be expired or deviceLimit is already reached
|
||||
func (pcr *CollectionLinkRepo) GetActiveCollectionLinkRow(ctx context.Context, collectionID int64) (ente.CollectionLinkRow, error) {
|
||||
func (pcr *PublicCollectionRepository) GetActivePublicCollectionToken(ctx context.Context, collectionID int64) (ente.PublicCollectionToken, error) {
|
||||
row := pcr.DB.QueryRowContext(ctx, `SELECT id, collection_id, access_token, valid_till, device_limit,
|
||||
is_disabled, pw_hash, pw_nonce, mem_limit, ops_limit, enable_download, enable_collect, enable_join FROM
|
||||
public_collection_tokens WHERE collection_id = $1 and is_disabled = FALSE`,
|
||||
collectionID)
|
||||
|
||||
//defer rows.Close()
|
||||
ret := ente.CollectionLinkRow{}
|
||||
ret := ente.PublicCollectionToken{}
|
||||
err := row.Scan(&ret.ID, &ret.CollectionID, &ret.Token, &ret.ValidTill, &ret.DeviceLimit,
|
||||
&ret.IsDisabled, &ret.PassHash, &ret.Nonce, &ret.MemLimit, &ret.OpsLimit, &ret.EnableDownload, &ret.EnableCollect, &ret.EnableJoin)
|
||||
if err != nil {
|
||||
return ente.CollectionLinkRow{}, stacktrace.Propagate(err, "")
|
||||
return ente.PublicCollectionToken{}, stacktrace.Propagate(err, "")
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// UpdatePublicCollectionToken will update the row for corresponding public collection token
|
||||
func (pcr *CollectionLinkRepo) UpdatePublicCollectionToken(ctx context.Context, pct ente.CollectionLinkRow) error {
|
||||
func (pcr *PublicCollectionRepository) UpdatePublicCollectionToken(ctx context.Context, pct ente.PublicCollectionToken) error {
|
||||
_, err := pcr.DB.ExecContext(ctx, `UPDATE public_collection_tokens SET valid_till = $1, device_limit = $2,
|
||||
pw_hash = $3, pw_nonce = $4, mem_limit = $5, ops_limit = $6, enable_download = $7, enable_collect = $8, enable_join = $9
|
||||
where id = $10`,
|
||||
@@ -119,7 +119,7 @@ func (pcr *CollectionLinkRepo) UpdatePublicCollectionToken(ctx context.Context,
|
||||
return stacktrace.Propagate(err, "failed to update public collection token")
|
||||
}
|
||||
|
||||
func (pcr *CollectionLinkRepo) RecordAbuseReport(ctx context.Context, accessCtx ente.PublicAccessContext,
|
||||
func (pcr *PublicCollectionRepository) RecordAbuseReport(ctx context.Context, accessCtx ente.PublicAccessContext,
|
||||
url string, reason string, details ente.AbuseReportDetails) error {
|
||||
_, err := pcr.DB.ExecContext(ctx, `INSERT INTO public_abuse_report
|
||||
(share_id, ip, user_agent, url, reason, details) VALUES ($1, $2, $3, $4, $5, $6)
|
||||
@@ -128,7 +128,7 @@ func (pcr *CollectionLinkRepo) RecordAbuseReport(ctx context.Context, accessCtx
|
||||
return stacktrace.Propagate(err, "failed to record abuse report")
|
||||
}
|
||||
|
||||
func (pcr *CollectionLinkRepo) GetAbuseReportCount(ctx context.Context, accessCtx ente.PublicAccessContext) (int64, error) {
|
||||
func (pcr *PublicCollectionRepository) GetAbuseReportCount(ctx context.Context, accessCtx ente.PublicAccessContext) (int64, error) {
|
||||
row := pcr.DB.QueryRowContext(ctx, `SELECT count(*) FROM public_abuse_report WHERE share_id = $1`, accessCtx.ID)
|
||||
var count int64 = 0
|
||||
err := row.Scan(&count)
|
||||
@@ -138,7 +138,7 @@ func (pcr *CollectionLinkRepo) GetAbuseReportCount(ctx context.Context, accessCt
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (pcr *CollectionLinkRepo) GetUniqueAccessCount(ctx context.Context, shareId int64) (int64, error) {
|
||||
func (pcr *PublicCollectionRepository) GetUniqueAccessCount(ctx context.Context, shareId int64) (int64, error) {
|
||||
row := pcr.DB.QueryRowContext(ctx, `SELECT count(*) FROM public_collection_access_history WHERE share_id = $1`, shareId)
|
||||
var count int64 = 0
|
||||
err := row.Scan(&count)
|
||||
@@ -148,7 +148,7 @@ func (pcr *CollectionLinkRepo) GetUniqueAccessCount(ctx context.Context, shareId
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (pcr *CollectionLinkRepo) RecordAccessHistory(ctx context.Context, shareID int64, ip string, ua string) error {
|
||||
func (pcr *PublicCollectionRepository) RecordAccessHistory(ctx context.Context, shareID int64, ip string, ua string) error {
|
||||
_, err := pcr.DB.ExecContext(ctx, `INSERT INTO public_collection_access_history
|
||||
(share_id, ip, user_agent) VALUES ($1, $2, $3)
|
||||
ON CONFLICT ON CONSTRAINT unique_access_sid_ip_ua DO NOTHING;`,
|
||||
@@ -157,7 +157,7 @@ func (pcr *CollectionLinkRepo) RecordAccessHistory(ctx context.Context, shareID
|
||||
}
|
||||
|
||||
// AccessedInPast returns true if the given ip, ua agent combination has accessed the url in the past
|
||||
func (pcr *CollectionLinkRepo) AccessedInPast(ctx context.Context, shareID int64, ip string, ua string) (bool, error) {
|
||||
func (pcr *PublicCollectionRepository) AccessedInPast(ctx context.Context, shareID int64, ip string, ua string) (bool, error) {
|
||||
row := pcr.DB.QueryRowContext(ctx, `select share_id from public_collection_access_history where share_id =$1 and ip = $2 and user_agent = $3`,
|
||||
shareID, ip, ua)
|
||||
var tempID int64
|
||||
@@ -168,7 +168,7 @@ func (pcr *CollectionLinkRepo) AccessedInPast(ctx context.Context, shareID int64
|
||||
return true, stacktrace.Propagate(err, "failed to record access history")
|
||||
}
|
||||
|
||||
func (pcr *CollectionLinkRepo) GetCollectionSummaryByToken(ctx context.Context, accessToken string) (ente.PublicCollectionSummary, error) {
|
||||
func (pcr *PublicCollectionRepository) GetCollectionSummaryByToken(ctx context.Context, accessToken string) (ente.PublicCollectionSummary, error) {
|
||||
row := pcr.DB.QueryRowContext(ctx,
|
||||
`SELECT sct.id, sct.collection_id, sct.is_disabled, sct.valid_till, sct.device_limit, sct.pw_hash,
|
||||
sct.created_at, sct.updated_at, count(ah.share_id)
|
||||
@@ -185,7 +185,7 @@ func (pcr *CollectionLinkRepo) GetCollectionSummaryByToken(ctx context.Context,
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (pcr *CollectionLinkRepo) GetActivePublicTokenForUser(ctx context.Context, userID int64) ([]int64, error) {
|
||||
func (pcr *PublicCollectionRepository) GetActivePublicTokenForUser(ctx context.Context, userID int64) ([]int64, error) {
|
||||
rows, err := pcr.DB.QueryContext(ctx, `select pt.collection_id from public_collection_tokens pt left join collections c on pt.collection_id = c.collection_id where pt.is_disabled = FALSE and c.owner_id= $1;`, userID)
|
||||
if err != nil {
|
||||
return nil, stacktrace.Propagate(err, "")
|
||||
@@ -204,7 +204,7 @@ func (pcr *CollectionLinkRepo) GetActivePublicTokenForUser(ctx context.Context,
|
||||
}
|
||||
|
||||
// CleanupAccessHistory public_collection_access_history where public_collection_tokens is disabled and the last updated time is older than 30 days
|
||||
func (pcr *CollectionLinkRepo) CleanupAccessHistory(ctx context.Context) error {
|
||||
func (pcr *PublicCollectionRepository) CleanupAccessHistory(ctx context.Context) error {
|
||||
_, err := pcr.DB.ExecContext(ctx, `DELETE FROM public_collection_access_history WHERE share_id IN (SELECT id FROM public_collection_tokens WHERE is_disabled = TRUE AND updated_at < (now_utc_micro_seconds() - (24::BIGINT * 30 * 60 * 60 * 1000 * 1000)))`)
|
||||
if err != nil {
|
||||
return stacktrace.Propagate(err, "failed to clean up public collection access history")
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/ente-io/museum/pkg/repo/public"
|
||||
"strings"
|
||||
|
||||
"github.com/ente-io/museum/ente"
|
||||
@@ -33,11 +32,10 @@ type FileWithUpdatedAt struct {
|
||||
}
|
||||
|
||||
type TrashRepository struct {
|
||||
DB *sql.DB
|
||||
ObjectRepo *ObjectRepository
|
||||
FileRepo *FileRepository
|
||||
QueueRepo *QueueRepository
|
||||
FileLinkRepo *public.FileLinkRepository
|
||||
DB *sql.DB
|
||||
ObjectRepo *ObjectRepository
|
||||
FileRepo *FileRepository
|
||||
QueueRepo *QueueRepository
|
||||
}
|
||||
|
||||
func (t *TrashRepository) InsertItems(ctx context.Context, tx *sql.Tx, userID int64, items []ente.TrashItemRequest) error {
|
||||
@@ -158,13 +156,6 @@ func (t *TrashRepository) TrashFiles(fileIDs []int64, userID int64, trash ente.T
|
||||
return stacktrace.Propagate(err, "")
|
||||
}
|
||||
err = tx.Commit()
|
||||
|
||||
if err == nil {
|
||||
removeLinkErr := t.FileLinkRepo.DisableLinkForFiles(ctx, fileIDs)
|
||||
if removeLinkErr != nil {
|
||||
return stacktrace.Propagate(removeLinkErr, "failed to disable file links for files being trashed")
|
||||
}
|
||||
}
|
||||
return stacktrace.Propagate(err, "")
|
||||
}
|
||||
|
||||
|
||||
@@ -17,9 +17,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
PublicAccessKey = "X-Public-Access-ID"
|
||||
FileLinkAccessKey = "X-Public-FileLink-Access-ID"
|
||||
CastContext = "X-Cast-Context"
|
||||
PublicAccessKey = "X-Public-Access-ID"
|
||||
CastContext = "X-Cast-Context"
|
||||
)
|
||||
|
||||
// GenerateRandomBytes returns securely generated random bytes.
|
||||
@@ -121,8 +120,6 @@ func GetCastToken(c *gin.Context) string {
|
||||
return token
|
||||
}
|
||||
|
||||
// GetAccessTokenJWT fetches the JWT access token from the request header or query parameters.
|
||||
// This token is issued by server on password verification of links that are protected by password.
|
||||
func GetAccessTokenJWT(c *gin.Context) string {
|
||||
token := c.GetHeader("X-Auth-Access-Token-JWT")
|
||||
if token == "" {
|
||||
@@ -135,10 +132,6 @@ func MustGetPublicAccessContext(c *gin.Context) ente.PublicAccessContext {
|
||||
return c.MustGet(PublicAccessKey).(ente.PublicAccessContext)
|
||||
}
|
||||
|
||||
func MustGetFileLinkAccessContext(c *gin.Context) *ente.FileLinkAccessContext {
|
||||
return c.MustGet(FileLinkAccessKey).(*ente.FileLinkAccessContext)
|
||||
}
|
||||
|
||||
func GetCastCtx(c *gin.Context) cast.AuthContext {
|
||||
return c.MustGet(CastContext).(cast.AuthContext)
|
||||
}
|
||||
|
||||