From ccd2a6b54ec1919f273c19de00a626b059640ade Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Mon, 19 Apr 2021 15:55:38 +0200 Subject: [PATCH 01/80] Mark further strings as untranslatable [ci skip] --- presentation/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 1d8d0ab2..e8129659 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -93,7 +93,7 @@ Move Empty folder - %1$s + %1$s modified %1$s ago Share with From ce74b4da113bc743778df1d0ab13f5dedb53ea0b Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Mon, 19 Apr 2021 16:02:13 +0200 Subject: [PATCH 02/80] New Crowdin updates --- presentation/src/main/res/values-es/strings.xml | 1 - presentation/src/main/res/values-fr/strings.xml | 1 - presentation/src/main/res/values-pl/strings.xml | 2 ++ 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/presentation/src/main/res/values-es/strings.xml b/presentation/src/main/res/values-es/strings.xml index ce79edd2..f88ab845 100644 --- a/presentation/src/main/res/values-es/strings.xml +++ b/presentation/src/main/res/values-es/strings.xml @@ -38,7 +38,6 @@ Nombre de caja fuerte: %1$s Mover Carpeta vacía - \"%1$s modificado hace %1$s Compartir con Elegir destino diff --git a/presentation/src/main/res/values-fr/strings.xml b/presentation/src/main/res/values-fr/strings.xml index 998efadd..09619bb9 100644 --- a/presentation/src/main/res/values-fr/strings.xml +++ b/presentation/src/main/res/values-fr/strings.xml @@ -64,7 +64,6 @@ Déplacer Dossier vide - %1$s Modifié il y à %1$s Partager avec Choisissez la destination diff --git a/presentation/src/main/res/values-pl/strings.xml b/presentation/src/main/res/values-pl/strings.xml index c1f0cdd8..a9979424 100644 --- a/presentation/src/main/res/values-pl/strings.xml +++ b/presentation/src/main/res/values-pl/strings.xml @@ -26,6 +26,7 @@ Nie udało się odszyfrować hasła WebDAV, proszę dodać je w ustawieniach Usługi Google Play nie są zainstalowane Przerwano biometryczną autoryzację + Lokalny plik nie jest już obecny po przełączeniu się z powrotem na Cryptomator. Możliwe zmiany nie mogą być przeniesione z powrotem do chmury. Pamięć wewnętrzna @@ -211,6 +212,7 @@ Zastąp Plik \'%1$s\' już istnieje. Czy chcesz go zastąpić? Pliki już istnieją. Czy chcesz go zastąpić? + %1$d pliki już istnieją. Czy chcesz je zastąpić? Zastąp plik? Zastąp pliki? Nie można udostępnić plików From 9ec774e3c711050be2656bfd19c3a6b401190a47 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Mon, 19 Apr 2021 16:04:43 +0200 Subject: [PATCH 03/80] Bump version to 1.5.15 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 93627bf2..a42b5112 100644 --- a/build.gradle +++ b/build.gradle @@ -42,7 +42,7 @@ allprojects { ext { androidApplicationId = 'org.cryptomator' androidVersionCode = getVersionCode() - androidVersionName = '1.6.0-SNAPSHOT' + androidVersionName = '1.5.15' } repositories { mavenCentral() From dc76322e610b5a96d49da0e3e224d830f5bf5a04 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Mon, 19 Apr 2021 18:19:57 +0200 Subject: [PATCH 04/80] Update release notes --- fastlane/metadata/android/de-DE/changelogs/default.txt | 9 +++++---- fastlane/metadata/android/en-US/changelogs/default.txt | 9 +++++---- fastlane/release-notes.html | 9 +++++---- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/fastlane/metadata/android/de-DE/changelogs/default.txt b/fastlane/metadata/android/de-DE/changelogs/default.txt index 5cdd56c1..c1d47ea9 100644 --- a/fastlane/metadata/android/de-DE/changelogs/default.txt +++ b/fastlane/metadata/android/de-DE/changelogs/default.txt @@ -1,4 +1,5 @@ -- Native pCloud-Unterstützung hinzugefügt (großen Dank an Manu für die Implementierung) -- App-Absturz beim Wiederherstellen von Cryptomator aus einem Backup behoben -- Verbesserte Anzeige von langen Einstellungen -- Verbessertes Löschen des letzten Bildes über die Vorschau. Springt jetzt zurück in die Tresor-Inhaltsliste \ No newline at end of file +- Added F-Droid repository +- Added Polish translation (thanks to FadeMind for this contribution) +- Enhanced tab order in modal dialogs when using hardware keyboards +- Fixed app crash on some devices when resuming Cryptomator after open file finished using 3party apps +- Fixed license screen bypass on rooted phones \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/default.txt b/fastlane/metadata/android/en-US/changelogs/default.txt index 30049bab..be8b74be 100644 --- a/fastlane/metadata/android/en-US/changelogs/default.txt +++ b/fastlane/metadata/android/en-US/changelogs/default.txt @@ -1,4 +1,5 @@ -- Added pCloud native support (thanks to Manu for this huge contribution) -- Fixed app crash when restoring Cryptomator from a backup -- Enhanced display of long settings -- Enhanced deletion of the last image via the preview. Now jumps back to the vault contents list \ No newline at end of file +- Added F-Droid repository +- Added Polish translation (thanks to FadeMind for this contribution) +- Enhanced tab order in modal dialogs when using hardware keyboards +- Fixed app crash on some devices when resuming Cryptomator after open file finished using 3party apps +- Fixed possibility to bypass the license screen on rooted phones \ No newline at end of file diff --git a/fastlane/release-notes.html b/fastlane/release-notes.html index f32f2534..848ec30a 100644 --- a/fastlane/release-notes.html +++ b/fastlane/release-notes.html @@ -1,6 +1,7 @@
    -
  • Added pCloud native support (thanks to Manu for this huge contribution)
  • -
  • Fixed app crash when restoring Cryptomator from a backup
  • -
  • Enhanced display of long settings
  • -
  • Enhanced deletion of the last image via the preview. Now jumps back to the vault contents list
  • +
  • Added F-Droid repository
  • +
  • Added Polish translation (thanks to FadeMind for this contribution)
  • +
  • Enhanced tab order in modal dialogs when using hardware keyboards
  • +
  • Fixed app crash on some devices when resuming Cryptomator after open file finished using 3party apps
  • +
  • Fixed possibility to bypass the license screen on rooted phones
\ No newline at end of file From c3f7761f3200ce673470d9319b481ee5c3465564 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Tue, 20 Apr 2021 10:28:25 +0200 Subject: [PATCH 05/80] =?UTF-8?q?Fix=20translation=20of=20release=20note?= =?UTF-8?q?=20=F0=9F=98=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [ci skip] --- fastlane/metadata/android/de-DE/changelogs/default.txt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/fastlane/metadata/android/de-DE/changelogs/default.txt b/fastlane/metadata/android/de-DE/changelogs/default.txt index c1d47ea9..b542d6db 100644 --- a/fastlane/metadata/android/de-DE/changelogs/default.txt +++ b/fastlane/metadata/android/de-DE/changelogs/default.txt @@ -1,5 +1,5 @@ -- Added F-Droid repository -- Added Polish translation (thanks to FadeMind for this contribution) -- Enhanced tab order in modal dialogs when using hardware keyboards -- Fixed app crash on some devices when resuming Cryptomator after open file finished using 3party apps -- Fixed license screen bypass on rooted phones \ No newline at end of file +- F-Droid-Repository hinzugefügt +- Polnische Übersetzung hinzugefügt (Vielen Dank dafür an FadeMind) +- Verbesserte Tab-Reihenfolge in modalen Dialogen bei Verwendung von Hardware-Tastaturen +- App-Absturz auf einigen Geräten behoben, wenn Cryptomator nach Beendigung des Öffnens einer Datei unter Verwendung von 3-Party-Apps fortgesetzt wird +- Möglichkeit zur Umgehung des Lizenzbildschirms auf gerooteten Geräten behoben \ No newline at end of file From d0298a2fdbe519ce6e923c6c7733772e5f465b75 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Tue, 20 Apr 2021 10:31:14 +0200 Subject: [PATCH 06/80] New Crowdin updates [ci skip] --- presentation/src/main/res/values-fr/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/presentation/src/main/res/values-fr/strings.xml b/presentation/src/main/res/values-fr/strings.xml index 09619bb9..a36ceb00 100644 --- a/presentation/src/main/res/values-fr/strings.xml +++ b/presentation/src/main/res/values-fr/strings.xml @@ -26,6 +26,7 @@ Le mot de passe WebDAV n\'a pas été déchiffré, veuillez l\'ajouter une nouvelle fois dans les paramètres Services Google play non installés Authentification biométrique avortée + Le fichier local n\'est plus présent après le retour à Cryptomator. Les éventuels modifications ne peuvent être propagées au nuage. Stockage local From 7b56cef05cb78f120e8c6bec5ad52ad086cec4fe Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Tue, 20 Apr 2021 15:00:22 +0200 Subject: [PATCH 07/80] Bump version to 1.6.0-SNAPSHOT --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index a42b5112..93627bf2 100644 --- a/build.gradle +++ b/build.gradle @@ -42,7 +42,7 @@ allprojects { ext { androidApplicationId = 'org.cryptomator' androidVersionCode = getVersionCode() - androidVersionName = '1.5.15' + androidVersionName = '1.6.0-SNAPSHOT' } repositories { mavenCentral() From 9d17854eaab14a6478c3594d0992fcb0a71e065b Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Wed, 21 Apr 2021 02:29:13 +0200 Subject: [PATCH 08/80] #303 Re-add LicensesFragment --- .../presentation/ui/fragment/LicensesFragment.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 presentation/src/main/java/org/cryptomator/presentation/ui/fragment/LicensesFragment.kt diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/LicensesFragment.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/LicensesFragment.kt new file mode 100644 index 00000000..4019a459 --- /dev/null +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/LicensesFragment.kt @@ -0,0 +1,13 @@ +package org.cryptomator.presentation.ui.fragment + +import android.os.Bundle +import androidx.preference.PreferenceFragmentCompat +import org.cryptomator.presentation.R + +// Don't delete this file as it isn't unused but referenced by layout file +class LicensesFragment : PreferenceFragmentCompat() { + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.licenses) + } +} From 3cb136171b4aaf2b7fdff2f9df154554797c70f8 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Fri, 23 Apr 2021 16:13:11 +0200 Subject: [PATCH 09/80] Add pCloud and S3 to Issue template --- .github/ISSUE_TEMPLATE/bug.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index 53d94eee..b0d1c1e5 100644 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -20,7 +20,7 @@ Please make sure to: * Android version: [Shown in the settings of Android] * Cryptomator version: [Shown in the settings of Cryptomator] -* Cloud type: [Dropbox/Google Drive/OneDrive/WebDAV/Local storage] +* Cloud type: [Dropbox/Google Drive/OneDrive/pCloud/WebDAV/S3/Local storage] ### Steps to Reproduce From fc6774d534ec9df9f644598c3649f9c1d2cc6e32 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Fri, 23 Apr 2021 16:59:42 +0200 Subject: [PATCH 10/80] Update to latest version of pcloud-sdk-java --- pcloud-sdk-java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pcloud-sdk-java b/pcloud-sdk-java index d12c6e6c..ce8e4bb6 160000 --- a/pcloud-sdk-java +++ b/pcloud-sdk-java @@ -1 +1 @@ -Subproject commit d12c6e6c4af8d0360812900663d5298ca093377b +Subproject commit ce8e4bb68ee4c36de0917230d44bb47f1488bd6a From be12aa6122366898bc9d1ae67ea7cff61dc20a44 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Fri, 23 Apr 2021 17:18:27 +0200 Subject: [PATCH 11/80] Update to latest version of pcloud-sdk-java --- pcloud-sdk-java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pcloud-sdk-java b/pcloud-sdk-java index ce8e4bb6..c99ebf65 160000 --- a/pcloud-sdk-java +++ b/pcloud-sdk-java @@ -1 +1 @@ -Subproject commit ce8e4bb68ee4c36de0917230d44bb47f1488bd6a +Subproject commit c99ebf651c18dd5a667dc4ecb106c3e43665cc6c From c9489a37950bd6aab766862001f9f4010c0ec37b Mon Sep 17 00:00:00 2001 From: Manuel Jenny Date: Fri, 23 Apr 2021 16:34:11 +0200 Subject: [PATCH 12/80] fix(pCloud): issue #305 NullReference Fix #305: NullReference when calling loadFolder with empty string --- .../org/cryptomator/data/cloud/pcloud/PCloudImpl.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudImpl.java b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudImpl.java index 22e077a2..1bee544f 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudImpl.java +++ b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudImpl.java @@ -109,7 +109,9 @@ class PCloudImpl { public boolean exists(PCloudNode node) throws IOException, BackendException { try { - if (node instanceof PCloudFolder) { + if (node instanceof RootPCloudFolder) { + client().loadFolder("/").execute(); + } else if (node instanceof PCloudFolder) { client().loadFolder(node.getPath()).execute(); } else { client().loadFile(node.getPath()).execute(); @@ -124,8 +126,13 @@ class PCloudImpl { public List list(PCloudFolder folder) throws IOException, BackendException { List result = new ArrayList<>(); + String path = folder.getPath(); + if (folder instanceof RootPCloudFolder) { + path = "/"; + } + try { - RemoteFolder listFolderResult = client().listFolder(folder.getPath()).execute(); + RemoteFolder listFolderResult = client().listFolder(path).execute(); List entryMetadata = listFolderResult.children(); for (RemoteEntry metadata : entryMetadata) { result.add(PCloudNodeFactory.from(folder, metadata)); From 6be01180f42075ff2dd964d7c2e6a45a871fca9f Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Fri, 23 Apr 2021 17:39:44 +0200 Subject: [PATCH 13/80] Update release notes [ci skip] --- fastlane/metadata/android/de-DE/changelogs/default.txt | 7 ++----- fastlane/metadata/android/en-US/changelogs/default.txt | 7 ++----- fastlane/release-notes.html | 7 ++----- 3 files changed, 6 insertions(+), 15 deletions(-) diff --git a/fastlane/metadata/android/de-DE/changelogs/default.txt b/fastlane/metadata/android/de-DE/changelogs/default.txt index b542d6db..4fc6bd79 100644 --- a/fastlane/metadata/android/de-DE/changelogs/default.txt +++ b/fastlane/metadata/android/de-DE/changelogs/default.txt @@ -1,5 +1,2 @@ -- F-Droid-Repository hinzugefügt -- Polnische Übersetzung hinzugefügt (Vielen Dank dafür an FadeMind) -- Verbesserte Tab-Reihenfolge in modalen Dialogen bei Verwendung von Hardware-Tastaturen -- App-Absturz auf einigen Geräten behoben, wenn Cryptomator nach Beendigung des Öffnens einer Datei unter Verwendung von 3-Party-Apps fortgesetzt wird -- Möglichkeit zur Umgehung des Lizenzbildschirms auf gerooteten Geräten behoben \ No newline at end of file +- Fehler beim Erstellen neuer Tresore in pCloud behoben +- Fehler in der Lizenz-Anzeige der Einstellungen behoben \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/default.txt b/fastlane/metadata/android/en-US/changelogs/default.txt index be8b74be..e0479683 100644 --- a/fastlane/metadata/android/en-US/changelogs/default.txt +++ b/fastlane/metadata/android/en-US/changelogs/default.txt @@ -1,5 +1,2 @@ -- Added F-Droid repository -- Added Polish translation (thanks to FadeMind for this contribution) -- Enhanced tab order in modal dialogs when using hardware keyboards -- Fixed app crash on some devices when resuming Cryptomator after open file finished using 3party apps -- Fixed possibility to bypass the license screen on rooted phones \ No newline at end of file +- Fixed creating new vaults in pCloud +- Fixed license screen in settings \ No newline at end of file diff --git a/fastlane/release-notes.html b/fastlane/release-notes.html index 848ec30a..8ff27481 100644 --- a/fastlane/release-notes.html +++ b/fastlane/release-notes.html @@ -1,7 +1,4 @@
    -
  • Added F-Droid repository
  • -
  • Added Polish translation (thanks to FadeMind for this contribution)
  • -
  • Enhanced tab order in modal dialogs when using hardware keyboards
  • -
  • Fixed app crash on some devices when resuming Cryptomator after open file finished using 3party apps
  • -
  • Fixed possibility to bypass the license screen on rooted phones
  • +
  • Fixed creating new vaults in pCloud
  • +
  • Fixed license screen in settings
\ No newline at end of file From 681f7743a5c28c91267dab124921ec8b78fef3f5 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Fri, 23 Apr 2021 17:42:01 +0200 Subject: [PATCH 14/80] Bump version to 1.5.16 [ci skip] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 93627bf2..cdb486f9 100644 --- a/build.gradle +++ b/build.gradle @@ -42,7 +42,7 @@ allprojects { ext { androidApplicationId = 'org.cryptomator' androidVersionCode = getVersionCode() - androidVersionName = '1.6.0-SNAPSHOT' + androidVersionName = '1.5.16' } repositories { mavenCentral() From ee131a5972cc34ab51e6e654e4ea8e1e2757f234 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Fri, 23 Apr 2021 17:50:52 +0200 Subject: [PATCH 15/80] New Crowdin updates [ci skip] --- .../src/main/res/values-pl/strings.xml | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/presentation/src/main/res/values-pl/strings.xml b/presentation/src/main/res/values-pl/strings.xml index a9979424..0742a7cf 100644 --- a/presentation/src/main/res/values-pl/strings.xml +++ b/presentation/src/main/res/values-pl/strings.xml @@ -20,7 +20,7 @@ Eksport nie powiódł się. Spróbuj usunąć znaki specjalne z nazw plików i spróbuj ponownie. Nie może zawierać znaków specjalnych. Nazwa pliku nie może zawierać znaków specjalnych. - Nazwa skarbca nie może zawierać znaków specjalnych. + Nazwa sejfu nie może zawierać znaków specjalnych. Błąd sprawdzania aktualizacji. Wystąpił błąd ogólny. Błąd sprawdzania aktualizacji. Brak połączenia z Internetem. Nie udało się odszyfrować hasła WebDAV, proszę dodać je w ustawieniach @@ -58,7 +58,7 @@ Sejf Wybierz plik klucza głównego Umieść tutaj - Nazwa skarbca: %1$s + Nazwa sejfu: %1$s Przenieś Pusty folder zmodyfikowano %1$s temu @@ -118,8 +118,8 @@ Login nie może być pusty. Hasło nie może być puste. - Nazwa skarbca nie może być pusta. - Nazwa skarbca + Nazwa sejfu nie może być pusta. + Nazwa sejfu Utwórz Ustaw hasło @@ -174,7 +174,7 @@ Przyspiesz odblokowanie Pobierz konfigurację sejfu w tle, gdy zostaniesz poproszony o wprowadzenie hasła lub autoryzację biometryczną Zachowaj odblokowany - Zachowaj odblokowane skarbce podczas edycji plików + Zachowaj odblokowane sejfy podczas edycji plików Połączenia WebDAV Połączenia pCloud @@ -187,7 +187,7 @@ \'%1$s\' jest nieosiągalne Cryptomator wykrył, że ten folder jest nieosiągalny. - Być może został usunięty przez inną aplikację lub wystąpiła nieprawidłowa synchronizacja z usługą chmury. \n\nSpróbuj przywrócić plik katalogu za pośrednictwem dostawcy chmury do poprzedniej wersji, która nie jest pusta. Omawiany plik to:\n%1$s\n\nJeśli to nie zadziała, możesz użyć Sanitizera do sprawdzenia swojego skarbca pod kątem problemów i ewentualnie przywrócić dane. + Być może został usunięty przez inną aplikację lub wystąpiła nieprawidłowa synchronizacja z usługą chmury. \n\nSpróbuj przywrócić plik katalogu za pośrednictwem dostawcy chmury do poprzedniej wersji, która nie jest pusta. Omawiany plik to:\n%1$s\n\nJeśli to nie zadziała, możesz użyć Sanitizera do sprawdzenia swojego sejfu pod kątem problemów i ewentualnie przywrócić dane. Więcej szczegółów na temat Sanitizera @@ -200,7 +200,7 @@ Nowe hasło nie może być puste. Hasła nie zgadzają się. - Skarbca %1$s nie odnaleziono + Sejfu %1$s nie odnaleziono Sejf został przeniesiony, usunięty albo zmieniono jego nazwę. Należy usunąć ten sejf z listy i dodać go ponownie. Chcesz to zrobić teraz? Usuń Plik już istnieje @@ -216,12 +216,12 @@ Zastąp plik? Zastąp pliki? Nie można udostępnić plików - Nie wczytałeś żadnych skarbców. Proszę najpierw utworzyć nowy sejf z aplikacją Cryptomator. + Nie wczytałeś żadnych sejfów. Proszę najpierw utworzyć nowy sejf z aplikacją Cryptomator. OK Utwórz sejf Nie można otworzyć %1$s Pobierz aplikację, która może otworzyć ten plik, a może chcesz zapisać ten plik na swoim urządzeniu? - Zmień nazwę skarbca + Zmień nazwę sejfu Zmień nazwę folderu Zmień nazwę pliku Istnieją niezapisane zmiany @@ -239,9 +239,9 @@ Uwierzytelnianie… Zmiana nazwy… Usuwanie… - Odblokowanie skarbca… + Odblokowanie sejfu… Zmiana hasła… - Tworzenie skarbca… + Tworzenie sejfu… Przesyłanie… Pobieranie… Szyfrowanie… @@ -271,7 +271,7 @@ Zamknij To ustawienie jest funkcją bezpieczeństwa i uniemożliwia innym aplikacjom oszukiwanie użytkowników do robienia rzeczy, których nie chcą robić.\n\nWyłączając je potwierdzasz, że jesteś świadomy ryzyka. Czy na pewno chcesz usunąć to połączenie z serwerem chmury? - Ta akcja usunie połączenie z usługą chmury i wszystkie skarbce w tej chmurze. + Ta akcja usunie połączenie z usługą chmury i wszystkimi sejfami w tej chmurze. Usunąć %1$d elementów? Na pewno chcesz usunąć wybrane elementy? Na pewno chcesz usunąć ten plik? @@ -298,9 +298,9 @@ Nie można załadować zawartości katalogu Folder \'%1$s\' w chmurze nie ma pliku katalogowego. Być może folder został utworzony na innym urządzeniu i nie został jeszcze w pełni zsynchronizowany z chmurą. Sprawdź w chmurze, czy następujący plik istnieje:\n%2$s Wersja Beta - To jest wydanie beta wprowadzająca obsługę formatu 7 skarbca. Przed kontynuowaniem upewnij się, że masz kopię zapasową skarbca oraz nie używasz wersji jego wersji produkcyjnej. + To jest wydanie beta wprowadzająca obsługę formatu 7 sejfu. Przed kontynuowaniem upewnij się, że masz kopię zapasową skarbca oraz nie używasz wersji jego wersji produkcyjnej. Brak obrazów do wyświetlenia… - Cryptomator potrzebuje dostępu do pamięci lokalnej, aby uzyskać dostęp do skarbca + Cryptomator potrzebuje dostępu do pamięci lokalnej, aby uzyskać dostęp do sejfu Cryptomator potrzebuje dostępu do pamięci lokalnej, aby automatycznie przesyłać zdjęcia @@ -329,17 +329,17 @@ Logowanie biometryczne Zaloguj się przy użyciu danych biometrycznych - Użyj hasła skarbca + Użyj hasła sejfu Nie można automatycznie przesłać plików - Odblokowane skarbce: %1$d + Odblokowane sejfy: %1$d Automatyczna blokada w %1$s Zablokuj wszystko Anuluj przesyłanie Automatyczne przesyłanie zdjęć w toku Przesyłanie %1d/%2d Automatyczne przesyłanie zdjęć zostało zakończone - %1$d zdjęć zostało przesłanych do skarbca + %1$d zdjęć zostało przesłanych do sejfu Automatyczne przesyłanie zdjęć nie powiodło się Wystąpił błąd podczas przesyłania. Wybrany folder do przesłania nie jest już dostępny. Przejdź do ustawień i wybierz nowy From 9a1d9b7e61b5999f2a9808f700f261651d2a587a Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Fri, 23 Apr 2021 18:18:08 +0200 Subject: [PATCH 16/80] Bump version to 1.6.0-SNAPSHOT [ci skip] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index cdb486f9..93627bf2 100644 --- a/build.gradle +++ b/build.gradle @@ -42,7 +42,7 @@ allprojects { ext { androidApplicationId = 'org.cryptomator' androidVersionCode = getVersionCode() - androidVersionName = '1.5.16' + androidVersionName = '1.6.0-SNAPSHOT' } repositories { mavenCentral() From 4f8f8178085754e8d143a080643c970e541cb797 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Sat, 24 Apr 2021 23:37:07 +0200 Subject: [PATCH 17/80] #307 fix pCloud login in F-Droid --- .../CloudContentRepositoryFactories.java | 3 + .../presenter/AuthenticateCloudPresenter.kt | 135 +++++++++++++++++- 2 files changed, 132 insertions(+), 6 deletions(-) diff --git a/data/src/foss/java/org/cryptomator/data/cloud/CloudContentRepositoryFactories.java b/data/src/foss/java/org/cryptomator/data/cloud/CloudContentRepositoryFactories.java index 277858d2..f2bb16fa 100644 --- a/data/src/foss/java/org/cryptomator/data/cloud/CloudContentRepositoryFactories.java +++ b/data/src/foss/java/org/cryptomator/data/cloud/CloudContentRepositoryFactories.java @@ -4,6 +4,7 @@ import org.cryptomator.data.cloud.crypto.CryptoCloudContentRepositoryFactory; import org.cryptomator.data.cloud.dropbox.DropboxCloudContentRepositoryFactory; import org.cryptomator.data.cloud.local.LocalStorageContentRepositoryFactory; import org.cryptomator.data.cloud.onedrive.OnedriveCloudContentRepositoryFactory; +import org.cryptomator.data.cloud.pcloud.PCloudContentRepositoryFactory; import org.cryptomator.data.cloud.webdav.WebDavCloudContentRepositoryFactory; import org.cryptomator.data.repository.CloudContentRepositoryFactory; import org.jetbrains.annotations.NotNull; @@ -23,12 +24,14 @@ public class CloudContentRepositoryFactories implements Iterable(exceptionHandlers) { @@ -44,6 +70,7 @@ class AuthenticateCloudPresenter @Inject constructor( // private val strategies = arrayOf( // DropboxAuthStrategy(), // OnedriveAuthStrategy(), // + PCloudAuthStrategy(), // WebDAVAuthStrategy(), // LocalStorageAuthStrategy() // ) @@ -221,6 +248,102 @@ class AuthenticateCloudPresenter @Inject constructor( // } } + private inner class PCloudAuthStrategy : AuthStrategy { + + private var authenticationStarted = false + + override fun supports(cloud: CloudModel): Boolean { + return cloud.cloudType() == CloudTypeModel.PCLOUD + } + + override fun resumed(intent: AuthenticateCloudIntent) { + when { + ExceptionUtil.contains(intent.error(), WrongCredentialsException::class.java) -> { + if (!authenticationStarted) { + startAuthentication() + Toast.makeText( + context(), + String.format(getString(R.string.error_authentication_failed_re_authenticate), intent.cloud().username()), + Toast.LENGTH_LONG).show() + } + } + else -> { + Timber.tag("AuthicateCloudPrester").e(intent.error()) + failAuthentication(intent.cloud().name()) + } + } + } + + private fun startAuthentication() { + authenticationStarted = true + val authIntent: Intent = AuthorizationActivity.createIntent( + context(), + AuthorizationRequest.create() + .setType(AuthorizationRequest.Type.TOKEN) + .setClientId(BuildConfig.PCLOUD_CLIENT_ID) + .setForceAccessApproval(true) + .addPermission("manageshares") + .build()) + requestActivityResult(ActivityResultCallbacks.pCloudReAuthenticationFinished(), // + authIntent) + } + } + + @Callback + fun pCloudReAuthenticationFinished(activityResult: ActivityResult) { + val authData: AuthorizationData = AuthorizationActivity.getResult(activityResult.intent()) + val result: AuthorizationResult = authData.result + + when (result) { + AuthorizationResult.ACCESS_GRANTED -> { + val accessToken: String = CredentialCryptor // + .getInstance(context()) // + .encrypt(authData.token) + val pCloudSkeleton: PCloud = PCloud.aPCloud() // + .withAccessToken(accessToken) + .withUrl(authData.apiHost) + .build(); + getUsernameUseCase // + .withCloud(pCloudSkeleton) // + .run(object : DefaultResultHandler() { + override fun onSuccess(username: String?) { + prepareForSavingPCloud(PCloud.aCopyOf(pCloudSkeleton).withUsername(username).build()) + } + }) + } + AuthorizationResult.ACCESS_DENIED -> { + Timber.tag("CloudConnListPresenter").e("Account access denied") + view?.showMessage(String.format(getString(R.string.screen_authenticate_auth_authentication_failed), getString(R.string.cloud_names_pcloud))) + } + AuthorizationResult.AUTH_ERROR -> { + Timber.tag("CloudConnListPresenter").e("""Account access grant error: ${authData.errorMessage}""".trimIndent()) + view?.showMessage(String.format(getString(R.string.screen_authenticate_auth_authentication_failed), getString(R.string.cloud_names_pcloud))) + } + AuthorizationResult.CANCELLED -> { + Timber.tag("CloudConnListPresenter").i("Account access grant cancelled") + view?.showMessage(String.format(getString(R.string.screen_authenticate_auth_authentication_failed), getString(R.string.cloud_names_pcloud))) + } + } + } + + fun prepareForSavingPCloud(cloud: PCloud) { + getCloudsUseCase // + .withCloudType(cloud.type()) // + .run(object : DefaultResultHandler>() { + override fun onSuccess(clouds: List) { + clouds.firstOrNull { + (it as PCloud).username() == cloud.username() + }?.let { + it as PCloud + succeedAuthenticationWith(PCloud.aCopyOf(it) // + .withUrl(cloud.url()) + .withAccessToken(cloud.accessToken()) + .build()) + } ?: succeedAuthenticationWith(cloud) + } + }) + } + private inner class WebDAVAuthStrategy : AuthStrategy { override fun supports(cloud: CloudModel): Boolean { @@ -342,6 +465,6 @@ class AuthenticateCloudPresenter @Inject constructor( // } init { - unsubscribeOnDestroy(addOrChangeCloudConnectionUseCase, getUsernameUseCase) + unsubscribeOnDestroy(addOrChangeCloudConnectionUseCase, getCloudsUseCase, getUsernameUseCase) } } From 67af7af5e2076614bf119d5a6a270dfa2f207c95 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Sat, 24 Apr 2021 23:44:02 +0200 Subject: [PATCH 18/80] Update release notes [ci skip] --- fastlane/metadata/android/de-DE/changelogs/default.txt | 3 +-- fastlane/metadata/android/en-US/changelogs/default.txt | 3 +-- fastlane/release-notes.html | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/fastlane/metadata/android/de-DE/changelogs/default.txt b/fastlane/metadata/android/de-DE/changelogs/default.txt index 4fc6bd79..e15151b6 100644 --- a/fastlane/metadata/android/de-DE/changelogs/default.txt +++ b/fastlane/metadata/android/de-DE/changelogs/default.txt @@ -1,2 +1 @@ -- Fehler beim Erstellen neuer Tresore in pCloud behoben -- Fehler in der Lizenz-Anzeige der Einstellungen behoben \ No newline at end of file +- Problem bei der pCloud-Anmeldung in der F-Droid-Variante behoben \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/default.txt b/fastlane/metadata/android/en-US/changelogs/default.txt index e0479683..8dfdac01 100644 --- a/fastlane/metadata/android/en-US/changelogs/default.txt +++ b/fastlane/metadata/android/en-US/changelogs/default.txt @@ -1,2 +1 @@ -- Fixed creating new vaults in pCloud -- Fixed license screen in settings \ No newline at end of file +- Fixed pCloud login using F-Droid \ No newline at end of file diff --git a/fastlane/release-notes.html b/fastlane/release-notes.html index 8ff27481..faa77e7b 100644 --- a/fastlane/release-notes.html +++ b/fastlane/release-notes.html @@ -1,4 +1,3 @@
    -
  • Fixed creating new vaults in pCloud
  • -
  • Fixed license screen in settings
  • +
  • Fixed pCloud login using F-Droid
\ No newline at end of file From 5c8b006c3a72d8ff3c17b957017b81cf4f9f0ad6 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Sat, 24 Apr 2021 23:46:27 +0200 Subject: [PATCH 19/80] Bump version to 1.5.17 [ci skip] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 93627bf2..55e719ae 100644 --- a/build.gradle +++ b/build.gradle @@ -42,7 +42,7 @@ allprojects { ext { androidApplicationId = 'org.cryptomator' androidVersionCode = getVersionCode() - androidVersionName = '1.6.0-SNAPSHOT' + androidVersionName = '1.5.17' } repositories { mavenCentral() From 02486eeb2c3cf01e51eb624882129905144c1b17 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Sun, 25 Apr 2021 00:50:13 +0200 Subject: [PATCH 20/80] Bump version to 1.6.0-SNAPSHOT [ci skip] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 55e719ae..93627bf2 100644 --- a/build.gradle +++ b/build.gradle @@ -42,7 +42,7 @@ allprojects { ext { androidApplicationId = 'org.cryptomator' androidVersionCode = getVersionCode() - androidVersionName = '1.5.17' + androidVersionName = '1.6.0-SNAPSHOT' } repositories { mavenCentral() From 79a0d5daa60f2698e9403e1ed9c9bfa78248682e Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Mon, 26 Apr 2021 15:20:56 +0200 Subject: [PATCH 21/80] #308 fixes files not found using the search under certain circumstances The search is not successful if globbing is enabled and live search is disabled --- .../presentation/ui/activity/BrowseFilesActivity.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/BrowseFilesActivity.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/BrowseFilesActivity.kt index 210cb3c6..04233a81 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/BrowseFilesActivity.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/BrowseFilesActivity.kt @@ -46,7 +46,6 @@ import org.cryptomator.presentation.ui.dialog.SymLinkDialog import org.cryptomator.presentation.ui.dialog.UploadCloudFileDialog import org.cryptomator.presentation.ui.fragment.BrowseFilesFragment import java.util.ArrayList -import java.util.Locale import java.util.regex.Pattern import javax.inject.Inject import kotlinx.android.synthetic.main.toolbar_layout.toolbar @@ -545,7 +544,7 @@ class BrowseFilesActivity : BaseActivity(), // } override fun onQueryTextSubmit(query: String?): Boolean { - updateFilter(query?.toLowerCase(Locale.getDefault())) + updateFilter(query) return false } From af051ccad0e882ac62c6308ad195be5c471263ba Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Mon, 26 Apr 2021 15:31:33 +0200 Subject: [PATCH 22/80] Update dependencies --- buildsystem/dependencies.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/buildsystem/dependencies.gradle b/buildsystem/dependencies.gradle index 3c0d40c6..0e89b586 100644 --- a/buildsystem/dependencies.gradle +++ b/buildsystem/dependencies.gradle @@ -26,7 +26,7 @@ ext { rxAndroidVersion = '2.1.1' rxBindingVersion = '2.2.0' - daggerVersion = '2.34.1' + daggerVersion = '2.35' gsonVersion = '2.8.6' @@ -37,7 +37,7 @@ ext { timberVersion = '4.7.1' - zxcvbnVersion = '1.4.1' + zxcvbnVersion = '1.5.0' scaleImageViewVersion = '3.10.0' From e5382b6d52032805513f913b41797d6954669718 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Thu, 29 Apr 2021 20:51:16 +0200 Subject: [PATCH 23/80] #278 Forbid to create a vault with a very bad password --- .../ui/dialog/ChangePasswordDialog.kt | 13 ++++++++----- .../ui/fragment/SetPasswordFragment.kt | 5 +++-- .../presentation/util/PasswordStrength.kt | 16 ++++++++++++---- .../presentation/util/PasswordStrengthUtil.java | 7 +++++-- presentation/src/main/res/values/strings.xml | 2 +- 5 files changed, 29 insertions(+), 14 deletions(-) diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/ChangePasswordDialog.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/ChangePasswordDialog.kt index 73a0e90e..a9942b62 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/ChangePasswordDialog.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/ChangePasswordDialog.kt @@ -51,6 +51,14 @@ class ChangePasswordDialog : BaseProgressErrorDialog et_new_retype_password.nextFocusForwardId = button.id } + + registerOnEditorDoneActionAndPerformButtonClick(et_new_retype_password) { changePasswordButton } + + PasswordStrengthUtil() // + .startUpdatingPasswordStrengthMeter(et_new_password, // + progressBarPwStrengthIndicator, // + textViewPwStrengthIndicator, // + changePasswordButton) } } @@ -83,11 +91,6 @@ class ChangePasswordDialog : BaseProgressErrorDialog): PasswordStrength { - return if (password.isEmpty()) { - EMPTY - } else { - forScore(zxcvbn.measure(password, sanitizedInputs).score).orElse(EMPTY) + return when { + password.isEmpty() -> { + EMPTY + } + password.length < MIN_PASSWORD_LENGTH -> { + EXTREMELY_WEAK + } + else -> { + forScore(zxcvbn.measure(password, sanitizedInputs).score).orElse(EMPTY) + } } } diff --git a/presentation/src/main/java/org/cryptomator/presentation/util/PasswordStrengthUtil.java b/presentation/src/main/java/org/cryptomator/presentation/util/PasswordStrengthUtil.java index 7b6a9589..7ecaa252 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/util/PasswordStrengthUtil.java +++ b/presentation/src/main/java/org/cryptomator/presentation/util/PasswordStrengthUtil.java @@ -1,6 +1,7 @@ package org.cryptomator.presentation.util; import android.graphics.PorterDuff; +import android.widget.Button; import android.widget.EditText; import android.widget.ProgressBar; import android.widget.TextView; @@ -40,9 +41,10 @@ public class PasswordStrengthUtil { public PasswordStrengthUtil() { } - public void startUpdatingPasswortStrengthMeter(EditText passwordInput, // + public void startUpdatingPasswordStrengthMeter(EditText passwordInput, // final ProgressBar strengthMeter, // - final TextView strengthLabel) { + final TextView strengthLabel, // + final Button button) { RxTextView.textChanges(passwordInput) // .observeOn(Schedulers.computation()) // .map(password -> PasswordStrength.Companion.forPassword(password.toString(), SANITIZED_INPUTS)) // @@ -51,6 +53,7 @@ public class PasswordStrengthUtil { strengthMeter.getProgressDrawable().setColorFilter(ResourceHelper.Companion.getColor(strength.getColor()), PorterDuff.Mode.SRC_IN); strengthLabel.setText(strength.getDescription()); strengthMeter.setProgress(strength.getScore() + 1); + button.setEnabled(strength.getScore() > PasswordStrength.EXTREMELY_WEAK.getScore()); }); } } diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index e8129659..85c2392a 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -187,7 +187,7 @@ @string/screen_webdav_settings_password_label Retype password - Very weak + Too weak to create a vault Weak Fair Strong From 0224532c458b00707295965341523e52cc39785e Mon Sep 17 00:00:00 2001 From: Manuel Jenny Date: Fri, 16 Apr 2021 14:00:28 +0200 Subject: [PATCH 24/80] feat(S3): add AWS S3 SDK dependency --- buildsystem/dependencies.gradle | 3 +++ data/build.gradle | 1 + 2 files changed, 4 insertions(+) diff --git a/buildsystem/dependencies.gradle b/buildsystem/dependencies.gradle index 0e89b586..e37d1608 100644 --- a/buildsystem/dependencies.gradle +++ b/buildsystem/dependencies.gradle @@ -51,6 +51,8 @@ ext { // do not update to 1.4.0 until minsdk is 7.x (or desugaring works better) otherwise it will crash on 6.x cryptolibVersion = '1.3.0' + awsS3Sdk = '1.11.999' + dropboxVersion = '4.0.0' googleApiServicesVersion = 'v3-rev197-1.25.0' @@ -101,6 +103,7 @@ ext { androidxViewpager : "androidx.viewpager:viewpager:${androidxViewpagerVersion}", androidxSwiperefresh : "androidx.swiperefreshlayout:swiperefreshlayout:${androidxSwiperefreshVersion}", androidxPreference : "androidx.preference:preference:${androidxPreferenceVersion}", + awsS3 : "com.amazonaws:aws-java-sdk-s3:${awsS3Sdk}", documentFile : "androidx.documentfile:documentfile:${androidxDocumentfileVersion}", recyclerView : "androidx.recyclerview:recyclerview:${androidxRecyclerViewVersion}", androidxTestCore : "androidx.test:core:${androidxTestCoreVersion}", diff --git a/data/build.gradle b/data/build.gradle index 2187a71f..e1437074 100644 --- a/data/build.gradle +++ b/data/build.gradle @@ -99,6 +99,7 @@ dependencies { annotationProcessor dependencies.daggerCompiler implementation dependencies.dagger // cloud + implementation dependencies.awsS3 implementation dependencies.dropbox implementation dependencies.msgraph From 00a63228c19e71e2f526dcc287c3d350b04f24c0 Mon Sep 17 00:00:00 2001 From: Manuel Jenny Date: Fri, 16 Apr 2021 14:09:05 +0200 Subject: [PATCH 25/80] feat(S3): initial structure (non functional S3Impl) --- .../data/cloud/s3/RootS3Folder.java | 24 ++ .../data/cloud/s3/S3ClientFactory.java | 39 ++ .../cloud/s3/S3CloudContentRepository.java | 193 +++++++++ .../s3/S3CloudContentRepositoryFactory.java | 35 ++ .../data/cloud/s3/S3CloudNodeFactory.java | 47 +++ .../org/cryptomator/data/cloud/s3/S3File.java | 55 +++ .../cryptomator/data/cloud/s3/S3Folder.java | 42 ++ .../org/cryptomator/data/cloud/s3/S3Impl.java | 370 ++++++++++++++++++ .../org/cryptomator/data/cloud/s3/S3Node.java | 10 + .../java/org/cryptomator/domain/S3Cloud.java | 167 ++++++++ 10 files changed, 982 insertions(+) create mode 100644 data/src/main/java/org/cryptomator/data/cloud/s3/RootS3Folder.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/s3/S3ClientFactory.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudContentRepository.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudContentRepositoryFactory.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudNodeFactory.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/s3/S3File.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/s3/S3Folder.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/s3/S3Node.java create mode 100644 domain/src/main/java/org/cryptomator/domain/S3Cloud.java diff --git a/data/src/main/java/org/cryptomator/data/cloud/s3/RootS3Folder.java b/data/src/main/java/org/cryptomator/data/cloud/s3/RootS3Folder.java new file mode 100644 index 00000000..e52be7eb --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/s3/RootS3Folder.java @@ -0,0 +1,24 @@ +package org.cryptomator.data.cloud.s3; + +import org.cryptomator.domain.Cloud; +import org.cryptomator.domain.S3Cloud; + +class RootS3Folder extends S3Folder { + + private final S3Cloud cloud; + + public RootS3Folder(S3Cloud cloud) { + super(null, "", ""); + this.cloud = cloud; + } + + @Override + public S3Cloud getCloud() { + return cloud; + } + + @Override + public S3Folder withCloud(Cloud cloud) { + return new RootS3Folder((S3Cloud) cloud); + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/s3/S3ClientFactory.java b/data/src/main/java/org/cryptomator/data/cloud/s3/S3ClientFactory.java new file mode 100644 index 00000000..208ff27c --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/s3/S3ClientFactory.java @@ -0,0 +1,39 @@ +package org.cryptomator.data.cloud.s3; + +import android.content.Context; + +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.client.builder.AwsClientBuilder; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; + +import org.cryptomator.domain.S3Cloud; +import org.cryptomator.util.crypto.CredentialCryptor; + +class S3ClientFactory { + + private AmazonS3 apiClient; + + public AmazonS3 getClient(S3Cloud cloud, Context context) { + if (apiClient == null) { + apiClient = createApiClient(cloud, context); + } + return apiClient; + } + + private AmazonS3 createApiClient(S3Cloud cloud, Context context) { + AwsClientBuilder.EndpointConfiguration endpointConfiguration = new AwsClientBuilder.EndpointConfiguration(cloud.s3Endpoint(), cloud.s3Region()); + + AWSCredentials credentials = new BasicAWSCredentials(cloud.accessKey(), decrypt(cloud.secretKey(), context)); + + return AmazonS3ClientBuilder.standard().withEndpointConfiguration(endpointConfiguration).withCredentials(new AWSStaticCredentialsProvider(credentials)).build(); + } + + private String decrypt(String password, Context context) { + return CredentialCryptor // + .getInstance(context) // + .decrypt(password); + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudContentRepository.java b/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudContentRepository.java new file mode 100644 index 00000000..c8350b94 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudContentRepository.java @@ -0,0 +1,193 @@ +package org.cryptomator.data.cloud.s3; + +import android.content.Context; + +import com.pcloud.sdk.ApiError; + +import org.cryptomator.data.cloud.InterceptingCloudContentRepository; +import org.cryptomator.domain.PCloud; +import org.cryptomator.domain.exception.BackendException; +import org.cryptomator.domain.exception.FatalBackendException; +import org.cryptomator.domain.exception.NetworkConnectionException; +import org.cryptomator.domain.exception.authentication.WrongCredentialsException; +import org.cryptomator.domain.repository.CloudContentRepository; +import org.cryptomator.domain.usecases.ProgressAware; +import org.cryptomator.domain.usecases.cloud.DataSource; +import org.cryptomator.domain.usecases.cloud.DownloadState; +import org.cryptomator.domain.usecases.cloud.UploadState; +import org.cryptomator.util.Optional; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.util.List; + +import static org.cryptomator.util.ExceptionUtil.contains; + +class S3CloudContentRepository extends InterceptingCloudContentRepository { + + private final PCloud cloud; + + public S3CloudContentRepository(PCloud cloud, Context context) { + super(new Intercepted(cloud, context)); + this.cloud = cloud; + } + + @Override + protected void throwWrappedIfRequired(Exception e) throws BackendException { + throwConnectionErrorIfRequired(e); + throwWrongCredentialsExceptionIfRequired(e); + } + + private void throwConnectionErrorIfRequired(Exception e) throws NetworkConnectionException { + if (contains(e, IOException.class)) { + throw new NetworkConnectionException(e); + } + } + + private void throwWrongCredentialsExceptionIfRequired(Exception e) { + if (e instanceof ApiError) { + int errorCode = ((ApiError) e).errorCode(); + if (errorCode == PCloudApiError.PCloudApiErrorCodes.INVALID_ACCESS_TOKEN.getValue() // + || errorCode == PCloudApiError.PCloudApiErrorCodes.ACCESS_TOKEN_REVOKED.getValue()) { + throw new WrongCredentialsException(cloud); + } + } + } + + private static class Intercepted implements CloudContentRepository { + + private final S3Impl cloud; + + public Intercepted(PCloud cloud, Context context) { + this.cloud = new S3Impl(context, cloud); + } + + public S3Folder root(PCloud cloud) { + return this.cloud.root(); + } + + @Override + public S3Folder resolve(PCloud cloud, String path) throws BackendException { + try { + return this.cloud.resolve(path); + } catch (IOException ex) { + throw new FatalBackendException(ex); + } + } + + @Override + public S3File file(S3Folder parent, String name) throws BackendException { + try { + return cloud.file(parent, name); + } catch (IOException ex) { + throw new FatalBackendException(ex); + } + } + + @Override + public S3File file(S3Folder parent, String name, Optional size) throws BackendException { + try { + return cloud.file(parent, name, size); + } catch (IOException ex) { + throw new FatalBackendException(ex); + } + } + + @Override + public S3Folder folder(S3Folder parent, String name) throws BackendException { + try { + return cloud.folder(parent, name); + } catch (IOException ex) { + throw new FatalBackendException(ex); + } + } + + @Override + public boolean exists(S3Node node) throws BackendException { + try { + return cloud.exists(node); + } catch (IOException e) { + throw new FatalBackendException(e); + } + } + + @Override + public List list(S3Folder folder) throws BackendException { + try { + return cloud.list(folder); + } catch (IOException e) { + throw new FatalBackendException(e); + } + } + + @Override + public S3Folder create(S3Folder folder) throws BackendException { + try { + return cloud.create(folder); + } catch (IOException e) { + throw new FatalBackendException(e); + } + } + + @Override + public S3Folder move(S3Folder source, S3Folder target) throws BackendException { + try { + return (S3Folder) cloud.move(source, target); + } catch (IOException e) { + throw new FatalBackendException(e); + } + } + + @Override + public S3File move(S3File source, S3File target) throws BackendException { + try { + return (S3File) cloud.move(source, target); + } catch (IOException e) { + throw new FatalBackendException(e); + } + } + + @Override + public S3File write(S3File uploadFile, DataSource data, ProgressAware progressAware, boolean replace, long size) throws BackendException { + try { + return cloud.write(uploadFile, data, progressAware, replace, size); + } catch (IOException e) { + throw new FatalBackendException(e); + } + } + + @Override + public void read(S3File file, Optional encryptedTmpFile, OutputStream data, ProgressAware progressAware) throws BackendException { + try { + cloud.read(file, encryptedTmpFile, data, progressAware); + } catch (IOException e) { + throw new FatalBackendException(e); + } + } + + @Override + public void delete(S3Node node) throws BackendException { + try { + cloud.delete(node); + } catch (IOException e) { + throw new FatalBackendException(e); + } + } + + @Override + public String checkAuthenticationAndRetrieveCurrentAccount(PCloud cloud) throws BackendException { + try { + return this.cloud.currentAccount(); + } catch (IOException e) { + throw new FatalBackendException(e); + } + } + + @Override + public void logout(PCloud cloud) throws BackendException { + // empty + } + } + +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudContentRepositoryFactory.java b/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudContentRepositoryFactory.java new file mode 100644 index 00000000..9fc9dd54 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudContentRepositoryFactory.java @@ -0,0 +1,35 @@ +package org.cryptomator.data.cloud.s3; + +import android.content.Context; + +import org.cryptomator.data.repository.CloudContentRepositoryFactory; +import org.cryptomator.domain.Cloud; +import org.cryptomator.domain.PCloud; +import org.cryptomator.domain.repository.CloudContentRepository; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import static org.cryptomator.domain.CloudType.PCLOUD; + +@Singleton +public class S3CloudContentRepositoryFactory implements CloudContentRepositoryFactory { + + private final Context context; + + @Inject + public S3CloudContentRepositoryFactory(Context context) { + this.context = context; + } + + @Override + public boolean supports(Cloud cloud) { + return cloud.type() == PCLOUD; + } + + @Override + public CloudContentRepository cloudContentRepositoryFor(Cloud cloud) { + return new S3CloudContentRepository((PCloud) cloud, context); + } + +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudNodeFactory.java b/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudNodeFactory.java new file mode 100644 index 00000000..ebd1ea0c --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudNodeFactory.java @@ -0,0 +1,47 @@ +package org.cryptomator.data.cloud.s3; + +import com.pcloud.sdk.RemoteEntry; +import com.pcloud.sdk.RemoteFile; +import com.pcloud.sdk.RemoteFolder; + +import org.cryptomator.util.Optional; + +class S3CloudNodeFactory { + + public static S3File file(S3Folder parent, RemoteFile file) { + return new S3File(parent, file.name(), getNodePath(parent, file.name()), Optional.ofNullable(file.size()), Optional.ofNullable(file.lastModified())); + } + + public static S3File file(S3Folder parent, String name, Optional size) { + return new S3File(parent, name, getNodePath(parent, name), size, Optional.empty()); + } + + public static S3File file(S3Folder folder, String name, Optional size, String path) { + return new S3File(folder, name, path, size, Optional.empty()); + } + + public static S3Folder folder(S3Folder parent, RemoteFolder folder) { + return new S3Folder(parent, folder.name(), getNodePath(parent, folder.name())); + } + + public static S3Folder folder(S3Folder parent, String name) { + return new S3Folder(parent, name, getNodePath(parent, name)); + } + + public static S3Folder folder(S3Folder parent, String name, String path) { + return new S3Folder(parent, name, path); + } + + public static String getNodePath(S3Folder parent, String name) { + return parent.getPath() + "/" + name; + } + + public static S3Node from(S3Folder parent, RemoteEntry remoteEntry) { + if (remoteEntry instanceof RemoteFile) { + return file(parent, remoteEntry.asFile()); + } else { + return folder(parent, remoteEntry.asFolder()); + } + } + +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/s3/S3File.java b/data/src/main/java/org/cryptomator/data/cloud/s3/S3File.java new file mode 100644 index 00000000..b7c22c63 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/s3/S3File.java @@ -0,0 +1,55 @@ +package org.cryptomator.data.cloud.s3; + +import org.cryptomator.domain.Cloud; +import org.cryptomator.domain.CloudFile; +import org.cryptomator.util.Optional; + +import java.util.Date; + +class S3File implements CloudFile, S3Node { + + private final S3Folder parent; + private final String name; + private final String path; + private final Optional size; + private final Optional modified; + + public S3File(S3Folder parent, String name, String path, Optional size, Optional modified) { + this.parent = parent; + this.name = name; + this.path = path; + this.size = size; + this.modified = modified; + } + + @Override + public Cloud getCloud() { + return parent.getCloud(); + } + + @Override + public String getName() { + return name; + } + + @Override + public String getPath() { + return path; + } + + @Override + public S3Folder getParent() { + return parent; + } + + @Override + public Optional getSize() { + return size; + } + + @Override + public Optional getModified() { + return modified; + } + +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/s3/S3Folder.java b/data/src/main/java/org/cryptomator/data/cloud/s3/S3Folder.java new file mode 100644 index 00000000..67b7f7fa --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/s3/S3Folder.java @@ -0,0 +1,42 @@ +package org.cryptomator.data.cloud.s3; + +import org.cryptomator.domain.Cloud; +import org.cryptomator.domain.CloudFolder; + +class S3Folder implements CloudFolder, S3Node { + + private final S3Folder parent; + private final String name; + private final String path; + + public S3Folder(S3Folder parent, String name, String path) { + this.parent = parent; + this.name = name; + this.path = path; + } + + @Override + public Cloud getCloud() { + return parent.getCloud(); + } + + @Override + public String getName() { + return name; + } + + @Override + public String getPath() { + return path; + } + + @Override + public S3Folder getParent() { + return parent; + } + + @Override + public S3Folder withCloud(Cloud cloud) { + return new S3Folder(parent.withCloud(cloud), name, path); + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java b/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java new file mode 100644 index 00000000..d2f26bb3 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java @@ -0,0 +1,370 @@ +package org.cryptomator.data.cloud.s3; + +import android.content.Context; + +import com.amazonaws.services.s3.AmazonS3; +import com.pcloud.sdk.ApiError; +import com.pcloud.sdk.DataSink; +import com.pcloud.sdk.DownloadOptions; +import com.pcloud.sdk.FileLink; +import com.pcloud.sdk.ProgressListener; +import com.pcloud.sdk.RemoteEntry; +import com.pcloud.sdk.RemoteFile; +import com.pcloud.sdk.RemoteFolder; +import com.pcloud.sdk.UploadOptions; +import com.pcloud.sdk.UserInfo; +import com.tomclaw.cache.DiskLruCache; + +import org.cryptomator.data.util.CopyStream; +import org.cryptomator.domain.S3Cloud; +import org.cryptomator.domain.exception.BackendException; +import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException; +import org.cryptomator.domain.exception.FatalBackendException; +import org.cryptomator.domain.exception.ForbiddenException; +import org.cryptomator.domain.exception.NetworkConnectionException; +import org.cryptomator.domain.exception.NoSuchCloudFileException; +import org.cryptomator.domain.exception.UnauthorizedException; +import org.cryptomator.domain.exception.authentication.NoAuthenticationProvidedException; +import org.cryptomator.domain.exception.authentication.WrongCredentialsException; +import org.cryptomator.domain.usecases.ProgressAware; +import org.cryptomator.domain.usecases.cloud.DataSource; +import org.cryptomator.domain.usecases.cloud.DownloadState; +import org.cryptomator.domain.usecases.cloud.Progress; +import org.cryptomator.domain.usecases.cloud.UploadState; +import org.cryptomator.util.Optional; +import org.cryptomator.util.SharedPreferencesHandler; +import org.cryptomator.util.file.LruFileCacheUtil; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Set; + +import okio.BufferedSink; +import okio.BufferedSource; +import okio.Okio; +import okio.Source; +import timber.log.Timber; + +import static org.cryptomator.domain.usecases.cloud.Progress.progress; +import static org.cryptomator.util.file.LruFileCacheUtil.Cache.PCLOUD; +import static org.cryptomator.util.file.LruFileCacheUtil.retrieveFromLruCache; +import static org.cryptomator.util.file.LruFileCacheUtil.storeToLruCache; + +class S3Impl { + + private final S3ClientFactory clientFactory = new S3ClientFactory(); + private final S3Cloud cloud; + private final RootS3Folder root; + private final Context context; + + private final SharedPreferencesHandler sharedPreferencesHandler; + private DiskLruCache diskLruCache; + + S3Impl(Context context, S3Cloud cloud) { + if (cloud.accessKey() == null || cloud.secretKey() == null) { + throw new NoAuthenticationProvidedException(cloud); + } + + this.context = context; + this.cloud = cloud; + this.root = new RootS3Folder(cloud); + this.sharedPreferencesHandler = new SharedPreferencesHandler(context); + } + + private AmazonS3 client() { + return clientFactory.getClient(cloud, context); + } + + public S3Folder root() { + return root; + } + + public S3Folder resolve(String path) throws IOException, BackendException { + if (path.startsWith("/")) { + path = path.substring(1); + } + String[] names = path.split("/"); + S3Folder folder = root; + for (String name : names) { + folder = folder(folder, name); + } + return folder; + } + + public S3File file(S3Folder parent, String name) throws BackendException, IOException { + return file(parent, name, Optional.empty()); + } + + public S3File file(S3Folder parent, String name, Optional size) throws BackendException, IOException { + return S3CloudNodeFactory.file(parent, name, size, parent.getPath() + "/" + name); + } + + public S3Folder folder(S3Folder parent, String name) throws IOException, BackendException { + return S3CloudNodeFactory.folder(parent, name, parent.getPath() + "/" + name); + } + + public boolean exists(S3Node node) throws IOException, BackendException { + try { + if (node instanceof S3Folder) { + client().loadFolder(node.getPath()).execute(); + } else { + client().loadFile(node.getPath()).execute(); + } + return true; + } catch (ApiError ex) { + handleApiError(ex, PCloudApiError.ignoreExistsSet, node.getName()); + return false; + } + } + + public List list(S3Folder folder) throws IOException, BackendException { + List result = new ArrayList<>(); + + try { + RemoteFolder listFolderResult = client().listFolder(folder.getPath()).execute(); + List entryMetadata = listFolderResult.children(); + for (RemoteEntry metadata : entryMetadata) { + result.add(S3CloudNodeFactory.from(folder, metadata)); + } + return result; + } catch (ApiError ex) { + handleApiError(ex, folder.getName()); + throw new FatalBackendException(ex); + } + } + + public S3Folder create(S3Folder folder) throws IOException, BackendException { + if (!exists(folder.getParent())) { + folder = new S3Folder( // + create(folder.getParent()), // + folder.getName(), folder.getPath() // + ); + } + + try { + RemoteFolder createdFolder = client() // + .createFolder(folder.getPath()) // + .execute(); + return S3CloudNodeFactory.folder(folder.getParent(), createdFolder); + } catch (ApiError ex) { + handleApiError(ex, folder.getName()); + throw new FatalBackendException(ex); + } + } + + public S3Node move(S3Node source, S3Node target) throws IOException, BackendException { + if (exists(target)) { + throw new CloudNodeAlreadyExistsException(target.getName()); + } + + try { + if (source instanceof S3Folder) { + return S3CloudNodeFactory.from(target.getParent(), client().moveFolder(source.getPath(), target.getPath()).execute()); + } else { + return S3CloudNodeFactory.from(target.getParent(), client().moveFile(source.getPath(), target.getPath()).execute()); + } + } catch (ApiError ex) { + if (PCloudApiError.isCloudNodeAlreadyExistsException(ex.errorCode())) { + throw new CloudNodeAlreadyExistsException(target.getName()); + } else if (PCloudApiError.isNoSuchCloudFileException(ex.errorCode())) { + throw new NoSuchCloudFileException(source.getName()); + } else { + handleApiError(ex, PCloudApiError.ignoreMoveSet, null); + } + throw new FatalBackendException(ex); + } + } + + public S3File write(S3File file, DataSource data, final ProgressAware progressAware, boolean replace, long size) throws IOException, BackendException { + if (!replace && exists(file)) { + throw new CloudNodeAlreadyExistsException("CloudNode already exists and replace is false"); + } + + progressAware.onProgress(Progress.started(UploadState.upload(file))); + UploadOptions uploadOptions = UploadOptions.DEFAULT; + if (replace) { + uploadOptions = UploadOptions.OVERRIDE_FILE; + } + + RemoteFile uploadedFile = uploadFile(file, data, progressAware, uploadOptions, size); + + progressAware.onProgress(Progress.completed(UploadState.upload(file))); + + return S3CloudNodeFactory.file(file.getParent(), uploadedFile); + + } + + private RemoteFile uploadFile(final S3File file, DataSource data, final ProgressAware progressAware, UploadOptions uploadOptions, final long size) // + throws IOException, BackendException { + ProgressListener listener = (done, total) -> progressAware.onProgress( // + progress(UploadState.upload(file)) // + .between(0) // + .and(size) // + .withValue(done)); + + com.pcloud.sdk.DataSource pCloudDataSource = new com.pcloud.sdk.DataSource() { + @Override + public long contentLength() { + return data.size(context).get(); + } + + @Override + public void writeTo(BufferedSink sink) throws IOException { + try (Source source = Okio.source(data.open(context))) { + sink.writeAll(source); + } + } + }; + + try { + return client() // + .createFile(file.getParent().getPath(), file.getName(), pCloudDataSource, new Date(), listener, uploadOptions) // + .execute(); + } catch (ApiError ex) { + handleApiError(ex, file.getName()); + throw new FatalBackendException(ex); + } + } + + public void read(S3File file, Optional encryptedTmpFile, OutputStream data, final ProgressAware progressAware) throws IOException, BackendException { + progressAware.onProgress(Progress.started(DownloadState.download(file))); + + Optional cacheKey = Optional.empty(); + Optional cacheFile = Optional.empty(); + + RemoteFile remoteFile; + + if (sharedPreferencesHandler.useLruCache() && createLruCache(sharedPreferencesHandler.lruCacheSize())) { + try { + remoteFile = client().loadFile(file.getPath()).execute().asFile(); + cacheKey = Optional.of(remoteFile.fileId() + remoteFile.hash()); + } catch (ApiError ex) { + handleApiError(ex, file.getName()); + } + + File cachedFile = diskLruCache.get(cacheKey.get()); + cacheFile = cachedFile != null ? Optional.of(cachedFile) : Optional.empty(); + } + + if (sharedPreferencesHandler.useLruCache() && cacheFile.isPresent()) { + try { + retrieveFromLruCache(cacheFile.get(), data); + } catch (IOException e) { + Timber.tag("PCloudImpl").w(e, "Error while retrieving content from Cache, get from web request"); + writeToData(file, data, encryptedTmpFile, cacheKey, progressAware); + } + } else { + writeToData(file, data, encryptedTmpFile, cacheKey, progressAware); + } + + progressAware.onProgress(Progress.completed(DownloadState.download(file))); + } + + private void writeToData(final S3File file, // + final OutputStream data, // + final Optional encryptedTmpFile, // + final Optional cacheKey, // + final ProgressAware progressAware) throws IOException, BackendException { + try { + FileLink fileLink = client().createFileLink(file.getPath(), DownloadOptions.DEFAULT).execute(); + + ProgressListener listener = (done, total) -> progressAware.onProgress( // + progress(DownloadState.download(file)) // + .between(0) // + .and(file.getSize().orElse(Long.MAX_VALUE)) // + .withValue(done)); + + DataSink sink = new DataSink() { + @Override + public void readAll(BufferedSource source) { + CopyStream.copyStreamToStream(source.inputStream(), data); + } + }; + + client().download(fileLink, sink, listener).execute(); + } catch (ApiError ex) { + handleApiError(ex, file.getName()); + } + + if (sharedPreferencesHandler.useLruCache() && encryptedTmpFile.isPresent() && cacheKey.isPresent()) { + try { + storeToLruCache(diskLruCache, cacheKey.get(), encryptedTmpFile.get()); + } catch (IOException e) { + Timber.tag("PCloudImpl").e(e, "Failed to write downloaded file in LRU cache"); + } + } + + } + + public void delete(S3Node node) throws IOException, BackendException { + try { + if (node instanceof S3Folder) { + client() // + .deleteFolder(node.getPath(), true).execute(); + } else { + client() // + .deleteFile(node.getPath()).execute(); + } + } catch (ApiError ex) { + handleApiError(ex, node.getName()); + } + } + + public String currentAccount() throws IOException, BackendException { + try { + UserInfo currentAccount = client() // + .getUserInfo() // + .execute(); + return currentAccount.email(); + } catch (ApiError ex) { + handleApiError(ex); + throw new FatalBackendException(ex); + } + } + + private boolean createLruCache(int cacheSize) { + if (diskLruCache == null) { + try { + diskLruCache = DiskLruCache.create(new LruFileCacheUtil(context).resolve(PCLOUD), cacheSize); + } catch (IOException e) { + Timber.tag("PCloudImpl").e(e, "Failed to setup LRU cache"); + return false; + } + } + + return true; + } + + private void handleApiError(ApiError ex) throws BackendException { + handleApiError(ex, null, null); + } + + private void handleApiError(ApiError ex, String name) throws BackendException { + handleApiError(ex, null, name); + } + + private void handleApiError(ApiError ex, Set errorCodes, String name) throws BackendException { + if (errorCodes == null || !errorCodes.contains(ex.errorCode())) { + int errorCode = ex.errorCode(); + if (PCloudApiError.isCloudNodeAlreadyExistsException(errorCode)) { + throw new CloudNodeAlreadyExistsException(name); + } else if (PCloudApiError.isForbiddenException(errorCode)) { + throw new ForbiddenException(); + } else if (PCloudApiError.isNetworkConnectionException(errorCode)) { + throw new NetworkConnectionException(ex); + } else if (PCloudApiError.isNoSuchCloudFileException(errorCode)) { + throw new NoSuchCloudFileException(name); + } else if (PCloudApiError.isWrongCredentialsException(errorCode)) { + throw new WrongCredentialsException(cloud); + } else if (PCloudApiError.isUnauthorizedException(errorCode)) { + throw new UnauthorizedException(); + } else { + throw new FatalBackendException(ex); + } + } + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/s3/S3Node.java b/data/src/main/java/org/cryptomator/data/cloud/s3/S3Node.java new file mode 100644 index 00000000..b5f241fe --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/s3/S3Node.java @@ -0,0 +1,10 @@ +package org.cryptomator.data.cloud.s3; + +import org.cryptomator.domain.CloudNode; + +interface S3Node extends CloudNode { + + @Override + S3Folder getParent(); + +} diff --git a/domain/src/main/java/org/cryptomator/domain/S3Cloud.java b/domain/src/main/java/org/cryptomator/domain/S3Cloud.java new file mode 100644 index 00000000..a5da084e --- /dev/null +++ b/domain/src/main/java/org/cryptomator/domain/S3Cloud.java @@ -0,0 +1,167 @@ +package org.cryptomator.domain; + +import org.jetbrains.annotations.NotNull; + +public class S3Cloud implements Cloud { + + private final Long id; + private final String accessKey; + private final String secretKey; + private final String s3Bucket; + private final String s3Endpoint; + private final String s3Region; + + private S3Cloud(Builder builder) { + this.id = builder.id; + this.accessKey = builder.accessKey; + this.secretKey = builder.secretKey; + this.s3Bucket = builder.s3Bucket; + this.s3Endpoint = builder.s3Endpoint; + this.s3Region = builder.s3Region; + } + + public static Builder aPCloud() { + return new Builder(); + } + + public static Builder aCopyOf(S3Cloud s3Cloud) { + return new Builder() // + .withId(s3Cloud.id()) // + .withAccessKey(s3Cloud.accessKey()) // + .withSecretKey(s3Cloud.secretKey()) // + .withS3Bucket(s3Cloud.s3Bucket()) // + .withS3Endpoint(s3Cloud.s3Endpoint()) // + .withS3Region(s3Cloud.s3Region()); + } + + @Override + public Long id() { + return id; + } + + public String accessKey() { + return accessKey; + } + + public String secretKey() { + return secretKey; + } + + public String s3Bucket() { + return s3Bucket; + } + + public String s3Endpoint() { + return s3Endpoint; + } + + public String s3Region() { + return s3Region; + } + + @Override + public CloudType type() { + return CloudType.PCLOUD; + } + + @Override + public boolean configurationMatches(Cloud cloud) { + return cloud instanceof S3Cloud && configurationMatches((S3Cloud) cloud); + } + + private boolean configurationMatches(S3Cloud cloud) { + //FIXME: figure out when it is necessary to create a new cloud + return s3Bucket.equals(cloud.s3Bucket) && s3Endpoint.equals(cloud.s3Endpoint) && s3Region.equals(cloud.s3Region); + } + + + @Override + public boolean predefined() { + return false; + } + + @Override + public boolean persistent() { + return true; + } + + @Override + public boolean requiresNetwork() { + return true; + } + + @NotNull + @Override + public String toString() { + return "S3"; + } + + @Override + public boolean equals(Object obj) { + if (obj == null || getClass() != obj.getClass()) { + return false; + } + if (obj == this) { + return true; + } + return internalEquals((S3Cloud) obj); + } + + @Override + public int hashCode() { + return id == null ? 0 : id.hashCode(); + } + + private boolean internalEquals(S3Cloud obj) { + return id != null && id.equals(obj.id); + } + + public static class Builder { + + private Long id; + private String accessKey; + private String secretKey; + private String s3Bucket; + private String s3Endpoint; + private String s3Region; + + private Builder() { + } + + public Builder withId(Long id) { + this.id = id; + return this; + } + + public Builder withAccessKey(String accessKey) { + this.accessKey = accessKey; + return this; + } + + public Builder withSecretKey(String secretKey) { + this.secretKey = secretKey; + return this; + } + + public Builder withS3Bucket(String s3Bucket) { + this.s3Bucket = s3Bucket; + return this; + } + + public Builder withS3Endpoint(String s3Endpoint) { + this.s3Endpoint = s3Endpoint; + return this; + } + + public Builder withS3Region(String s3Region) { + this.s3Region = s3Region; + return this; + } + + public S3Cloud build() { + return new S3Cloud(this); + } + + } + +} From 250e711d4a62a1410176b5658186f8b3f5f9127f Mon Sep 17 00:00:00 2001 From: Manuel Jenny Date: Mon, 19 Apr 2021 10:34:22 +0200 Subject: [PATCH 26/80] feat(S3): implement S3CloudNodeFactory --- .../data/cloud/s3/S3CloudNodeFactory.java | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudNodeFactory.java b/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudNodeFactory.java index ebd1ea0c..88c2e439 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudNodeFactory.java +++ b/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudNodeFactory.java @@ -1,15 +1,13 @@ package org.cryptomator.data.cloud.s3; -import com.pcloud.sdk.RemoteEntry; -import com.pcloud.sdk.RemoteFile; -import com.pcloud.sdk.RemoteFolder; +import com.amazonaws.services.s3.model.S3ObjectSummary; import org.cryptomator.util.Optional; class S3CloudNodeFactory { - public static S3File file(S3Folder parent, RemoteFile file) { - return new S3File(parent, file.name(), getNodePath(parent, file.name()), Optional.ofNullable(file.size()), Optional.ofNullable(file.lastModified())); + public static S3File file(S3Folder parent, S3ObjectSummary file) { + return new S3File(parent, file.getKey(), getNodePath(parent, file.getKey()), Optional.ofNullable(file.getSize()), Optional.ofNullable(file.getLastModified())); } public static S3File file(S3Folder parent, String name, Optional size) { @@ -20,8 +18,8 @@ class S3CloudNodeFactory { return new S3File(folder, name, path, size, Optional.empty()); } - public static S3Folder folder(S3Folder parent, RemoteFolder folder) { - return new S3Folder(parent, folder.name(), getNodePath(parent, folder.name())); + public static S3Folder folder(S3Folder parent, S3ObjectSummary folder) { + return new S3Folder(parent, folder.getKey(), getNodePath(parent, folder.getKey())); } public static S3Folder folder(S3Folder parent, String name) { @@ -36,11 +34,11 @@ class S3CloudNodeFactory { return parent.getPath() + "/" + name; } - public static S3Node from(S3Folder parent, RemoteEntry remoteEntry) { - if (remoteEntry instanceof RemoteFile) { - return file(parent, remoteEntry.asFile()); + public static S3Node from(S3Folder parent, S3ObjectSummary objectSummary) { + if (objectSummary.getKey().endsWith("/")) { + return folder(parent, objectSummary); } else { - return folder(parent, remoteEntry.asFolder()); + return file(parent, objectSummary); } } From 3cf028ddce25ebe913281f86ed2db407af93c922 Mon Sep 17 00:00:00 2001 From: Manuel Jenny Date: Mon, 19 Apr 2021 10:35:19 +0200 Subject: [PATCH 27/80] feat(S3): implement list() and create() --- .../org/cryptomator/data/cloud/s3/S3Impl.java | 43 +++++++++++-------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java b/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java index d2f26bb3..6d29fdb1 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java +++ b/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java @@ -3,6 +3,13 @@ package org.cryptomator.data.cloud.s3; import android.content.Context; import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.AbstractPutObjectRequest; +import com.amazonaws.services.s3.model.ListObjectsV2Result; +import com.amazonaws.services.s3.model.ObjectListing; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import com.amazonaws.services.s3.model.PutObjectResult; +import com.amazonaws.services.s3.model.S3ObjectSummary; import com.pcloud.sdk.ApiError; import com.pcloud.sdk.DataSink; import com.pcloud.sdk.DownloadOptions; @@ -35,8 +42,10 @@ import org.cryptomator.util.Optional; import org.cryptomator.util.SharedPreferencesHandler; import org.cryptomator.util.file.LruFileCacheUtil; +import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Date; @@ -56,6 +65,8 @@ import static org.cryptomator.util.file.LruFileCacheUtil.storeToLruCache; class S3Impl { + private static final String SUFFIX = "/"; + private final S3ClientFactory clientFactory = new S3ClientFactory(); private final S3Cloud cloud; private final RootS3Folder root; @@ -124,17 +135,11 @@ class S3Impl { public List list(S3Folder folder) throws IOException, BackendException { List result = new ArrayList<>(); - try { - RemoteFolder listFolderResult = client().listFolder(folder.getPath()).execute(); - List entryMetadata = listFolderResult.children(); - for (RemoteEntry metadata : entryMetadata) { - result.add(S3CloudNodeFactory.from(folder, metadata)); - } - return result; - } catch (ApiError ex) { - handleApiError(ex, folder.getName()); - throw new FatalBackendException(ex); + ListObjectsV2Result objectListing = client().listObjectsV2(cloud.s3Bucket(), folder.getPath()); + for (S3ObjectSummary objectSummary : objectListing.getObjectSummaries()) { + result.add(S3CloudNodeFactory.from(folder, objectSummary)); } + return result; } public S3Folder create(S3Folder folder) throws IOException, BackendException { @@ -145,15 +150,15 @@ class S3Impl { ); } - try { - RemoteFolder createdFolder = client() // - .createFolder(folder.getPath()) // - .execute(); - return S3CloudNodeFactory.folder(folder.getParent(), createdFolder); - } catch (ApiError ex) { - handleApiError(ex, folder.getName()); - throw new FatalBackendException(ex); - } + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentLength(0); + + InputStream emptyContent = new ByteArrayInputStream(new byte[0]); + + PutObjectRequest putObjectRequest = new PutObjectRequest(cloud.s3Bucket(), folder.getPath() + SUFFIX, emptyContent, metadata); + client().putObject(putObjectRequest); + + return S3CloudNodeFactory.folder(folder.getParent(), folder.getName()); } public S3Node move(S3Node source, S3Node target) throws IOException, BackendException { From fe21e3988d995dc2af22ee25f2018c371863f90b Mon Sep 17 00:00:00 2001 From: Manuel Jenny Date: Tue, 20 Apr 2021 13:34:37 +0200 Subject: [PATCH 28/80] chore(pCloud): rename folder to parent --- .../org/cryptomator/data/cloud/pcloud/PCloudNodeFactory.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudNodeFactory.java b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudNodeFactory.java index 55e72da9..2b91ca70 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudNodeFactory.java +++ b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudNodeFactory.java @@ -16,8 +16,8 @@ class PCloudNodeFactory { return new PCloudFile(parent, name, getNodePath(parent, name), size, Optional.empty()); } - public static PCloudFile file(PCloudFolder folder, String name, Optional size, String path) { - return new PCloudFile(folder, name, path, size, Optional.empty()); + public static PCloudFile file(PCloudFolder parent, String name, Optional size, String path) { + return new PCloudFile(parent, name, path, size, Optional.empty()); } public static PCloudFolder folder(PCloudFolder parent, RemoteFolder folder) { From c63db47e56b7160cc930218494d04c54f7e4cd05 Mon Sep 17 00:00:00 2001 From: Manuel Jenny Date: Tue, 20 Apr 2021 13:38:49 +0200 Subject: [PATCH 29/80] feat(S3): support PutObjectResult, fix key / name - add S3File creation with `PutObjectResult` support. - properly extract name of file / folder from key. --- .../data/cloud/s3/S3CloudNodeFactory.java | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudNodeFactory.java b/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudNodeFactory.java index 88c2e439..9a24d710 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudNodeFactory.java +++ b/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudNodeFactory.java @@ -1,25 +1,35 @@ package org.cryptomator.data.cloud.s3; +import com.amazonaws.services.s3.model.PutObjectResult; import com.amazonaws.services.s3.model.S3ObjectSummary; import org.cryptomator.util.Optional; class S3CloudNodeFactory { + private static final String SUFFIX = "/"; + public static S3File file(S3Folder parent, S3ObjectSummary file) { - return new S3File(parent, file.getKey(), getNodePath(parent, file.getKey()), Optional.ofNullable(file.getSize()), Optional.ofNullable(file.getLastModified())); + String name = getNameFromKey(file.getKey()); + return new S3File(parent, name, getNodePath(parent, name), Optional.ofNullable(file.getSize()), Optional.ofNullable(file.getLastModified())); } + public static S3File file(S3Folder parent, String name, PutObjectResult file) { + return new S3File(parent, name, getNodePath(parent, name), Optional.ofNullable(file.getMetadata().getContentLength()), Optional.ofNullable(file.getMetadata().getLastModified())); + } + + public static S3File file(S3Folder parent, String name, Optional size) { return new S3File(parent, name, getNodePath(parent, name), size, Optional.empty()); } - public static S3File file(S3Folder folder, String name, Optional size, String path) { - return new S3File(folder, name, path, size, Optional.empty()); + public static S3File file(S3Folder parent, String name, Optional size, String path) { + return new S3File(parent, name, path, size, Optional.empty()); } public static S3Folder folder(S3Folder parent, S3ObjectSummary folder) { - return new S3Folder(parent, folder.getKey(), getNodePath(parent, folder.getKey())); + String name = getNameFromKey(folder.getKey()); + return new S3Folder(parent, name, getNodePath(parent, name)); } public static S3Folder folder(S3Folder parent, String name) { @@ -30,12 +40,16 @@ class S3CloudNodeFactory { return new S3Folder(parent, name, path); } - public static String getNodePath(S3Folder parent, String name) { - return parent.getPath() + "/" + name; + private static String getNodePath(S3Folder parent, String name) { + return parent.getPath() + SUFFIX + name; + } + + private static String getNameFromKey(String key) { + return key.substring(key.lastIndexOf(SUFFIX) + 1); } public static S3Node from(S3Folder parent, S3ObjectSummary objectSummary) { - if (objectSummary.getKey().endsWith("/")) { + if (objectSummary.getKey().endsWith(SUFFIX)) { return folder(parent, objectSummary); } else { return file(parent, objectSummary); From 4afd6bc7038b569dfc66a295bc491c7763246707 Mon Sep 17 00:00:00 2001 From: Manuel Jenny Date: Tue, 20 Apr 2021 13:43:08 +0200 Subject: [PATCH 30/80] feat(S3): implement exists(), write(), read() and delete() - add implementation for `exists()` - add implementation for `write()` - add implementation for `read()` - add implementation for `delete()` - add support for `currentAccount()` --- .../org/cryptomator/data/cloud/s3/S3Impl.java | 161 +++++++----------- 1 file changed, 62 insertions(+), 99 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java b/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java index 6d29fdb1..ff11839a 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java +++ b/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java @@ -2,24 +2,20 @@ package org.cryptomator.data.cloud.s3; import android.content.Context; +import com.amazonaws.event.ProgressListener; import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.model.AbstractPutObjectRequest; +import com.amazonaws.services.s3.model.DeleteObjectsRequest; +import com.amazonaws.services.s3.model.DeleteObjectsRequest.KeyVersion; +import com.amazonaws.services.s3.model.GetObjectRequest; import com.amazonaws.services.s3.model.ListObjectsV2Result; import com.amazonaws.services.s3.model.ObjectListing; import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.Owner; import com.amazonaws.services.s3.model.PutObjectRequest; import com.amazonaws.services.s3.model.PutObjectResult; +import com.amazonaws.services.s3.model.S3Object; import com.amazonaws.services.s3.model.S3ObjectSummary; import com.pcloud.sdk.ApiError; -import com.pcloud.sdk.DataSink; -import com.pcloud.sdk.DownloadOptions; -import com.pcloud.sdk.FileLink; -import com.pcloud.sdk.ProgressListener; -import com.pcloud.sdk.RemoteEntry; -import com.pcloud.sdk.RemoteFile; -import com.pcloud.sdk.RemoteFolder; -import com.pcloud.sdk.UploadOptions; -import com.pcloud.sdk.UserInfo; import com.tomclaw.cache.DiskLruCache; import org.cryptomator.data.util.CopyStream; @@ -48,14 +44,9 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; -import java.util.Date; import java.util.List; import java.util.Set; -import okio.BufferedSink; -import okio.BufferedSource; -import okio.Okio; -import okio.Source; import timber.log.Timber; import static org.cryptomator.domain.usecases.cloud.Progress.progress; @@ -111,23 +102,24 @@ class S3Impl { } public S3File file(S3Folder parent, String name, Optional size) throws BackendException, IOException { - return S3CloudNodeFactory.file(parent, name, size, parent.getPath() + "/" + name); + return S3CloudNodeFactory.file(parent, name, size, parent.getPath() + SUFFIX + name); } public S3Folder folder(S3Folder parent, String name) throws IOException, BackendException { - return S3CloudNodeFactory.folder(parent, name, parent.getPath() + "/" + name); + return S3CloudNodeFactory.folder(parent, name, parent.getPath() + SUFFIX + name + SUFFIX); } - public boolean exists(S3Node node) throws IOException, BackendException { - try { - if (node instanceof S3Folder) { - client().loadFolder(node.getPath()).execute(); - } else { - client().loadFile(node.getPath()).execute(); - } + public boolean exists(S3Node node) { + String path = node.getPath(); + if (node instanceof S3Folder) { + path += SUFFIX; + } + + ObjectListing result = client().listObjects(cloud.s3Bucket(), path); + + if (result.getObjectSummaries().size() > 0) { return true; - } catch (ApiError ex) { - handleApiError(ex, PCloudApiError.ignoreExistsSet, node.getName()); + } else { return false; } } @@ -190,49 +182,30 @@ class S3Impl { } progressAware.onProgress(Progress.started(UploadState.upload(file))); - UploadOptions uploadOptions = UploadOptions.DEFAULT; - if (replace) { - uploadOptions = UploadOptions.OVERRIDE_FILE; - } - RemoteFile uploadedFile = uploadFile(file, data, progressAware, uploadOptions, size); + PutObjectResult result = uploadFile(file, data, progressAware, size); progressAware.onProgress(Progress.completed(UploadState.upload(file))); - return S3CloudNodeFactory.file(file.getParent(), uploadedFile); + return S3CloudNodeFactory.file(file.getParent(), file.getName(), result); } - private RemoteFile uploadFile(final S3File file, DataSource data, final ProgressAware progressAware, UploadOptions uploadOptions, final long size) // + private PutObjectResult uploadFile(final S3File file, DataSource data, final ProgressAware progressAware, final long size) // throws IOException, BackendException { - ProgressListener listener = (done, total) -> progressAware.onProgress( // + ProgressListener listener = progressEvent -> progressAware.onProgress( // progress(UploadState.upload(file)) // .between(0) // .and(size) // - .withValue(done)); + .withValue(progressEvent.getBytesTransferred())); - com.pcloud.sdk.DataSource pCloudDataSource = new com.pcloud.sdk.DataSource() { - @Override - public long contentLength() { - return data.size(context).get(); - } + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentLength(data.size(context).get()); - @Override - public void writeTo(BufferedSink sink) throws IOException { - try (Source source = Okio.source(data.open(context))) { - sink.writeAll(source); - } - } - }; + PutObjectRequest request = new PutObjectRequest(cloud.s3Bucket(), file.getPath(), data.open(context), metadata); + request.setGeneralProgressListener(listener); - try { - return client() // - .createFile(file.getParent().getPath(), file.getName(), pCloudDataSource, new Date(), listener, uploadOptions) // - .execute(); - } catch (ApiError ex) { - handleApiError(ex, file.getName()); - throw new FatalBackendException(ex); - } + return client().putObject(request); } public void read(S3File file, Optional encryptedTmpFile, OutputStream data, final ProgressAware progressAware) throws IOException, BackendException { @@ -241,15 +214,15 @@ class S3Impl { Optional cacheKey = Optional.empty(); Optional cacheFile = Optional.empty(); - RemoteFile remoteFile; + ObjectListing objectListing; if (sharedPreferencesHandler.useLruCache() && createLruCache(sharedPreferencesHandler.lruCacheSize())) { - try { - remoteFile = client().loadFile(file.getPath()).execute().asFile(); - cacheKey = Optional.of(remoteFile.fileId() + remoteFile.hash()); - } catch (ApiError ex) { - handleApiError(ex, file.getName()); + objectListing = client().listObjects(cloud.s3Bucket(), file.getPath()); + if (objectListing.getObjectSummaries().size() != 1) { + throw new NoSuchCloudFileException(file.getPath()); } + S3ObjectSummary summary = objectListing.getObjectSummaries().get(0); + cacheKey = Optional.of(summary.getKey() + summary.getETag()); File cachedFile = diskLruCache.get(cacheKey.get()); cacheFile = cachedFile != null ? Optional.of(cachedFile) : Optional.empty(); @@ -259,7 +232,7 @@ class S3Impl { try { retrieveFromLruCache(cacheFile.get(), data); } catch (IOException e) { - Timber.tag("PCloudImpl").w(e, "Error while retrieving content from Cache, get from web request"); + Timber.tag("S3Impl").w(e, "Error while retrieving content from Cache, get from web request"); writeToData(file, data, encryptedTmpFile, cacheKey, progressAware); } } else { @@ -274,61 +247,51 @@ class S3Impl { final Optional encryptedTmpFile, // final Optional cacheKey, // final ProgressAware progressAware) throws IOException, BackendException { - try { - FileLink fileLink = client().createFileLink(file.getPath(), DownloadOptions.DEFAULT).execute(); - ProgressListener listener = (done, total) -> progressAware.onProgress( // - progress(DownloadState.download(file)) // - .between(0) // - .and(file.getSize().orElse(Long.MAX_VALUE)) // - .withValue(done)); + ProgressListener listener = progressEvent -> progressAware.onProgress( // + progress(DownloadState.download(file)) // + .between(0) // + .and(file.getSize().orElse(Long.MAX_VALUE)) // + .withValue(progressEvent.getBytesTransferred())); - DataSink sink = new DataSink() { - @Override - public void readAll(BufferedSource source) { - CopyStream.copyStreamToStream(source.inputStream(), data); - } - }; + GetObjectRequest request = new GetObjectRequest(cloud.s3Bucket(), file.getPath()); + request.setGeneralProgressListener(listener); - client().download(fileLink, sink, listener).execute(); - } catch (ApiError ex) { - handleApiError(ex, file.getName()); - } + S3Object s3Object = client().getObject(request); + + CopyStream.copyStreamToStream(s3Object.getObjectContent(), data); if (sharedPreferencesHandler.useLruCache() && encryptedTmpFile.isPresent() && cacheKey.isPresent()) { try { storeToLruCache(diskLruCache, cacheKey.get(), encryptedTmpFile.get()); } catch (IOException e) { - Timber.tag("PCloudImpl").e(e, "Failed to write downloaded file in LRU cache"); + Timber.tag("S3Impl").e(e, "Failed to write downloaded file in LRU cache"); } } } public void delete(S3Node node) throws IOException, BackendException { - try { - if (node instanceof S3Folder) { - client() // - .deleteFolder(node.getPath(), true).execute(); - } else { - client() // - .deleteFile(node.getPath()).execute(); + if (node instanceof S3Folder) { + ObjectListing listing = client().listObjects(cloud.s3Bucket(), node.getPath() + SUFFIX); + List keys = new ArrayList<>(); + for (S3ObjectSummary summary : listing.getObjectSummaries()) { + keys.add(new KeyVersion(summary.getKey())); } - } catch (ApiError ex) { - handleApiError(ex, node.getName()); + + DeleteObjectsRequest request = new DeleteObjectsRequest(cloud.s3Bucket()); + request.withKeys(keys); + + client().deleteObjects(request); + } else { + client().deleteObject(cloud.s3Bucket(), node.getPath()); } } - public String currentAccount() throws IOException, BackendException { - try { - UserInfo currentAccount = client() // - .getUserInfo() // - .execute(); - return currentAccount.email(); - } catch (ApiError ex) { - handleApiError(ex); - throw new FatalBackendException(ex); - } + public String currentAccount() { + Owner currentAccount = client() // + .getS3AccountOwner(); + return currentAccount.getDisplayName(); } private boolean createLruCache(int cacheSize) { From 7d9c20d137ddfe2f4feefbc6120815c461d9c9e8 Mon Sep 17 00:00:00 2001 From: Manuel Jenny Date: Tue, 20 Apr 2021 14:00:25 +0200 Subject: [PATCH 31/80] feat(S3): cleanup S3CloudContentRepository - use proper signature - add TODO for error handling --- .../cloud/s3/S3CloudContentRepository.java | 78 ++++++++----------- 1 file changed, 31 insertions(+), 47 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudContentRepository.java b/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudContentRepository.java index c8350b94..f97cc5ef 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudContentRepository.java +++ b/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudContentRepository.java @@ -2,10 +2,8 @@ package org.cryptomator.data.cloud.s3; import android.content.Context; -import com.pcloud.sdk.ApiError; - import org.cryptomator.data.cloud.InterceptingCloudContentRepository; -import org.cryptomator.domain.PCloud; +import org.cryptomator.domain.S3Cloud; import org.cryptomator.domain.exception.BackendException; import org.cryptomator.domain.exception.FatalBackendException; import org.cryptomator.domain.exception.NetworkConnectionException; @@ -24,56 +22,54 @@ import java.util.List; import static org.cryptomator.util.ExceptionUtil.contains; -class S3CloudContentRepository extends InterceptingCloudContentRepository { +class S3CloudContentRepository extends InterceptingCloudContentRepository { - private final PCloud cloud; + private final S3Cloud cloud; - public S3CloudContentRepository(PCloud cloud, Context context) { + public S3CloudContentRepository(S3Cloud cloud, Context context) { super(new Intercepted(cloud, context)); this.cloud = cloud; } + //TODO: add proper error handling + @Override protected void throwWrappedIfRequired(Exception e) throws BackendException { - throwConnectionErrorIfRequired(e); - throwWrongCredentialsExceptionIfRequired(e); +// throwConnectionErrorIfRequired(e); +// throwWrongCredentialsExceptionIfRequired(e); } - private void throwConnectionErrorIfRequired(Exception e) throws NetworkConnectionException { - if (contains(e, IOException.class)) { - throw new NetworkConnectionException(e); - } - } +// private void throwConnectionErrorIfRequired(Exception e) throws NetworkConnectionException { +// if (contains(e, IOException.class)) { +// throw new NetworkConnectionException(e); +// } +// } +// +// private void throwWrongCredentialsExceptionIfRequired(Exception e) { +// if (e instanceof ApiError) { +// int errorCode = ((ApiError) e).errorCode(); +// if (errorCode == PCloudApiError.PCloudApiErrorCodes.INVALID_ACCESS_TOKEN.getValue() // +// || errorCode == PCloudApiError.PCloudApiErrorCodes.ACCESS_TOKEN_REVOKED.getValue()) { +// throw new WrongCredentialsException(cloud); +// } +// } +// } - private void throwWrongCredentialsExceptionIfRequired(Exception e) { - if (e instanceof ApiError) { - int errorCode = ((ApiError) e).errorCode(); - if (errorCode == PCloudApiError.PCloudApiErrorCodes.INVALID_ACCESS_TOKEN.getValue() // - || errorCode == PCloudApiError.PCloudApiErrorCodes.ACCESS_TOKEN_REVOKED.getValue()) { - throw new WrongCredentialsException(cloud); - } - } - } - - private static class Intercepted implements CloudContentRepository { + private static class Intercepted implements CloudContentRepository { private final S3Impl cloud; - public Intercepted(PCloud cloud, Context context) { + public Intercepted(S3Cloud cloud, Context context) { this.cloud = new S3Impl(context, cloud); } - public S3Folder root(PCloud cloud) { + public S3Folder root(S3Cloud cloud) { return this.cloud.root(); } @Override - public S3Folder resolve(PCloud cloud, String path) throws BackendException { - try { + public S3Folder resolve(S3Cloud cloud, String path) throws BackendException { return this.cloud.resolve(path); - } catch (IOException ex) { - throw new FatalBackendException(ex); - } } @Override @@ -96,20 +92,12 @@ class S3CloudContentRepository extends InterceptingCloudContentRepository Date: Tue, 20 Apr 2021 14:02:23 +0200 Subject: [PATCH 32/80] feat(S3): use SUFFIX, remove unthrowable exceptions --- .../java/org/cryptomator/data/cloud/s3/S3Impl.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java b/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java index ff11839a..8a5db94f 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java +++ b/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java @@ -85,11 +85,11 @@ class S3Impl { return root; } - public S3Folder resolve(String path) throws IOException, BackendException { - if (path.startsWith("/")) { + public S3Folder resolve(String path) { + if (path.startsWith(SUFFIX)) { path = path.substring(1); } - String[] names = path.split("/"); + String[] names = path.split(SUFFIX); S3Folder folder = root; for (String name : names) { folder = folder(folder, name); @@ -105,7 +105,7 @@ class S3Impl { return S3CloudNodeFactory.file(parent, name, size, parent.getPath() + SUFFIX + name); } - public S3Folder folder(S3Folder parent, String name) throws IOException, BackendException { + public S3Folder folder(S3Folder parent, String name) { return S3CloudNodeFactory.folder(parent, name, parent.getPath() + SUFFIX + name + SUFFIX); } @@ -192,7 +192,7 @@ class S3Impl { } private PutObjectResult uploadFile(final S3File file, DataSource data, final ProgressAware progressAware, final long size) // - throws IOException, BackendException { + throws IOException { ProgressListener listener = progressEvent -> progressAware.onProgress( // progress(UploadState.upload(file)) // .between(0) // From 24e96b720548957b76b925019bc20b6646ef2451 Mon Sep 17 00:00:00 2001 From: Manuel Jenny Date: Tue, 20 Apr 2021 14:05:07 +0200 Subject: [PATCH 33/80] fix(S3): make sure the build works --- .../org/cryptomator/data/cloud/s3/S3Impl.java | 78 +++++++++---------- 1 file changed, 35 insertions(+), 43 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java b/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java index 8a5db94f..93585983 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java +++ b/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java @@ -158,22 +158,12 @@ class S3Impl { throw new CloudNodeAlreadyExistsException(target.getName()); } - try { - if (source instanceof S3Folder) { - return S3CloudNodeFactory.from(target.getParent(), client().moveFolder(source.getPath(), target.getPath()).execute()); - } else { - return S3CloudNodeFactory.from(target.getParent(), client().moveFile(source.getPath(), target.getPath()).execute()); - } - } catch (ApiError ex) { - if (PCloudApiError.isCloudNodeAlreadyExistsException(ex.errorCode())) { - throw new CloudNodeAlreadyExistsException(target.getName()); - } else if (PCloudApiError.isNoSuchCloudFileException(ex.errorCode())) { - throw new NoSuchCloudFileException(source.getName()); - } else { - handleApiError(ex, PCloudApiError.ignoreMoveSet, null); - } - throw new FatalBackendException(ex); - } +// if (source instanceof S3Folder) { +// return S3CloudNodeFactory.from(target.getParent(), client().moveFolder(source.getPath(), target.getPath()).execute()); +// } else { +// return S3CloudNodeFactory.from(target.getParent(), client().moveFile(source.getPath(), target.getPath()).execute()); +// } + return null; } public S3File write(S3File file, DataSource data, final ProgressAware progressAware, boolean replace, long size) throws IOException, BackendException { @@ -307,32 +297,34 @@ class S3Impl { return true; } - private void handleApiError(ApiError ex) throws BackendException { - handleApiError(ex, null, null); - } + //TODO: add proper error handling or remove entirely - private void handleApiError(ApiError ex, String name) throws BackendException { - handleApiError(ex, null, name); - } - - private void handleApiError(ApiError ex, Set errorCodes, String name) throws BackendException { - if (errorCodes == null || !errorCodes.contains(ex.errorCode())) { - int errorCode = ex.errorCode(); - if (PCloudApiError.isCloudNodeAlreadyExistsException(errorCode)) { - throw new CloudNodeAlreadyExistsException(name); - } else if (PCloudApiError.isForbiddenException(errorCode)) { - throw new ForbiddenException(); - } else if (PCloudApiError.isNetworkConnectionException(errorCode)) { - throw new NetworkConnectionException(ex); - } else if (PCloudApiError.isNoSuchCloudFileException(errorCode)) { - throw new NoSuchCloudFileException(name); - } else if (PCloudApiError.isWrongCredentialsException(errorCode)) { - throw new WrongCredentialsException(cloud); - } else if (PCloudApiError.isUnauthorizedException(errorCode)) { - throw new UnauthorizedException(); - } else { - throw new FatalBackendException(ex); - } - } - } +// private void handleApiError(ApiError ex) throws BackendException { +// handleApiError(ex, null, null); +// } +// +// private void handleApiError(ApiError ex, String name) throws BackendException { +// handleApiError(ex, null, name); +// } +// +// private void handleApiError(ApiError ex, Set errorCodes, String name) throws BackendException { +// if (errorCodes == null || !errorCodes.contains(ex.errorCode())) { +// int errorCode = ex.errorCode(); +// if (PCloudApiError.isCloudNodeAlreadyExistsException(errorCode)) { +// throw new CloudNodeAlreadyExistsException(name); +// } else if (PCloudApiError.isForbiddenException(errorCode)) { +// throw new ForbiddenException(); +// } else if (PCloudApiError.isNetworkConnectionException(errorCode)) { +// throw new NetworkConnectionException(ex); +// } else if (PCloudApiError.isNoSuchCloudFileException(errorCode)) { +// throw new NoSuchCloudFileException(name); +// } else if (PCloudApiError.isWrongCredentialsException(errorCode)) { +// throw new WrongCredentialsException(cloud); +// } else if (PCloudApiError.isUnauthorizedException(errorCode)) { +// throw new UnauthorizedException(); +// } else { +// throw new FatalBackendException(ex); +// } +// } +// } } From fe15c748bc100f10fa9913a0f3b47934ae050b61 Mon Sep 17 00:00:00 2001 From: Manuel Jenny Date: Tue, 20 Apr 2021 14:12:51 +0200 Subject: [PATCH 34/80] fix(S3): ContentRepositoryFactory to use proper Cloud --- .../data/cloud/s3/S3CloudContentRepositoryFactory.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudContentRepositoryFactory.java b/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudContentRepositoryFactory.java index 9fc9dd54..63954c73 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudContentRepositoryFactory.java +++ b/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudContentRepositoryFactory.java @@ -4,7 +4,7 @@ import android.content.Context; import org.cryptomator.data.repository.CloudContentRepositoryFactory; import org.cryptomator.domain.Cloud; -import org.cryptomator.domain.PCloud; +import org.cryptomator.domain.S3Cloud; import org.cryptomator.domain.repository.CloudContentRepository; import javax.inject.Inject; @@ -29,7 +29,7 @@ public class S3CloudContentRepositoryFactory implements CloudContentRepositoryFa @Override public CloudContentRepository cloudContentRepositoryFor(Cloud cloud) { - return new S3CloudContentRepository((PCloud) cloud, context); + return new S3CloudContentRepository((S3Cloud) cloud, context); } } From ddbb59b831ec6286f3610db78e5219cfd7f098f2 Mon Sep 17 00:00:00 2001 From: Manuel Jenny Date: Tue, 20 Apr 2021 15:57:25 +0200 Subject: [PATCH 35/80] feat(S3): add new file() method --- .../org/cryptomator/data/cloud/s3/S3CloudNodeFactory.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudNodeFactory.java b/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudNodeFactory.java index 9a24d710..f17f4475 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudNodeFactory.java +++ b/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudNodeFactory.java @@ -5,6 +5,8 @@ import com.amazonaws.services.s3.model.S3ObjectSummary; import org.cryptomator.util.Optional; +import java.util.Date; + class S3CloudNodeFactory { private static final String SUFFIX = "/"; @@ -27,6 +29,10 @@ class S3CloudNodeFactory { return new S3File(parent, name, path, size, Optional.empty()); } + public static S3File file(S3Folder parent, String name, Optional size, Optional lastModified) { + return new S3File(parent, name, getNodePath(parent, name), size, lastModified); + } + public static S3Folder folder(S3Folder parent, S3ObjectSummary folder) { String name = getNameFromKey(folder.getKey()); return new S3Folder(parent, name, getNodePath(parent, name)); From a9525b44778a7020f561cc258749cf28a70a2a77 Mon Sep 17 00:00:00 2001 From: Manuel Jenny Date: Tue, 20 Apr 2021 16:00:39 +0200 Subject: [PATCH 36/80] feat(S3): implement move() --- .../org/cryptomator/data/cloud/s3/S3Impl.java | 70 +++++++------------ 1 file changed, 26 insertions(+), 44 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java b/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java index 93585983..bb27f1b5 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java +++ b/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java @@ -4,6 +4,7 @@ import android.content.Context; import com.amazonaws.event.ProgressListener; import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.CopyObjectResult; import com.amazonaws.services.s3.model.DeleteObjectsRequest; import com.amazonaws.services.s3.model.DeleteObjectsRequest.KeyVersion; import com.amazonaws.services.s3.model.GetObjectRequest; @@ -15,20 +16,14 @@ import com.amazonaws.services.s3.model.PutObjectRequest; import com.amazonaws.services.s3.model.PutObjectResult; import com.amazonaws.services.s3.model.S3Object; import com.amazonaws.services.s3.model.S3ObjectSummary; -import com.pcloud.sdk.ApiError; import com.tomclaw.cache.DiskLruCache; import org.cryptomator.data.util.CopyStream; import org.cryptomator.domain.S3Cloud; import org.cryptomator.domain.exception.BackendException; import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException; -import org.cryptomator.domain.exception.FatalBackendException; -import org.cryptomator.domain.exception.ForbiddenException; -import org.cryptomator.domain.exception.NetworkConnectionException; import org.cryptomator.domain.exception.NoSuchCloudFileException; -import org.cryptomator.domain.exception.UnauthorizedException; import org.cryptomator.domain.exception.authentication.NoAuthenticationProvidedException; -import org.cryptomator.domain.exception.authentication.WrongCredentialsException; import org.cryptomator.domain.usecases.ProgressAware; import org.cryptomator.domain.usecases.cloud.DataSource; import org.cryptomator.domain.usecases.cloud.DownloadState; @@ -45,7 +40,6 @@ import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.List; -import java.util.Set; import timber.log.Timber; @@ -158,12 +152,31 @@ class S3Impl { throw new CloudNodeAlreadyExistsException(target.getName()); } -// if (source instanceof S3Folder) { -// return S3CloudNodeFactory.from(target.getParent(), client().moveFolder(source.getPath(), target.getPath()).execute()); -// } else { -// return S3CloudNodeFactory.from(target.getParent(), client().moveFile(source.getPath(), target.getPath()).execute()); -// } - return null; + if (source instanceof S3Folder) { + ObjectListing listing = client().listObjects(cloud.s3Bucket(), source.getPath() + SUFFIX); + + if (listing.getObjectSummaries().size() > 0) { + String sourceKey = source.getPath() + SUFFIX; + String targetKey = target.getPath() + SUFFIX; + + List objectsToDelete = new ArrayList<>(); + + for (S3ObjectSummary summary : listing.getObjectSummaries()) { + objectsToDelete.add(new DeleteObjectsRequest.KeyVersion(summary.getKey())); + String destinationKey = summary.getKey().replace(sourceKey, targetKey); + + client().copyObject(cloud.s3Bucket(), summary.getKey(), cloud.s3Bucket(), destinationKey); + } + client().deleteObjects(new DeleteObjectsRequest(cloud.s3Bucket()).withKeys(objectsToDelete)); + } else { + throw new NoSuchCloudFileException(source.getPath()); + } + return S3CloudNodeFactory.folder(target.getParent(), target.getName()); + } else { + CopyObjectResult result = client().copyObject(cloud.s3Bucket(), source.getPath(), cloud.s3Bucket(), target.getPath()); + client().deleteObject(cloud.s3Bucket(), source.getPath()); + return S3CloudNodeFactory.file(target.getParent(), target.getName(), ((S3File)source).getSize(), Optional.of(result.getLastModifiedDate())); + } } public S3File write(S3File file, DataSource data, final ProgressAware progressAware, boolean replace, long size) throws IOException, BackendException { @@ -296,35 +309,4 @@ class S3Impl { return true; } - - //TODO: add proper error handling or remove entirely - -// private void handleApiError(ApiError ex) throws BackendException { -// handleApiError(ex, null, null); -// } -// -// private void handleApiError(ApiError ex, String name) throws BackendException { -// handleApiError(ex, null, name); -// } -// -// private void handleApiError(ApiError ex, Set errorCodes, String name) throws BackendException { -// if (errorCodes == null || !errorCodes.contains(ex.errorCode())) { -// int errorCode = ex.errorCode(); -// if (PCloudApiError.isCloudNodeAlreadyExistsException(errorCode)) { -// throw new CloudNodeAlreadyExistsException(name); -// } else if (PCloudApiError.isForbiddenException(errorCode)) { -// throw new ForbiddenException(); -// } else if (PCloudApiError.isNetworkConnectionException(errorCode)) { -// throw new NetworkConnectionException(ex); -// } else if (PCloudApiError.isNoSuchCloudFileException(errorCode)) { -// throw new NoSuchCloudFileException(name); -// } else if (PCloudApiError.isWrongCredentialsException(errorCode)) { -// throw new WrongCredentialsException(cloud); -// } else if (PCloudApiError.isUnauthorizedException(errorCode)) { -// throw new UnauthorizedException(); -// } else { -// throw new FatalBackendException(ex); -// } -// } -// } } From 57e2bb8655900d7b3d0233a3a2ab4093f1c9dbc2 Mon Sep 17 00:00:00 2001 From: Manuel Jenny Date: Tue, 20 Apr 2021 16:40:23 +0200 Subject: [PATCH 37/80] feat(S3): add S3 to LruFileCacheUtil --- .../main/java/org/cryptomator/util/file/LruFileCacheUtil.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/util/src/main/java/org/cryptomator/util/file/LruFileCacheUtil.kt b/util/src/main/java/org/cryptomator/util/file/LruFileCacheUtil.kt index c4082926..7223491b 100644 --- a/util/src/main/java/org/cryptomator/util/file/LruFileCacheUtil.kt +++ b/util/src/main/java/org/cryptomator/util/file/LruFileCacheUtil.kt @@ -21,7 +21,7 @@ class LruFileCacheUtil(context: Context) { private val parent: File = context.cacheDir enum class Cache { - DROPBOX, WEBDAV, PCLOUD, ONEDRIVE, GOOGLE_DRIVE + DROPBOX, WEBDAV, PCLOUD, S3, ONEDRIVE, GOOGLE_DRIVE } fun resolve(cache: Cache?): File { @@ -29,6 +29,7 @@ class LruFileCacheUtil(context: Context) { Cache.DROPBOX -> File(parent, "LruCacheDropbox") Cache.WEBDAV -> File(parent, "LruCacheWebdav") Cache.PCLOUD -> File(parent, "LruCachePCloud") + Cache.S3 -> File(parent, "LruCacheS3") Cache.ONEDRIVE -> File(parent, "LruCacheOneDrive") Cache.GOOGLE_DRIVE -> File(parent, "LruCacheGoogleDrive") else -> throw IllegalStateException() From 4e082d5f8a285e413139d99cd0014685b7cfff18 Mon Sep 17 00:00:00 2001 From: Manuel Jenny Date: Tue, 20 Apr 2021 16:42:30 +0200 Subject: [PATCH 38/80] fix(S3): use trailing slash for folders --- .../org/cryptomator/data/cloud/s3/S3Impl.java | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java b/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java index bb27f1b5..2e9c04e6 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java +++ b/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java @@ -86,7 +86,9 @@ class S3Impl { String[] names = path.split(SUFFIX); S3Folder folder = root; for (String name : names) { - folder = folder(folder, name); + if (!name.isEmpty()) { + folder = folder(folder, name); + } } return folder; } @@ -96,18 +98,15 @@ class S3Impl { } public S3File file(S3Folder parent, String name, Optional size) throws BackendException, IOException { - return S3CloudNodeFactory.file(parent, name, size, parent.getPath() + SUFFIX + name); + return S3CloudNodeFactory.file(parent, name, size, parent.getPath() + name); } public S3Folder folder(S3Folder parent, String name) { - return S3CloudNodeFactory.folder(parent, name, parent.getPath() + SUFFIX + name + SUFFIX); + return S3CloudNodeFactory.folder(parent, name, parent.getPath() + name + SUFFIX); } public boolean exists(S3Node node) { String path = node.getPath(); - if (node instanceof S3Folder) { - path += SUFFIX; - } ObjectListing result = client().listObjects(cloud.s3Bucket(), path); @@ -141,7 +140,7 @@ class S3Impl { InputStream emptyContent = new ByteArrayInputStream(new byte[0]); - PutObjectRequest putObjectRequest = new PutObjectRequest(cloud.s3Bucket(), folder.getPath() + SUFFIX, emptyContent, metadata); + PutObjectRequest putObjectRequest = new PutObjectRequest(cloud.s3Bucket(), folder.getPath(), emptyContent, metadata); client().putObject(putObjectRequest); return S3CloudNodeFactory.folder(folder.getParent(), folder.getName()); @@ -153,17 +152,15 @@ class S3Impl { } if (source instanceof S3Folder) { - ObjectListing listing = client().listObjects(cloud.s3Bucket(), source.getPath() + SUFFIX); + ObjectListing listing = client().listObjects(cloud.s3Bucket(), source.getPath()); if (listing.getObjectSummaries().size() > 0) { - String sourceKey = source.getPath() + SUFFIX; - String targetKey = target.getPath() + SUFFIX; List objectsToDelete = new ArrayList<>(); for (S3ObjectSummary summary : listing.getObjectSummaries()) { objectsToDelete.add(new DeleteObjectsRequest.KeyVersion(summary.getKey())); - String destinationKey = summary.getKey().replace(sourceKey, targetKey); + String destinationKey = summary.getKey().replace(source.getPath(), target.getPath()); client().copyObject(cloud.s3Bucket(), summary.getKey(), cloud.s3Bucket(), destinationKey); } @@ -276,7 +273,7 @@ class S3Impl { public void delete(S3Node node) throws IOException, BackendException { if (node instanceof S3Folder) { - ObjectListing listing = client().listObjects(cloud.s3Bucket(), node.getPath() + SUFFIX); + ObjectListing listing = client().listObjects(cloud.s3Bucket(), node.getPath()); List keys = new ArrayList<>(); for (S3ObjectSummary summary : listing.getObjectSummaries()) { keys.add(new KeyVersion(summary.getKey())); From cf715bc000dd3a7470cd8ea93f56d554586155d6 Mon Sep 17 00:00:00 2001 From: Manuel Jenny Date: Tue, 20 Apr 2021 16:43:01 +0200 Subject: [PATCH 39/80] fix(S3): use own S3 Lru Cache --- .../src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java b/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java index 2e9c04e6..85490dd2 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java +++ b/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java @@ -44,7 +44,7 @@ import java.util.List; import timber.log.Timber; import static org.cryptomator.domain.usecases.cloud.Progress.progress; -import static org.cryptomator.util.file.LruFileCacheUtil.Cache.PCLOUD; +import static org.cryptomator.util.file.LruFileCacheUtil.Cache.S3; import static org.cryptomator.util.file.LruFileCacheUtil.retrieveFromLruCache; import static org.cryptomator.util.file.LruFileCacheUtil.storeToLruCache; @@ -297,9 +297,9 @@ class S3Impl { private boolean createLruCache(int cacheSize) { if (diskLruCache == null) { try { - diskLruCache = DiskLruCache.create(new LruFileCacheUtil(context).resolve(PCLOUD), cacheSize); + diskLruCache = DiskLruCache.create(new LruFileCacheUtil(context).resolve(S3), cacheSize); } catch (IOException e) { - Timber.tag("PCloudImpl").e(e, "Failed to setup LRU cache"); + Timber.tag("S3Impl").e(e, "Failed to setup LRU cache"); return false; } } From b0f288f47956abd9139e10d2d21602f11103475b Mon Sep 17 00:00:00 2001 From: Manuel Jenny Date: Tue, 20 Apr 2021 17:19:19 +0200 Subject: [PATCH 40/80] fix(S3): use listObjectsV2() --- .../org/cryptomator/data/cloud/s3/S3Impl.java | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java b/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java index 85490dd2..e31afaa7 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java +++ b/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java @@ -9,7 +9,6 @@ import com.amazonaws.services.s3.model.DeleteObjectsRequest; import com.amazonaws.services.s3.model.DeleteObjectsRequest.KeyVersion; import com.amazonaws.services.s3.model.GetObjectRequest; import com.amazonaws.services.s3.model.ListObjectsV2Result; -import com.amazonaws.services.s3.model.ObjectListing; import com.amazonaws.services.s3.model.ObjectMetadata; import com.amazonaws.services.s3.model.Owner; import com.amazonaws.services.s3.model.PutObjectRequest; @@ -108,7 +107,7 @@ class S3Impl { public boolean exists(S3Node node) { String path = node.getPath(); - ObjectListing result = client().listObjects(cloud.s3Bucket(), path); + ListObjectsV2Result result = client().listObjectsV2(cloud.s3Bucket(), path); if (result.getObjectSummaries().size() > 0) { return true; @@ -120,8 +119,8 @@ class S3Impl { public List list(S3Folder folder) throws IOException, BackendException { List result = new ArrayList<>(); - ListObjectsV2Result objectListing = client().listObjectsV2(cloud.s3Bucket(), folder.getPath()); - for (S3ObjectSummary objectSummary : objectListing.getObjectSummaries()) { + ListObjectsV2Result listObjects = client().listObjectsV2(cloud.s3Bucket(), folder.getPath()); + for (S3ObjectSummary objectSummary : listObjects.getObjectSummaries()) { result.add(S3CloudNodeFactory.from(folder, objectSummary)); } return result; @@ -152,13 +151,13 @@ class S3Impl { } if (source instanceof S3Folder) { - ObjectListing listing = client().listObjects(cloud.s3Bucket(), source.getPath()); + ListObjectsV2Result listObjects = client().listObjectsV2(cloud.s3Bucket(), source.getPath()); - if (listing.getObjectSummaries().size() > 0) { + if (listObjects.getObjectSummaries().size() > 0) { List objectsToDelete = new ArrayList<>(); - for (S3ObjectSummary summary : listing.getObjectSummaries()) { + for (S3ObjectSummary summary : listObjects.getObjectSummaries()) { objectsToDelete.add(new DeleteObjectsRequest.KeyVersion(summary.getKey())); String destinationKey = summary.getKey().replace(source.getPath(), target.getPath()); @@ -214,14 +213,14 @@ class S3Impl { Optional cacheKey = Optional.empty(); Optional cacheFile = Optional.empty(); - ObjectListing objectListing; + ListObjectsV2Result listObjects; if (sharedPreferencesHandler.useLruCache() && createLruCache(sharedPreferencesHandler.lruCacheSize())) { - objectListing = client().listObjects(cloud.s3Bucket(), file.getPath()); - if (objectListing.getObjectSummaries().size() != 1) { + listObjects = client().listObjectsV2(cloud.s3Bucket(), file.getPath()); + if (listObjects.getObjectSummaries().size() != 1) { throw new NoSuchCloudFileException(file.getPath()); } - S3ObjectSummary summary = objectListing.getObjectSummaries().get(0); + S3ObjectSummary summary = listObjects.getObjectSummaries().get(0); cacheKey = Optional.of(summary.getKey() + summary.getETag()); File cachedFile = diskLruCache.get(cacheKey.get()); @@ -273,9 +272,9 @@ class S3Impl { public void delete(S3Node node) throws IOException, BackendException { if (node instanceof S3Folder) { - ObjectListing listing = client().listObjects(cloud.s3Bucket(), node.getPath()); + ListObjectsV2Result listObjects = client().listObjectsV2(cloud.s3Bucket(), node.getPath()); List keys = new ArrayList<>(); - for (S3ObjectSummary summary : listing.getObjectSummaries()) { + for (S3ObjectSummary summary : listObjects.getObjectSummaries()) { keys.add(new KeyVersion(summary.getKey())); } From a736a33d88580f520a4e2ce365d018574414d591 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Wed, 21 Apr 2021 17:35:09 +0200 Subject: [PATCH 41/80] Update database and UI to support S3 --- data/build.gradle | 2 +- .../s3/S3CloudContentRepositoryFactory.java | 4 +- .../cryptomator/data/db/DatabaseUpgrades.java | 6 +- .../org/cryptomator/data/db/Upgrade5To6.kt | 76 ++++++++++++ .../data/db/entities/CloudEntity.java | 37 +++++- .../data/db/entities/VaultEntity.java | 4 +- .../data/db/mappers/CloudEntityMapper.java | 28 ++++- .../CloudContentRepositoryFactories.java | 3 + .../org/cryptomator/domain/CloudType.java | 2 +- .../java/org/cryptomator/domain/S3Cloud.java | 4 +- .../domain/usecases/cloud/ConnectToS3.java | 23 ++++ presentation/src/main/AndroidManifest.xml | 1 + .../di/component/ActivityComponent.java | 6 + .../intent/S3AddOrChangeIntent.java | 13 ++ .../presentation/model/CloudTypeModel.kt | 5 + .../presentation/model/S3CloudModel.kt | 48 ++++++++ .../model/mappers/CloudModelMapper.kt | 4 +- .../presenter/CloudConnectionListPresenter.kt | 27 +++-- .../presenter/CloudSettingsPresenter.kt | 15 ++- .../presenter/S3AddOrChangePresenter.kt | 97 +++++++++++++++ .../ui/activity/S3AddOrChangeActivity.kt | 37 ++++++ .../ui/activity/view/S3AddOrChangeView.kt | 7 ++ .../ui/adapter/CloudSettingsAdapter.kt | 38 ++---- .../ui/fragment/S3AddOrChangeFragment.kt | 112 ++++++++++++++++++ .../src/main/res/layout/fragment_setup_s3.xml | 112 ++++++++++++++++++ presentation/src/main/res/values/strings.xml | 11 ++ 26 files changed, 665 insertions(+), 57 deletions(-) create mode 100644 data/src/main/java/org/cryptomator/data/db/Upgrade5To6.kt create mode 100644 domain/src/main/java/org/cryptomator/domain/usecases/cloud/ConnectToS3.java create mode 100644 presentation/src/main/java/org/cryptomator/presentation/intent/S3AddOrChangeIntent.java create mode 100644 presentation/src/main/java/org/cryptomator/presentation/model/S3CloudModel.kt create mode 100644 presentation/src/main/java/org/cryptomator/presentation/presenter/S3AddOrChangePresenter.kt create mode 100644 presentation/src/main/java/org/cryptomator/presentation/ui/activity/S3AddOrChangeActivity.kt create mode 100644 presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/S3AddOrChangeView.kt create mode 100644 presentation/src/main/java/org/cryptomator/presentation/ui/fragment/S3AddOrChangeFragment.kt create mode 100644 presentation/src/main/res/layout/fragment_setup_s3.xml diff --git a/data/build.gradle b/data/build.gradle index e1437074..02722fd1 100644 --- a/data/build.gradle +++ b/data/build.gradle @@ -74,7 +74,7 @@ android { } greendao { - schemaVersion 5 + schemaVersion 6 } configurations.all { diff --git a/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudContentRepositoryFactory.java b/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudContentRepositoryFactory.java index 63954c73..da7177e4 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudContentRepositoryFactory.java +++ b/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudContentRepositoryFactory.java @@ -10,7 +10,7 @@ import org.cryptomator.domain.repository.CloudContentRepository; import javax.inject.Inject; import javax.inject.Singleton; -import static org.cryptomator.domain.CloudType.PCLOUD; +import static org.cryptomator.domain.CloudType.S3; @Singleton public class S3CloudContentRepositoryFactory implements CloudContentRepositoryFactory { @@ -24,7 +24,7 @@ public class S3CloudContentRepositoryFactory implements CloudContentRepositoryFa @Override public boolean supports(Cloud cloud) { - return cloud.type() == PCLOUD; + return cloud.type() == S3; } @Override diff --git a/data/src/main/java/org/cryptomator/data/db/DatabaseUpgrades.java b/data/src/main/java/org/cryptomator/data/db/DatabaseUpgrades.java index b116cb04..100755cd 100644 --- a/data/src/main/java/org/cryptomator/data/db/DatabaseUpgrades.java +++ b/data/src/main/java/org/cryptomator/data/db/DatabaseUpgrades.java @@ -23,14 +23,16 @@ class DatabaseUpgrades { Upgrade1To2 upgrade1To2, // Upgrade2To3 upgrade2To3, // Upgrade3To4 upgrade3To4, // - Upgrade4To5 upgrade4To5) { + Upgrade4To5 upgrade4To5, // + Upgrade5To6 upgrade5To6) { availableUpgrades = defineUpgrades( // upgrade0To1, // upgrade1To2, // upgrade2To3, // upgrade3To4, // - upgrade4To5); + upgrade4To5, // + upgrade5To6); } private static Comparator reverseOrder() { diff --git a/data/src/main/java/org/cryptomator/data/db/Upgrade5To6.kt b/data/src/main/java/org/cryptomator/data/db/Upgrade5To6.kt new file mode 100644 index 00000000..1274ecd9 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/db/Upgrade5To6.kt @@ -0,0 +1,76 @@ +package org.cryptomator.data.db + +import org.greenrobot.greendao.database.Database +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class Upgrade5To6 @Inject constructor() : DatabaseUpgrade(5, 6) { + + override fun internalApplyTo(db: Database, origin: Int) { + db.beginTransaction() + try { + changeWebdavUrlInCloudEntityToUrl(db) + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + } + + private fun changeWebdavUrlInCloudEntityToUrl(db: Database) { + Sql.alterTable("CLOUD_ENTITY").renameTo("CLOUD_ENTITY_OLD").executeOn(db) + + Sql.createTable("CLOUD_ENTITY") // + .id() // + .requiredText("TYPE") // + .optionalText("ACCESS_TOKEN") // + .optionalText("URL") // + .optionalText("USERNAME") // + .optionalText("WEBDAV_CERTIFICATE") // + .optionalText("S3_BUCKET") // + .optionalText("S3_REGION") // + .optionalText("S3_SECRET_KEY") // + .executeOn(db); + + Sql.insertInto("CLOUD_ENTITY") // + .select("_id", "TYPE", "ACCESS_TOKEN", "URL", "USERNAME", "WEBDAV_CERTIFICATE") // + .columns("_id", "TYPE", "ACCESS_TOKEN", "URL", "USERNAME", "WEBDAV_CERTIFICATE") // + .from("CLOUD_ENTITY_OLD") // + .executeOn(db) + + recreateVaultEntity(db) + + Sql.dropTable("CLOUD_ENTITY_OLD").executeOn(db) + } + + private fun recreateVaultEntity(db: Database) { + Sql.alterTable("VAULT_ENTITY").renameTo("VAULT_ENTITY_OLD").executeOn(db) + Sql.createTable("VAULT_ENTITY") // + .id() // + .optionalInt("FOLDER_CLOUD_ID") // + .optionalText("FOLDER_PATH") // + .optionalText("FOLDER_NAME") // + .requiredText("CLOUD_TYPE") // + .optionalText("PASSWORD") // + .optionalInt("POSITION") // + .foreignKey("FOLDER_CLOUD_ID", "CLOUD_ENTITY", Sql.SqlCreateTableBuilder.ForeignKeyBehaviour.ON_DELETE_SET_NULL) // + .executeOn(db) + + Sql.insertInto("VAULT_ENTITY") // + .select("_id", "FOLDER_CLOUD_ID", "FOLDER_PATH", "FOLDER_NAME", "PASSWORD", "POSITION", "CLOUD_ENTITY.TYPE") // + .columns("_id", "FOLDER_CLOUD_ID", "FOLDER_PATH", "FOLDER_NAME", "PASSWORD", "POSITION", "CLOUD_TYPE") // + .from("VAULT_ENTITY_OLD") // + .join("CLOUD_ENTITY", "VAULT_ENTITY_OLD.FOLDER_CLOUD_ID") // + .executeOn(db) + + Sql.dropIndex("IDX_VAULT_ENTITY_FOLDER_PATH_FOLDER_CLOUD_ID").executeOn(db) + + Sql.createUniqueIndex("IDX_VAULT_ENTITY_FOLDER_PATH_FOLDER_CLOUD_ID") // + .on("VAULT_ENTITY") // + .asc("FOLDER_PATH") // + .asc("FOLDER_CLOUD_ID") // + .executeOn(db) + + Sql.dropTable("VAULT_ENTITY_OLD").executeOn(db) + } +} diff --git a/data/src/main/java/org/cryptomator/data/db/entities/CloudEntity.java b/data/src/main/java/org/cryptomator/data/db/entities/CloudEntity.java index 0ce2c8a1..b939e925 100644 --- a/data/src/main/java/org/cryptomator/data/db/entities/CloudEntity.java +++ b/data/src/main/java/org/cryptomator/data/db/entities/CloudEntity.java @@ -22,14 +22,23 @@ public class CloudEntity extends DatabaseEntity { private String webdavCertificate; - @Generated(hash = 361171073) - public CloudEntity(Long id, @NotNull String type, String accessToken, String url, String username, String webdavCertificate) { + private String s3Bucket; + + private String s3Region; + + private String s3SecretKey; + + @Generated(hash = 1685351705) + public CloudEntity(Long id, @NotNull String type, String accessToken, String url, String username, String webdavCertificate, String s3Bucket, String s3Region, String s3SecretKey) { this.id = id; this.type = type; this.accessToken = accessToken; this.url = url; this.username = username; this.webdavCertificate = webdavCertificate; + this.s3Bucket = s3Bucket; + this.s3Region = s3Region; + this.s3SecretKey = s3SecretKey; } @Generated(hash = 1354152224) @@ -83,4 +92,28 @@ public class CloudEntity extends DatabaseEntity { public void setWebdavCertificate(String webdavCertificate) { this.webdavCertificate = webdavCertificate; } + + public String getS3Bucket() { + return this.s3Bucket; + } + + public void setS3Bucket(String s3Bucket) { + this.s3Bucket = s3Bucket; + } + + public String getS3Region() { + return this.s3Region; + } + + public void setS3Region(String s3Region) { + this.s3Region = s3Region; + } + + public String getS3SecretKey() { + return this.s3SecretKey; + } + + public void setS3SecretKey(String s3SecretKey) { + this.s3SecretKey = s3SecretKey; + } } diff --git a/data/src/main/java/org/cryptomator/data/db/entities/VaultEntity.java b/data/src/main/java/org/cryptomator/data/db/entities/VaultEntity.java index af12e1ef..b8d683fc 100644 --- a/data/src/main/java/org/cryptomator/data/db/entities/VaultEntity.java +++ b/data/src/main/java/org/cryptomator/data/db/entities/VaultEntity.java @@ -182,9 +182,7 @@ public class VaultEntity extends DatabaseEntity { this.position = position; } - /** - * called by internal mechanisms, do not call yourself. - */ + /** called by internal mechanisms, do not call yourself. */ @Generated(hash = 674742652) public void __setDaoSession(DaoSession daoSession) { this.daoSession = daoSession; diff --git a/data/src/main/java/org/cryptomator/data/db/mappers/CloudEntityMapper.java b/data/src/main/java/org/cryptomator/data/db/mappers/CloudEntityMapper.java index 4b637b25..53f99fed 100644 --- a/data/src/main/java/org/cryptomator/data/db/mappers/CloudEntityMapper.java +++ b/data/src/main/java/org/cryptomator/data/db/mappers/CloudEntityMapper.java @@ -8,6 +8,7 @@ import org.cryptomator.domain.GoogleDriveCloud; import org.cryptomator.domain.LocalStorageCloud; import org.cryptomator.domain.OnedriveCloud; import org.cryptomator.domain.PCloud; +import org.cryptomator.domain.S3Cloud; import org.cryptomator.domain.WebDavCloud; import javax.inject.Inject; @@ -18,6 +19,7 @@ import static org.cryptomator.domain.GoogleDriveCloud.aGoogleDriveCloud; import static org.cryptomator.domain.LocalStorageCloud.aLocalStorage; import static org.cryptomator.domain.OnedriveCloud.aOnedriveCloud; import static org.cryptomator.domain.PCloud.aPCloud; +import static org.cryptomator.domain.S3Cloud.aS3Cloud; import static org.cryptomator.domain.WebDavCloud.aWebDavCloudCloud; @Singleton @@ -43,6 +45,10 @@ public class CloudEntityMapper extends EntityMapper { .withAccessToken(entity.getAccessToken()) // .withUsername(entity.getUsername()) // .build(); + case LOCAL: + return aLocalStorage() // + .withId(entity.getId()) // + .withRootUri(entity.getAccessToken()).build(); case ONEDRIVE: return aOnedriveCloud() // .withId(entity.getId()) // @@ -56,10 +62,15 @@ public class CloudEntityMapper extends EntityMapper { .withAccessToken(entity.getAccessToken()) // .withUsername(entity.getUsername()) // .build(); - case LOCAL: - return aLocalStorage() // + case S3: + aS3Cloud() // .withId(entity.getId()) // - .withRootUri(entity.getAccessToken()).build(); + .withS3Endpoint(entity.getUrl()) // + .withS3Region(entity.getS3Region()) // + .withAccessKey(entity.getAccessToken()) // + .withSecretKey(entity.getS3SecretKey()) // + .withS3Bucket(entity.getS3Bucket()) // + .build(); case WEBDAV: return aWebDavCloudCloud() // .withId(entity.getId()) // @@ -87,6 +98,9 @@ public class CloudEntityMapper extends EntityMapper { result.setAccessToken(((GoogleDriveCloud) domainObject).accessToken()); result.setUsername(((GoogleDriveCloud) domainObject).username()); break; + case LOCAL: + result.setAccessToken(((LocalStorageCloud) domainObject).rootUri()); + break; case ONEDRIVE: result.setAccessToken(((OnedriveCloud) domainObject).accessToken()); result.setUsername(((OnedriveCloud) domainObject).username()); @@ -96,8 +110,12 @@ public class CloudEntityMapper extends EntityMapper { result.setUrl(((PCloud) domainObject).url()); result.setUsername(((PCloud) domainObject).username()); break; - case LOCAL: - result.setAccessToken(((LocalStorageCloud) domainObject).rootUri()); + case S3: + result.setUrl(((S3Cloud) domainObject).s3Endpoint()); + result.setS3Region(((S3Cloud) domainObject).s3Region()); + result.setAccessToken(((S3Cloud) domainObject).accessKey()); + result.setS3SecretKey(((S3Cloud) domainObject).secretKey()); + result.setS3Bucket(((S3Cloud) domainObject).s3Bucket()); break; case WEBDAV: result.setAccessToken(((WebDavCloud) domainObject).password()); diff --git a/data/src/notFoss/java/org/cryptomator/data/cloud/CloudContentRepositoryFactories.java b/data/src/notFoss/java/org/cryptomator/data/cloud/CloudContentRepositoryFactories.java index ce4f9b41..918d4dfb 100644 --- a/data/src/notFoss/java/org/cryptomator/data/cloud/CloudContentRepositoryFactories.java +++ b/data/src/notFoss/java/org/cryptomator/data/cloud/CloudContentRepositoryFactories.java @@ -6,6 +6,7 @@ import org.cryptomator.data.cloud.googledrive.GoogleDriveCloudContentRepositoryF import org.cryptomator.data.cloud.local.LocalStorageContentRepositoryFactory; import org.cryptomator.data.cloud.onedrive.OnedriveCloudContentRepositoryFactory; import org.cryptomator.data.cloud.pcloud.PCloudContentRepositoryFactory; +import org.cryptomator.data.cloud.s3.S3CloudContentRepositoryFactory; import org.cryptomator.data.cloud.webdav.WebDavCloudContentRepositoryFactory; import org.cryptomator.data.repository.CloudContentRepositoryFactory; import org.jetbrains.annotations.NotNull; @@ -27,6 +28,7 @@ public class CloudContentRepositoryFactories implements Iterable + () return when (CloudTypeModel.valueOf(domainObject.type())) { CloudTypeModel.DROPBOX -> DropboxCloudModel(domainObject) CloudTypeModel.GOOGLE_DRIVE -> GoogleDriveCloudModel(domainObject) + CloudTypeModel.LOCAL -> LocalStorageModel(domainObject) CloudTypeModel.ONEDRIVE -> OnedriveCloudModel(domainObject) CloudTypeModel.PCLOUD -> PCloudModel(domainObject) + CloudTypeModel.S3 -> S3CloudModel(domainObject) CloudTypeModel.CRYPTO -> CryptoCloudModel(domainObject) - CloudTypeModel.LOCAL -> LocalStorageModel(domainObject) CloudTypeModel.WEBDAV -> WebDavCloudModel(domainObject) } } diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudConnectionListPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudConnectionListPresenter.kt index e5cab69e..00ee88aa 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudConnectionListPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudConnectionListPresenter.kt @@ -29,6 +29,7 @@ import org.cryptomator.presentation.intent.Intents import org.cryptomator.presentation.model.CloudModel import org.cryptomator.presentation.model.CloudTypeModel import org.cryptomator.presentation.model.LocalStorageModel +import org.cryptomator.presentation.model.S3CloudModel import org.cryptomator.presentation.model.WebDavCloudModel import org.cryptomator.presentation.model.mappers.CloudModelMapper import org.cryptomator.presentation.ui.activity.view.CloudConnectionListView @@ -129,7 +130,7 @@ class CloudConnectionListPresenter @Inject constructor( // fun onAddConnectionClicked() { when (selectedCloudType.get()) { - CloudTypeModel.WEBDAV -> requestActivityResult(ActivityResultCallbacks.addChangeWebDavCloud(), // + CloudTypeModel.WEBDAV -> requestActivityResult(ActivityResultCallbacks.addChangeMultiCloud(), // Intents.webDavAddOrChangeIntent()) CloudTypeModel.PCLOUD -> { val authIntent: Intent = AuthorizationActivity.createIntent( @@ -143,6 +144,8 @@ class CloudConnectionListPresenter @Inject constructor( // requestActivityResult(ActivityResultCallbacks.pCloudAuthenticationFinished(), // authIntent) } + CloudTypeModel.S3 -> requestActivityResult(ActivityResultCallbacks.addChangeMultiCloud(), // + Intents.s3AddOrChangeIntent()) CloudTypeModel.LOCAL -> openDocumentTree() } } @@ -165,12 +168,20 @@ class CloudConnectionListPresenter @Inject constructor( // } fun onChangeCloudClicked(cloudModel: CloudModel) { - if (cloudModel.cloudType() == CloudTypeModel.WEBDAV) { - requestActivityResult(ActivityResultCallbacks.addChangeWebDavCloud(), // - Intents.webDavAddOrChangeIntent() // - .withWebDavCloud(cloudModel as WebDavCloudModel)) - } else { - throw IllegalStateException("Change cloud with type " + cloudModel.cloudType() + " is not supported") + when { + cloudModel.cloudType() == CloudTypeModel.WEBDAV -> { + requestActivityResult(ActivityResultCallbacks.addChangeMultiCloud(), // + Intents.webDavAddOrChangeIntent() // + .withWebDavCloud(cloudModel as WebDavCloudModel)) + } + cloudModel.cloudType() == CloudTypeModel.S3 -> { + requestActivityResult(ActivityResultCallbacks.addChangeMultiCloud(), // + Intents.s3AddOrChangeIntent() // + .withS3Cloud(cloudModel as S3CloudModel)) + } + else -> { + throw IllegalStateException("Change cloud with type " + cloudModel.cloudType() + " is not supported") + } } } @@ -179,7 +190,7 @@ class CloudConnectionListPresenter @Inject constructor( // } @Callback - fun addChangeWebDavCloud(result: ActivityResult?) { + fun addChangeMultiCloud(result: ActivityResult?) { loadCloudList() } diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudSettingsPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudSettingsPresenter.kt index 4c00f108..3badaf4a 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudSettingsPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudSettingsPresenter.kt @@ -3,6 +3,7 @@ package org.cryptomator.presentation.presenter import org.cryptomator.domain.Cloud import org.cryptomator.domain.LocalStorageCloud import org.cryptomator.domain.PCloud +import org.cryptomator.domain.S3Cloud import org.cryptomator.domain.WebDavCloud import org.cryptomator.domain.di.PerView import org.cryptomator.domain.exception.FatalBackendException @@ -18,6 +19,7 @@ import org.cryptomator.presentation.model.CloudModel import org.cryptomator.presentation.model.CloudTypeModel import org.cryptomator.presentation.model.LocalStorageModel import org.cryptomator.presentation.model.PCloudModel +import org.cryptomator.presentation.model.S3CloudModel import org.cryptomator.presentation.model.WebDavCloudModel import org.cryptomator.presentation.model.mappers.CloudModelMapper import org.cryptomator.presentation.ui.activity.view.CloudSettingsView @@ -37,6 +39,7 @@ class CloudSettingsPresenter @Inject constructor( // CloudTypeModel.CRYPTO, // CloudTypeModel.LOCAL, // CloudTypeModel.PCLOUD, // + CloudTypeModel.S3, // CloudTypeModel.WEBDAV) fun loadClouds() { @@ -44,7 +47,7 @@ class CloudSettingsPresenter @Inject constructor( // } fun onCloudClicked(cloudModel: CloudModel) { - if (isWebdavOrPCloudOrLocal(cloudModel)) { + if (cloudModel.cloudType().isMultiInstance) { startConnectionListActivity(cloudModel.cloudType()) } else { if (isLoggedIn(cloudModel)) { @@ -61,10 +64,6 @@ class CloudSettingsPresenter @Inject constructor( // } } - private fun isWebdavOrPCloudOrLocal(cloudModel: CloudModel): Boolean { - return cloudModel is WebDavCloudModel || cloudModel is LocalStorageModel || cloudModel is PCloudModel - } - private fun loginCloud(cloudModel: CloudModel) { getCloudsUseCase // .withCloudType(CloudTypeModel.valueOf(cloudModel.cloudType())) // @@ -95,6 +94,7 @@ class CloudSettingsPresenter @Inject constructor( // when (cloudTypeModel) { CloudTypeModel.WEBDAV -> return context().getString(R.string.screen_cloud_settings_webdav_connections) CloudTypeModel.PCLOUD -> return context().getString(R.string.screen_cloud_settings_pcloud_connections) + CloudTypeModel.S3 -> return context().getString(R.string.screen_cloud_settings_s3_connections) CloudTypeModel.LOCAL -> return context().getString(R.string.screen_cloud_settings_local_storage_locations) } return context().getString(R.string.screen_cloud_settings_title) @@ -128,6 +128,7 @@ class CloudSettingsPresenter @Inject constructor( // .also { it.add(aWebdavCloud()) it.add(aPCloud()) + it.add(aS3Cloud()) it.add(aLocalCloud()) } view?.render(cloudModel) @@ -141,6 +142,10 @@ class CloudSettingsPresenter @Inject constructor( // return PCloudModel(PCloud.aPCloud().build()) } + private fun aS3Cloud(): S3CloudModel { + return S3CloudModel(S3Cloud.aS3Cloud().build()) + } + private fun aLocalCloud(): CloudModel { return LocalStorageModel(LocalStorageCloud.aLocalStorage().build()) } diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/S3AddOrChangePresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/S3AddOrChangePresenter.kt new file mode 100644 index 00000000..a7c6390f --- /dev/null +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/S3AddOrChangePresenter.kt @@ -0,0 +1,97 @@ +package org.cryptomator.presentation.presenter + +import android.widget.Toast +import org.cryptomator.domain.Cloud +import org.cryptomator.domain.S3Cloud +import org.cryptomator.domain.di.PerView +import org.cryptomator.domain.usecases.cloud.AddOrChangeCloudConnectionUseCase +import org.cryptomator.domain.usecases.cloud.ConnectToS3UseCase +import org.cryptomator.presentation.exception.ExceptionHandlers +import org.cryptomator.presentation.model.ProgressModel +import org.cryptomator.presentation.model.ProgressStateModel +import org.cryptomator.presentation.ui.activity.view.S3AddOrChangeView +import org.cryptomator.util.crypto.CredentialCryptor +import javax.inject.Inject + +@PerView +class S3AddOrChangePresenter @Inject internal constructor( // + private val addOrChangeCloudConnectionUseCase: AddOrChangeCloudConnectionUseCase, // + private val connectToS3UseCase: ConnectToS3UseCase, // + exceptionMappings: ExceptionHandlers) : Presenter(exceptionMappings) { + + fun checkUserInput(accessKey: String, secretKey: String, bucket: String, endpoint: String?, region: String?, cloudId: Long?) { + var statusMessage: String? = null + + /*if (accessKey.isEmpty()) { + statusMessage = getString(R.string.screen_webdav_settings_msg_password_must_not_be_empty) + } + if (secretKey.isEmpty()) { + statusMessage = getString(R.string.screen_webdav_settings_msg_username_must_not_be_empty) + } + if (bucket.isEmpty()) { + statusMessage = getString(R.string.screen_webdav_settings_msg_url_must_not_be_empty) + }*/ // FIXME define what is required + + if (statusMessage != null) { + // FIXME showError instead of displaying a toast + Toast.makeText(context(), statusMessage, Toast.LENGTH_SHORT).show() + } else { + view?.onCheckUserInputSucceeded(encrypt(accessKey), encrypt(secretKey), bucket, endpoint, region, cloudId) + } + } + + private fun encrypt(text: String): String { + return CredentialCryptor // + .getInstance(context()) // + .encrypt(text) + } + + private fun mapToCloud(accessKey: String, secretKey: String, bucket: String, endpoint: String?, region: String?, cloudId: Long?): S3Cloud { + var builder = S3Cloud // + .aS3Cloud() // + .withAccessKey(accessKey) // + .withSecretKey(secretKey) // + .withS3Bucket(bucket) // + .withS3Endpoint(endpoint) // + .withS3Region(region) + + cloudId?.let { builder = builder.withId(cloudId) } + + return builder.build() + } + + fun authenticate(accessKey: String, secretKey: String, bucket: String, endpoint: String?, region: String?, cloudId: Long?) { + authenticate(mapToCloud(accessKey, secretKey, bucket, endpoint, region, cloudId)) + } + + private fun authenticate(cloud: S3Cloud) { + view?.showProgress(ProgressModel(ProgressStateModel.AUTHENTICATION)) + connectToS3UseCase // + .withCloud(cloud) // + .run(object : DefaultResultHandler() { + override fun onSuccess(void: Void?) { + onCloudAuthenticated(cloud) + } + + override fun onError(e: Throwable) { + view?.showProgress(ProgressModel.COMPLETED) + super.onError(e) + } + }) + } + + private fun onCloudAuthenticated(cloud: Cloud) { + save(cloud) + finishWithResult(CloudConnectionListPresenter.SELECTED_CLOUD, cloud) + } + + private fun save(cloud: Cloud) { + addOrChangeCloudConnectionUseCase // + .withCloud(cloud) // + .run(DefaultResultHandler()) + } + + init { + unsubscribeOnDestroy(addOrChangeCloudConnectionUseCase, connectToS3UseCase) + } +} diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/S3AddOrChangeActivity.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/S3AddOrChangeActivity.kt new file mode 100644 index 00000000..624ee5af --- /dev/null +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/S3AddOrChangeActivity.kt @@ -0,0 +1,37 @@ +package org.cryptomator.presentation.ui.activity + +import androidx.fragment.app.Fragment +import org.cryptomator.generator.Activity +import org.cryptomator.generator.InjectIntent +import org.cryptomator.presentation.R +import org.cryptomator.presentation.intent.S3AddOrChangeIntent +import org.cryptomator.presentation.presenter.S3AddOrChangePresenter +import org.cryptomator.presentation.ui.activity.view.S3AddOrChangeView +import org.cryptomator.presentation.ui.fragment.S3AddOrChangeFragment +import javax.inject.Inject +import kotlinx.android.synthetic.main.toolbar_layout.toolbar + +@Activity +class S3AddOrChangeActivity : BaseActivity(), S3AddOrChangeView { + + @Inject + lateinit var s3AddOrChangePresenter: S3AddOrChangePresenter + + @InjectIntent + lateinit var s3AddOrChangeIntent: S3AddOrChangeIntent + + override fun setupView() { + toolbar.setTitle(R.string.screen_s3_settings_title) + setSupportActionBar(toolbar) + } + + override fun createFragment(): Fragment = S3AddOrChangeFragment.newInstance(s3AddOrChangeIntent.s3Cloud()) + + override fun onCheckUserInputSucceeded(accessKey: String, secretKey: String, bucket: String, endpoint: String?, region: String?, cloudId: Long?) { + s3AddOrChangeFragment().hideKeyboard() + s3AddOrChangePresenter.authenticate(accessKey, secretKey, bucket, endpoint, region, cloudId) + } + + private fun s3AddOrChangeFragment(): S3AddOrChangeFragment = getCurrentFragment(R.id.fragmentContainer) as S3AddOrChangeFragment + +} diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/S3AddOrChangeView.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/S3AddOrChangeView.kt new file mode 100644 index 00000000..17e3db67 --- /dev/null +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/S3AddOrChangeView.kt @@ -0,0 +1,7 @@ +package org.cryptomator.presentation.ui.activity.view + +interface S3AddOrChangeView : View { + + fun onCheckUserInputSucceeded(accessKey: String, secretKey: String, bucket: String, endpoint: String?, region: String?, cloudId: Long?) + +} diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/CloudSettingsAdapter.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/CloudSettingsAdapter.kt index 0d157021..560a4966 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/CloudSettingsAdapter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/CloudSettingsAdapter.kt @@ -39,19 +39,19 @@ constructor(private val context: Context) : RecyclerViewBaseAdapter itemView.cloudName.text = context.getString(R.string.screen_cloud_settings_pcloud_connections) + CloudTypeModel.S3 -> itemView.cloudName.text = context.getString(R.string.screen_cloud_settings_s3_connections) + CloudTypeModel.WEBDAV -> itemView.cloudName.text = context.getString(R.string.screen_cloud_settings_webdav_connections) + CloudTypeModel.LOCAL -> itemView.cloudName.text = context.getString(R.string.screen_cloud_settings_local_storage_locations) + else -> { + itemView.cloudName.text = getCloudNameText(isAlreadyLoggedIn(cloudModel), cloudModel) + if (isAlreadyLoggedIn(cloudModel)) { + itemView.cloudUsername.text = cloudModel.username() + itemView.cloudUsername.visibility = View.VISIBLE + } else { + itemView.cloudUsername.visibility = View.GONE + } } } @@ -73,16 +73,4 @@ constructor(private val context: Context) : RecyclerViewBaseAdapter + if (actionId == EditorInfo.IME_ACTION_DONE) { + createCloud() + } + false + } + toggleCustomS3.setOnClickListener { switch -> + toggleCustomS3Changed((switch as SwitchMaterial).isChecked) + } + + showEditableCloudContent(s3CloudModel) + } + + private fun toggleCustomS3Changed(checked: Boolean) = if(checked) { + ll_custom_s3.visibility = View.GONE + } else { + ll_custom_s3.visibility = View.VISIBLE + } + + private fun showEditableCloudContent(s3CloudModel: S3CloudModel?) { + s3CloudModel?.let { + cloudId = s3CloudModel.id() + accessKeyEditText.setText(decrypt(s3CloudModel.accessKey())) + secretKeyEditText.setText(decrypt(s3CloudModel.secretKey())) + bucketEditText.setText(s3CloudModel.s3Bucket()) + endpointEditText.setText(s3CloudModel.s3Endpoint()) + regionEditText.setText(s3CloudModel.s3Region()) + } + } + + private fun decrypt(text: String?): String { + return if (text != null) { + try { + CredentialCryptor // + .getInstance(activity?.applicationContext) // + .decrypt(text) + } catch (e: RuntimeException) { + Timber.tag("S3AddOrChangeFragment").e(e, "Unable to decrypt password, clearing it") + "" + } + } else "" + } + + private fun createCloud() { + val accessKey = accessKeyEditText.text.toString().trim() + val secretKey = secretKeyEditText.text.toString().trim() + val bucket = bucketEditText.text.toString().trim() + + var endpoint: String? = null + var region: String? = null + if(ll_custom_s3.isVisible) { + endpoint = endpointEditText.text.toString().trim() + region = regionEditText.text.toString().trim() + } + + s3AddOrChangePresenter.checkUserInput(accessKey, secretKey, bucket, endpoint, region, cloudId) + } + + fun hideKeyboard() { + hideKeyboard(bucketEditText) + } + + companion object { + + private const val ARG_S3_CLOUD = "S3_CLOUD" + + fun newInstance(cloudModel: S3CloudModel?): S3AddOrChangeFragment { + val result = S3AddOrChangeFragment() + val args = Bundle() + args.putSerializable(ARG_S3_CLOUD, cloudModel) + result.arguments = args + return result + } + } + +} diff --git a/presentation/src/main/res/layout/fragment_setup_s3.xml b/presentation/src/main/res/layout/fragment_setup_s3.xml new file mode 100644 index 00000000..5e2acc2e --- /dev/null +++ b/presentation/src/main/res/layout/fragment_setup_s3.xml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +