From 7deebbd91937af69cd6d4a0033833ebb2e868a91 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Mon, 5 Jul 2021 17:45:51 +0200 Subject: [PATCH 1/6] Refactore further code to Kotlin --- buildsystem/dependencies.gradle | 7 +- data/build.gradle | 1 + .../InterceptingCloudContentRepository.java | 178 --- .../InterceptingCloudContentRepository.kt | 216 ++++ .../crypto/BackupFileIdSuffixGenerator.java | 28 - .../crypto/BackupFileIdSuffixGenerator.kt | 28 + .../crypto/CryptoCloudContentRepository.java | 138 --- .../crypto/CryptoCloudContentRepository.kt | 128 ++ .../CryptoCloudContentRepositoryFactory.java | 7 +- .../data/cloud/crypto/CryptoCloudFactory.java | 27 +- .../cloud/crypto/CryptoCloudProvider.java | 3 +- .../data/cloud/crypto/CryptoConstants.java | 26 - .../data/cloud/crypto/CryptoConstants.kt | 22 + .../data/cloud/crypto/CryptoFile.java | 82 -- .../data/cloud/crypto/CryptoFile.kt | 37 + .../data/cloud/crypto/CryptoFolder.java | 72 -- .../data/cloud/crypto/CryptoFolder.kt | 38 + .../cloud/crypto/CryptoImplDecorator.java | 425 ------- .../data/cloud/crypto/CryptoImplDecorator.kt | 457 +++++++ .../cloud/crypto/CryptoImplVaultFormat7.java | 549 --------- .../cloud/crypto/CryptoImplVaultFormat7.kt | 540 ++++++++ .../cloud/crypto/CryptoImplVaultFormat8.java | 16 - .../cloud/crypto/CryptoImplVaultFormat8.kt | 21 + .../crypto/CryptoImplVaultFormatPre7.java | 269 ---- .../cloud/crypto/CryptoImplVaultFormatPre7.kt | 281 +++++ .../data/cloud/crypto/CryptoNode.java | 7 - .../data/cloud/crypto/CryptoNode.kt | 5 + .../data/cloud/crypto/CryptoSymlink.java | 82 -- .../data/cloud/crypto/CryptoSymlink.kt | 40 + .../data/cloud/crypto/Cryptors.java | 144 --- .../cryptomator/data/cloud/crypto/Cryptors.kt | 130 ++ .../data/cloud/crypto/DirIdCache.java | 38 - .../data/cloud/crypto/DirIdCache.kt | 21 + .../data/cloud/crypto/DirIdCacheFormat7.java | 80 -- .../data/cloud/crypto/DirIdCacheFormat7.kt | 74 ++ .../cloud/crypto/DirIdCacheFormatPre7.java | 88 -- .../data/cloud/crypto/DirIdCacheFormatPre7.kt | 86 ++ .../crypto/MasterkeyCryptoCloudProvider.java | 324 ----- .../crypto/MasterkeyCryptoCloudProvider.kt | 293 +++++ .../data/cloud/crypto/RootCryptoFolder.java | 27 - .../data/cloud/crypto/RootCryptoFolder.kt | 18 + .../data/cloud/crypto/VaultCipherCombo.java | 34 - .../data/cloud/crypto/VaultConfig.kt | 9 +- .../cloud/dropbox/DropboxClientFactory.java | 56 - .../cloud/dropbox/DropboxClientFactory.kt | 60 + .../DropboxCloudContentRepository.java | 213 ---- .../dropbox/DropboxCloudContentRepository.kt | 205 ++++ .../DropboxCloudContentRepositoryFactory.java | 2 +- .../dropbox/DropboxCloudNodeFactory.java | 39 - .../cloud/dropbox/DropboxCloudNodeFactory.kt | 39 + .../data/cloud/dropbox/DropboxFile.java | 55 - .../data/cloud/dropbox/DropboxFile.kt | 11 + .../data/cloud/dropbox/DropboxFolder.java | 42 - .../data/cloud/dropbox/DropboxFolder.kt | 14 + .../data/cloud/dropbox/DropboxImpl.java | 466 ------- .../data/cloud/dropbox/DropboxImpl.kt | 397 ++++++ .../data/cloud/dropbox/DropboxNode.java | 7 - .../data/cloud/dropbox/DropboxNode.kt | 9 + .../data/cloud/dropbox/RootDropboxFolder.java | 24 - .../data/cloud/dropbox/RootDropboxFolder.kt | 11 + .../data/cloud/local/file/LocalFile.java | 54 - .../data/cloud/local/file/LocalFile.kt | 11 + .../data/cloud/local/file/LocalFolder.java | 42 - .../data/cloud/local/file/LocalFolder.kt | 14 + .../data/cloud/local/file/LocalNode.java | 7 - .../data/cloud/local/file/LocalNode.kt | 9 + .../file/LocalStorageContentRepository.java | 121 -- .../file/LocalStorageContentRepository.kt | 111 ++ .../cloud/local/file/LocalStorageImpl.java | 193 --- .../data/cloud/local/file/LocalStorageImpl.kt | 165 +++ .../local/file/LocalStorageNodeFactory.java | 34 - .../local/file/LocalStorageNodeFactory.kt | 36 + .../cloud/local/file/RootLocalFolder.java | 26 - .../data/cloud/local/file/RootLocalFolder.kt | 15 + .../DocumentIdCache.java | 74 -- .../storageaccessframework/DocumentIdCache.kt | 51 + .../LocalStorageAccessFile.java | 96 -- .../LocalStorageAccessFile.kt | 42 + .../LocalStorageAccessFolder.java | 87 -- .../LocalStorageAccessFolder.kt | 40 + ...orageAccessFrameworkContentRepository.java | 123 -- ...StorageAccessFrameworkContentRepository.kt | 111 ++ .../LocalStorageAccessFrameworkImpl.java | 536 -------- .../LocalStorageAccessFrameworkImpl.kt | 399 ++++++ ...ocalStorageAccessFrameworkNodeFactory.java | 124 -- .../LocalStorageAccessFrameworkNodeFactory.kt | 133 ++ .../LocalStorageAccessNode.java | 15 - .../LocalStorageAccessNode.kt | 11 + .../RootLocalStorageAccessFolder.java | 39 - .../RootLocalStorageAccessFolder.kt | 25 + .../data/cloud/okhttplogging/HeaderNames.java | 20 - .../data/cloud/okhttplogging/HeaderNames.kt | 16 + .../okhttplogging/HttpLoggingInterceptor.java | 147 --- .../okhttplogging/HttpLoggingInterceptor.kt | 135 ++ .../cloud/onedrive/OnedriveClientFactory.java | 76 -- .../cloud/onedrive/OnedriveClientFactory.kt | 63 + .../OnedriveCloudContentRepository.java | 165 --- .../OnedriveCloudContentRepository.kt | 152 +++ ...OnedriveCloudContentRepositoryFactory.java | 2 +- .../onedrive/OnedriveCloudNodeFactory.java | 77 -- .../onedrive/OnedriveCloudNodeFactory.kt | 69 ++ .../data/cloud/onedrive/OnedriveFile.java | 59 - .../data/cloud/onedrive/OnedriveFile.kt | 13 + .../data/cloud/onedrive/OnedriveFolder.java | 47 - .../data/cloud/onedrive/OnedriveFolder.kt | 16 + .../data/cloud/onedrive/OnedriveIdCache.java | 89 -- .../data/cloud/onedrive/OnedriveIdCache.kt | 54 + .../cloud/onedrive/OnedriveIdCloudNode.java | 11 - .../cloud/onedrive/OnedriveIdCloudNode.kt | 10 + .../data/cloud/onedrive/OnedriveImpl.java | 548 --------- .../data/cloud/onedrive/OnedriveImpl.kt | 509 ++++++++ .../data/cloud/onedrive/OnedriveNode.java | 15 - .../data/cloud/onedrive/OnedriveNode.kt | 12 + .../cloud/onedrive/RootOnedriveFolder.java | 24 - .../data/cloud/onedrive/RootOnedriveFolder.kt | 11 + .../data/cloud/pcloud/PCloudApiError.java | 112 -- .../data/cloud/pcloud/PCloudApiError.kt | 110 ++ .../cloud/pcloud/PCloudClientFactory.java | 53 - .../data/cloud/pcloud/PCloudClientFactory.kt | 57 + .../cloud/pcloud/PCloudContentRepository.java | 193 --- .../cloud/pcloud/PCloudContentRepository.kt | 178 +++ .../PCloudContentRepositoryFactory.java | 2 +- .../data/cloud/pcloud/PCloudFile.java | 55 - .../data/cloud/pcloud/PCloudFile.kt | 11 + .../data/cloud/pcloud/PCloudFolder.java | 42 - .../data/cloud/pcloud/PCloudFolder.kt | 14 + .../data/cloud/pcloud/PCloudImpl.java | 377 ------ .../data/cloud/pcloud/PCloudImpl.kt | 374 ++++++ .../data/cloud/pcloud/PCloudNode.java | 10 - .../data/cloud/pcloud/PCloudNode.kt | 8 + .../data/cloud/pcloud/PCloudNodeFactory.java | 47 - .../data/cloud/pcloud/PCloudNodeFactory.kt | 47 + .../data/cloud/pcloud/RootPCloudFolder.java | 24 - .../data/cloud/pcloud/RootPCloudFolder.kt | 11 + .../data/cloud/s3/RootS3Folder.java | 29 - .../cryptomator/data/cloud/s3/RootS3Folder.kt | 14 + .../data/cloud/s3/S3ClientFactory.java | 98 -- .../data/cloud/s3/S3ClientFactory.kt | 89 ++ .../data/cloud/s3/S3CloudApiErrorCodes.java | 15 - .../data/cloud/s3/S3CloudApiErrorCodes.kt | 12 + .../data/cloud/s3/S3CloudApiExceptions.java | 12 - .../data/cloud/s3/S3CloudApiExceptions.kt | 16 + .../cloud/s3/S3CloudContentRepository.java | 190 --- .../data/cloud/s3/S3CloudContentRepository.kt | 176 +++ .../s3/S3CloudContentRepositoryFactory.java | 2 +- .../data/cloud/s3/S3CloudNodeFactory.java | 47 - .../data/cloud/s3/S3CloudNodeFactory.kt | 44 + .../org/cryptomator/data/cloud/s3/S3File.java | 65 - .../org/cryptomator/data/cloud/s3/S3File.kt | 22 + .../cryptomator/data/cloud/s3/S3Folder.java | 52 - .../org/cryptomator/data/cloud/s3/S3Folder.kt | 23 + .../org/cryptomator/data/cloud/s3/S3Impl.java | 404 ------ .../org/cryptomator/data/cloud/s3/S3Impl.kt | 383 ++++++ .../org/cryptomator/data/cloud/s3/S3Node.java | 12 - .../org/cryptomator/data/cloud/s3/S3Node.kt | 10 + .../data/cloud/webdav/RootWebDavFolder.java | 24 - .../data/cloud/webdav/RootWebDavFolder.kt | 11 + .../webdav/WebDavCloudContentRepository.java | 235 ---- .../webdav/WebDavCloudContentRepository.kt | 244 ++++ .../WebDavCloudContentRepositoryFactory.java | 10 +- .../data/cloud/webdav/WebDavFile.java | 58 - .../data/cloud/webdav/WebDavFile.kt | 13 + .../data/cloud/webdav/WebDavFolder.java | 55 - .../data/cloud/webdav/WebDavFolder.kt | 14 + .../data/cloud/webdav/WebDavImpl.java | 254 ---- .../data/cloud/webdav/WebDavImpl.kt | 185 +++ .../data/cloud/webdav/WebDavNode.java | 9 - .../data/cloud/webdav/WebDavNode.kt | 9 + .../network/ConnectionHandlerFactory.java | 23 - .../network/ConnectionHandlerFactory.kt | 14 + .../network/ConnectionHandlerHandlerImpl.java | 57 - .../network/ConnectionHandlerHandlerImpl.kt | 55 + .../network/DataSourceBasedRequestBody.java | 42 - .../webdav/network/DefaultTrustManager.java | 66 - .../webdav/network/DefaultTrustManager.kt | 67 + .../InputStreamSourceBasedRequestBody.kt | 37 + .../webdav/network/PinningTrustManager.java | 91 -- .../webdav/network/PinningTrustManager.kt | 81 ++ .../webdav/network/PropfindEntryData.java | 89 -- .../cloud/webdav/network/PropfindEntryData.kt | 81 ++ .../network/PropfindResponseParser.java | 188 --- .../webdav/network/PropfindResponseParser.kt | 186 +++ .../webdav/network/SSLSocketFactories.java | 24 - .../webdav/network/SSLSocketFactories.kt | 21 + .../cloud/webdav/network/WebDavClient.java | 317 ----- .../data/cloud/webdav/network/WebDavClient.kt | 270 ++++ .../network/WebDavCompatibleHttpClient.java | 165 --- .../network/WebDavCompatibleHttpClient.kt | 172 +++ .../webdav/network/WebDavRedirectHandler.java | 96 -- .../webdav/network/WebDavRedirectHandler.kt | 80 ++ .../org/cryptomator/data/db/Upgrade0To1.java | 105 -- .../org/cryptomator/data/db/Upgrade0To1.kt | 92 ++ .../org/cryptomator/data/db/Upgrade1To2.java | 52 - .../org/cryptomator/data/db/Upgrade1To2.kt | 42 + .../data/db/entities/VaultEntity.java | 4 +- .../data/repository/CloudRepositoryImpl.java | 3 +- .../DispatchingCloudContentRepository.java | 251 ---- .../DispatchingCloudContentRepository.kt | 235 ++++ .../repository/UpdateCheckRepositoryImpl.java | 4 +- .../org/cryptomator/data/util/CopyStream.java | 66 - .../org/cryptomator/data/util/CopyStream.kt | 62 + .../data/util/NetworkConnectionCheck.java | 52 - .../data/util/NetworkConnectionCheck.kt | 33 + .../cryptomator/data/util/NetworkTimeout.java | 32 - .../cryptomator/data/util/NetworkTimeout.kt | 13 + .../TransferredBytesAwareInputStream.java | 68 -- .../util/TransferredBytesAwareInputStream.kt | 63 + .../TransferredBytesAwareOutputStream.java | 50 - .../util/TransferredBytesAwareOutputStream.kt | 41 + .../data/util/UserAgentInterceptor.java | 21 - .../data/util/UserAgentInterceptor.kt | 18 + .../data/util/X509CertificateHelper.java | 41 - .../data/util/X509CertificateHelper.kt | 41 + .../FixedGoogleAccountCredential.java | 2 +- .../googledrive/GoogleDriveClientFactory.java | 65 - .../googledrive/GoogleDriveClientFactory.kt | 59 + .../GoogleDriveCloudContentRepository.java | 213 ---- .../GoogleDriveCloudContentRepository.kt | 195 +++ ...gleDriveCloudContentRepositoryFactory.java | 2 +- .../GoogleDriveCloudNodeFactory.java | 59 - .../GoogleDriveCloudNodeFactory.kt | 55 + .../cloud/googledrive/GoogleDriveFile.java | 61 - .../data/cloud/googledrive/GoogleDriveFile.kt | 19 + .../cloud/googledrive/GoogleDriveFolder.java | 49 - .../cloud/googledrive/GoogleDriveFolder.kt | 14 + .../cloud/googledrive/GoogleDriveIdCache.java | 77 -- .../cloud/googledrive/GoogleDriveIdCache.kt | 51 + .../googledrive/GoogleDriveIdCloudNode.java | 9 - .../googledrive/GoogleDriveIdCloudNode.kt | 9 + .../cloud/googledrive/GoogleDriveImpl.java | 446 ------- .../data/cloud/googledrive/GoogleDriveImpl.kt | 416 +++++++ .../cloud/googledrive/GoogleDriveNode.java | 10 - .../data/cloud/googledrive/GoogleDriveNode.kt | 7 + .../googledrive/RootGoogleDriveFolder.java | 24 - .../googledrive/RootGoogleDriveFolder.kt | 11 + ...redBytesAwareGoogleContentInputStream.java | 51 - ...erredBytesAwareGoogleContentInputStream.kt | 46 + .../crypto/CryptoImplVaultFormat7Test.java | 1087 ---------------- .../crypto/CryptoImplVaultFormat7Test.kt | 1088 +++++++++++++++++ .../crypto/CryptoImplVaultFormatPre7Test.java | 936 -------------- .../crypto/CryptoImplVaultFormatPre7Test.kt | 969 +++++++++++++++ .../MasterkeyCryptoCloudProviderTest.java | 336 ----- .../MasterkeyCryptoCloudProviderTest.kt | 366 ++++++ .../data/cloud/crypto/TestFile.java | 11 +- .../network/PropfindResponseParserTest.java | 9 +- domain/build.gradle | 1 + .../java/org/cryptomator/domain/Cloud.java | 18 - .../main/java/org/cryptomator/domain/Cloud.kt | 13 + .../org/cryptomator/domain/CloudFile.java | 13 - .../java/org/cryptomator/domain/CloudFile.kt | 10 + .../org/cryptomator/domain/CloudFolder.java | 7 - .../org/cryptomator/domain/CloudFolder.kt | 6 + .../org/cryptomator/domain/CloudNode.java | 14 - .../java/org/cryptomator/domain/CloudNode.kt | 11 + .../domain/{CloudType.java => CloudType.kt} | 6 +- .../ParentFolderDoesNotExistException.java | 8 + .../ParentFolderIsNullException.java | 9 + .../AuthenticationException.java | 5 +- ...serRecoverableAuthenticationException.java | 5 +- .../repository/CloudContentRepository.java | 77 -- .../repository/CloudContentRepository.kt | 90 ++ .../domain/repository/CloudRepository.java | 2 +- .../repository/UpdateCheckRepository.java | 2 +- .../domain/usecases/DoUpdateCheck.java | 2 +- .../domain/usecases/ProgressAware.java | 7 +- .../usecases/cloud/ByteArrayDataSource.java | 43 - .../usecases/cloud/ByteArrayDataSource.kt | 35 + .../usecases/cloud/CancelAwareDataSource.java | 49 - .../usecases/cloud/CancelAwareDataSource.kt | 41 + .../domain/usecases/cloud/DataSource.java | 20 - .../domain/usecases/cloud/DataSource.kt | 18 + .../domain/usecases/cloud/DownloadFiles.java | 3 +- .../usecases/cloud/FileBasedDataSource.java | 43 - .../usecases/cloud/FileBasedDataSource.kt | 36 + .../domain/usecases/cloud/UploadFiles.java | 7 +- .../domain/usecases/vault/ChangePassword.java | 2 +- .../usecases/vault/CheckVaultPassword.java | 2 +- .../vault/GetUnverifiedVaultConfig.java | 5 +- .../domain/usecases/vault/PrepareUnlock.java | 2 +- .../vault/UnlockVaultUsingMasterkey.java | 2 +- .../usecases/vault/VaultOrUnlockToken.java | 7 +- .../cloud/DataSourceCapturingAnswer.java | 44 - .../cloud/DataSourceCapturingAnswer.kt | 40 + .../usecases/cloud/DownloadFileTest.java | 5 +- .../domain/usecases/cloud/UploadFileTest.java | 122 -- .../domain/usecases/cloud/UploadFileTest.kt | 136 +++ .../vault/UnlockVaultUsingMasterkeyTest.java | 3 +- .../CloudContentRepositoryBlackboxTest.java | 9 +- .../presentation/CloudNodeMatchers.java | 2 +- .../presenter/AuthenticateCloudPresenter.kt | 4 +- .../presentation/CryptomatorApp.kt | 2 +- .../presentation/logging/FormattedTime.kt | 6 +- .../presentation/model/CloudFileModel.kt | 5 +- .../presentation/model/CloudFolderModel.kt | 2 +- .../presentation/model/CloudNodeModel.kt | 7 +- .../presentation/model/LocalStorageModel.kt | 6 +- .../presentation/model/PCloudModel.kt | 2 +- .../presentation/model/S3CloudModel.kt | 2 +- .../presentation/model/WebDavCloudModel.kt | 2 +- ...CloudNodeModelDateNewestFirstComparator.kt | 8 +- ...CloudNodeModelDateOldestFirstComparator.kt | 8 +- ...loudNodeModelSizeBiggestFirstComparator.kt | 8 +- ...oudNodeModelSizeSmallestFirstComparator.kt | 8 +- .../model/mappers/CloudModelMapper.kt | 3 +- .../AutoUploadChooseVaultPresenter.kt | 5 +- .../BiometricAuthSettingsPresenter.kt | 15 +- .../presenter/BrowseFilesPresenter.kt | 108 +- .../presenter/CloudConnectionListPresenter.kt | 1 - .../presentation/presenter/Presenter.kt | 10 +- .../presenter/SettingsPresenter.kt | 14 +- .../presenter/SharedFilesPresenter.kt | 22 +- .../presenter/TextEditorPresenter.kt | 46 +- .../presenter/UnlockVaultPresenter.kt | 17 +- .../presenter/UriBasedDataSource.kt | 5 +- .../presenter/VaultListPresenter.kt | 7 +- .../service/AutoUploadService.java | 10 +- .../presentation/service/CryptorsService.java | 2 +- .../presentation/service/PhotoContentJob.kt | 7 +- .../ui/activity/BrowseFilesActivity.kt | 5 +- .../ui/activity/ImagePreviewActivity.kt | 15 +- .../ui/adapter/BrowseFilesAdapter.kt | 28 +- .../presentation/ui/dialog/BaseDialog.kt | 2 +- .../ui/dialog/ChangePasswordDialog.kt | 7 +- .../ui/dialog/CloudNodeRenameDialog.kt | 6 +- .../ui/dialog/CreateFolderDialog.kt | 8 +- .../ui/dialog/EnterPasswordDialog.kt | 2 +- .../presentation/ui/dialog/FileNameDialog.kt | 2 +- .../ui/dialog/UpdateLicenseDialog.kt | 2 +- .../ui/dialog/VaultRenameDialog.kt | 2 +- .../ui/fragment/BrowseFilesFragment.kt | 6 +- .../util/BiometricAuthentication.kt | 1 - .../presentation/util/DateHelper.kt | 14 +- .../presentation/util/FileIcon.java | 24 +- .../presentation/util/FileSizeHelper.kt | 22 +- .../cryptomator/presentation/util/FileUtil.kt | 9 +- .../presentation/util/PasswordStrength.kt | 10 +- .../presentation/util/ShareFileHelper.kt | 2 +- .../presenter/AuthenticateCloudPresenter.kt | 4 +- .../presentation/SvgValidationTest.java | 4 +- util/build.gradle | 1 + .../java/org/cryptomator/util/Consumer.java | 7 - .../java/org/cryptomator/util/Encodings.kt | 13 - .../org/cryptomator/util/ExceptionUtil.java | 9 +- .../java/org/cryptomator/util/Function.java | 7 - .../java/org/cryptomator/util/Optional.java | 3 + .../java/org/cryptomator/util/Predicate.java | 7 - .../java/org/cryptomator/util/Predicates.java | 13 - .../util/SharedPreferencesHandler.kt | 6 +- .../java/org/cryptomator/util/Supplier.java | 7 - .../util/concurrent/CompletableFuture.java | 116 -- .../util/crypto/BiometricAuthCryptor.java | 14 +- .../util/crypto/CredentialCryptor.java | 8 +- .../cryptomator/util/file/FileCacheUtils.kt | 8 +- .../cryptomator/util/file/LruFileCacheUtil.kt | 4 +- .../org/cryptomator/util/file/MimeType.kt | 4 +- .../org/cryptomator/util/file/MimeTypes.kt | 14 +- .../concurrent/CompletableFutureTest.java | 251 ---- .../cryptomator/util/file/MimeTypesTest.java | 105 -- .../cryptomator/util/file/MimeTypesTest.kt | 95 ++ .../util/matchers/OptionalMatchers.java | 7 +- 360 files changed, 13406 insertions(+), 15546 deletions(-) delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/InterceptingCloudContentRepository.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/InterceptingCloudContentRepository.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/crypto/BackupFileIdSuffixGenerator.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/crypto/BackupFileIdSuffixGenerator.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudContentRepository.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudContentRepository.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoConstants.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoConstants.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoFile.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoFile.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoFolder.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoFolder.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat8.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat8.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoNode.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoNode.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoSymlink.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoSymlink.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/crypto/Cryptors.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/crypto/Cryptors.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/crypto/DirIdCache.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/crypto/DirIdCache.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/crypto/DirIdCacheFormat7.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/crypto/DirIdCacheFormat7.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/crypto/DirIdCacheFormatPre7.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/crypto/DirIdCacheFormatPre7.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProvider.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProvider.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/crypto/RootCryptoFolder.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/crypto/RootCryptoFolder.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/crypto/VaultCipherCombo.java delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxClientFactory.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxClientFactory.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxCloudContentRepository.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxCloudContentRepository.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxCloudNodeFactory.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxCloudNodeFactory.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxFile.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxFile.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxFolder.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxFolder.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxImpl.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxImpl.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxNode.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxNode.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/dropbox/RootDropboxFolder.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/dropbox/RootDropboxFolder.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/local/file/LocalFile.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/local/file/LocalFile.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/local/file/LocalFolder.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/local/file/LocalFolder.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/local/file/LocalNode.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/local/file/LocalNode.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/local/file/LocalStorageContentRepository.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/local/file/LocalStorageContentRepository.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/local/file/LocalStorageImpl.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/local/file/LocalStorageImpl.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/local/file/LocalStorageNodeFactory.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/local/file/LocalStorageNodeFactory.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/local/file/RootLocalFolder.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/local/file/RootLocalFolder.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/DocumentIdCache.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/DocumentIdCache.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFile.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFile.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFolder.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFolder.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFrameworkContentRepository.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFrameworkContentRepository.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFrameworkImpl.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFrameworkImpl.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFrameworkNodeFactory.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFrameworkNodeFactory.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessNode.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessNode.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/RootLocalStorageAccessFolder.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/RootLocalStorageAccessFolder.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/okhttplogging/HeaderNames.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/okhttplogging/HeaderNames.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/okhttplogging/HttpLoggingInterceptor.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/okhttplogging/HttpLoggingInterceptor.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveClientFactory.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveClientFactory.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveCloudContentRepository.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveCloudContentRepository.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveCloudNodeFactory.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveCloudNodeFactory.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveFile.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveFile.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveFolder.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveFolder.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveIdCache.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveIdCache.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveIdCloudNode.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveIdCloudNode.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveImpl.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveImpl.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveNode.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveNode.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/onedrive/RootOnedriveFolder.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/onedrive/RootOnedriveFolder.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudApiError.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudApiError.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudClientFactory.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudClientFactory.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudContentRepository.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudContentRepository.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudFile.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudFile.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudFolder.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudFolder.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudImpl.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudImpl.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudNode.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudNode.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudNodeFactory.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudNodeFactory.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/pcloud/RootPCloudFolder.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/pcloud/RootPCloudFolder.kt delete 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/RootS3Folder.kt delete 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/S3ClientFactory.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudApiErrorCodes.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudApiErrorCodes.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudApiExceptions.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudApiExceptions.kt delete 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/S3CloudContentRepository.kt delete 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/S3CloudNodeFactory.kt delete 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/S3File.kt delete 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/S3Folder.kt delete 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/S3Impl.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/s3/S3Node.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/s3/S3Node.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/webdav/RootWebDavFolder.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/webdav/RootWebDavFolder.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavCloudContentRepository.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavCloudContentRepository.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavFile.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavFile.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavFolder.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavFolder.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavImpl.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavImpl.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavNode.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavNode.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/webdav/network/ConnectionHandlerFactory.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/webdav/network/ConnectionHandlerFactory.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/webdav/network/ConnectionHandlerHandlerImpl.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/webdav/network/ConnectionHandlerHandlerImpl.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/webdav/network/DataSourceBasedRequestBody.java delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/webdav/network/DefaultTrustManager.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/webdav/network/DefaultTrustManager.kt create mode 100644 data/src/main/java/org/cryptomator/data/cloud/webdav/network/InputStreamSourceBasedRequestBody.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/webdav/network/PinningTrustManager.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/webdav/network/PinningTrustManager.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/webdav/network/PropfindEntryData.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/webdav/network/PropfindEntryData.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/webdav/network/PropfindResponseParser.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/webdav/network/PropfindResponseParser.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/webdav/network/SSLSocketFactories.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/webdav/network/SSLSocketFactories.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/webdav/network/WebDavClient.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/webdav/network/WebDavClient.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/webdav/network/WebDavCompatibleHttpClient.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/webdav/network/WebDavCompatibleHttpClient.kt delete mode 100644 data/src/main/java/org/cryptomator/data/cloud/webdav/network/WebDavRedirectHandler.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/webdav/network/WebDavRedirectHandler.kt delete mode 100644 data/src/main/java/org/cryptomator/data/db/Upgrade0To1.java create mode 100644 data/src/main/java/org/cryptomator/data/db/Upgrade0To1.kt delete mode 100644 data/src/main/java/org/cryptomator/data/db/Upgrade1To2.java create mode 100644 data/src/main/java/org/cryptomator/data/db/Upgrade1To2.kt delete mode 100644 data/src/main/java/org/cryptomator/data/repository/DispatchingCloudContentRepository.java create mode 100644 data/src/main/java/org/cryptomator/data/repository/DispatchingCloudContentRepository.kt delete mode 100644 data/src/main/java/org/cryptomator/data/util/CopyStream.java create mode 100644 data/src/main/java/org/cryptomator/data/util/CopyStream.kt delete mode 100644 data/src/main/java/org/cryptomator/data/util/NetworkConnectionCheck.java create mode 100644 data/src/main/java/org/cryptomator/data/util/NetworkConnectionCheck.kt delete mode 100644 data/src/main/java/org/cryptomator/data/util/NetworkTimeout.java create mode 100644 data/src/main/java/org/cryptomator/data/util/NetworkTimeout.kt delete mode 100644 data/src/main/java/org/cryptomator/data/util/TransferredBytesAwareInputStream.java create mode 100644 data/src/main/java/org/cryptomator/data/util/TransferredBytesAwareInputStream.kt delete mode 100644 data/src/main/java/org/cryptomator/data/util/TransferredBytesAwareOutputStream.java create mode 100644 data/src/main/java/org/cryptomator/data/util/TransferredBytesAwareOutputStream.kt delete mode 100644 data/src/main/java/org/cryptomator/data/util/UserAgentInterceptor.java create mode 100644 data/src/main/java/org/cryptomator/data/util/UserAgentInterceptor.kt delete mode 100644 data/src/main/java/org/cryptomator/data/util/X509CertificateHelper.java create mode 100644 data/src/main/java/org/cryptomator/data/util/X509CertificateHelper.kt delete mode 100644 data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveClientFactory.java create mode 100644 data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveClientFactory.kt delete mode 100644 data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveCloudContentRepository.java create mode 100644 data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveCloudContentRepository.kt delete mode 100644 data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveCloudNodeFactory.java create mode 100644 data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveCloudNodeFactory.kt delete mode 100644 data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveFile.java create mode 100644 data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveFile.kt delete mode 100644 data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveFolder.java create mode 100644 data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveFolder.kt delete mode 100644 data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveIdCache.java create mode 100644 data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveIdCache.kt delete mode 100644 data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveIdCloudNode.java create mode 100644 data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveIdCloudNode.kt delete mode 100644 data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveImpl.java create mode 100644 data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveImpl.kt delete mode 100644 data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveNode.java create mode 100644 data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveNode.kt delete mode 100644 data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/RootGoogleDriveFolder.java create mode 100644 data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/RootGoogleDriveFolder.kt delete mode 100644 data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/TransferredBytesAwareGoogleContentInputStream.java create mode 100644 data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/TransferredBytesAwareGoogleContentInputStream.kt delete mode 100644 data/src/test/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7Test.java create mode 100644 data/src/test/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7Test.kt delete mode 100644 data/src/test/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7Test.java create mode 100644 data/src/test/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7Test.kt delete mode 100644 data/src/test/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProviderTest.java create mode 100644 data/src/test/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProviderTest.kt delete mode 100644 domain/src/main/java/org/cryptomator/domain/Cloud.java create mode 100644 domain/src/main/java/org/cryptomator/domain/Cloud.kt delete mode 100644 domain/src/main/java/org/cryptomator/domain/CloudFile.java create mode 100644 domain/src/main/java/org/cryptomator/domain/CloudFile.kt delete mode 100644 domain/src/main/java/org/cryptomator/domain/CloudFolder.java create mode 100644 domain/src/main/java/org/cryptomator/domain/CloudFolder.kt delete mode 100755 domain/src/main/java/org/cryptomator/domain/CloudNode.java create mode 100755 domain/src/main/java/org/cryptomator/domain/CloudNode.kt rename domain/src/main/java/org/cryptomator/domain/{CloudType.java => CloudType.kt} (55%) create mode 100644 domain/src/main/java/org/cryptomator/domain/exception/ParentFolderIsNullException.java delete mode 100644 domain/src/main/java/org/cryptomator/domain/repository/CloudContentRepository.java create mode 100644 domain/src/main/java/org/cryptomator/domain/repository/CloudContentRepository.kt delete mode 100644 domain/src/main/java/org/cryptomator/domain/usecases/cloud/ByteArrayDataSource.java create mode 100644 domain/src/main/java/org/cryptomator/domain/usecases/cloud/ByteArrayDataSource.kt delete mode 100644 domain/src/main/java/org/cryptomator/domain/usecases/cloud/CancelAwareDataSource.java create mode 100644 domain/src/main/java/org/cryptomator/domain/usecases/cloud/CancelAwareDataSource.kt delete mode 100644 domain/src/main/java/org/cryptomator/domain/usecases/cloud/DataSource.java create mode 100644 domain/src/main/java/org/cryptomator/domain/usecases/cloud/DataSource.kt delete mode 100644 domain/src/main/java/org/cryptomator/domain/usecases/cloud/FileBasedDataSource.java create mode 100644 domain/src/main/java/org/cryptomator/domain/usecases/cloud/FileBasedDataSource.kt delete mode 100644 domain/src/test/java/org/cryptomator/domain/usecases/cloud/DataSourceCapturingAnswer.java create mode 100644 domain/src/test/java/org/cryptomator/domain/usecases/cloud/DataSourceCapturingAnswer.kt delete mode 100644 domain/src/test/java/org/cryptomator/domain/usecases/cloud/UploadFileTest.java create mode 100644 domain/src/test/java/org/cryptomator/domain/usecases/cloud/UploadFileTest.kt delete mode 100644 util/src/main/java/org/cryptomator/util/Consumer.java delete mode 100644 util/src/main/java/org/cryptomator/util/Encodings.kt delete mode 100644 util/src/main/java/org/cryptomator/util/Function.java delete mode 100644 util/src/main/java/org/cryptomator/util/Predicate.java delete mode 100644 util/src/main/java/org/cryptomator/util/Predicates.java delete mode 100644 util/src/main/java/org/cryptomator/util/Supplier.java delete mode 100644 util/src/main/java/org/cryptomator/util/concurrent/CompletableFuture.java delete mode 100644 util/src/test/java/org/cryptomator/util/concurrent/CompletableFutureTest.java delete mode 100644 util/src/test/java/org/cryptomator/util/file/MimeTypesTest.java create mode 100644 util/src/test/java/org/cryptomator/util/file/MimeTypesTest.kt diff --git a/buildsystem/dependencies.gradle b/buildsystem/dependencies.gradle index 937197be..12da0d11 100644 --- a/buildsystem/dependencies.gradle +++ b/buildsystem/dependencies.gradle @@ -51,7 +51,7 @@ ext { // cloud provider libs // do not update to 1.4.0 until minsdk is 7.x (or desugaring works better) otherwise it will crash on 6.x - cryptolibVersion = '2.0.0-rc2' + cryptolibVersion = '2.0.0-rc6' dropboxVersion = '4.0.0' @@ -74,7 +74,7 @@ ext { jUnit4Version = '4.13.1' assertJVersion = '1.7.1' mockitoVersion = '3.10.0' - mockitoInlineVersion = '3.10.0' + mockitoKotlinVersion = '3.2.0' hamcrestVersion = '1.3' dexmakerVersion = '1.0' espressoVersion = '3.3.0' @@ -132,7 +132,8 @@ ext { junit4Engine : "org.junit.vintage:junit-vintage-engine:${jUnitVersion}", minIo : "io.minio:minio:${minIoVersion}", mockito : "org.mockito:mockito-core:${mockitoVersion}", - mockitoInline : "org.mockito:mockito-inline:${mockitoInlineVersion}", + mockitoInline : "org.mockito:mockito-inline:${mockitoVersion}", + mockitoKotlin : "org.mockito.kotlin:mockito-kotlin:${mockitoKotlinVersion}", msgraph : "com.microsoft.graph:microsoft-graph:${msgraphVersion}", multidex : "androidx.multidex:multidex:${multidexVersion}", okHttp : "com.squareup.okhttp3:okhttp:${okHttpVersion}", diff --git a/data/build.gradle b/data/build.gradle index bf7d426b..fefdbf88 100644 --- a/data/build.gradle +++ b/data/build.gradle @@ -164,6 +164,7 @@ dependencies { testRuntimeOnly dependencies.junit4Engine testImplementation dependencies.mockito + testImplementation dependencies.mockitoKotlin testImplementation dependencies.mockitoInline testImplementation dependencies.hamcrest } diff --git a/data/src/main/java/org/cryptomator/data/cloud/InterceptingCloudContentRepository.java b/data/src/main/java/org/cryptomator/data/cloud/InterceptingCloudContentRepository.java deleted file mode 100644 index 734bd80c..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/InterceptingCloudContentRepository.java +++ /dev/null @@ -1,178 +0,0 @@ -package org.cryptomator.data.cloud; - -import org.cryptomator.domain.Cloud; -import org.cryptomator.domain.CloudFile; -import org.cryptomator.domain.CloudFolder; -import org.cryptomator.domain.CloudNode; -import org.cryptomator.domain.exception.BackendException; -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.OutputStream; -import java.util.List; - -public abstract class InterceptingCloudContentRepository implements CloudContentRepository { - - private final CloudContentRepository delegate; - - protected InterceptingCloudContentRepository(CloudContentRepository delegate) { - this.delegate = delegate; - } - - protected abstract void throwWrappedIfRequired(Exception e) throws BackendException; - - @Override - public DirType root(CloudType cloud) throws BackendException { - try { - return delegate.root(cloud); - } catch (BackendException | RuntimeException e) { - throwWrappedIfRequired(e); - throw e; - } - } - - @Override - public DirType resolve(CloudType cloud, String path) throws BackendException { - try { - return delegate.resolve(cloud, path); - } catch (BackendException | RuntimeException e) { - throwWrappedIfRequired(e); - throw e; - } - } - - @Override - public FileType file(DirType parent, String name) throws BackendException { - try { - return delegate.file(parent, name); - } catch (BackendException | RuntimeException e) { - throwWrappedIfRequired(e); - throw e; - } - } - - @Override - public FileType file(DirType parent, String name, Optional size) throws BackendException { - try { - return delegate.file(parent, name, size); - } catch (BackendException | RuntimeException e) { - throwWrappedIfRequired(e); - throw e; - } - } - - @Override - public DirType folder(DirType parent, String name) throws BackendException { - try { - return delegate.folder(parent, name); - } catch (BackendException | RuntimeException e) { - throwWrappedIfRequired(e); - throw e; - } - } - - @Override - public boolean exists(NodeType node) throws BackendException { - try { - return delegate.exists(node); - } catch (BackendException | RuntimeException e) { - throwWrappedIfRequired(e); - throw e; - } - } - - @Override - public List list(DirType folder) throws BackendException { - try { - return delegate.list(folder); - } catch (BackendException | RuntimeException e) { - throwWrappedIfRequired(e); - throw e; - } - } - - @Override - public DirType create(DirType folder) throws BackendException { - try { - return delegate.create(folder); - } catch (BackendException | RuntimeException e) { - throwWrappedIfRequired(e); - throw e; - } - } - - @Override - public DirType move(DirType source, DirType target) throws BackendException { - try { - return delegate.move(source, target); - } catch (BackendException | RuntimeException e) { - throwWrappedIfRequired(e); - throw e; - } - } - - @Override - public FileType move(FileType source, FileType target) throws BackendException { - try { - return delegate.move(source, target); - } catch (BackendException | RuntimeException e) { - throwWrappedIfRequired(e); - throw e; - } - } - - @Override - public FileType write(FileType file, DataSource data, ProgressAware progressAware, boolean replace, long size) throws BackendException { - try { - return delegate.write(file, data, progressAware, replace, size); - } catch (BackendException | RuntimeException e) { - throwWrappedIfRequired(e); - throw e; - } - } - - @Override - public void read(FileType file, Optional encryptedTmpFile, OutputStream data, ProgressAware progressAware) throws BackendException { - try { - delegate.read(file, encryptedTmpFile, data, progressAware); - } catch (BackendException | RuntimeException e) { - throwWrappedIfRequired(e); - throw e; - } - } - - @Override - public void delete(NodeType node) throws BackendException { - try { - delegate.delete(node); - } catch (BackendException | RuntimeException e) { - throwWrappedIfRequired(e); - throw e; - } - } - - @Override - public String checkAuthenticationAndRetrieveCurrentAccount(CloudType cloud) throws BackendException { - try { - return delegate.checkAuthenticationAndRetrieveCurrentAccount(cloud); - } catch (BackendException | RuntimeException e) { - throwWrappedIfRequired(e); - throw e; - } - } - - @Override - public void logout(CloudType cloud) throws BackendException { - try { - delegate.logout(cloud); - } catch (BackendException | RuntimeException e) { - throwWrappedIfRequired(e); - throw e; - } - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/InterceptingCloudContentRepository.kt b/data/src/main/java/org/cryptomator/data/cloud/InterceptingCloudContentRepository.kt new file mode 100644 index 00000000..d2c993ea --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/InterceptingCloudContentRepository.kt @@ -0,0 +1,216 @@ +package org.cryptomator.data.cloud + +import org.cryptomator.domain.Cloud +import org.cryptomator.domain.CloudFile +import org.cryptomator.domain.CloudFolder +import org.cryptomator.domain.CloudNode +import org.cryptomator.domain.exception.BackendException +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 java.io.File +import java.io.OutputStream + +abstract class InterceptingCloudContentRepository protected constructor(private val delegate: CloudContentRepository) : + CloudContentRepository { + + @Throws(BackendException::class) + protected abstract fun throwWrappedIfRequired(e: Exception) + + @Throws(BackendException::class) + override fun root(cloud: CloudType): DirType { + return try { + delegate.root(cloud) + } catch (e: BackendException) { + throwWrappedIfRequired(e) + throw e + } catch (e: RuntimeException) { + throwWrappedIfRequired(e) + throw e + } + } + + @Throws(BackendException::class) + override fun resolve(cloud: CloudType, path: String): DirType { + return try { + delegate.resolve(cloud, path) + } catch (e: BackendException) { + throwWrappedIfRequired(e) + throw e + } catch (e: RuntimeException) { + throwWrappedIfRequired(e) + throw e + } + } + + @Throws(BackendException::class) + override fun file(parent: DirType, name: String): FileType { + return try { + delegate.file(parent, name) + } catch (e: BackendException) { + throwWrappedIfRequired(e) + throw e + } catch (e: RuntimeException) { + throwWrappedIfRequired(e) + throw e + } + } + + @Throws(BackendException::class) + override fun file(parent: DirType, name: String, size: Long?): FileType { + return try { + delegate.file(parent, name, size) + } catch (e: BackendException) { + throwWrappedIfRequired(e) + throw e + } catch (e: RuntimeException) { + throwWrappedIfRequired(e) + throw e + } + } + + @Throws(BackendException::class) + override fun folder(parent: DirType, name: String): DirType { + return try { + delegate.folder(parent, name) + } catch (e: BackendException) { + throwWrappedIfRequired(e) + throw e + } catch (e: RuntimeException) { + throwWrappedIfRequired(e) + throw e + } + } + + @Throws(BackendException::class) + override fun exists(node: NodeType): Boolean { + return try { + delegate.exists(node) + } catch (e: BackendException) { + throwWrappedIfRequired(e) + throw e + } catch (e: RuntimeException) { + throwWrappedIfRequired(e) + throw e + } + } + + @Throws(BackendException::class) + override fun list(folder: DirType): List { + return try { + delegate.list(folder) + } catch (e: BackendException) { + throwWrappedIfRequired(e) + throw e + } catch (e: RuntimeException) { + throwWrappedIfRequired(e) + throw e + } + } + + @Throws(BackendException::class) + override fun create(folder: DirType): DirType { + return try { + delegate.create(folder) + } catch (e: BackendException) { + throwWrappedIfRequired(e) + throw e + } catch (e: RuntimeException) { + throwWrappedIfRequired(e) + throw e + } + } + + @Throws(BackendException::class) + override fun move(source: DirType, target: DirType): DirType { + return try { + delegate.move(source, target) + } catch (e: BackendException) { + throwWrappedIfRequired(e) + throw e + } catch (e: RuntimeException) { + throwWrappedIfRequired(e) + throw e + } + } + + @Throws(BackendException::class) + override fun move(source: FileType, target: FileType): FileType { + return try { + delegate.move(source, target) + } catch (e: BackendException) { + throwWrappedIfRequired(e) + throw e + } catch (e: RuntimeException) { + throwWrappedIfRequired(e) + throw e + } + } + + @Throws(BackendException::class) + override fun write(file: FileType, data: DataSource, progressAware: ProgressAware, replace: Boolean, size: Long): FileType { + return try { + delegate.write(file, data, progressAware, replace, size) + } catch (e: BackendException) { + throwWrappedIfRequired(e) + throw e + } catch (e: RuntimeException) { + throwWrappedIfRequired(e) + throw e + } + } + + @Throws(BackendException::class) + override fun read(file: FileType, encryptedTmpFile: File?, data: OutputStream, progressAware: ProgressAware) { + try { + delegate.read(file, encryptedTmpFile, data, progressAware) + } catch (e: BackendException) { + throwWrappedIfRequired(e) + throw e + } catch (e: RuntimeException) { + throwWrappedIfRequired(e) + throw e + } + } + + @Throws(BackendException::class) + override fun delete(node: NodeType) { + try { + delegate.delete(node) + } catch (e: BackendException) { + throwWrappedIfRequired(e) + throw e + } catch (e: RuntimeException) { + throwWrappedIfRequired(e) + throw e + } + } + + @Throws(BackendException::class) + override fun checkAuthenticationAndRetrieveCurrentAccount(cloud: CloudType): String { + return try { + delegate.checkAuthenticationAndRetrieveCurrentAccount(cloud) + } catch (e: BackendException) { + throwWrappedIfRequired(e) + throw e + } catch (e: RuntimeException) { + throwWrappedIfRequired(e) + throw e + } + } + + @Throws(BackendException::class) + override fun logout(cloud: CloudType) { + try { + delegate.logout(cloud) + } catch (e: BackendException) { + throwWrappedIfRequired(e) + throw e + } catch (e: RuntimeException) { + throwWrappedIfRequired(e) + throw e + } + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/BackupFileIdSuffixGenerator.java b/data/src/main/java/org/cryptomator/data/cloud/crypto/BackupFileIdSuffixGenerator.java deleted file mode 100644 index 7a5017c9..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/BackupFileIdSuffixGenerator.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.cryptomator.data.cloud.crypto; - -import com.google.common.io.BaseEncoding; - -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -/** - * Utility class for generating a suffix for the backup file to make it unique to its original master key file. - */ -class BackupFileIdSuffixGenerator { - - /** - * Computes the SHA-256 digest of the given byte array and returns a file suffix containing the first 4 bytes in hex string format. - * - * @param fileBytes the input byte for which the digest is computed - * @return "." + first 4 bytes of SHA-256 digest in hex string format - */ - static String generate(byte[] fileBytes) { - try { - MessageDigest md = MessageDigest.getInstance("SHA-256"); - byte[] digest = md.digest(fileBytes); - return "." + BaseEncoding.base16().encode(digest, 0, 4); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException("Every Java Platform must support the Message Digest algorithm SHA-256", e); - } - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/BackupFileIdSuffixGenerator.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/BackupFileIdSuffixGenerator.kt new file mode 100644 index 00000000..54cdfec5 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/BackupFileIdSuffixGenerator.kt @@ -0,0 +1,28 @@ +package org.cryptomator.data.cloud.crypto + +import com.google.common.io.BaseEncoding +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException + +/** + * Utility class for generating a suffix for the backup file to make it unique to its original master key file. + */ +internal object BackupFileIdSuffixGenerator { + + /** + * Computes the SHA-256 digest of the given byte array and returns a file suffix containing the first 4 bytes in hex string format. + * + * @param fileBytes the input byte for which the digest is computed + * @return "." + first 4 bytes of SHA-256 digest in hex string format + */ + @JvmStatic + fun generate(fileBytes: ByteArray): String { + return try { + val md = MessageDigest.getInstance("SHA-256") + val digest = md.digest(fileBytes) + "." + BaseEncoding.base16().encode(digest, 0, 4) + } catch (e: NoSuchAlgorithmException) { + throw IllegalStateException("Every Java Platform must support the Message Digest algorithm SHA-256", e) + } + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudContentRepository.java b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudContentRepository.java deleted file mode 100644 index ac83afa7..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudContentRepository.java +++ /dev/null @@ -1,138 +0,0 @@ -package org.cryptomator.data.cloud.crypto; - -import android.content.Context; - -import org.cryptomator.cryptolib.api.Cryptor; -import org.cryptomator.domain.CloudFolder; -import org.cryptomator.domain.exception.BackendException; -import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException; -import org.cryptomator.domain.exception.FatalBackendException; -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 org.cryptomator.util.Supplier; - -import java.io.File; -import java.io.OutputStream; -import java.util.List; - -import static java.lang.String.format; - -class CryptoCloudContentRepository implements CloudContentRepository { - - private final CryptoImplDecorator cryptoImpl; - - CryptoCloudContentRepository(Context context, CloudContentRepository cloudContentRepository, CryptoCloud cloud, Supplier cryptor) { - CloudFolder vaultLocation; - try { - vaultLocation = cloudContentRepository.resolve(cloud.getVault().getCloud(), cloud.getVault().getPath()); - } catch (BackendException e) { - throw new FatalBackendException(e); - } - - switch (cloud.getVault().getFormat()) { - case 7: - this.cryptoImpl = new CryptoImplVaultFormat7(context, cryptor, cloudContentRepository, vaultLocation, new DirIdCacheFormat7()); - break; - case 8: - this.cryptoImpl = new CryptoImplVaultFormat8(context, cryptor, cloudContentRepository, vaultLocation, new DirIdCacheFormat7(), cloud.getVault().getShorteningThreshold()); - break; - case 6: - case 5: - this.cryptoImpl = new CryptoImplVaultFormatPre7(context, cryptor, cloudContentRepository, vaultLocation, new DirIdCacheFormatPre7()); - break; - default: - throw new IllegalStateException(format("No CryptoImpl for vault format %d.", cloud.getVault().getFormat())); - } - } - - @Override - public synchronized CryptoFolder root(CryptoCloud cloud) throws BackendException { - return cryptoImpl.root(cloud); - } - - @Override - public CryptoFolder resolve(CryptoCloud cloud, String path) throws BackendException { - return cryptoImpl.resolve(cloud, path); - } - - @Override - public CryptoFile file(CryptoFolder parent, String name) throws BackendException { - return cryptoImpl.file(parent, name); - } - - @Override - public CryptoFile file(CryptoFolder parent, String name, Optional size) throws BackendException { - return cryptoImpl.file(parent, name, size); - } - - @Override - public CryptoFolder folder(CryptoFolder parent, String name) throws BackendException { - return cryptoImpl.folder(parent, name); - } - - @Override - public boolean exists(CryptoNode node) throws BackendException { - return cryptoImpl.exists(node); - } - - @Override - public List list(CryptoFolder folder) throws BackendException { - return cryptoImpl.list(folder); - } - - @Override - public CryptoFolder create(CryptoFolder folder) throws BackendException { - try { - return cryptoImpl.create(folder); - } catch (CloudNodeAlreadyExistsException e) { - throw new CloudNodeAlreadyExistsException(folder.getName()); - } - } - - @Override - public CryptoFolder move(CryptoFolder source, CryptoFolder target) throws BackendException { - try { - return cryptoImpl.move(source, target); - } catch (CloudNodeAlreadyExistsException e) { - throw new CloudNodeAlreadyExistsException(target.getName()); - } - } - - @Override - public CryptoFile move(CryptoFile source, CryptoFile target) throws BackendException { - try { - return cryptoImpl.move(source, target); - } catch (CloudNodeAlreadyExistsException e) { - throw new CloudNodeAlreadyExistsException(target.getName()); - } - } - - @Override - public CryptoFile write(CryptoFile file, DataSource data, ProgressAware progressAware, boolean replace, long length) throws BackendException { - return cryptoImpl.write(file, data, progressAware, replace, length); - } - - @Override - public void read(CryptoFile file, Optional tmpEncryptedFile, OutputStream data, ProgressAware progressAware) throws BackendException { - cryptoImpl.read(file, data, progressAware); - } - - @Override - public void delete(CryptoNode node) throws BackendException { - cryptoImpl.delete(node); - } - - @Override - public String checkAuthenticationAndRetrieveCurrentAccount(CryptoCloud cloud) throws BackendException { - return cryptoImpl.currentAccount(cloud); - } - - @Override - public void logout(CryptoCloud cloud) throws BackendException { - // empty - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudContentRepository.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudContentRepository.kt new file mode 100644 index 00000000..09519341 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudContentRepository.kt @@ -0,0 +1,128 @@ +package org.cryptomator.data.cloud.crypto + +import android.content.Context +import org.cryptomator.cryptolib.api.Cryptor +import org.cryptomator.domain.Cloud +import org.cryptomator.domain.CloudFile +import org.cryptomator.domain.CloudFolder +import org.cryptomator.domain.CloudNode +import org.cryptomator.domain.exception.BackendException +import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException +import org.cryptomator.domain.exception.FatalBackendException +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 java.io.File +import java.io.OutputStream +import java.util.function.Supplier + +internal class CryptoCloudContentRepository(context: Context, cloudContentRepository: CloudContentRepository, cloud: CryptoCloud, cryptor: Supplier) : + CloudContentRepository { + + private var cryptoImpl: CryptoImplDecorator + + @Synchronized + @Throws(BackendException::class) + override fun root(cloud: CryptoCloud): CryptoFolder { + return cryptoImpl.root(cloud) + } + + override fun resolve(cloud: CryptoCloud, path: String): CryptoFolder { + return cryptoImpl.resolve(cloud, path) + } + + @Throws(BackendException::class) + override fun file(parent: CryptoFolder, name: String): CryptoFile { + return cryptoImpl.file(parent, name) + } + + @Throws(BackendException::class) + override fun file(parent: CryptoFolder, name: String, size: Long?): CryptoFile { + return cryptoImpl.file(parent, name, size) + } + + @Throws(BackendException::class) + override fun folder(parent: CryptoFolder, name: String): CryptoFolder { + return cryptoImpl.folder(parent, name) + } + + @Throws(BackendException::class) + override fun exists(node: CryptoNode): Boolean { + return cryptoImpl.exists(node) + } + + @Throws(BackendException::class) + override fun list(folder: CryptoFolder): List { + return cryptoImpl.list(folder) + } + + @Throws(BackendException::class) + override fun create(folder: CryptoFolder): CryptoFolder { + return try { + cryptoImpl.create(folder) + } catch (e: CloudNodeAlreadyExistsException) { + throw CloudNodeAlreadyExistsException(folder.name) + } + } + + @Throws(BackendException::class) + override fun move(source: CryptoFolder, target: CryptoFolder): CryptoFolder { + return try { + cryptoImpl.move(source, target) + } catch (e: CloudNodeAlreadyExistsException) { + throw CloudNodeAlreadyExistsException(target.name) + } + } + + @Throws(BackendException::class) + override fun move(source: CryptoFile, target: CryptoFile): CryptoFile { + return try { + cryptoImpl.move(source, target) + } catch (e: CloudNodeAlreadyExistsException) { + throw CloudNodeAlreadyExistsException(target.name) + } + } + + @Throws(BackendException::class) + override fun write(file: CryptoFile, data: DataSource, progressAware: ProgressAware, replace: Boolean, size: Long): CryptoFile { + return cryptoImpl.write(file, data, progressAware, replace, size) + } + + @Throws(BackendException::class) + override fun read(file: CryptoFile, encryptedTmpFile: File?, data: OutputStream, progressAware: ProgressAware) { + cryptoImpl.read(file, data, progressAware) + } + + @Throws(BackendException::class) + override fun delete(node: CryptoNode) { + cryptoImpl.delete(node) + } + + @Throws(BackendException::class) + override fun checkAuthenticationAndRetrieveCurrentAccount(cloud: CryptoCloud): String { + return cryptoImpl.currentAccount(cloud) + } + + @Throws(BackendException::class) + override fun logout(cloud: CryptoCloud) { + // empty + } + + init { + val vaultLocation: CloudFolder = try { + cloudContentRepository.resolve(cloud.vault.cloud, cloud.vault.path) + } catch (e: BackendException) { + throw FatalBackendException(e) + } + + cryptoImpl = when (cloud.vault.format) { + 7 -> CryptoImplVaultFormat7(context, cryptor, cloudContentRepository, vaultLocation, DirIdCacheFormat7()) + 8 -> CryptoImplVaultFormat8(context, cryptor, cloudContentRepository, vaultLocation, DirIdCacheFormat7(), cloud.vault.shorteningThreshold) + 6, 5 -> CryptoImplVaultFormatPre7(context, cryptor, cloudContentRepository, vaultLocation, DirIdCacheFormatPre7()) + else -> throw IllegalStateException(String.format("No CryptoImpl for vault format %d.", cloud.vault.format)) + } + } + +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudContentRepositoryFactory.java b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudContentRepositoryFactory.java index a4f29c31..50c53d37 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudContentRepositoryFactory.java +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudContentRepositoryFactory.java @@ -2,13 +2,14 @@ package org.cryptomator.data.cloud.crypto; import android.content.Context; +import com.google.common.base.Optional; + import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.data.repository.CloudContentRepositoryFactory; import org.cryptomator.domain.Cloud; import org.cryptomator.domain.Vault; import org.cryptomator.domain.exception.MissingCryptorException; import org.cryptomator.domain.repository.CloudContentRepository; -import org.cryptomator.util.Optional; import javax.inject.Inject; import javax.inject.Singleton; @@ -38,7 +39,7 @@ public class CryptoCloudContentRepositoryFactory implements CloudContentReposito } @Override - public CloudContentRepository cloudContentRepositoryFor(Cloud cloud) { + public CloudContentRepository cloudContentRepositoryFor(Cloud cloud) { CryptoCloud cryptoCloud = (CryptoCloud) cloud; Vault vault = cryptoCloud.getVault(); return new CryptoCloudContentRepository(context, cloudContentRepository.get(), cryptoCloud, cryptors.get(vault)); @@ -50,7 +51,7 @@ public class CryptoCloudContentRepositoryFactory implements CloudContentReposito public void deregisterCryptor(Vault vault, boolean assertPresent) { Optional cryptor = cryptors.remove(vault); - if (cryptor.isAbsent()) { + if (!cryptor.isPresent()) { if (assertPresent) { throw new IllegalStateException(format("No cryptor registered for vault %s", vault)); } diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudFactory.java b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudFactory.java index 07f4275c..727b292b 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudFactory.java +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudFactory.java @@ -1,17 +1,21 @@ package org.cryptomator.data.cloud.crypto; +import com.google.common.base.Optional; + import org.cryptomator.domain.Cloud; import org.cryptomator.domain.CloudFile; import org.cryptomator.domain.CloudFolder; +import org.cryptomator.domain.CloudNode; import org.cryptomator.domain.UnverifiedVaultConfig; import org.cryptomator.domain.Vault; import org.cryptomator.domain.exception.BackendException; import org.cryptomator.domain.repository.CloudContentRepository; +import org.cryptomator.domain.usecases.ProgressAware; import org.cryptomator.domain.usecases.cloud.Flag; import org.cryptomator.domain.usecases.vault.UnlockToken; -import org.cryptomator.util.Optional; import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import javax.inject.Inject; @@ -20,25 +24,23 @@ import javax.inject.Singleton; import static org.cryptomator.data.cloud.crypto.CryptoConstants.MASTERKEY_SCHEME; import static org.cryptomator.data.cloud.crypto.CryptoConstants.VAULT_FILE_NAME; import static org.cryptomator.domain.Vault.aCopyOf; -import static org.cryptomator.domain.usecases.ProgressAware.NO_OP_PROGRESS_AWARE; -import static org.cryptomator.util.Encodings.UTF_8; @Singleton public class CryptoCloudFactory { - private final CloudContentRepository cloudContentRepository; + private final CloudContentRepository cloudContentRepository; private final CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory; private final SecureRandom secureRandom = new SecureRandom(); @Inject - public CryptoCloudFactory(CloudContentRepository cloudContentRepository, // + public CryptoCloudFactory(CloudContentRepository/**/ cloudContentRepository, // CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory) { this.cloudContentRepository = cloudContentRepository; this.cryptoCloudContentRepositoryFactory = cryptoCloudContentRepositoryFactory; } public void create(CloudFolder location, CharSequence password) throws BackendException { - cryptoCloudProvider(Optional.empty()).create(location, password); + cryptoCloudProvider(Optional.absent()).create(location, password); } public Cloud decryptedViewOf(Vault vault) throws BackendException { @@ -47,14 +49,14 @@ public class CryptoCloudFactory { public Optional unverifiedVaultConfig(Vault vault) throws BackendException { CloudFolder vaultLocation = cloudContentRepository.resolve(vault.getCloud(), vault.getPath()); - String jwt = new String(readConfigFileData(vaultLocation), UTF_8); + String jwt = new String(readConfigFileData(vaultLocation), StandardCharsets.UTF_8); return Optional.of(VaultConfig.decode(jwt)); } private byte[] readConfigFileData(CloudFolder location) throws BackendException { ByteArrayOutputStream data = new ByteArrayOutputStream(); CloudFile vaultFile = cloudContentRepository.file(location, VAULT_FILE_NAME); - cloudContentRepository.read(vaultFile, Optional.empty(), data, NO_OP_PROGRESS_AWARE); + cloudContentRepository.read(vaultFile, null, data, ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD); return data.toByteArray(); } @@ -84,13 +86,10 @@ public class CryptoCloudFactory { private CryptoCloudProvider cryptoCloudProvider(Optional unverifiedVaultConfigOptional) { if (unverifiedVaultConfigOptional.isPresent()) { - switch (unverifiedVaultConfigOptional.get().getKeyId().getScheme()) { - case MASTERKEY_SCHEME: { - return new MasterkeyCryptoCloudProvider(cloudContentRepository, cryptoCloudContentRepositoryFactory, secureRandom); - } - default: - throw new IllegalStateException(String.format("Provider with scheme %s not supported", unverifiedVaultConfigOptional.get().getKeyId().getScheme())); + if (MASTERKEY_SCHEME.equals(unverifiedVaultConfigOptional.get().getKeyId().getScheme())) { + return new MasterkeyCryptoCloudProvider(cloudContentRepository, cryptoCloudContentRepositoryFactory, secureRandom); } + throw new IllegalStateException(String.format("Provider with scheme %s not supported", unverifiedVaultConfigOptional.get().getKeyId().getScheme())); } else { return new MasterkeyCryptoCloudProvider(cloudContentRepository, cryptoCloudContentRepositoryFactory, secureRandom); } diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudProvider.java b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudProvider.java index a2c2d133..83a92dfc 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudProvider.java +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudProvider.java @@ -1,12 +1,13 @@ package org.cryptomator.data.cloud.crypto; +import com.google.common.base.Optional; + import org.cryptomator.domain.CloudFolder; import org.cryptomator.domain.UnverifiedVaultConfig; import org.cryptomator.domain.Vault; import org.cryptomator.domain.exception.BackendException; import org.cryptomator.domain.usecases.cloud.Flag; import org.cryptomator.domain.usecases.vault.UnlockToken; -import org.cryptomator.util.Optional; public interface CryptoCloudProvider { diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoConstants.java b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoConstants.java deleted file mode 100644 index 38987362..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoConstants.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.cryptomator.data.cloud.crypto; - -public class CryptoConstants { - - public static final String MASTERKEY_SCHEME = "masterkeyfile"; - - static final String MASTERKEY_FILE_NAME = "masterkey.cryptomator"; - - static final String ROOT_DIR_ID = ""; - static final String DATA_DIR_NAME = "d"; - static final String VAULT_FILE_NAME = "vault.cryptomator"; - static final String MASTERKEY_BACKUP_FILE_EXT = ".bkup"; - - static final int DEFAULT_MASTERKEY_FILE_VERSION = 999; - static final int MAX_VAULT_VERSION = 8; - static final int MAX_VAULT_VERSION_WITHOUT_VAULT_CONFIG = 7; - static final int VERSION_WITH_NORMALIZED_PASSWORDS = 6; - static final int MIN_VAULT_VERSION = 5; - - static final int DEFAULT_MAX_FILE_NAME = 220; - - static final byte[] PEPPER = new byte[0]; - - static final VaultCipherCombo DEFAULT_CIPHER_COMBO = VaultCipherCombo.SIV_CTRMAC; - -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoConstants.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoConstants.kt new file mode 100644 index 00000000..76a66a55 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoConstants.kt @@ -0,0 +1,22 @@ +package org.cryptomator.data.cloud.crypto + +import org.cryptomator.cryptolib.api.CryptorProvider + +object CryptoConstants { + + const val MASTERKEY_SCHEME = "masterkeyfile" + const val MASTERKEY_FILE_NAME = "masterkey.cryptomator" + const val ROOT_DIR_ID = "" + const val DATA_DIR_NAME = "d" + const val VAULT_FILE_NAME = "vault.cryptomator" + const val MASTERKEY_BACKUP_FILE_EXT = ".bkup" + const val DEFAULT_MASTERKEY_FILE_VERSION = 999 + const val MAX_VAULT_VERSION = 8 + const val MAX_VAULT_VERSION_WITHOUT_VAULT_CONFIG = 7 + const val VERSION_WITH_NORMALIZED_PASSWORDS = 6 + const val MIN_VAULT_VERSION = 5 + const val DEFAULT_MAX_FILE_NAME = 220 + val PEPPER = ByteArray(0) + val DEFAULT_CIPHER_COMBO = CryptorProvider.Scheme.SIV_CTRMAC + +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoFile.java b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoFile.java deleted file mode 100644 index a53a9c55..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoFile.java +++ /dev/null @@ -1,82 +0,0 @@ -package org.cryptomator.data.cloud.crypto; - -import org.cryptomator.domain.Cloud; -import org.cryptomator.domain.CloudFile; -import org.cryptomator.util.Optional; - -import java.util.Date; - -class CryptoFile implements CloudFile, CryptoNode { - - private final String name; - private final String path; - private final Optional size; - private final CloudFile cloudFile; - private final CryptoFolder parent; - - public CryptoFile(CryptoFolder parent, String name, String path, Optional size, CloudFile cloudFile) { - this.parent = parent; - this.name = name; - this.path = path; - this.size = size; - this.cloudFile = cloudFile; - } - - @Override - public Cloud getCloud() { - return parent.getCloud(); - } - - @Override - public String getName() { - return name; - } - - @Override - public String getPath() { - return path; - } - - @Override - public CryptoFolder getParent() { - return parent; - } - - @Override - public Optional getSize() { - return size; - } - - @Override - public Optional getModified() { - return cloudFile.getModified(); - } - - /** - * @return The actual file in the underlying, i.e. decorated, CloudContentRepository - */ - CloudFile getCloudFile() { - return cloudFile; - } - - @Override - public boolean equals(Object obj) { - if (obj == null || getClass() != obj.getClass()) { - return false; - } - if (obj == this) { - return true; - } - return internalEquals((CryptoFile) obj); - } - - private boolean internalEquals(CryptoFile obj) { - return path != null && path.equals(obj.path); - } - - @Override - public int hashCode() { - return path == null ? 0 : path.hashCode(); - } - -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoFile.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoFile.kt new file mode 100644 index 00000000..a8284f60 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoFile.kt @@ -0,0 +1,37 @@ +package org.cryptomator.data.cloud.crypto + +import org.cryptomator.domain.Cloud +import org.cryptomator.domain.CloudFile +import java.util.Date + +class CryptoFile( + override val parent: CryptoFolder, override val name: String, override val path: String, override val size: Long?, + /** + * @return The actual file in the underlying, i.e. decorated, CloudContentRepository + */ + val cloudFile: CloudFile +) : CloudFile, CryptoNode { + + override val cloud: Cloud? + get() = parent.cloud + + override val modified: Date? + get() = cloudFile.modified + + override fun equals(other: Any?): Boolean { + if (other == null || javaClass != other.javaClass) { + return false + } + return if (other === this) { + true + } else internalEquals(other as CryptoFile) + } + + private fun internalEquals(obj: CryptoFile): Boolean { + return path == obj.path + } + + override fun hashCode(): Int { + return path.hashCode() + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoFolder.java b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoFolder.java deleted file mode 100644 index 6f58d150..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoFolder.java +++ /dev/null @@ -1,72 +0,0 @@ -package org.cryptomator.data.cloud.crypto; - -import org.cryptomator.domain.Cloud; -import org.cryptomator.domain.CloudFile; -import org.cryptomator.domain.CloudFolder; - -class CryptoFolder implements CloudFolder, CryptoNode { - - private final String name; - private final String path; - private final CryptoFolder parent; - private final CloudFile dirFile; - - CryptoFolder(CryptoFolder parent, String name, String path, CloudFile dirFile) { - this.parent = parent; - this.name = name; - this.path = path; - this.dirFile = dirFile; - } - - @Override - public Cloud getCloud() { - return parent.getCloud(); - } - - @Override - public String getName() { - return name; - } - - @Override - public String getPath() { - return path; - } - - @Override - public CryptoFolder getParent() { - return parent; - } - - /** - * @return the file containing the directory id, in the underlying, i.e. decorated, CloudContentRepository - */ - CloudFile getDirFile() { - return dirFile; - } - - @Override - public boolean equals(Object obj) { - if (obj == null || getClass() != obj.getClass()) { - return false; - } - if (obj == this) { - return true; - } - return internalEquals((CryptoFolder) obj); - } - - private boolean internalEquals(CryptoFolder obj) { - return path != null && path.equals(obj.path); - } - - @Override - public int hashCode() { - return path == null ? 0 : path.hashCode(); - } - - @Override - public CryptoFolder withCloud(Cloud cloud) { - return new CryptoFolder(parent.withCloud(cloud), name, path, dirFile); - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoFolder.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoFolder.kt new file mode 100644 index 00000000..67a04e73 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoFolder.kt @@ -0,0 +1,38 @@ +package org.cryptomator.data.cloud.crypto + +import org.cryptomator.domain.Cloud +import org.cryptomator.domain.CloudFile +import org.cryptomator.domain.CloudFolder + +open class CryptoFolder( + override val parent: CryptoFolder?, override val name: String, override val path: String, + /** + * @return the file containing the directory id, in the underlying, i.e. decorated, CloudContentRepository + */ + val dirFile: CloudFile? +) : CloudFolder, CryptoNode { + + override val cloud: Cloud? + get() = parent?.cloud + + override fun equals(other: Any?): Boolean { + if (other == null || javaClass != other.javaClass) { + return false + } + return if (other === this) { + true + } else internalEquals(other as CryptoFolder) + } + + private fun internalEquals(obj: CryptoFolder): Boolean { + return path == obj.path + } + + override fun hashCode(): Int { + return path.hashCode() + } + + override fun withCloud(cloud: Cloud?): CryptoFolder? { + return CryptoFolder(parent?.withCloud(cloud), name, path, dirFile) + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.java b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.java deleted file mode 100644 index f88e9b2c..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.java +++ /dev/null @@ -1,425 +0,0 @@ -package org.cryptomator.data.cloud.crypto; - -import android.content.Context; - -import org.cryptomator.cryptolib.Cryptors; -import org.cryptomator.cryptolib.DecryptingReadableByteChannel; -import org.cryptomator.cryptolib.EncryptingWritableByteChannel; -import org.cryptomator.cryptolib.api.Cryptor; -import org.cryptomator.data.cloud.crypto.DirIdCache.DirIdInfo; -import org.cryptomator.domain.Cloud; -import org.cryptomator.domain.CloudFile; -import org.cryptomator.domain.CloudFolder; -import org.cryptomator.domain.CloudNode; -import org.cryptomator.domain.exception.BackendException; -import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException; -import org.cryptomator.domain.exception.EmptyDirFileException; -import org.cryptomator.domain.exception.FatalBackendException; -import org.cryptomator.domain.exception.NoDirFileException; -import org.cryptomator.domain.repository.CloudContentRepository; -import org.cryptomator.domain.usecases.DownloadFileReplacingProgressAware; -import org.cryptomator.domain.usecases.ProgressAware; -import org.cryptomator.domain.usecases.UploadFileReplacingProgressAware; -import org.cryptomator.domain.usecases.cloud.DataSource; -import org.cryptomator.domain.usecases.cloud.DownloadState; -import org.cryptomator.domain.usecases.cloud.FileBasedDataSource; -import org.cryptomator.domain.usecases.cloud.Progress; -import org.cryptomator.domain.usecases.cloud.UploadState; -import org.cryptomator.util.Optional; -import org.cryptomator.util.Supplier; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.ByteBuffer; -import java.nio.channels.Channels; -import java.nio.channels.ReadableByteChannel; -import java.nio.channels.WritableByteChannel; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import java.util.Queue; -import java.util.UUID; - -import static org.cryptomator.data.cloud.crypto.CryptoConstants.DATA_DIR_NAME; -import static org.cryptomator.domain.usecases.ProgressAware.NO_OP_PROGRESS_AWARE; -import static org.cryptomator.domain.usecases.cloud.Progress.progress; - -abstract class CryptoImplDecorator { - - final CloudContentRepository cloudContentRepository; - final Context context; - final DirIdCache dirIdCache; - final int shorteningThreshold; - - private final Supplier cryptor; - private final CloudFolder storageLocation; - - private RootCryptoFolder root; - - CryptoImplDecorator(Context context, Supplier cryptor, CloudContentRepository cloudContentRepository, CloudFolder storageLocation, DirIdCache dirIdCache, int shorteningThreshold) { - this.context = context; - this.cryptor = cryptor; - this.cloudContentRepository = cloudContentRepository; - this.storageLocation = storageLocation; - this.dirIdCache = dirIdCache; - this.shorteningThreshold = shorteningThreshold; - } - - abstract CryptoFolder folder(CryptoFolder cryptoParent, String cleartextName) throws BackendException; - - abstract String decryptName(String dirId, String encryptedName); - - abstract String encryptName(CryptoFolder cryptoParent, String name) throws BackendException; - - abstract Optional extractEncryptedName(String ciphertextName); - - abstract List list(CryptoFolder cryptoFolder) throws BackendException; - - abstract String encryptFolderName(CryptoFolder cryptoFolder, String name) throws BackendException; - - abstract CryptoSymlink symlink(CryptoFolder cryptoParent, String cleartextName, String target) throws BackendException; - - abstract CryptoFolder create(CryptoFolder folder) throws BackendException; - - abstract CryptoFolder move(CryptoFolder source, CryptoFolder target) throws BackendException; - - abstract CryptoFile move(CryptoFile source, CryptoFile target) throws BackendException; - - abstract void delete(CloudNode node) throws BackendException; - - abstract CryptoFile write(CryptoFile cryptoFile, DataSource data, ProgressAware progressAware, boolean replace, long length) throws BackendException; - - abstract String loadDirId(CryptoFolder folder) throws BackendException, EmptyDirFileException; - - abstract DirIdInfo createDirIdInfo(CryptoFolder folder) throws BackendException; - - private String dirHash(String directoryId) { - return cryptor().fileNameCryptor().hashDirectoryId(directoryId); - } - - private CloudFolder dataFolder() throws BackendException { - return cloudContentRepository.folder(storageLocation, DATA_DIR_NAME); - } - - String path(CloudFolder base, String name) { - return base.getPath() + "/" + name; - } - - File getInternalCache() { - return context.getCacheDir(); - } - - List deepCollectSubfolders(CryptoFolder source) throws BackendException { - Queue queue = new LinkedList<>(); - queue.add(source); - - List result = new LinkedList<>(); - while (!queue.isEmpty()) { - CryptoFolder folder = queue.remove(); - List subfolders = shallowCollectSubfolders(folder); - queue.addAll(subfolders); - result.addAll(subfolders); - } - - Collections.reverse(result); - - return result; - } - - private List shallowCollectSubfolders(CryptoFolder source) throws BackendException { - List result = new LinkedList<>(); - - try { - List list = list(source); - for (CloudNode node : list) { - if (node instanceof CryptoFolder) { - result.add((CryptoFolder) node); - } - } - } catch (NoDirFileException e) { - // Ignoring because nothing can be done if the dir-file doesn't exists in the cloud - } - - return result; - } - - public RootCryptoFolder root(CryptoCloud cryptoCloud) throws BackendException { - if (root == null) { - root = new RootCryptoFolder(cryptoCloud); - } - return root; - } - - public CryptoFolder resolve(CryptoCloud cloud, String path) throws BackendException { - if (path.startsWith("/")) { - path = path.substring(1); - } - String[] names = path.split("/"); - CryptoFolder folder = root(cloud); - for (String name : names) { - folder = folder(folder, name); - } - return folder; - } - - public CryptoFile file(CryptoFolder cryptoParent, String cleartextName) throws BackendException { - return file(cryptoParent, cleartextName, Optional.empty()); - } - - public CryptoFile file(CryptoFolder cryptoParent, String cleartextName, Optional cleartextSize) throws BackendException { - String ciphertextName = encryptFileName(cryptoParent, cleartextName); - return file(cryptoParent, cleartextName, ciphertextName, cleartextSize); - } - - private CryptoFile file(CryptoFolder cryptoParent, String cleartextName, String ciphertextName, Optional cleartextSize) throws BackendException { - Optional ciphertextSize; - if (cleartextSize.isPresent()) { - ciphertextSize = Optional.of(Cryptors.ciphertextSize(cleartextSize.get(), cryptor()) + cryptor().fileHeaderCryptor().headerSize()); - } else { - ciphertextSize = Optional.empty(); - } - CloudFile cloudFile = cloudContentRepository.file(dirIdInfo(cryptoParent).getCloudFolder(), ciphertextName, ciphertextSize); - return file(cryptoParent, cleartextName, cloudFile, cleartextSize); - } - - CryptoFile file(CryptoFile cryptoFile, CloudFile cloudFile, Optional cleartextSize) throws BackendException { - return file(cryptoFile.getParent(), cryptoFile.getName(), cloudFile, cleartextSize); - } - - CryptoFile file(CryptoFolder cryptoParent, String cleartextName, CloudFile cloudFile, Optional cleartextSize) throws BackendException { - return new CryptoFile(cryptoParent, cleartextName, path(cryptoParent, cleartextName), cleartextSize, cloudFile); - } - - private String encryptFileName(CryptoFolder cryptoParent, String name) throws BackendException { - return encryptName(cryptoParent, name); - } - - CryptoFolder folder(CryptoFolder cryptoParent, String cleartextName, CloudFile dirFile) throws BackendException { - return new CryptoFolder(cryptoParent, cleartextName, path(cryptoParent, cleartextName), dirFile); - } - - CryptoFolder folder(CryptoFolder cryptoFolder, CloudFile dirFile) throws BackendException { - return new CryptoFolder(cryptoFolder.getParent(), cryptoFolder.getName(), cryptoFolder.getPath(), dirFile); - } - - boolean exists(CloudNode node) throws BackendException { - if (node instanceof CryptoFolder) { - return exists((CryptoFolder) node); - } else if (node instanceof CryptoFile) { - return exists((CryptoFile) node); - } else if (node instanceof CryptoSymlink) { - return exists((CryptoSymlink) node); - } else { - throw new IllegalArgumentException("Unexpected CloudNode type: " + node.getClass()); - } - } - - private boolean exists(CryptoFolder folder) throws BackendException { - return cloudContentRepository.exists(folder.getDirFile()) && cloudContentRepository.exists(dirIdInfo(folder).getCloudFolder()); - } - - private boolean exists(CryptoFile file) throws BackendException { - return cloudContentRepository.exists(file.getCloudFile()); - } - - private boolean exists(CryptoSymlink symlink) throws BackendException { - return cloudContentRepository.exists(symlink.getCloudFile()); - } - - void assertCryptoFolderAlreadyExists(CryptoFolder cryptoFolder) throws BackendException { - if (cloudContentRepository.exists(cryptoFolder.getDirFile()) // - || cloudContentRepository.exists(file(cryptoFolder.getParent(), cryptoFolder.getName()))) { - throw new CloudNodeAlreadyExistsException(cryptoFolder.getName()); - } - } - - void assertCryptoFileAlreadyExists(CryptoFile cryptoFile) throws BackendException { - if (cloudContentRepository.exists(cryptoFile.getCloudFile()) // - || cloudContentRepository.exists(folder(cryptoFile.getParent(), cryptoFile.getName()).getDirFile())) { - throw new CloudNodeAlreadyExistsException("CloudNode already exists and replace is false"); - } - } - - private CryptoFile writeFromTmpFile(DataSource originalDataSource, final CryptoFile cryptoFile, File encryptedFile, final ProgressAware progressAware, boolean replace) throws BackendException, IOException { - CryptoFile targetFile = targetFile(cryptoFile, replace); - return file(targetFile, // - cloudContentRepository.write( // - targetFile.getCloudFile(), // - originalDataSource.decorate(FileBasedDataSource.from(encryptedFile)), // - new UploadFileReplacingProgressAware(cryptoFile, progressAware), // - replace, // - encryptedFile.length()), // - cryptoFile.getSize()); - } - - private CryptoFile targetFile(CryptoFile cryptoFile, boolean replace) throws BackendException { - if (replace || !cloudContentRepository.exists(cryptoFile)) { - return cryptoFile; - } - return firstNonExistingAutoRenamedFile(cryptoFile); - } - - private CryptoFile firstNonExistingAutoRenamedFile(CryptoFile original) throws BackendException { - String name = original.getName(); - String nameWithoutExtension = nameWithoutExtension(name); - String extension = extension(name); - int counter = 1; - CryptoFile result; - do { - String newFileName = nameWithoutExtension + " (" + counter + ")" + extension; - result = file(original.getParent(), newFileName, original.getSize()); - counter++; - } while (cloudContentRepository.exists(result)); - return result; - } - - String nameWithoutExtension(String name) { - int lastDot = name.lastIndexOf("."); - if (lastDot == -1) { - return name; - } - return name.substring(0, lastDot); - } - - String extension(String name) { - int lastDot = name.lastIndexOf("."); - if (lastDot == -1) { - return ""; - } - return name.substring(lastDot + 1); - } - - public void read(CryptoFile cryptoFile, OutputStream data, ProgressAware progressAware) throws BackendException { - CloudFile ciphertextFile = cryptoFile.getCloudFile(); - try { - File encryptedTmpFile = readToTmpFile(cryptoFile, ciphertextFile, progressAware); - progressAware.onProgress(Progress.started(DownloadState.decryption(cryptoFile))); - try (ReadableByteChannel readableByteChannel = Channels.newChannel(new FileInputStream(encryptedTmpFile)); // - ReadableByteChannel decryptingReadableByteChannel = new DecryptingReadableByteChannel(readableByteChannel, cryptor(), true)) { - ByteBuffer buff = ByteBuffer.allocate(cryptor().fileContentCryptor().ciphertextChunkSize()); - long cleartextSize = cryptoFile.getSize().orElse(Long.MAX_VALUE); - long decrypted = 0; - int read; - while ((read = decryptingReadableByteChannel.read(buff)) > 0) { - buff.flip(); - data.write(buff.array(), 0, buff.remaining()); - decrypted += read; - progressAware.onProgress(progress(DownloadState.decryption(cryptoFile)).between(0).and(cleartextSize).withValue(decrypted)); - } - } finally { - encryptedTmpFile.delete(); - progressAware.onProgress(Progress.completed(DownloadState.decryption(cryptoFile))); - } - } catch (IOException e) { - throw new FatalBackendException(e); - } - } - - private File readToTmpFile(CryptoFile cryptoFile, CloudFile file, ProgressAware progressAware) throws BackendException, IOException { - File encryptedTmpFile = File.createTempFile(UUID.randomUUID().toString(), ".crypto", getInternalCache()); - try (OutputStream encryptedData = new FileOutputStream(encryptedTmpFile)) { - cloudContentRepository.read(file, Optional.of(encryptedTmpFile), encryptedData, new DownloadFileReplacingProgressAware(cryptoFile, progressAware)); - return encryptedTmpFile; - } - } - - public String currentAccount(Cloud cloud) throws BackendException { - return cloudContentRepository.checkAuthenticationAndRetrieveCurrentAccount(cloud); - } - - DirIdInfo dirIdInfo(CryptoFolder folder) throws BackendException { - DirIdInfo dirIdInfo = dirIdCache.get(folder); - if (dirIdInfo == null) { - return createDirIdInfo(folder); - } - return dirIdInfo; - } - - DirIdInfo createDirIdInfoFor(String dirId) throws BackendException { - String dirHash = dirHash(dirId); - CloudFolder lvl2Dir = lvl2Dir(dirHash); - return new DirIdInfo(dirId, lvl2Dir); - } - - byte[] loadContentsOfDirFile(CryptoFolder folder) throws BackendException, EmptyDirFileException { - try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { - cloudContentRepository.read(folder.getDirFile(), Optional.empty(), out, NO_OP_PROGRESS_AWARE); - if (dirfileIsEmpty(out)) { - throw new EmptyDirFileException(folder.getName(), folder.getDirFile().getPath()); - } - return out.toByteArray(); - } catch (IOException e) { - throw new FatalBackendException(e); - } - } - - String newDirId() { - return UUID.randomUUID().toString(); - } - - boolean dirfileIsEmpty(ByteArrayOutputStream out) { - return out.size() == 0; - } - - private CloudFolder lvl2Dir(String dirHash) throws BackendException { - return cloudContentRepository.folder(lvl1Dir(dirHash), dirHash.substring(2)); - } - - private CloudFolder lvl1Dir(String dirHash) throws BackendException { - return cloudContentRepository.folder(dataFolder(), dirHash.substring(0, 2)); - } - - Cryptor cryptor() { - return cryptor.get(); - } - - CloudFolder storageLocation() { - return storageLocation; - } - - void addFolderToCache(CryptoFolder result, DirIdCache.DirIdInfo dirInfo) { - dirIdCache.put(result, dirInfo); - } - - void evictFromCache(CryptoFolder cryptoFolder) { - dirIdCache.evict(cryptoFolder); - } - - CryptoFile writeShortNameFile(CryptoFile cryptoFile, DataSource data, ProgressAware progressAware, boolean replace, long length) throws BackendException { - if (!replace) { - assertCryptoFileAlreadyExists(cryptoFile); - } - try (InputStream stream = data.open(context)) { - File encryptedTmpFile = File.createTempFile(UUID.randomUUID().toString(), ".crypto", getInternalCache()); - try (WritableByteChannel writableByteChannel = Channels.newChannel(new FileOutputStream(encryptedTmpFile)); // - WritableByteChannel encryptingWritableByteChannel = new EncryptingWritableByteChannel(writableByteChannel, cryptor())) { - progressAware.onProgress(Progress.started(UploadState.encryption(cryptoFile))); - ByteBuffer buff = ByteBuffer.allocate(cryptor().fileContentCryptor().cleartextChunkSize()); - long ciphertextSize = Cryptors.ciphertextSize(cryptoFile.getSize().get(), cryptor()) + cryptor().fileHeaderCryptor().headerSize(); - int read; - long encrypted = 0; - while ((read = stream.read(buff.array())) > 0) { - buff.limit(read); - int written = encryptingWritableByteChannel.write(buff); - buff.flip(); - encrypted += written; - progressAware.onProgress(progress(UploadState.encryption(cryptoFile)).between(0).and(ciphertextSize).withValue(encrypted)); - } - encryptingWritableByteChannel.close(); - progressAware.onProgress(Progress.completed(UploadState.encryption(cryptoFile))); - return writeFromTmpFile(data, cryptoFile, encryptedTmpFile, progressAware, replace); - } catch (Throwable e) { - throw e; - } finally { - encryptedTmpFile.delete(); - } - } catch (IOException e) { - throw new FatalBackendException(e); - } - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt new file mode 100644 index 00000000..646099c0 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt @@ -0,0 +1,457 @@ +package org.cryptomator.data.cloud.crypto + +import android.content.Context +import org.cryptomator.cryptolib.api.Cryptor +import org.cryptomator.cryptolib.common.DecryptingReadableByteChannel +import org.cryptomator.cryptolib.common.EncryptingWritableByteChannel +import org.cryptomator.data.cloud.crypto.DirIdCache.DirIdInfo +import org.cryptomator.domain.Cloud +import org.cryptomator.domain.CloudFile +import org.cryptomator.domain.CloudFolder +import org.cryptomator.domain.CloudNode +import org.cryptomator.domain.exception.BackendException +import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException +import org.cryptomator.domain.exception.EmptyDirFileException +import org.cryptomator.domain.exception.FatalBackendException +import org.cryptomator.domain.exception.NoDirFileException +import org.cryptomator.domain.exception.ParentFolderIsNullException +import org.cryptomator.domain.repository.CloudContentRepository +import org.cryptomator.domain.usecases.DownloadFileReplacingProgressAware +import org.cryptomator.domain.usecases.ProgressAware +import org.cryptomator.domain.usecases.UploadFileReplacingProgressAware +import org.cryptomator.domain.usecases.cloud.DataSource +import org.cryptomator.domain.usecases.cloud.DownloadState +import org.cryptomator.domain.usecases.cloud.FileBasedDataSource.Companion.from +import org.cryptomator.domain.usecases.cloud.Progress +import org.cryptomator.domain.usecases.cloud.UploadState +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.IOException +import java.io.OutputStream +import java.nio.ByteBuffer +import java.nio.channels.Channels +import java.util.LinkedList +import java.util.Queue +import java.util.UUID +import java.util.function.Supplier + + +abstract class CryptoImplDecorator( + val context: Context, + private val cryptor: Supplier, + val cloudContentRepository: CloudContentRepository, + private val storageLocation: CloudFolder, + val dirIdCache: DirIdCache, + val shorteningThreshold: Int +) { + + @Volatile + private var root: RootCryptoFolder? = null + + @Throws(BackendException::class) + abstract fun folder(cryptoParent: CryptoFolder, cleartextName: String): CryptoFolder + + abstract fun decryptName(dirId: String, encryptedName: String): String? + + @Throws(BackendException::class) + abstract fun encryptName(cryptoParent: CryptoFolder, name: String): String + + abstract fun extractEncryptedName(ciphertextName: String): String? + + @Throws(BackendException::class) + abstract fun list(cryptoFolder: CryptoFolder): List + + @Throws(BackendException::class) + abstract fun encryptFolderName(cryptoFolder: CryptoFolder, name: String): String + + @Throws(BackendException::class) + abstract fun symlink(cryptoParent: CryptoFolder, cleartextName: String, target: String): CryptoSymlink + + @Throws(BackendException::class) + abstract fun create(folder: CryptoFolder): CryptoFolder + + @Throws(BackendException::class) + abstract fun move(source: CryptoFolder, target: CryptoFolder): CryptoFolder + + @Throws(BackendException::class) + abstract fun move(source: CryptoFile, target: CryptoFile): CryptoFile + + @Throws(BackendException::class) + abstract fun delete(node: CloudNode) + + @Throws(BackendException::class) + abstract fun write(cryptoFile: CryptoFile, data: DataSource, progressAware: ProgressAware, replace: Boolean, length: Long): CryptoFile + + @Throws(BackendException::class, EmptyDirFileException::class) + abstract fun loadDirId(folder: CryptoFolder): String + + @Throws(BackendException::class) + abstract fun createDirIdInfo(folder: CryptoFolder): DirIdInfo + + private fun dirHash(directoryId: String): String { + return cryptor().fileNameCryptor().hashDirectoryId(directoryId) + } + + @Throws(BackendException::class) + private fun dataFolder(): CloudFolder { + return cloudContentRepository.folder(storageLocation, CryptoConstants.DATA_DIR_NAME) + } + + fun path(base: CloudFolder, name: String): String { + return base.path + "/" + name + } + + val internalCache: File + get() = context.cacheDir + + @Throws(BackendException::class) + fun deepCollectSubfolders(source: CryptoFolder): List { + + val queue: Queue = LinkedList() + queue.add(source) + val result: MutableList = LinkedList() + + while (!queue.isEmpty()) { + val folder = queue.remove() + val subfolders = shallowCollectSubfolders(folder) + queue.addAll(subfolders) + result.addAll(subfolders) + } + + result.reverse() + return result + } + + @Throws(BackendException::class) + private fun shallowCollectSubfolders(source: CryptoFolder): List { + return try { + list(source).filterIsInstance() + } catch (e: NoDirFileException) { + // Ignoring because nothing can be done if the dir-file doesn't exists in the cloud + emptyList() + } + } + + @Throws(BackendException::class) + @Synchronized + fun root(cryptoCloud: CryptoCloud): RootCryptoFolder = root ?: RootCryptoFolder(cryptoCloud).also { root = it } + + @Throws(BackendException::class) + fun resolve(cloud: CryptoCloud, path: String): CryptoFolder { + val names = path.removePrefix("/").split("/").toTypedArray() + var folder: CryptoFolder = root(cloud) + for (name in names) { + folder = folder(folder, name) + } + return folder + } + + @Throws(BackendException::class) + fun file(cryptoParent: CryptoFolder, cleartextName: String): CryptoFile { + return file(cryptoParent, cleartextName, null) + } + + @Throws(BackendException::class) + fun file(cryptoParent: CryptoFolder, cleartextName: String, cleartextSize: Long?): CryptoFile { + val ciphertextName = encryptFileName(cryptoParent, cleartextName) + return file(cryptoParent, cleartextName, ciphertextName, cleartextSize) + } + + @Throws(BackendException::class) + private fun file(cryptoParent: CryptoFolder, cleartextName: String, ciphertextName: String, cleartextSize: Long?): CryptoFile { + val ciphertextSize = cleartextSize?.let { cryptor().fileContentCryptor().ciphertextSize(it) + cryptor().fileHeaderCryptor().headerSize() } + val cloudFile = cloudContentRepository.file(dirIdInfo(cryptoParent).cloudFolder, ciphertextName, ciphertextSize) + return file(cryptoParent, cleartextName, cloudFile, cleartextSize) + } + + @Throws(BackendException::class) + fun file(cryptoFile: CryptoFile, cloudFile: CloudFile, cleartextSize: Long?): CryptoFile { + return file(cryptoFile.parent, cryptoFile.name, cloudFile, cleartextSize) + } + + @Throws(BackendException::class) + fun file(cryptoParent: CryptoFolder, cleartextName: String, cloudFile: CloudFile, cleartextSize: Long?): CryptoFile { + return CryptoFile(cryptoParent, cleartextName, path(cryptoParent, cleartextName), cleartextSize, cloudFile) + } + + @Throws(BackendException::class) + private fun encryptFileName(cryptoParent: CryptoFolder, name: String): String { + return encryptName(cryptoParent, name) + } + + @Throws(BackendException::class) + fun folder(cryptoParent: CryptoFolder, cleartextName: String, dirFile: CloudFile): CryptoFolder { + return CryptoFolder(cryptoParent, cleartextName, path(cryptoParent, cleartextName), dirFile) + } + + @Throws(BackendException::class) + fun folder(cryptoFolder: CryptoFolder, dirFile: CloudFile): CryptoFolder { + return CryptoFolder(cryptoFolder.parent, cryptoFolder.name, cryptoFolder.path, dirFile) + } + + @Throws(BackendException::class) + fun exists(node: CloudNode): Boolean { + return when (node) { + is CryptoFolder -> { + exists(node) + } + is CryptoFile -> { + exists(node) + } + is CryptoSymlink -> { + exists(node) + } + else -> { + throw IllegalArgumentException("Unexpected CloudNode type: " + node.javaClass) + } + } + } + + @Throws(BackendException::class) + private fun exists(folder: CryptoFolder): Boolean { + requireNotNull(folder.dirFile) + return cloudContentRepository.exists(folder.dirFile) && cloudContentRepository.exists(dirIdInfo(folder).cloudFolder) + } + + @Throws(BackendException::class) + private fun exists(file: CryptoFile): Boolean { + return cloudContentRepository.exists(file.cloudFile) + } + + @Throws(BackendException::class) + private fun exists(symlink: CryptoSymlink): Boolean { + return cloudContentRepository.exists(symlink.cloudFile) + } + + @Throws(BackendException::class) + fun assertCryptoFolderAlreadyExists(cryptoFolder: CryptoFolder) { + requireNotNull(cryptoFolder.dirFile) + requireNotNull(cryptoFolder.parent) + cryptoFolder.parent?.let { cryptosParent -> + if (cloudContentRepository.exists(cryptoFolder.dirFile) + || cloudContentRepository.exists(file(cryptosParent, cryptoFolder.name)) + ) { + throw CloudNodeAlreadyExistsException(cryptoFolder.name) + } + } ?: throw ParentFolderIsNullException(cryptoFolder.name) + } + + @Throws(BackendException::class) + fun assertCryptoFileAlreadyExists(cryptoFile: CryptoFile) { + val dirFile = folder(cryptoFile.parent, cryptoFile.name).dirFile + requireNotNull(dirFile) + if (cloudContentRepository.exists(cryptoFile.cloudFile) // + || cloudContentRepository.exists(dirFile) + ) { + throw CloudNodeAlreadyExistsException("CloudNode already exists and replace is false") + } + } + + @Throws(BackendException::class, IOException::class) + private fun writeFromTmpFile(originalDataSource: DataSource, cryptoFile: CryptoFile, encryptedFile: File, progressAware: ProgressAware, replace: Boolean): CryptoFile { + val targetFile = targetFile(cryptoFile, replace) + return file( + targetFile, // + cloudContentRepository.write( // + targetFile.cloudFile, // + originalDataSource.decorate(from(encryptedFile)), // + UploadFileReplacingProgressAware(cryptoFile, progressAware), // + replace, // + encryptedFile.length() + ), // + cryptoFile.size + ) + } + + @Throws(BackendException::class) + private fun targetFile(cryptoFile: CryptoFile, replace: Boolean): CryptoFile { + return if (replace || !cloudContentRepository.exists(cryptoFile)) { + cryptoFile + } else firstNonExistingAutoRenamedFile(cryptoFile) + } + + @Throws(BackendException::class) + private fun firstNonExistingAutoRenamedFile(original: CryptoFile): CryptoFile { + val name = original.name + val nameWithoutExtension = nameWithoutExtension(name) + val extension = extension(name) + var counter = 1 + var result: CryptoFile + do { + val newFileName = "$nameWithoutExtension ($counter)$extension" + result = file(original.parent, newFileName, original.size) + counter++ + } while (cloudContentRepository.exists(result)) + return result + } + + fun nameWithoutExtension(name: String): String { + val lastDot = name.lastIndexOf(".") + return if (lastDot == -1) { + name + } else name.substring(0, lastDot) + } + + fun extension(name: String): String { + val lastDot = name.lastIndexOf(".") + return if (lastDot == -1) { + "" + } else name.substring(lastDot + 1) + } + + @Throws(BackendException::class) + fun read(cryptoFile: CryptoFile, data: OutputStream, progressAware: ProgressAware) { + val ciphertextFile = cryptoFile.cloudFile + try { + val encryptedTmpFile = readToTmpFile(cryptoFile, ciphertextFile, progressAware) + progressAware.onProgress(Progress.started(DownloadState.decryption(cryptoFile))) + try { + Channels.newChannel(FileInputStream(encryptedTmpFile)).use { readableByteChannel -> + DecryptingReadableByteChannel(readableByteChannel, cryptor(), true).use { decryptingReadableByteChannel -> + val buff = ByteBuffer.allocate(cryptor().fileContentCryptor().ciphertextChunkSize()) + val cleartextSize = cryptoFile.size ?: Long.MAX_VALUE + var decrypted: Long = 0 + var read: Int + while (decryptingReadableByteChannel.read(buff).also { read = it } > 0) { + buff.flip() + data.write(buff.array(), 0, buff.remaining()) + decrypted += read.toLong() + progressAware + .onProgress( + Progress.progress(DownloadState.decryption(cryptoFile)) // + .between(0) // + .and(cleartextSize) // + .withValue(decrypted) + ) + } + } + } + } finally { + encryptedTmpFile.delete() + progressAware.onProgress(Progress.completed(DownloadState.decryption(cryptoFile))) + } + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class, IOException::class) + private fun readToTmpFile(cryptoFile: CryptoFile, file: CloudFile, progressAware: ProgressAware): File { + val encryptedTmpFile = File.createTempFile(UUID.randomUUID().toString(), ".crypto", internalCache) + FileOutputStream(encryptedTmpFile).use { encryptedData -> + cloudContentRepository.read(file, encryptedTmpFile, encryptedData, DownloadFileReplacingProgressAware(cryptoFile, progressAware)) + return encryptedTmpFile + } + } + + @Throws(BackendException::class) + fun currentAccount(cloud: Cloud): String { + return cloudContentRepository.checkAuthenticationAndRetrieveCurrentAccount(cloud) + } + + @Throws(BackendException::class) + fun dirIdInfo(folder: CryptoFolder): DirIdInfo { + return dirIdCache[folder] ?: return createDirIdInfo(folder) + } + + @Throws(BackendException::class) + fun createDirIdInfoFor(dirId: String): DirIdInfo { + val dirHash = dirHash(dirId) + val lvl2Dir = lvl2Dir(dirHash) + return DirIdInfo(dirId, lvl2Dir) + } + + @Throws(BackendException::class, EmptyDirFileException::class) + fun loadContentsOfDirFile(folder: CryptoFolder): ByteArray { + folder.dirFile?.let { + try { + ByteArrayOutputStream().use { out -> + cloudContentRepository.read(it, null, out, ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD) + if (dirfileIsEmpty(out)) { + throw EmptyDirFileException(folder.name, folder.dirFile.path) + } + return out.toByteArray() + } + } catch (e: IOException) { + throw FatalBackendException(e) + } + } ?: throw FatalBackendException("Dir file is null") + } + + fun newDirId(): String { + return UUID.randomUUID().toString() + } + + fun dirfileIsEmpty(out: ByteArrayOutputStream): Boolean { + return out.size() == 0 + } + + @Throws(BackendException::class) + private fun lvl2Dir(dirHash: String): CloudFolder { + return cloudContentRepository.folder(lvl1Dir(dirHash), dirHash.substring(2)) + } + + @Throws(BackendException::class) + private fun lvl1Dir(dirHash: String): CloudFolder { + return cloudContentRepository.folder(dataFolder(), dirHash.substring(0, 2)) + } + + fun cryptor(): Cryptor { + return cryptor.get() + } + + fun storageLocation(): CloudFolder { + return storageLocation + } + + fun addFolderToCache(result: CryptoFolder, dirInfo: DirIdInfo) { + dirIdCache.put(result, dirInfo) + } + + fun evictFromCache(cryptoFolder: CryptoFolder) { + dirIdCache.evict(cryptoFolder) + } + + @Throws(BackendException::class) + fun writeShortNameFile(cryptoFile: CryptoFile, data: DataSource, progressAware: ProgressAware, replace: Boolean, length: Long): CryptoFile { + if (!replace) { + assertCryptoFileAlreadyExists(cryptoFile) + } + try { + data.open(context).use { stream -> + requireNotNull(stream) + requireNotNull(cryptoFile.size) + val encryptedTmpFile = File.createTempFile(UUID.randomUUID().toString(), ".crypto", internalCache) + try { + Channels.newChannel(FileOutputStream(encryptedTmpFile)).use { writableByteChannel -> + EncryptingWritableByteChannel(writableByteChannel, cryptor()).use { encryptingWritableByteChannel -> + progressAware.onProgress(Progress.started(UploadState.encryption(cryptoFile))) + val buff = ByteBuffer.allocate(cryptor().fileContentCryptor().cleartextChunkSize()) + val ciphertextSize = cryptor().fileContentCryptor().ciphertextSize(cryptoFile.size) + cryptor().fileHeaderCryptor().headerSize() + var read: Int + var encrypted: Long = 0 + while (stream.read(buff.array()).also { read = it } > 0) { + buff.limit(read) + val written = encryptingWritableByteChannel.write(buff) + buff.flip() + encrypted += written.toLong() + progressAware.onProgress(Progress.progress(UploadState.encryption(cryptoFile)).between(0).and(ciphertextSize).withValue(encrypted)) + } + encryptingWritableByteChannel.close() + progressAware.onProgress(Progress.completed(UploadState.encryption(cryptoFile))) + return writeFromTmpFile(data, cryptoFile, encryptedTmpFile, progressAware, replace) + } + } + } catch (e: Throwable) { + throw e + } finally { + encryptedTmpFile.delete() + } + } + } catch (e: IOException) { + throw FatalBackendException(e) + } + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.java b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.java deleted file mode 100644 index 085a1ba9..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.java +++ /dev/null @@ -1,549 +0,0 @@ -package org.cryptomator.data.cloud.crypto; - -import android.content.Context; - -import com.google.common.io.BaseEncoding; - -import org.cryptomator.cryptolib.Cryptors; -import org.cryptomator.cryptolib.EncryptingWritableByteChannel; -import org.cryptomator.cryptolib.api.AuthenticationFailedException; -import org.cryptomator.cryptolib.api.Cryptor; -import org.cryptomator.cryptolib.common.MessageDigestSupplier; -import org.cryptomator.domain.CloudFile; -import org.cryptomator.domain.CloudFolder; -import org.cryptomator.domain.CloudNode; -import org.cryptomator.domain.exception.BackendException; -import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException; -import org.cryptomator.domain.exception.EmptyDirFileException; -import org.cryptomator.domain.exception.FatalBackendException; -import org.cryptomator.domain.exception.NoDirFileException; -import org.cryptomator.domain.exception.NoSuchCloudFileException; -import org.cryptomator.domain.exception.SymLinkException; -import org.cryptomator.domain.repository.CloudContentRepository; -import org.cryptomator.domain.usecases.ProgressAware; -import org.cryptomator.domain.usecases.UploadFileReplacingProgressAware; -import org.cryptomator.domain.usecases.cloud.ByteArrayDataSource; -import org.cryptomator.domain.usecases.cloud.DataSource; -import org.cryptomator.domain.usecases.cloud.FileBasedDataSource; -import org.cryptomator.domain.usecases.cloud.Progress; -import org.cryptomator.domain.usecases.cloud.UploadState; -import org.cryptomator.util.Optional; -import org.cryptomator.util.Supplier; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; -import java.nio.channels.Channels; -import java.nio.channels.WritableByteChannel; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.UUID; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import timber.log.Timber; - -import static org.cryptomator.domain.usecases.ProgressAware.NO_OP_PROGRESS_AWARE; -import static org.cryptomator.domain.usecases.cloud.Progress.progress; -import static org.cryptomator.util.Encodings.UTF_8; - -class CryptoImplVaultFormat7 extends CryptoImplDecorator { - - private static final String CLOUD_NODE_EXT = ".c9r"; - private static final String LONG_NODE_FILE_EXT = ".c9s"; - private static final String CLOUD_FOLDER_DIR_FILE_PRE = "dir"; - private static final String LONG_NODE_FILE_CONTENT_CONTENTS = "contents"; - private static final String LONG_NODE_FILE_CONTENT_NAME = "name"; - private static final String CLOUD_NODE_SYMLINK_PRE = "symlink"; - private static final Pattern BASE64_ENCRYPTED_NAME_PATTERN = Pattern.compile("^([A-Za-z0-9+/\\-_]{4})*([A-Za-z0-9+/\\-]{4}|[A-Za-z0-9+/\\-_]{3}=|[A-Za-z0-9+/\\-_]{2}==)?$"); - - private static final BaseEncoding BASE64 = BaseEncoding.base64Url(); - - CryptoImplVaultFormat7(Context context, Supplier cryptor, CloudContentRepository cloudContentRepository, CloudFolder storageLocation, DirIdCache dirIdCache) { - super(context, cryptor, cloudContentRepository, storageLocation, dirIdCache, CryptoConstants.DEFAULT_MAX_FILE_NAME); - } - - CryptoImplVaultFormat7(Context context, Supplier cryptor, CloudContentRepository cloudContentRepository, CloudFolder storageLocation, DirIdCache dirIdCache, int shorteningThreshold) { - super(context, cryptor, cloudContentRepository, storageLocation, dirIdCache, shorteningThreshold); - } - - @Override - CryptoFolder folder(CryptoFolder cryptoParent, String cleartextName) throws BackendException { - String dirFileName = encryptFolderName(cryptoParent, cleartextName); - CloudFolder dirFolder = cloudContentRepository.folder(dirIdInfo(cryptoParent).getCloudFolder(), dirFileName); - CloudFile dirFile = cloudContentRepository.file(dirFolder, CLOUD_FOLDER_DIR_FILE_PRE + CLOUD_NODE_EXT); - return folder(cryptoParent, cleartextName, dirFile); - } - - @Override - String encryptName(CryptoFolder cryptoFolder, String name) throws BackendException { - String ciphertextName = cryptor() // - .fileNameCryptor() // - .encryptFilename(BASE64, name, dirIdInfo(cryptoFolder).getId().getBytes(UTF_8)) + CLOUD_NODE_EXT; - - if (ciphertextName.length() > shorteningThreshold) { - ciphertextName = deflate(cryptoFolder, ciphertextName); - } - return ciphertextName; - } - - private String deflate(CryptoFolder cryptoParent, String longFileName) throws BackendException { - byte[] longFilenameBytes = longFileName.getBytes(UTF_8); - byte[] hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes); - String shortFileName = BASE64.encode(hash) + LONG_NODE_FILE_EXT; - - CloudFolder dirFolder = cloudContentRepository.folder(dirIdInfo(cryptoParent).getCloudFolder(), shortFileName); - - // if folder already exists in case of renaming - if (!cloudContentRepository.exists(dirFolder)) { - dirFolder = cloudContentRepository.create(dirFolder); - } - - byte[] data = longFileName.getBytes(UTF_8); - CloudFile cloudFile = cloudContentRepository.file(dirFolder, LONG_NODE_FILE_CONTENT_NAME + LONG_NODE_FILE_EXT, Optional.of((long) data.length)); - cloudContentRepository.write(cloudFile, ByteArrayDataSource.from(data), NO_OP_PROGRESS_AWARE, true, data.length); - return shortFileName; - } - - private CloudFile metadataFile(CloudNode cloudNode) throws BackendException { - CloudFolder cloudFolder; - - if (cloudNode instanceof CloudFile) { - cloudFolder = cloudNode.getParent(); - } else if (cloudNode instanceof CloudFolder) { - cloudFolder = (CloudFolder) cloudNode; - } else { - throw new IllegalStateException("Should be file or folder"); - } - - return cloudContentRepository.file(cloudFolder, LONG_NODE_FILE_CONTENT_NAME + LONG_NODE_FILE_EXT); - } - - private String inflate(CloudNode cloudNode) throws BackendException { - CloudFile metadataFile = metadataFile(cloudNode); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - cloudContentRepository.read(metadataFile, Optional.empty(), out, NO_OP_PROGRESS_AWARE); - return new String(out.toByteArray(), UTF_8); - } - - @Override - String decryptName(String dirId, String encryptedName) { - Optional ciphertextName = extractEncryptedName(encryptedName); - if (ciphertextName.isPresent()) { - return cryptor().fileNameCryptor().decryptFilename(BASE64, ciphertextName.get(), dirId.getBytes(UTF_8)); - } else { - return null; - } - } - - @Override - List list(CryptoFolder cryptoFolder) throws BackendException { - dirIdCache.evictSubFoldersOf(cryptoFolder); - - DirIdCache.DirIdInfo dirIdInfo = dirIdInfo(cryptoFolder); - String dirId = dirIdInfo(cryptoFolder).getId(); - CloudFolder lvl2Dir = dirIdInfo.getCloudFolder(); - - List ciphertextNodes; - - try { - ciphertextNodes = cloudContentRepository.list(lvl2Dir); - } catch (NoSuchCloudFileException e) { - if (cryptoFolder instanceof RootCryptoFolder) { - Timber.tag("CryptoFs").e("No lvl2Dir exists for root folder in %s", lvl2Dir.getPath()); - throw new FatalBackendException(String.format("No lvl2Dir exists for root folder in %s", lvl2Dir.getPath()), e); - } else if (cloudContentRepository.exists(cloudContentRepository.file(cryptoFolder.getDirFile().getParent(), CLOUD_NODE_SYMLINK_PRE + CLOUD_NODE_EXT))) { - throw new SymLinkException(); - } else if (!cloudContentRepository.exists(cryptoFolder.getDirFile())) { - Timber.tag("CryptoFs").e("No dir file exists in %s", cryptoFolder.getDirFile().getPath()); - throw new NoDirFileException(cryptoFolder.getName(), cryptoFolder.getDirFile().getPath()); - } - return Collections.emptyList(); - } - - List result = new ArrayList<>(); - for (CloudNode node : ciphertextNodes) { - ciphertextToCleartextNode(cryptoFolder, dirId, node).ifPresent(result::add); - } - - return result; - } - - private Optional ciphertextToCleartextNode(CryptoFolder cryptoFolder, String dirId, CloudNode cloudNode) throws BackendException { - String ciphertextName = cloudNode.getName(); - Optional longNameFolderDirFile = Optional.empty(); - Optional longNameFile = Optional.empty(); - - if (ciphertextName.endsWith(CLOUD_NODE_EXT)) { - ciphertextName = nameWithoutExtension(ciphertextName); - } else if (ciphertextName.endsWith(LONG_NODE_FILE_EXT)) { - Optional ciphertextNameOption = longNodeCiphertextName(cloudNode); - if (ciphertextNameOption.isPresent()) { - ciphertextName = ciphertextNameOption.get(); - } else { - return Optional.empty(); - } - - List subfiles = cloudContentRepository.list((CloudFolder) cloudNode); - - for (CloudNode cloudNode1 : subfiles) { - switch (cloudNode1.getName()) { - case LONG_NODE_FILE_CONTENT_CONTENTS + CLOUD_NODE_EXT: - longNameFile = Optional.of((CloudFile) cloudNode1); - break; - case CLOUD_FOLDER_DIR_FILE_PRE + CLOUD_NODE_EXT: - longNameFolderDirFile = Optional.of((CloudFile) cloudNode1); - break; - case CLOUD_NODE_SYMLINK_PRE + CLOUD_NODE_EXT: - return Optional.empty(); - } - } - } - - try { - String cleartextName = decryptName(dirId, ciphertextName); - - if (cleartextName == null) { - Timber.tag("CryptoFs").w("Failed to parse cipher text name of: %s", cloudNode.getPath()); - return Optional.empty(); - } - - return cloudNodeFromName(cloudNode, cryptoFolder, cleartextName, longNameFile, longNameFolderDirFile); - } catch (AuthenticationFailedException e) { - Timber.tag("CryptoFs").w(e, "File/Folder name authentication failed: %s", cloudNode.getPath()); - return Optional.empty(); - } catch (IllegalArgumentException e) { - Timber.tag("CryptoFs").w(e, "Illegal ciphertext filename/folder: %s", cloudNode.getPath()); - return Optional.empty(); - } - } - - private Optional cloudNodeFromName(CloudNode cloudNode, CryptoFolder cryptoFolder, String cleartextName, Optional longNameFile, Optional dirFile) throws BackendException { - if (cloudNode instanceof CloudFile) { - CloudFile cloudFile = (CloudFile) cloudNode; - Optional cleartextSize = Optional.empty(); - if (cloudFile.getSize().isPresent()) { - long ciphertextSizeWithoutHeader = cloudFile.getSize().get() - cryptor().fileHeaderCryptor().headerSize(); - if (ciphertextSizeWithoutHeader >= 0) { - cleartextSize = Optional.of(Cryptors.cleartextSize(ciphertextSizeWithoutHeader, cryptor())); - } - } - return Optional.of(file(cryptoFolder, cleartextName, cloudFile, cleartextSize)); - } else if (cloudNode instanceof CloudFolder) { - if (longNameFile.isPresent()) { - // long file - Optional cleartextSize = Optional.empty(); - if (longNameFile.get().getSize().isPresent()) { - long ciphertextSizeWithoutHeader = longNameFile.get().getSize().get() - cryptor().fileHeaderCryptor().headerSize(); - if (ciphertextSizeWithoutHeader >= 0) { - cleartextSize = Optional.of(Cryptors.cleartextSize(ciphertextSizeWithoutHeader, cryptor())); - } - } - - return Optional.of(file(cryptoFolder, cleartextName, longNameFile.get(), cleartextSize)); - } else { - // folder - if (dirFile.isPresent()) { - return Optional.of(folder(cryptoFolder, cleartextName, dirFile.get())); - } else { - CloudFile constructedDirFile = cloudContentRepository.file((CloudFolder) cloudNode, "dir" + CLOUD_NODE_EXT); - return Optional.of(folder(cryptoFolder, cleartextName, constructedDirFile)); - } - } - } - - return Optional.empty(); - } - - private Optional longNodeCiphertextName(CloudNode cloudNode) { - try { - String ciphertextName = inflate(cloudNode); - ciphertextName = nameWithoutExtension(ciphertextName); - return Optional.of(ciphertextName); - } catch (NoSuchCloudFileException e) { - Timber.tag("CryptoFs").e("Missing %s%s for cloud node: %s", LONG_NODE_FILE_CONTENT_NAME, LONG_NODE_FILE_EXT, cloudNode.getPath()); - return Optional.empty(); - } catch (BackendException e) { - Timber.tag("CryptoFs").e(e, "Failed to read %s%s for cloud node: %s", LONG_NODE_FILE_CONTENT_NAME, LONG_NODE_FILE_EXT, cloudNode.getPath()); - return Optional.empty(); - } - } - - @Override - DirIdCache.DirIdInfo createDirIdInfo(CryptoFolder folder) throws BackendException { - String dirId = loadDirId(folder); - return dirIdCache.put(folder, createDirIdInfoFor(dirId)); - } - - @Override - String encryptFolderName(CryptoFolder cryptoFolder, String name) throws BackendException { - return encryptName(cryptoFolder, name); - } - - @Override - CryptoSymlink symlink(CryptoFolder cryptoParent, String cleartextName, String target) throws BackendException { - return null; - } - - @Override - String loadDirId(CryptoFolder folder) throws BackendException, EmptyDirFileException { - CloudFile dirFile = null; - - if (folder.getDirFile() != null) { - dirFile = folder.getDirFile(); - } - - if (RootCryptoFolder.isRoot(folder)) { - return CryptoConstants.ROOT_DIR_ID; - } else if (dirFile != null && cloudContentRepository.exists(dirFile)) { - return new String(loadContentsOfDirFile(dirFile), UTF_8); - } else { - return newDirId(); - } - } - - private byte[] loadContentsOfDirFile(CloudFile file) throws BackendException, EmptyDirFileException { - try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { - cloudContentRepository.read(file, Optional.empty(), out, NO_OP_PROGRESS_AWARE); - if (dirfileIsEmpty(out)) { - throw new EmptyDirFileException(file.getName(), file.getPath()); - } - return out.toByteArray(); - } catch (IOException e) { - throw new FatalBackendException(e); - } - } - - @Override - CryptoFolder create(CryptoFolder folder) throws BackendException { - boolean shortName = false; - if (folder.getDirFile().getParent().getName().endsWith(LONG_NODE_FILE_EXT)) { - assertCryptoLongDirFileAlreadyExists(folder); - } else { - assertCryptoFolderAlreadyExists(folder); - shortName = true; - } - - DirIdCache.DirIdInfo dirIdInfo = dirIdInfo(folder); - CloudFolder createdCloudFolder = cloudContentRepository.create(dirIdInfo.getCloudFolder()); - - CloudFolder dirFolder = folder.getDirFile().getParent(); - CloudFile dirFile = folder.getDirFile(); - if (shortName) { - dirFolder = cloudContentRepository.create(dirFolder); - dirFile = cloudContentRepository.file(dirFolder, folder.getDirFile().getName()); - } - - byte[] dirId = dirIdInfo.getId().getBytes(UTF_8); - CloudFile createdDirFile = cloudContentRepository.write(dirFile, ByteArrayDataSource.from(dirId), NO_OP_PROGRESS_AWARE, false, dirId.length); - CryptoFolder result = folder(folder, createdDirFile); - addFolderToCache(result, dirIdInfo.withCloudFolder(createdCloudFolder)); - return result; - } - - @Override - Optional extractEncryptedName(String ciphertextName) { - final Matcher matcher = BASE64_ENCRYPTED_NAME_PATTERN.matcher(ciphertextName); - if (matcher.find(0)) { - return Optional.of(matcher.group()); - } else { - return Optional.empty(); - } - } - - @Override - CryptoFolder move(CryptoFolder source, CryptoFolder target) throws BackendException { - boolean shortName = false; - if (target.getDirFile().getParent().getName().endsWith(LONG_NODE_FILE_EXT)) { - assertCryptoLongDirFileAlreadyExists(target); - } else { - assertCryptoFolderAlreadyExists(target); - shortName = true; - } - - CloudFile targetDirFile = target.getDirFile(); - if (shortName) { - CloudFolder targetDirFolder = cloudContentRepository.create(target.getDirFile().getParent()); - targetDirFile = cloudContentRepository.file(targetDirFolder, target.getDirFile().getName()); - } - - CryptoFolder result = folder(target.getParent(), target.getName(), cloudContentRepository.move(source.getDirFile(), targetDirFile)); - - cloudContentRepository.delete(source.getDirFile().getParent()); - - evictFromCache(source); - evictFromCache(target); - - return result; - } - - @Override - CryptoFile move(CryptoFile source, CryptoFile target) throws BackendException { - if (source.getCloudFile().getParent().getName().endsWith(LONG_NODE_FILE_EXT)) { - CloudFolder targetDirFolder = cloudContentRepository.folder(target.getCloudFile().getParent(), target.getCloudFile().getName()); - CryptoFile cryptoFile; - if (target.getCloudFile().getName().endsWith(LONG_NODE_FILE_EXT)) { - assertCryptoLongDirFileAlreadyExists(targetDirFolder); - cryptoFile = moveLongFileToLongFile(source, target, targetDirFolder); - } else { - assertCryptoFileAlreadyExists(target); - cryptoFile = moveLongFileToShortFile(source, target); - } - CloudFolder sourceDirFolder = cloudContentRepository.folder(source.getCloudFile().getParent().getParent(), source.getCloudFile().getParent().getName()); - cloudContentRepository.delete(sourceDirFolder); - return cryptoFile; - } else { - CloudFolder targetDirFolder = cloudContentRepository.folder(target.getCloudFile().getParent(), target.getCloudFile().getName()); - if (target.getCloudFile().getName().endsWith(LONG_NODE_FILE_EXT)) { - assertCryptoLongDirFileAlreadyExists(targetDirFolder); - return moveShortFileToLongFile(source, target, targetDirFolder); - } else { - assertCryptoFileAlreadyExists(target); - return file(target, cloudContentRepository.move(source.getCloudFile(), target.getCloudFile()), source.getSize()); - } - } - } - - private CryptoFile moveLongFileToLongFile(CryptoFile source, CryptoFile target, CloudFolder targetDirFolder) throws BackendException { - CloudFile sourceFile = cloudContentRepository.file(source.getCloudFile().getParent(), LONG_NODE_FILE_CONTENT_CONTENTS + CLOUD_NODE_EXT); - CloudFile movedFile = cloudContentRepository.move(sourceFile, cloudContentRepository.file(targetDirFolder, LONG_NODE_FILE_CONTENT_CONTENTS + CLOUD_NODE_EXT)); - return file(target, movedFile, movedFile.getSize()); - } - - private CryptoFile moveLongFileToShortFile(CryptoFile source, CryptoFile target) throws BackendException { - CloudFile sourceFile = cloudContentRepository.file(source.getCloudFile().getParent(), LONG_NODE_FILE_CONTENT_CONTENTS + CLOUD_NODE_EXT); - CloudFile movedFile = cloudContentRepository.move(sourceFile, target.getCloudFile()); - return file(target, movedFile, movedFile.getSize()); - } - - private CryptoFile moveShortFileToLongFile(CryptoFile source, CryptoFile target, CloudFolder targetDirFolder) throws BackendException { - CloudFile movedFile = cloudContentRepository.move(source.getCloudFile(), cloudContentRepository.file(targetDirFolder, LONG_NODE_FILE_CONTENT_CONTENTS + CLOUD_NODE_EXT)); - return file(target, movedFile, movedFile.getSize()); - } - - @Override - void delete(CloudNode node) throws BackendException { - if (node instanceof CryptoFolder) { - CryptoFolder cryptoFolder = (CryptoFolder) node; - List cryptoSubfolders = deepCollectSubfolders(cryptoFolder); - for (CryptoFolder cryptoSubfolder : cryptoSubfolders) { - try { - cloudContentRepository.delete(dirIdInfo(cryptoSubfolder).getCloudFolder()); - } catch (NoSuchCloudFileException e) { - // Ignoring because nothing can be done if the dir-file doesn't exists in the cloud - } - } - - try { - cloudContentRepository.delete(dirIdInfo(cryptoFolder).getCloudFolder()); - } catch (NoSuchCloudFileException e) { - // Ignoring because nothing can be done if the dir-file doesn't exists in the cloud - } - - cloudContentRepository.delete(cryptoFolder.getDirFile().getParent()); - - evictFromCache(cryptoFolder); - } else if (node instanceof CryptoFile) { - CryptoFile cryptoFile = (CryptoFile) node; - if (cryptoFile.getCloudFile().getParent().getName().endsWith(LONG_NODE_FILE_EXT)) { - cloudContentRepository.delete(cryptoFile.getCloudFile().getParent()); - } else { - cloudContentRepository.delete(cryptoFile.getCloudFile()); - } - } - } - - @Override - public CryptoFile write(CryptoFile cryptoFile, DataSource data, ProgressAware progressAware, boolean replace, long length) throws BackendException { - if (cryptoFile.getCloudFile().getName().endsWith(LONG_NODE_FILE_EXT)) { - return writeLongFile(cryptoFile, data, progressAware, replace, length); - } else { - return writeShortNameFile(cryptoFile, data, progressAware, replace, length); - } - } - - private CryptoFile writeLongFile(CryptoFile cryptoFile, DataSource data, ProgressAware progressAware, boolean replace, long length) throws BackendException { - CloudFolder dirFolder = cloudContentRepository.folder(cryptoFile.getCloudFile().getParent(), cryptoFile.getCloudFile().getName()); - CloudFile cloudFile = cloudContentRepository.file(dirFolder, LONG_NODE_FILE_CONTENT_CONTENTS + CLOUD_NODE_EXT, data.size(context)); - - assertCryptoLongDirFileAlreadyExists(dirFolder); - - try (InputStream stream = data.open(context)) { - File encryptedTmpFile = File.createTempFile(UUID.randomUUID().toString(), ".crypto", getInternalCache()); - try (WritableByteChannel writableByteChannel = Channels.newChannel(new FileOutputStream(encryptedTmpFile)); // - WritableByteChannel encryptingWritableByteChannel = new EncryptingWritableByteChannel(writableByteChannel, cryptor())) { - progressAware.onProgress(Progress.started(UploadState.encryption(cloudFile))); - ByteBuffer buff = ByteBuffer.allocate(cryptor().fileContentCryptor().cleartextChunkSize()); - long ciphertextSize = Cryptors.ciphertextSize(cloudFile.getSize().get(), cryptor()) + cryptor().fileHeaderCryptor().headerSize(); - int read; - long encrypted = 0; - while ((read = stream.read(buff.array())) > 0) { - buff.limit(read); - int written = encryptingWritableByteChannel.write(buff); - buff.flip(); - encrypted += written; - progressAware.onProgress(progress(UploadState.encryption(cloudFile)).between(0).and(ciphertextSize).withValue(encrypted)); - } - encryptingWritableByteChannel.close(); - progressAware.onProgress(Progress.completed(UploadState.encryption(cloudFile))); - - CloudFile targetFile = targetFile(cryptoFile, cloudFile, replace); - - return file(cryptoFile, // - cloudContentRepository.write( // - targetFile, // - data.decorate(FileBasedDataSource.from(encryptedTmpFile)), // - new UploadFileReplacingProgressAware(cryptoFile, progressAware), // - replace, // - encryptedTmpFile.length()), // - cryptoFile.getSize()); - } catch (Throwable e) { - throw e; - } finally { - encryptedTmpFile.delete(); - } - } catch (IOException e) { - throw new FatalBackendException(e); - } - } - - private CloudFile targetFile(CryptoFile cryptoFile, CloudFile cloudFile, boolean replace) throws BackendException { - if (replace || !cloudContentRepository.exists(cloudFile)) { - return cloudFile; - } - return firstNonExistingAutoRenamedFile(cryptoFile); - } - - private CloudFile firstNonExistingAutoRenamedFile(CryptoFile original) throws BackendException { - String name = original.getName(); - String nameWithoutExtension = nameWithoutExtension(name); - String extension = extension(name); - - if (!extension.isEmpty()) { - extension = "." + extension; - } - - int counter = 1; - CryptoFile result; - CloudFile cloudFile; - do { - String newFileName = nameWithoutExtension + " (" + counter + ")" + extension; - result = file(original.getParent(), newFileName, original.getSize()); - counter++; - - CloudFolder dirFolder = cloudContentRepository.folder(result.getCloudFile().getParent(), result.getCloudFile().getName()); - cloudFile = cloudContentRepository.file(dirFolder, LONG_NODE_FILE_CONTENT_CONTENTS + CLOUD_NODE_EXT, result.getSize()); - } while (cloudContentRepository.exists(cloudFile)); - return cloudFile; - } - - private void assertCryptoLongDirFileAlreadyExists(CloudFolder cryptoFolder) throws BackendException { - if (cloudContentRepository.exists(cloudContentRepository.file(cryptoFolder, CLOUD_FOLDER_DIR_FILE_PRE + CLOUD_NODE_EXT))) { - throw new CloudNodeAlreadyExistsException("CloudNode already exists and replace is false"); - } - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt new file mode 100644 index 00000000..87698d10 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt @@ -0,0 +1,540 @@ +package org.cryptomator.data.cloud.crypto + +import android.content.Context +import com.google.common.io.BaseEncoding +import org.cryptomator.cryptolib.api.AuthenticationFailedException +import org.cryptomator.cryptolib.api.Cryptor +import org.cryptomator.cryptolib.common.EncryptingWritableByteChannel +import org.cryptomator.cryptolib.common.MessageDigestSupplier +import org.cryptomator.data.cloud.crypto.DirIdCache.DirIdInfo +import org.cryptomator.domain.Cloud +import org.cryptomator.domain.CloudFile +import org.cryptomator.domain.CloudFolder +import org.cryptomator.domain.CloudNode +import org.cryptomator.domain.exception.BackendException +import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException +import org.cryptomator.domain.exception.EmptyDirFileException +import org.cryptomator.domain.exception.FatalBackendException +import org.cryptomator.domain.exception.NoDirFileException +import org.cryptomator.domain.exception.NoSuchCloudFileException +import org.cryptomator.domain.exception.ParentFolderIsNullException +import org.cryptomator.domain.exception.SymLinkException +import org.cryptomator.domain.repository.CloudContentRepository +import org.cryptomator.domain.usecases.ProgressAware +import org.cryptomator.domain.usecases.UploadFileReplacingProgressAware +import org.cryptomator.domain.usecases.cloud.ByteArrayDataSource.Companion.from +import org.cryptomator.domain.usecases.cloud.DataSource +import org.cryptomator.domain.usecases.cloud.FileBasedDataSource.Companion.from +import org.cryptomator.domain.usecases.cloud.Progress +import org.cryptomator.domain.usecases.cloud.UploadState +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.nio.ByteBuffer +import java.nio.channels.Channels +import java.nio.charset.StandardCharsets +import java.util.UUID +import java.util.function.Supplier +import java.util.regex.Pattern +import kotlin.streams.toList +import timber.log.Timber + +open class CryptoImplVaultFormat7 : CryptoImplDecorator { + constructor( + context: Context, + cryptor: Supplier, + cloudContentRepository: CloudContentRepository, + storageLocation: CloudFolder, + dirIdCache: DirIdCache + ) : super( + context, cryptor, cloudContentRepository, storageLocation, dirIdCache, CryptoConstants.DEFAULT_MAX_FILE_NAME + ) + + constructor( + context: Context, + cryptor: Supplier, + cloudContentRepository: CloudContentRepository, + storageLocation: CloudFolder, + dirIdCache: DirIdCache, + shorteningThreshold: Int + ) : super( + context, cryptor, cloudContentRepository, storageLocation, dirIdCache, shorteningThreshold + ) + + @Throws(BackendException::class) + override fun folder(cryptoParent: CryptoFolder, cleartextName: String): CryptoFolder { + val dirFileName = encryptFolderName(cryptoParent, cleartextName) + val dirFolder = cloudContentRepository.folder(dirIdInfo(cryptoParent).cloudFolder, dirFileName) + val dirFile = cloudContentRepository.file(dirFolder, CLOUD_FOLDER_DIR_FILE_PRE + CLOUD_NODE_EXT) + return folder(cryptoParent, cleartextName, dirFile) + } + + @Throws(BackendException::class) + override fun encryptName(cryptoParent: CryptoFolder, name: String): String { + var ciphertextName: String = cryptor() // + .fileNameCryptor() // + .encryptFilename(BaseEncoding.base64Url(), name, dirIdInfo(cryptoParent).id.toByteArray(StandardCharsets.UTF_8)) + CLOUD_NODE_EXT + if (ciphertextName.length > shorteningThreshold) { + ciphertextName = deflate(cryptoParent, ciphertextName) + } + return ciphertextName + } + + @Throws(BackendException::class) + private fun deflate(cryptoParent: CryptoFolder, longFileName: String): String { + val longFilenameBytes = longFileName.toByteArray(StandardCharsets.UTF_8) + val hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes) + val shortFileName = BaseEncoding.base64Url().encode(hash) + LONG_NODE_FILE_EXT + var dirFolder = cloudContentRepository.folder(dirIdInfo(cryptoParent).cloudFolder, shortFileName) + + // if folder already exists in case of renaming + if (!cloudContentRepository.exists(dirFolder)) { + dirFolder = cloudContentRepository.create(dirFolder) + } + + val data = longFileName.toByteArray(StandardCharsets.UTF_8) + val cloudFile = cloudContentRepository.file(dirFolder, LONG_NODE_FILE_CONTENT_NAME + LONG_NODE_FILE_EXT, data.size.toLong()) + cloudContentRepository.write(cloudFile, from(data), ProgressAware.NO_OP_PROGRESS_AWARE_UPLOAD, true, data.size.toLong()) + return shortFileName + } + + @Throws(BackendException::class) + private fun metadataFile(cloudNode: CloudNode): CloudFile { + val cloudFolder = when (cloudNode) { + is CloudFile -> { + cloudNode.parent + } + is CloudFolder -> { + cloudNode + } + else -> { + throw IllegalStateException("Should be file or folder") + } + } + return cloudContentRepository.file(cloudFolder, LONG_NODE_FILE_CONTENT_NAME + LONG_NODE_FILE_EXT) + } + + @Throws(BackendException::class) + private fun inflate(cloudNode: CloudNode): String { + val metadataFile = metadataFile(cloudNode) + val out = ByteArrayOutputStream() + cloudContentRepository.read(metadataFile, null, out, ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD) + return String(out.toByteArray(), StandardCharsets.UTF_8) + } + + override fun decryptName(dirId: String, encryptedName: String): String? { + return extractEncryptedName(encryptedName)?.let { + return cryptor().fileNameCryptor().decryptFilename(BaseEncoding.base64Url(), it, dirId.toByteArray(StandardCharsets.UTF_8)) + } + } + + @Throws(BackendException::class) + override fun list(cryptoFolder: CryptoFolder): List { + dirIdCache.evictSubFoldersOf(cryptoFolder) + + val dirIdInfo = dirIdInfo(cryptoFolder) + val dirId = dirIdInfo(cryptoFolder).id + val lvl2Dir = dirIdInfo.cloudFolder + + val ciphertextNodes: List = try { + cloudContentRepository.list(lvl2Dir) + } catch (e: NoSuchCloudFileException) { + if (cryptoFolder is RootCryptoFolder) { + Timber.tag("CryptoFs").e("No lvl2Dir exists for root folder in %s", lvl2Dir.path) + throw FatalBackendException(String.format("No lvl2Dir exists for root folder in %s", lvl2Dir.path), e) + } else if (cryptoFolder.dirFile == null) { + Timber.tag("CryptoFs").e(String.format("Dir-file of folder is null %s", lvl2Dir.path)) + throw FatalBackendException(String.format("Dir-file of folder is null %s", lvl2Dir.path)) + } else if (cloudContentRepository.exists(cloudContentRepository.file(cryptoFolder.dirFile.parent, CLOUD_NODE_SYMLINK_PRE + CLOUD_NODE_EXT))) { + throw SymLinkException() + } else if (!cloudContentRepository.exists(cryptoFolder.dirFile)) { + Timber.tag("CryptoFs").e("No dir file exists in %s", cryptoFolder.dirFile.path) + throw NoDirFileException(cryptoFolder.name, cryptoFolder.dirFile.path) + } + return emptyList() + } + + return ciphertextNodes + .parallelStream() + .map { node -> + ciphertextToCleartextNode(cryptoFolder, dirId, node) + } + .toList() + .filterNotNull() + } + + @Throws(BackendException::class) + private fun ciphertextToCleartextNode(cryptoFolder: CryptoFolder, dirId: String, cloudNode: CloudNode): CryptoNode? { + var ciphertextName = cloudNode.name + var longNameFolderDirFile: CloudFile? = null + var longNameFile: CloudFile? = null + + if (ciphertextName.endsWith(CLOUD_NODE_EXT)) { + ciphertextName = nameWithoutExtension(ciphertextName) + } else if (ciphertextName.endsWith(LONG_NODE_FILE_EXT)) { + ciphertextName = (longNodeCiphertextName(cloudNode) ?: return null) + for (node in cloudContentRepository.list((cloudNode as CloudFolder))) { + when (node.name) { + LONG_NODE_FILE_CONTENT_CONTENTS + CLOUD_NODE_EXT -> longNameFile = node as CloudFile + CLOUD_FOLDER_DIR_FILE_PRE + CLOUD_NODE_EXT -> longNameFolderDirFile = node as CloudFile + } + } + } + return try { + val cleartextName = decryptName(dirId, ciphertextName) + if (cleartextName == null) { + Timber.tag("CryptoFs").w("Failed to parse cipher text name of: %s", cloudNode.path) + return null + } + cloudNodeFromName(cloudNode, cryptoFolder, cleartextName, longNameFile, longNameFolderDirFile) + } catch (e: AuthenticationFailedException) { + Timber.tag("CryptoFs").w(e, "File/Folder name authentication failed: %s", cloudNode.path) + null + } catch (e: IllegalArgumentException) { + Timber.tag("CryptoFs").w(e, "Illegal ciphertext filename/folder: %s", cloudNode.path) + null + } + } + + @Throws(BackendException::class) + private fun cloudNodeFromName(cloudNode: CloudNode, cryptoFolder: CryptoFolder, cleartextName: String, longNameFile: CloudFile?, dirFile: CloudFile?): CryptoNode? { + if (cloudNode is CloudFile) { + val cleartextSize = cloudNode.size?.let { + val ciphertextSizeWithoutHeader = it - cryptor().fileHeaderCryptor().headerSize() + if (ciphertextSizeWithoutHeader >= 0) { + cryptor().fileContentCryptor().cleartextSize(ciphertextSizeWithoutHeader) + } else { + null + } + } + return file(cryptoFolder, cleartextName, cloudNode, cleartextSize) + } else if (cloudNode is CloudFolder) { + return if (longNameFile != null) { + // long file + val cleartextSize = longNameFile.size?.let { + val ciphertextSizeWithoutHeader: Long = it - cryptor().fileHeaderCryptor().headerSize() + if (ciphertextSizeWithoutHeader >= 0) { + cryptor().fileContentCryptor().cleartextSize(ciphertextSizeWithoutHeader) + } else { + null + } + } + + file(cryptoFolder, cleartextName, longNameFile, cleartextSize) + } else { + // folder + if (dirFile != null) { + folder(cryptoFolder, cleartextName, dirFile) + } else { + val constructedDirFile = cloudContentRepository.file(cloudNode, "dir$CLOUD_NODE_EXT") + folder(cryptoFolder, cleartextName, constructedDirFile) + } + } + } + return null + } + + private fun longNodeCiphertextName(cloudNode: CloudNode): String? { + return try { + val ciphertextName = inflate(cloudNode) + nameWithoutExtension(ciphertextName) + } catch (e: NoSuchCloudFileException) { + Timber.tag("CryptoFs").e("Missing %s%s for cloud node: %s", LONG_NODE_FILE_CONTENT_NAME, LONG_NODE_FILE_EXT, cloudNode.path) + null + } catch (e: BackendException) { + Timber.tag("CryptoFs").e(e, "Failed to read %s%s for cloud node: %s", LONG_NODE_FILE_CONTENT_NAME, LONG_NODE_FILE_EXT, cloudNode.path) + null + } + } + + @Throws(BackendException::class) + override fun createDirIdInfo(folder: CryptoFolder): DirIdInfo { + val dirId = loadDirId(folder) + return dirIdCache.put(folder, createDirIdInfoFor(dirId)) + } + + @Throws(BackendException::class) + override fun encryptFolderName(cryptoFolder: CryptoFolder, name: String): String { + return encryptName(cryptoFolder, name) + } + + @Throws(BackendException::class) + override fun symlink(cryptoParent: CryptoFolder, cleartextName: String, target: String): CryptoSymlink { + throw FatalBackendException("FOOOO") // FIXME + } + + @Throws(BackendException::class, EmptyDirFileException::class) + override fun loadDirId(folder: CryptoFolder): String { + var dirFile: CloudFile? = null + if (folder.dirFile != null) { + dirFile = folder.dirFile + } + return if (RootCryptoFolder.isRoot(folder)) { + CryptoConstants.ROOT_DIR_ID + } else if (dirFile != null && cloudContentRepository.exists(dirFile)) { + String(loadContentsOfDirFile(dirFile), StandardCharsets.UTF_8) + } else { + newDirId() + } + } + + @Throws(BackendException::class, EmptyDirFileException::class) + private fun loadContentsOfDirFile(file: CloudFile): ByteArray { + try { + ByteArrayOutputStream().use { out -> + cloudContentRepository.read(file, null, out, ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD) + if (dirfileIsEmpty(out)) { + throw EmptyDirFileException(file.name, file.path) + } + return out.toByteArray() + } + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun create(folder: CryptoFolder): CryptoFolder { + requireNotNull(folder.dirFile) + var shortName = false + if (folder.dirFile.parent.name.endsWith(LONG_NODE_FILE_EXT)) { + assertCryptoLongDirFileAlreadyExists(folder) + } else { + assertCryptoFolderAlreadyExists(folder) + shortName = true + } + val dirIdInfo = dirIdInfo(folder) + val createdCloudFolder = cloudContentRepository.create(dirIdInfo.cloudFolder) + var dirFolder = folder.dirFile.parent + var dirFile = folder.dirFile + if (shortName) { + dirFolder = cloudContentRepository.create(dirFolder) + dirFile = cloudContentRepository.file(dirFolder, folder.dirFile.name) + } + val dirId = dirIdInfo.id.toByteArray(StandardCharsets.UTF_8) + val createdDirFile = cloudContentRepository.write(dirFile, from(dirId), ProgressAware.NO_OP_PROGRESS_AWARE_UPLOAD, false, dirId.size.toLong()) + val result = folder(folder, createdDirFile) + addFolderToCache(result, dirIdInfo.withCloudFolder(createdCloudFolder)) + return result + } + + override fun extractEncryptedName(ciphertextName: String): String? { + val matcher = BASE64_ENCRYPTED_NAME_PATTERN.matcher(ciphertextName) + return if (matcher.find(0)) { + matcher.group() + } else { + null + } + } + + @Throws(BackendException::class) + override fun move(source: CryptoFolder, target: CryptoFolder): CryptoFolder { + requireNotNull(source.dirFile) + requireNotNull(target.dirFile) + target.parent?.let { targetsParent -> + var shortName = false + if (target.dirFile.parent.name.endsWith(LONG_NODE_FILE_EXT)) { + assertCryptoLongDirFileAlreadyExists(target) + } else { + assertCryptoFolderAlreadyExists(target) + shortName = true + } + var targetDirFile = target.dirFile + if (shortName) { + val targetDirFolder = cloudContentRepository.create(target.dirFile.parent) + targetDirFile = cloudContentRepository.file(targetDirFolder, target.dirFile.name) + } + val result = folder(targetsParent, target.name, cloudContentRepository.move(source.dirFile, targetDirFile)) + cloudContentRepository.delete(source.dirFile.parent) + evictFromCache(source) + evictFromCache(target) + return result + } ?: throw ParentFolderIsNullException(target.name) + } + + @Throws(BackendException::class) + override fun move(source: CryptoFile, target: CryptoFile): CryptoFile { + return if (source.cloudFile.parent.name.endsWith(LONG_NODE_FILE_EXT)) { + val targetDirFolder = cloudContentRepository.folder(target.cloudFile.parent, target.cloudFile.name) + val cryptoFile: CryptoFile = if (target.cloudFile.name.endsWith(LONG_NODE_FILE_EXT)) { + assertCryptoLongDirFileAlreadyExists(targetDirFolder) + moveLongFileToLongFile(source, target, targetDirFolder) + } else { + assertCryptoFileAlreadyExists(target) + moveLongFileToShortFile(source, target) + } + source.cloudFile.parent.parent?.let { + val sourceDirFolder = cloudContentRepository.folder(it, source.cloudFile.parent.name) + cloudContentRepository.delete(sourceDirFolder) + } + cryptoFile + } else { + if (target.cloudFile.name.endsWith(LONG_NODE_FILE_EXT)) { + val targetDirFolder = cloudContentRepository.folder(target.cloudFile.parent, target.cloudFile.name) + assertCryptoLongDirFileAlreadyExists(targetDirFolder) + moveShortFileToLongFile(source, target, targetDirFolder) + } else { + assertCryptoFileAlreadyExists(target) + file(target, cloudContentRepository.move(source.cloudFile, target.cloudFile), source.size) + } + } + } + + @Throws(BackendException::class) + private fun moveLongFileToLongFile(source: CryptoFile, target: CryptoFile, targetDirFolder: CloudFolder): CryptoFile { + requireNotNull(source.cloudFile.parent) + val sourceFile = cloudContentRepository.file(source.cloudFile.parent, LONG_NODE_FILE_CONTENT_CONTENTS + CLOUD_NODE_EXT) + val movedFile = cloudContentRepository.move(sourceFile, cloudContentRepository.file(targetDirFolder, LONG_NODE_FILE_CONTENT_CONTENTS + CLOUD_NODE_EXT)) + return file(target, movedFile, movedFile.size) + } + + @Throws(BackendException::class) + private fun moveLongFileToShortFile(source: CryptoFile, target: CryptoFile): CryptoFile { + requireNotNull(source.cloudFile.parent) + val sourceFile = cloudContentRepository.file(source.cloudFile.parent, LONG_NODE_FILE_CONTENT_CONTENTS + CLOUD_NODE_EXT) + val movedFile = cloudContentRepository.move(sourceFile, target.cloudFile) + return file(target, movedFile, movedFile.size) + } + + @Throws(BackendException::class) + private fun moveShortFileToLongFile(source: CryptoFile, target: CryptoFile, targetDirFolder: CloudFolder): CryptoFile { + val movedFile = cloudContentRepository.move(source.cloudFile, cloudContentRepository.file(targetDirFolder, LONG_NODE_FILE_CONTENT_CONTENTS + CLOUD_NODE_EXT)) + return file(target, movedFile, movedFile.size) + } + + @Throws(BackendException::class) + override fun delete(node: CloudNode) { + if (node is CryptoFolder) { + requireNotNull(node.dirFile) + val cryptoSubfolders = deepCollectSubfolders(node) + for (cryptoSubfolder in cryptoSubfolders) { + try { + cloudContentRepository.delete(dirIdInfo(cryptoSubfolder).cloudFolder) + } catch (e: NoSuchCloudFileException) { + // Ignoring because nothing can be done if the dir-file doesn't exists in the cloud + } + } + try { + cloudContentRepository.delete(dirIdInfo(node).cloudFolder) + } catch (e: NoSuchCloudFileException) { + // Ignoring because nothing can be done if the dir-file doesn't exists in the cloud + } + cloudContentRepository.delete(node.dirFile.parent) + evictFromCache(node) + } else if (node is CryptoFile) { + if (node.cloudFile.parent.name.endsWith(LONG_NODE_FILE_EXT)) { + cloudContentRepository.delete(node.cloudFile.parent) + } else { + cloudContentRepository.delete(node.cloudFile) + } + } + } + + @Throws(BackendException::class) + override fun write(cryptoFile: CryptoFile, data: DataSource, progressAware: ProgressAware, replace: Boolean, length: Long): CryptoFile { + return if (cryptoFile.cloudFile.name.endsWith(LONG_NODE_FILE_EXT)) { + writeLongFile(cryptoFile, data, progressAware, replace, length) + } else { + writeShortNameFile(cryptoFile, data, progressAware, replace, length) + } + } + + @Throws(BackendException::class) + private fun writeLongFile(cryptoFile: CryptoFile, data: DataSource, progressAware: ProgressAware, replace: Boolean, length: Long): CryptoFile { + val dirFolder = cloudContentRepository.folder(cryptoFile.cloudFile.parent, cryptoFile.cloudFile.name) + val cloudFile = cloudContentRepository.file(dirFolder, LONG_NODE_FILE_CONTENT_CONTENTS + CLOUD_NODE_EXT, data.size(context)) + assertCryptoLongDirFileAlreadyExists(dirFolder) + try { + data.open(context)?.use { stream -> + val encryptedTmpFile = File.createTempFile(UUID.randomUUID().toString(), ".crypto", internalCache) + try { + Channels.newChannel(FileOutputStream(encryptedTmpFile)).use { writableByteChannel -> + EncryptingWritableByteChannel(writableByteChannel, cryptor()).use { encryptingWritableByteChannel -> + cloudFile.size?.let { size -> + progressAware.onProgress(Progress.started(UploadState.encryption(cloudFile))) + val buff = ByteBuffer.allocate(cryptor().fileContentCryptor().cleartextChunkSize()) + val ciphertextSize = cryptor().fileContentCryptor().ciphertextSize(size) + cryptor().fileHeaderCryptor().headerSize() + var read: Int + var encrypted: Long = 0 + while (stream.read(buff.array()).also { read = it } > 0) { + buff.limit(read) + val written = encryptingWritableByteChannel.write(buff) + buff.flip() + encrypted += written.toLong() + progressAware.onProgress(Progress.progress(UploadState.encryption(cloudFile)).between(0).and(ciphertextSize).withValue(encrypted)) + } + encryptingWritableByteChannel.close() + progressAware.onProgress(Progress.completed(UploadState.encryption(cloudFile))) + val targetFile = targetFile(cryptoFile, cloudFile, replace) + return file( + cryptoFile, // + cloudContentRepository.write( // + targetFile, // + data.decorate(from(encryptedTmpFile)), + UploadFileReplacingProgressAware(cryptoFile, progressAware), // + replace, // + encryptedTmpFile.length() + ), // + cryptoFile.size + ) + } ?: throw FatalBackendException("CloudFile size shouldn't be null") + } + } + } catch (e: Throwable) { + throw e + } finally { + encryptedTmpFile.delete() + } + } ?: throw FatalBackendException("InputStream shouldn't be null") + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + private fun targetFile(cryptoFile: CryptoFile, cloudFile: CloudFile, replace: Boolean): CloudFile { + return if (replace || !cloudContentRepository.exists(cloudFile)) { + cloudFile + } else firstNonExistingAutoRenamedFile(cryptoFile) + } + + @Throws(BackendException::class) + private fun firstNonExistingAutoRenamedFile(original: CryptoFile): CloudFile { + val name = original.name + val nameWithoutExtension = nameWithoutExtension(name) + var extension = extension(name) + if (extension.isNotEmpty()) { + extension = ".$extension" + } + var counter = 1 + var result: CryptoFile + var cloudFile: CloudFile + do { + val newFileName = "$nameWithoutExtension ($counter)$extension" + result = file(original.parent, newFileName, original.size) + counter++ + val dirFolder = cloudContentRepository.folder(result.cloudFile.parent, result.cloudFile.name) + cloudFile = cloudContentRepository.file(dirFolder, LONG_NODE_FILE_CONTENT_CONTENTS + CLOUD_NODE_EXT, result.size) + } while (cloudContentRepository.exists(cloudFile)) + return cloudFile + } + + @Throws(BackendException::class) + private fun assertCryptoLongDirFileAlreadyExists(cryptoFolder: CloudFolder) { + if (cloudContentRepository.exists(cloudContentRepository.file(cryptoFolder, CLOUD_FOLDER_DIR_FILE_PRE + CLOUD_NODE_EXT))) { + throw CloudNodeAlreadyExistsException("CloudNode already exists and replace is false") + } + } + + companion object { + + private const val CLOUD_NODE_EXT = ".c9r" + private const val LONG_NODE_FILE_EXT = ".c9s" + private const val CLOUD_FOLDER_DIR_FILE_PRE = "dir" + private const val LONG_NODE_FILE_CONTENT_CONTENTS = "contents" + private const val LONG_NODE_FILE_CONTENT_NAME = "name" + private const val CLOUD_NODE_SYMLINK_PRE = "symlink" + private val BASE64_ENCRYPTED_NAME_PATTERN = Pattern.compile("^([A-Za-z0-9+/\\-_]{4})*([A-Za-z0-9+/\\-]{4}|[A-Za-z0-9+/\\-_]{3}=|[A-Za-z0-9+/\\-_]{2}==)?$") + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat8.java b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat8.java deleted file mode 100644 index 7da228a6..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat8.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.cryptomator.data.cloud.crypto; - -import android.content.Context; - -import org.cryptomator.cryptolib.api.Cryptor; -import org.cryptomator.domain.CloudFolder; -import org.cryptomator.domain.repository.CloudContentRepository; -import org.cryptomator.util.Supplier; - -public class CryptoImplVaultFormat8 extends CryptoImplVaultFormat7 { - - CryptoImplVaultFormat8(Context context, Supplier cryptor, CloudContentRepository cloudContentRepository, CloudFolder storageLocation, DirIdCache dirIdCache, int shorteningThreshold) { - super(context, cryptor, cloudContentRepository, storageLocation, dirIdCache, shorteningThreshold); - } - -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat8.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat8.kt new file mode 100644 index 00000000..b35d991e --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat8.kt @@ -0,0 +1,21 @@ +package org.cryptomator.data.cloud.crypto + +import android.content.Context +import org.cryptomator.cryptolib.api.Cryptor +import org.cryptomator.domain.Cloud +import org.cryptomator.domain.CloudFile +import org.cryptomator.domain.CloudFolder +import org.cryptomator.domain.CloudNode +import org.cryptomator.domain.repository.CloudContentRepository +import java.util.function.Supplier + +class CryptoImplVaultFormat8 internal constructor( + context: Context, + cryptor: Supplier, + cloudContentRepository: CloudContentRepository, + storageLocation: CloudFolder, + dirIdCache: DirIdCache, + shorteningThreshold: Int +) : CryptoImplVaultFormat7( + context, cryptor, cloudContentRepository, storageLocation, dirIdCache, shorteningThreshold +) diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.java b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.java deleted file mode 100644 index 45a2916e..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.java +++ /dev/null @@ -1,269 +0,0 @@ -package org.cryptomator.data.cloud.crypto; - -import android.content.Context; - -import org.apache.commons.codec.binary.Base32; -import org.apache.commons.codec.binary.BaseNCodec; -import org.cryptomator.cryptolib.Cryptors; -import org.cryptomator.cryptolib.api.AuthenticationFailedException; -import org.cryptomator.cryptolib.api.Cryptor; -import org.cryptomator.cryptolib.common.MessageDigestSupplier; -import org.cryptomator.domain.CloudFile; -import org.cryptomator.domain.CloudFolder; -import org.cryptomator.domain.CloudNode; -import org.cryptomator.domain.exception.AlreadyExistException; -import org.cryptomator.domain.exception.BackendException; -import org.cryptomator.domain.exception.EmptyDirFileException; -import org.cryptomator.domain.exception.NoSuchCloudFileException; -import org.cryptomator.domain.repository.CloudContentRepository; -import org.cryptomator.domain.usecases.ProgressAware; -import org.cryptomator.domain.usecases.cloud.ByteArrayDataSource; -import org.cryptomator.domain.usecases.cloud.DataSource; -import org.cryptomator.domain.usecases.cloud.UploadState; -import org.cryptomator.util.Optional; -import org.cryptomator.util.Supplier; - -import java.io.ByteArrayOutputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import timber.log.Timber; - -import static org.cryptomator.domain.usecases.ProgressAware.NO_OP_PROGRESS_AWARE; -import static org.cryptomator.util.Encodings.UTF_8; - -final class CryptoImplVaultFormatPre7 extends CryptoImplDecorator { - - static final int SHORTENING_THRESHOLD = 129; - private static final String DIR_PREFIX = "0"; - private static final String SYMLINK_PREFIX = "1S"; - private static final String LONG_NAME_FILE_EXT = ".lng"; - private static final String METADATA_DIR_NAME = "m"; - private static final BaseNCodec BASE32 = new Base32(); - private static final Pattern BASE32_ENCRYPTED_NAME_PATTERN = Pattern.compile("^(0|1S)?(([A-Z2-7]{8})*[A-Z2-7=]{8})$"); - - CryptoImplVaultFormatPre7(Context context, Supplier cryptor, CloudContentRepository cloudContentRepository, CloudFolder storageLocation, DirIdCache dirIdCache) { - super(context, cryptor, cloudContentRepository, storageLocation, dirIdCache, SHORTENING_THRESHOLD); - } - - @Override - CryptoFolder folder(CryptoFolder cryptoParent, String cleartextName) throws BackendException { - String dirFileName = encryptFolderName(cryptoParent, cleartextName); - CloudFile dirFile = cloudContentRepository.file(dirIdInfo(cryptoParent).getCloudFolder(), dirFileName); - return folder(cryptoParent, cleartextName, dirFile); - } - - @Override - CryptoFolder create(CryptoFolder folder) throws BackendException { - assertCryptoFolderAlreadyExists(folder); - DirIdCache.DirIdInfo dirIdInfo = dirIdInfo(folder); - CloudFolder createdCloudFolder = cloudContentRepository.create(dirIdInfo.getCloudFolder()); - byte[] dirId = dirIdInfo.getId().getBytes(UTF_8); - CloudFile createdDirFile = cloudContentRepository.write(folder.getDirFile(), ByteArrayDataSource.from(dirId), NO_OP_PROGRESS_AWARE, false, dirId.length); - CryptoFolder result = folder(folder, createdDirFile); - addFolderToCache(result, dirIdInfo.withCloudFolder(createdCloudFolder)); - return result; - } - - @Override - String encryptName(CryptoFolder cryptoParent, String name) throws BackendException { - return encryptName(cryptoParent, name, ""); - } - - private String encryptName(CryptoFolder cryptoParent, String name, String prefix) throws BackendException { - String ciphertextName = prefix + cryptor().fileNameCryptor().encryptFilename(name, dirIdInfo(cryptoParent).getId().getBytes(UTF_8)); - if (ciphertextName.length() > shorteningThreshold) { - ciphertextName = deflate(ciphertextName); - } - return ciphertextName; - } - - private String deflate(String longFileName) throws BackendException { - byte[] longFilenameBytes = longFileName.getBytes(UTF_8); - byte[] hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes); - String shortFileName = BASE32.encodeAsString(hash) + LONG_NAME_FILE_EXT; - CloudFile metadataFile = metadataFile(shortFileName); - byte[] data = longFileName.getBytes(UTF_8); - try { - cloudContentRepository.create(metadataFile.getParent()); - } catch (AlreadyExistException e) { - } - cloudContentRepository.write(metadataFile, ByteArrayDataSource.from(data), NO_OP_PROGRESS_AWARE, true, data.length); - return shortFileName; - } - - private String inflate(String shortFileName) throws BackendException { - CloudFile metadataFile = metadataFile(shortFileName); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - cloudContentRepository.read(metadataFile, Optional.empty(), out, NO_OP_PROGRESS_AWARE); - return new String(out.toByteArray(), UTF_8); - } - - private CloudFile inflatePermanently(CloudFile cloudFile, String longFileName) throws BackendException { - Timber.tag("CryptoFs").i("inflatePermanently: %s -> %s", cloudFile.getName(), longFileName); - CloudFile newCiphertextFile = cloudContentRepository.file(cloudFile.getParent(), longFileName); - cloudContentRepository.move(cloudFile, newCiphertextFile); - return newCiphertextFile; - } - - private CloudFile metadataFile(String shortFilename) throws BackendException { - CloudFolder firstLevelFolder = cloudContentRepository.folder(metadataFolder(), shortFilename.substring(0, 2)); - CloudFolder secondLevelFolder = cloudContentRepository.folder(firstLevelFolder, shortFilename.substring(2, 4)); - return cloudContentRepository.file(secondLevelFolder, shortFilename); - } - - private CloudFolder metadataFolder() throws BackendException { - return cloudContentRepository.folder(storageLocation(), METADATA_DIR_NAME); - } - - @Override - List list(CryptoFolder cryptoFolder) throws BackendException { - DirIdCache.DirIdInfo dirIdInfo = dirIdInfo(cryptoFolder); - String dirId = dirIdInfo(cryptoFolder).getId(); - CloudFolder lvl2Dir = dirIdInfo.getCloudFolder(); - List ciphertextNodes = cloudContentRepository.list(lvl2Dir); - List result = new ArrayList<>(); - for (CloudNode node : ciphertextNodes) { - if (node instanceof CloudFile) { - ciphertextToCleartextNode(cryptoFolder, dirId, node).ifPresent(result::add); - } - } - return result; - } - - private Optional ciphertextToCleartextNode(CryptoFolder cryptoFolder, String dirId, CloudNode cloudNode) throws BackendException { - CloudFile cloudFile = (CloudFile) cloudNode; - String ciphertextName = cloudFile.getName(); - if (ciphertextName.endsWith(LONG_NAME_FILE_EXT)) { - try { - ciphertextName = inflate(ciphertextName); - if (ciphertextName.length() <= shorteningThreshold) { - cloudFile = inflatePermanently(cloudFile, ciphertextName); - } - } catch (NoSuchCloudFileException e) { - Timber.tag("CryptoFs").e("Missing mFile: %s", ciphertextName); - return Optional.empty(); - } catch (BackendException e) { - Timber.tag("CryptoFs").e(e, "Failed to read mFile: %s", ciphertextName); - return Optional.empty(); - } - } - String cleartextName; - try { - cleartextName = decryptName(dirId, ciphertextName.toUpperCase()); - } catch (AuthenticationFailedException e) { - Timber.tag("CryptoFs").w("File name authentication failed: %s", cloudFile.getPath()); - return Optional.empty(); - } catch (IllegalArgumentException e) { - Timber.tag("CryptoFs").d("Illegal ciphertext filename: %s", cloudFile.getPath()); - return Optional.empty(); - } - if (cleartextName == null || ciphertextName.startsWith(SYMLINK_PREFIX)) { - return Optional.empty(); - } else if (ciphertextName.startsWith(DIR_PREFIX)) { - return Optional.of(folder(cryptoFolder, cleartextName, cloudFile)); - } else { - Optional cleartextSize = Optional.empty(); - if (cloudFile.getSize().isPresent()) { - long ciphertextSizeWithoutHeader = cloudFile.getSize().get() - cryptor().fileHeaderCryptor().headerSize(); - if (ciphertextSizeWithoutHeader >= 0) { - cleartextSize = Optional.of(Cryptors.cleartextSize(ciphertextSizeWithoutHeader, cryptor())); - } - } - return Optional.of(file(cryptoFolder, cleartextName, cloudFile, cleartextSize)); - } - } - - @Override - String decryptName(String dirId, String encryptedName) { - Optional ciphertextName = extractEncryptedName(encryptedName); - if (ciphertextName.isPresent()) { - return cryptor().fileNameCryptor().decryptFilename(ciphertextName.get(), dirId.getBytes(UTF_8)); - } else { - return null; - } - } - - @Override - Optional extractEncryptedName(String ciphertextName) { - Matcher matcher = BASE32_ENCRYPTED_NAME_PATTERN.matcher(ciphertextName); - if (matcher.find(0)) { - return Optional.of(matcher.group(2)); - } else { - return Optional.empty(); - } - } - - @Override - CryptoSymlink symlink(CryptoFolder cryptoParent, String cleartextName, String target) throws BackendException { - String ciphertextName = encryptSymlinkName(cryptoParent, cleartextName); - CloudFile cloudFile = cloudContentRepository.file(dirIdInfo(cryptoParent).getCloudFolder(), ciphertextName); - return new CryptoSymlink(cryptoParent, cleartextName, path(cryptoParent, cleartextName), target, cloudFile); - } - - private String encryptSymlinkName(CryptoFolder cryptoFolder, String name) throws BackendException { - return encryptName(cryptoFolder, name, SYMLINK_PREFIX); - } - - @Override - String encryptFolderName(CryptoFolder cryptoFolder, String name) throws BackendException { - return encryptName(cryptoFolder, name, DIR_PREFIX); - } - - @Override - CryptoFolder move(CryptoFolder source, CryptoFolder target) throws BackendException { - assertCryptoFolderAlreadyExists(target); - CryptoFolder result = folder(target.getParent(), target.getName(), cloudContentRepository.move(source.getDirFile(), target.getDirFile())); - - evictFromCache(source); - evictFromCache(target); - return result; - } - - @Override - CryptoFile move(CryptoFile source, CryptoFile target) throws BackendException { - assertCryptoFileAlreadyExists(target); - return file(target, cloudContentRepository.move(source.getCloudFile(), target.getCloudFile()), source.getSize()); - } - - @Override - void delete(CloudNode node) throws BackendException { - if (node instanceof CryptoFolder) { - CryptoFolder cryptoFolder = (CryptoFolder) node; - List cryptoSubfolders = deepCollectSubfolders(cryptoFolder); - for (CryptoFolder cryptoSubfolder : cryptoSubfolders) { - cloudContentRepository.delete(dirIdInfo(cryptoSubfolder).getCloudFolder()); - } - cloudContentRepository.delete(dirIdInfo(cryptoFolder).getCloudFolder()); - cloudContentRepository.delete(cryptoFolder.getDirFile()); - evictFromCache(cryptoFolder); - } else if (node instanceof CryptoFile) { - CryptoFile cryptoFile = (CryptoFile) node; - cloudContentRepository.delete(cryptoFile.getCloudFile()); - } - } - - @Override - String loadDirId(CryptoFolder folder) throws BackendException, EmptyDirFileException { - if (RootCryptoFolder.isRoot(folder)) { - return CryptoConstants.ROOT_DIR_ID; - } else if (cloudContentRepository.exists(folder.getDirFile())) { - return new String(loadContentsOfDirFile(folder), UTF_8); - } else { - return newDirId(); - } - } - - @Override - DirIdCache.DirIdInfo createDirIdInfo(CryptoFolder folder) throws BackendException { - String dirId = loadDirId(folder); - return dirIdCache.put(folder, createDirIdInfoFor(dirId)); - } - - @Override - public CryptoFile write(CryptoFile cryptoFile, DataSource data, ProgressAware progressAware, boolean replace, long length) throws BackendException { - return writeShortNameFile(cryptoFile, data, progressAware, replace, length); - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt new file mode 100644 index 00000000..f3514360 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt @@ -0,0 +1,281 @@ +package org.cryptomator.data.cloud.crypto + +import android.content.Context +import com.google.common.io.BaseEncoding +import org.apache.commons.codec.binary.Base32 +import org.apache.commons.codec.binary.BaseNCodec +import org.cryptomator.cryptolib.api.AuthenticationFailedException +import org.cryptomator.cryptolib.api.Cryptor +import org.cryptomator.cryptolib.common.MessageDigestSupplier +import org.cryptomator.data.cloud.crypto.DirIdCache.DirIdInfo +import org.cryptomator.data.cloud.crypto.RootCryptoFolder.Companion.isRoot +import org.cryptomator.domain.Cloud +import org.cryptomator.domain.CloudFile +import org.cryptomator.domain.CloudFolder +import org.cryptomator.domain.CloudNode +import org.cryptomator.domain.exception.AlreadyExistException +import org.cryptomator.domain.exception.BackendException +import org.cryptomator.domain.exception.EmptyDirFileException +import org.cryptomator.domain.exception.NoSuchCloudFileException +import org.cryptomator.domain.repository.CloudContentRepository +import org.cryptomator.domain.usecases.ProgressAware +import org.cryptomator.domain.usecases.cloud.ByteArrayDataSource.Companion.from +import org.cryptomator.domain.usecases.cloud.DataSource +import org.cryptomator.domain.usecases.cloud.UploadState +import java.io.ByteArrayOutputStream +import java.nio.charset.StandardCharsets +import java.util.function.Supplier +import java.util.regex.Pattern +import kotlin.streams.toList +import timber.log.Timber + +internal class CryptoImplVaultFormatPre7( + context: Context, + cryptor: Supplier, + cloudContentRepository: CloudContentRepository, + storageLocation: CloudFolder, + dirIdCache: DirIdCache +) : + CryptoImplDecorator( + context, cryptor, cloudContentRepository, storageLocation, dirIdCache, SHORTENING_THRESHOLD + ) { + + @Throws(BackendException::class) + override fun folder(cryptoParent: CryptoFolder, cleartextName: String): CryptoFolder { + val dirFileName = encryptFolderName(cryptoParent, cleartextName) + val dirFile = cloudContentRepository.file(dirIdInfo(cryptoParent).cloudFolder, dirFileName) + return folder(cryptoParent, cleartextName, dirFile) + } + + @Throws(BackendException::class) + override fun create(folder: CryptoFolder): CryptoFolder { + requireNotNull(folder.dirFile) + assertCryptoFolderAlreadyExists(folder) + val dirIdInfo = dirIdInfo(folder) + val createdCloudFolder = cloudContentRepository.create(dirIdInfo.cloudFolder) + val dirId = dirIdInfo.id.toByteArray(StandardCharsets.UTF_8) + val createdDirFile = cloudContentRepository.write(folder.dirFile, from(dirId), ProgressAware.NO_OP_PROGRESS_AWARE_UPLOAD, false, dirId.size.toLong()) + return folder(folder, createdDirFile).also { + addFolderToCache(it, dirIdInfo.withCloudFolder(createdCloudFolder)) + } + } + + @Throws(BackendException::class) + override fun encryptName(cryptoParent: CryptoFolder, name: String): String { + return encryptName(cryptoParent, name, "") + } + + @Throws(BackendException::class) + private fun encryptName(cryptoParent: CryptoFolder, name: String, prefix: String): String { + var ciphertextName = prefix + cryptor().fileNameCryptor().encryptFilename(BaseEncoding.base32(), name, dirIdInfo(cryptoParent).id.toByteArray(StandardCharsets.UTF_8)) + if (ciphertextName.length > shorteningThreshold) { + ciphertextName = deflate(ciphertextName) + } + return ciphertextName + } + + @Throws(BackendException::class) + private fun deflate(longFileName: String): String { + val longFilenameBytes = longFileName.toByteArray(StandardCharsets.UTF_8) + val hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes) + val shortFileName = BASE32.encodeAsString(hash) + LONG_NAME_FILE_EXT + val metadataFile = metadataFile(shortFileName) + val data = longFileName.toByteArray(StandardCharsets.UTF_8) + try { + cloudContentRepository.create(metadataFile.parent) + } catch (e: AlreadyExistException) { + } + cloudContentRepository.write(metadataFile, from(data), ProgressAware.NO_OP_PROGRESS_AWARE_UPLOAD, true, data.size.toLong()) + return shortFileName + } + + @Throws(BackendException::class) + private fun inflate(shortFileName: String): String { + val metadataFile = metadataFile(shortFileName) + val out = ByteArrayOutputStream() + cloudContentRepository.read(metadataFile, null, out, ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD) + return String(out.toByteArray(), StandardCharsets.UTF_8) + } + + @Throws(BackendException::class) + private fun inflatePermanently(cloudFile: CloudFile, longFileName: String): CloudFile { + Timber.tag("CryptoFs").i("inflatePermanently: %s -> %s", cloudFile.name, longFileName) + val newCiphertextFile = cloudContentRepository.file(cloudFile.parent, longFileName) + cloudContentRepository.move(cloudFile, newCiphertextFile) + return newCiphertextFile + } + + @Throws(BackendException::class) + private fun metadataFile(shortFilename: String): CloudFile { + val firstLevelFolder = cloudContentRepository.folder(metadataFolder(), shortFilename.substring(0, 2)) + val secondLevelFolder = cloudContentRepository.folder(firstLevelFolder, shortFilename.substring(2, 4)) + return cloudContentRepository.file(secondLevelFolder, shortFilename) + } + + @Throws(BackendException::class) + private fun metadataFolder(): CloudFolder { + return cloudContentRepository.folder(storageLocation(), METADATA_DIR_NAME) + } + + @Throws(BackendException::class) + override fun list(cryptoFolder: CryptoFolder): List { + val dirIdInfo = dirIdInfo(cryptoFolder) + val dirId = dirIdInfo(cryptoFolder).id + val lvl2Dir = dirIdInfo.cloudFolder + return cloudContentRepository + .list(lvl2Dir) + .filterIsInstance() + .parallelStream() + .map { node -> + ciphertextToCleartextNode(cryptoFolder, dirId, node) + } + .toList() + .filterNotNull() + } + + @Throws(BackendException::class) + private fun ciphertextToCleartextNode(cryptoFolder: CryptoFolder, dirId: String, cloudNode: CloudFile): CryptoNode? { + var cloudFile = cloudNode + var ciphertextName = cloudFile.name + if (ciphertextName.endsWith(LONG_NAME_FILE_EXT)) { + try { + ciphertextName = inflate(ciphertextName) + if (ciphertextName.length <= shorteningThreshold) { + cloudFile = inflatePermanently(cloudFile, ciphertextName) + } + } catch (e: NoSuchCloudFileException) { + Timber.tag("CryptoFs").e("Missing mFile: %s", ciphertextName) + return null + } catch (e: BackendException) { + Timber.tag("CryptoFs").e(e, "Failed to read mFile: %s", ciphertextName) + return null + } + } + val cleartextName: String? = try { + decryptName(dirId, ciphertextName.uppercase()) + } catch (e: AuthenticationFailedException) { + Timber.tag("CryptoFs").w("File name authentication failed: %s", cloudFile.path) + return null + } catch (e: IllegalArgumentException) { + Timber.tag("CryptoFs").d("Illegal ciphertext filename: %s", cloudFile.path) + return null + } + return if (cleartextName == null || ciphertextName.startsWith(SYMLINK_PREFIX)) { + null + } else if (ciphertextName.startsWith(DIR_PREFIX)) { + folder(cryptoFolder, cleartextName, cloudFile) + } else { + val cleartextSize = cloudFile.size?.let { + val ciphertextSizeWithoutHeader: Long = it - cryptor().fileHeaderCryptor().headerSize() + if (ciphertextSizeWithoutHeader >= 0) { + cryptor().fileContentCryptor().cleartextSize(ciphertextSizeWithoutHeader) + } else { + null + } + } + file(cryptoFolder, cleartextName, cloudFile, cleartextSize) + } + } + + override fun decryptName(dirId: String, encryptedName: String): String? { + val ciphertextName = extractEncryptedName(encryptedName) + return if (ciphertextName != null) { + cryptor().fileNameCryptor().decryptFilename(BaseEncoding.base32(), ciphertextName, dirId.toByteArray(StandardCharsets.UTF_8)) + } else { + null + } + } + + override fun extractEncryptedName(ciphertextName: String): String? { + val matcher = BASE32_ENCRYPTED_NAME_PATTERN.matcher(ciphertextName) + return if (matcher.find(0)) { + matcher.group(2) + } else { + null + } + } + + @Throws(BackendException::class) + override fun symlink(cryptoParent: CryptoFolder, cleartextName: String, target: String): CryptoSymlink { + val ciphertextName = encryptSymlinkName(cryptoParent, cleartextName) + val cloudFile = cloudContentRepository.file(dirIdInfo(cryptoParent).cloudFolder, ciphertextName) + return CryptoSymlink(cryptoParent, cleartextName, path(cryptoParent, cleartextName), target, cloudFile) + } + + @Throws(BackendException::class) + private fun encryptSymlinkName(cryptoFolder: CryptoFolder, name: String): String { + return encryptName(cryptoFolder, name, SYMLINK_PREFIX) + } + + @Throws(BackendException::class) + override fun encryptFolderName(cryptoFolder: CryptoFolder, name: String): String { + return encryptName(cryptoFolder, name, DIR_PREFIX) + } + + @Throws(BackendException::class) + override fun move(source: CryptoFolder, target: CryptoFolder): CryptoFolder { + requireNotNull(target.parent) + requireNotNull(source.dirFile) + requireNotNull(target.dirFile) + assertCryptoFolderAlreadyExists(target) + val result = folder(target.parent!!, target.name, cloudContentRepository.move(source.dirFile, target.dirFile)) + evictFromCache(source) + evictFromCache(target) + return result + } + + @Throws(BackendException::class) + override fun move(source: CryptoFile, target: CryptoFile): CryptoFile { + assertCryptoFileAlreadyExists(target) + return file(target, cloudContentRepository.move(source.cloudFile, target.cloudFile), source.size) + } + + @Throws(BackendException::class) + override fun delete(node: CloudNode) { + if (node is CryptoFolder) { + requireNotNull(node.dirFile) + val cryptoSubfolders = deepCollectSubfolders(node) + for (cryptoSubfolder in cryptoSubfolders) { + cloudContentRepository.delete(dirIdInfo(cryptoSubfolder).cloudFolder) + } + cloudContentRepository.delete(dirIdInfo(node).cloudFolder) + cloudContentRepository.delete(node.dirFile) + evictFromCache(node) + } else if (node is CryptoFile) { + cloudContentRepository.delete(node.cloudFile) + } + } + + @Throws(BackendException::class, EmptyDirFileException::class) + override fun loadDirId(folder: CryptoFolder): String { + return if (isRoot(folder)) { + CryptoConstants.ROOT_DIR_ID + } else if (folder.dirFile != null && cloudContentRepository.exists(folder.dirFile)) { + String(loadContentsOfDirFile(folder), StandardCharsets.UTF_8) + } else { + newDirId() + } + } + + @Throws(BackendException::class) + override fun createDirIdInfo(folder: CryptoFolder): DirIdInfo { + val dirId = loadDirId(folder) + return dirIdCache.put(folder, createDirIdInfoFor(dirId)) + } + + @Throws(BackendException::class) + override fun write(cryptoFile: CryptoFile, data: DataSource, progressAware: ProgressAware, replace: Boolean, length: Long): CryptoFile { + return writeShortNameFile(cryptoFile, data, progressAware, replace, length) + } + + companion object { + + const val SHORTENING_THRESHOLD = 129 + private const val DIR_PREFIX = "0" + private const val SYMLINK_PREFIX = "1S" + private const val LONG_NAME_FILE_EXT = ".lng" + private const val METADATA_DIR_NAME = "m" + private val BASE32: BaseNCodec = Base32() + private val BASE32_ENCRYPTED_NAME_PATTERN = Pattern.compile("^(0|1S)?(([A-Z2-7]{8})*[A-Z2-7=]{8})$") + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoNode.java b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoNode.java deleted file mode 100644 index fc4fd2ff..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoNode.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.cryptomator.data.cloud.crypto; - -import org.cryptomator.domain.CloudNode; - -interface CryptoNode extends CloudNode { - -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoNode.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoNode.kt new file mode 100644 index 00000000..8735b010 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoNode.kt @@ -0,0 +1,5 @@ +package org.cryptomator.data.cloud.crypto + +import org.cryptomator.domain.CloudNode + +interface CryptoNode : CloudNode diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoSymlink.java b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoSymlink.java deleted file mode 100644 index 8960aea2..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoSymlink.java +++ /dev/null @@ -1,82 +0,0 @@ -package org.cryptomator.data.cloud.crypto; - -import org.cryptomator.domain.Cloud; -import org.cryptomator.domain.CloudFile; -import org.cryptomator.util.Optional; - -import java.util.Date; - -class CryptoSymlink implements CloudFile, CryptoNode { - - private final String name; - private final String path; - private final String target; - private final CloudFile cloudFile; - private final CryptoFolder parent; - - public CryptoSymlink(CryptoFolder parent, String name, String path, String target, CloudFile cloudFile) { - this.parent = parent; - this.name = name; - this.path = path; - this.target = target; - this.cloudFile = cloudFile; - } - - @Override - public Cloud getCloud() { - return parent.getCloud(); - } - - @Override - public String getName() { - return name; - } - - @Override - public String getPath() { - return path; - } - - @Override - public CryptoFolder getParent() { - return parent; - } - - @Override - public Optional getSize() { - return Optional.of((long) target.length()); - } - - @Override - public Optional getModified() { - return cloudFile.getModified(); - } - - /** - * @return The actual file in the underlying, i.e. decorated, CloudContentRepository - */ - CloudFile getCloudFile() { - return cloudFile; - } - - @Override - public boolean equals(Object obj) { - if (obj == null || getClass() != obj.getClass()) { - return false; - } - if (obj == this) { - return true; - } - return internalEquals((CryptoSymlink) obj); - } - - private boolean internalEquals(CryptoSymlink obj) { - return path != null && path.equals(obj.path); - } - - @Override - public int hashCode() { - return path == null ? 0 : path.hashCode(); - } - -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoSymlink.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoSymlink.kt new file mode 100644 index 00000000..88e5e570 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoSymlink.kt @@ -0,0 +1,40 @@ +package org.cryptomator.data.cloud.crypto + +import org.cryptomator.domain.Cloud +import org.cryptomator.domain.CloudFile +import java.util.Date + +class CryptoSymlink( + override val parent: CryptoFolder, override val name: String, override val path: String, private val target: String, + /** + * @return The actual file in the underlying, i.e. decorated, CloudContentRepository + */ + val cloudFile: CloudFile +) : CloudFile, CryptoNode { + + override val cloud: Cloud? + get() = parent.cloud + + override val size: Long + get() = target.length.toLong() + + override val modified: Date? + get() = cloudFile.modified + + override fun equals(other: Any?): Boolean { + if (other == null || javaClass != other.javaClass) { + return false + } + return if (other === this) { + true + } else internalEquals(other as CryptoSymlink) + } + + private fun internalEquals(obj: CryptoSymlink): Boolean { + return path == obj.path + } + + override fun hashCode(): Int { + return path.hashCode() + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/Cryptors.java b/data/src/main/java/org/cryptomator/data/cloud/crypto/Cryptors.java deleted file mode 100644 index 73f079bd..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/Cryptors.java +++ /dev/null @@ -1,144 +0,0 @@ -package org.cryptomator.data.cloud.crypto; - -import org.cryptomator.cryptolib.api.Cryptor; -import org.cryptomator.domain.Vault; -import org.cryptomator.domain.exception.MissingCryptorException; -import org.cryptomator.util.Optional; -import org.cryptomator.util.Supplier; - -import java.util.Iterator; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; - -public abstract class Cryptors { - - Cryptors() { - } - - public abstract boolean isEmpty(); - - public abstract int size(); - - public abstract Supplier get(Vault vault); - - public abstract Optional remove(Vault vault); - - public abstract boolean putIfAbsent(Vault vault, Cryptor cryptor); - - public static class Delegating extends Cryptors { - - private final Cryptors.Default fallback = new Cryptors.Default(); - - private volatile Cryptors.Default delegate; - - public synchronized void setDelegate(Cryptors.Default delegate) { - delegate.putAll(fallback.cryptors); - this.delegate = delegate; - } - - public synchronized void removeDelegate() { - fallback.putAll(delegate.cryptors); - this.delegate = null; - } - - @Override - public synchronized boolean isEmpty() { - return delegate().isEmpty(); - } - - @Override - public synchronized int size() { - return delegate().size(); - } - - @Override - public synchronized Supplier get(Vault vault) { - return delegate().get(vault); - } - - @Override - public synchronized Optional remove(Vault vault) { - return delegate().remove(vault); - } - - @Override - public synchronized boolean putIfAbsent(Vault vault, Cryptor cryptor) { - return delegate().putIfAbsent(vault, cryptor); - } - - private synchronized Cryptors delegate() { - if (delegate == null) { - return fallback; - } else { - return delegate; - } - } - - } - - public static class Default extends Cryptors { - - private final ConcurrentMap cryptors = new ConcurrentHashMap<>(); - - private Runnable onChangeListener = () -> { - }; - - public boolean isEmpty() { - return cryptors.isEmpty(); - } - - public int size() { - return cryptors.size(); - } - - public Supplier get(final Vault vault) { - return () -> { - Cryptor cryptor = cryptors.get(vault); - if (cryptor == null) { - throw new MissingCryptorException(); - } else { - return cryptor; - } - }; - } - - public Optional remove(Vault vault) { - Optional result = Optional.ofNullable(cryptors.remove(vault)); - if (result.isPresent()) { - onChangeListener.run(); - } - return result; - } - - public boolean putIfAbsent(Vault vault, Cryptor cryptor) { - if (cryptors.putIfAbsent(vault, cryptor) == null) { - onChangeListener.run(); - return true; - } else { - return false; - } - } - - public void setOnChangeListener(Runnable onChangeListener) { - this.onChangeListener = onChangeListener; - } - - public void putAll(Map cryptors) { - this.cryptors.putAll(cryptors); - onChangeListener.run(); - } - - public void destroyAll() { - while (!isEmpty()) { - Iterator cryptorIterator = cryptors.values().iterator(); - while (cryptorIterator.hasNext()) { - cryptorIterator.next().destroy(); - cryptorIterator.remove(); - } - } - onChangeListener.run(); - } - } - -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/Cryptors.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/Cryptors.kt new file mode 100644 index 00000000..e20117f9 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/Cryptors.kt @@ -0,0 +1,130 @@ +package org.cryptomator.data.cloud.crypto + +import com.google.common.base.Optional +import org.cryptomator.cryptolib.api.Cryptor +import org.cryptomator.domain.Vault +import org.cryptomator.domain.exception.MissingCryptorException +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentMap +import java.util.function.Supplier + +abstract class Cryptors internal constructor() { + + abstract fun isEmpty(): Boolean + + abstract fun size(): Int + + abstract operator fun get(vault: Vault): Supplier + + abstract fun remove(vault: Vault): Optional + + abstract fun putIfAbsent(vault: Vault, cryptor: Cryptor): Boolean + + class Delegating : Cryptors() { + + private val fallback = Default() + + @Volatile + private var delegate: Default? = null + + @Synchronized + fun setDelegate(delegate: Default) { + delegate.putAll(fallback.cryptors) + this.delegate = delegate + } + + @Synchronized + fun removeDelegate() { + fallback.putAll(delegate!!.cryptors) + delegate = null + } + + @Synchronized + override fun isEmpty(): Boolean { + return delegate().isEmpty() + } + + @Synchronized + override fun size(): Int { + return delegate().size() + } + + @Synchronized + override fun get(vault: Vault): Supplier { + return delegate()[vault] + } + + @Synchronized + override fun remove(vault: Vault): Optional { + return delegate().remove(vault) + } + + @Synchronized + override fun putIfAbsent(vault: Vault, cryptor: Cryptor): Boolean { + return delegate().putIfAbsent(vault, cryptor) + } + + @Synchronized + private fun delegate(): Cryptors { + return delegate ?: fallback + } + } + + class Default : Cryptors() { + + val cryptors: ConcurrentMap = ConcurrentHashMap() + + private var onChangeListener = Runnable {} + + override fun isEmpty(): Boolean { + return cryptors.isEmpty() + } + + override fun size(): Int { + return cryptors.size + } + + override fun get(vault: Vault): Supplier { + return Supplier { + cryptors[vault] ?: throw MissingCryptorException() + } + } + + override fun remove(vault: Vault): Optional { + val result = Optional.fromNullable(cryptors.remove(vault)) + if (result.isPresent) { + onChangeListener.run() + } + return result + } + + override fun putIfAbsent(vault: Vault, cryptor: Cryptor): Boolean { + return if (cryptors.putIfAbsent(vault, cryptor) == null) { + onChangeListener.run() + true + } else { + false + } + } + + fun setOnChangeListener(onChangeListener: Runnable) { + this.onChangeListener = onChangeListener + } + + fun putAll(cryptors: Map) { + this.cryptors.putAll(cryptors) + onChangeListener.run() + } + + fun destroyAll() { + while (!isEmpty()) { + val cryptorIterator = cryptors.values.iterator() + while (cryptorIterator.hasNext()) { + cryptorIterator.next().destroy() + cryptorIterator.remove() + } + } + onChangeListener.run() + } + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/DirIdCache.java b/data/src/main/java/org/cryptomator/data/cloud/crypto/DirIdCache.java deleted file mode 100644 index 289d2d84..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/DirIdCache.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.cryptomator.data.cloud.crypto; - -import org.cryptomator.domain.CloudFolder; - -interface DirIdCache { - - DirIdInfo get(CryptoFolder folder); - - DirIdInfo put(CryptoFolder folder, DirIdInfo dirIdInfo); - - void evict(CryptoFolder folder); - - void evictSubFoldersOf(CryptoFolder cryptoFolder); - - class DirIdInfo { - - private final String id; - private final CloudFolder cloudFolder; - - DirIdInfo(String id, CloudFolder cloudFolder) { - this.id = id; - this.cloudFolder = cloudFolder; - } - - public String getId() { - return id; - } - - public CloudFolder getCloudFolder() { - return cloudFolder; - } - - DirIdInfo withCloudFolder(CloudFolder cloudFolder) { - return new DirIdInfo(id, cloudFolder); - } - } - -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/DirIdCache.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/DirIdCache.kt new file mode 100644 index 00000000..eb8634ea --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/DirIdCache.kt @@ -0,0 +1,21 @@ +package org.cryptomator.data.cloud.crypto + +import org.cryptomator.domain.CloudFolder + +interface DirIdCache { + + operator fun get(folder: CryptoFolder): DirIdInfo? + + fun put(folder: CryptoFolder, dirIdInfo: DirIdInfo): DirIdInfo + + fun evict(folder: CryptoFolder) + + fun evictSubFoldersOf(cryptoFolder: CryptoFolder) + + class DirIdInfo internal constructor(val id: String, val cloudFolder: CloudFolder) { + + fun withCloudFolder(cloudFolder: CloudFolder): DirIdInfo { + return DirIdInfo(id, cloudFolder) + } + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/DirIdCacheFormat7.java b/data/src/main/java/org/cryptomator/data/cloud/crypto/DirIdCacheFormat7.java deleted file mode 100644 index e42ae764..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/DirIdCacheFormat7.java +++ /dev/null @@ -1,80 +0,0 @@ -package org.cryptomator.data.cloud.crypto; - -import android.util.LruCache; - -import java.util.Map; - -class DirIdCacheFormat7 implements DirIdCache { - - private static final int MAX_SIZE = 1024; - - private final LruCache cache = new LruCache<>(MAX_SIZE); - - DirIdCacheFormat7() { - } - - @Override - public DirIdInfo get(CryptoFolder folder) { - return cache.get(DirIdCacheKey.toKey(folder)); - } - - @Override - public DirIdInfo put(CryptoFolder folder, DirIdInfo dirIdInfo) { - DirIdCacheKey key = DirIdCacheKey.toKey(folder); - cache.put(key, dirIdInfo); - return dirIdInfo; - } - - @Override - public void evict(CryptoFolder folder) { - DirIdCacheKey key = DirIdCacheKey.toKey(folder); - cache.remove(key); - } - - @Override - public void evictSubFoldersOf(CryptoFolder folder) { - Map cacheSnapshot = cache.snapshot(); - for (Map.Entry cacheEntry : cacheSnapshot.entrySet()) { - if (cacheEntry.getKey().path.startsWith(folder.getPath() + "/")) { - cache.remove(cacheEntry.getKey()); - } - } - } - - private static class DirIdCacheKey { - - private final String path; - - private DirIdCacheKey(String path) { - this.path = path; - } - - static DirIdCacheKey toKey(CryptoFolder folder) { - return new DirIdCacheKey(folder.getPath()); - } - - @Override - public boolean equals(Object obj) { - if (obj == this) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - return internalEquals((DirIdCacheKey) obj); - } - - private boolean internalEquals(DirIdCacheKey o) { - return (path == null ? o.path == null : path.equals(o.path)); - } - - @Override - public int hashCode() { - final int prime = 31; - int hash = 1940604225; - hash = hash * prime + (path == null ? 0 : path.hashCode()); - return hash; - } - - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/DirIdCacheFormat7.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/DirIdCacheFormat7.kt new file mode 100644 index 00000000..37dfa1be --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/DirIdCacheFormat7.kt @@ -0,0 +1,74 @@ +package org.cryptomator.data.cloud.crypto + +import android.util.LruCache +import org.cryptomator.data.cloud.crypto.DirIdCache.DirIdInfo + +internal class DirIdCacheFormat7 : DirIdCache { + + private val cache = LruCache(MAX_SIZE) + + override fun get(folder: CryptoFolder): DirIdInfo? { + return cache[DirIdCacheKey.toKey(folder)] + } + + override fun put(folder: CryptoFolder, dirIdInfo: DirIdInfo): DirIdInfo { + val key = DirIdCacheKey.toKey(folder) + cache.put(key, dirIdInfo) + return dirIdInfo + } + + override fun evict(folder: CryptoFolder) { + val key = DirIdCacheKey.toKey(folder) + cache.remove(key) + } + + override fun evictSubFoldersOf(cryptoFolder: CryptoFolder) { + val cacheSnapshot = cache.snapshot() + cacheSnapshot.forEach { (key) -> + if (key.path?.startsWith(cryptoFolder.path + "/") == true) { + cache.remove(key) + } + } + } + + private class DirIdCacheKey private constructor(path: String) { + + val path: String? + + override fun equals(other: Any?): Boolean { + if (other === this) { + return true + } + return if (other == null || javaClass != other.javaClass) { + false + } else internalEquals(other as DirIdCacheKey) + } + + private fun internalEquals(o: DirIdCacheKey): Boolean { + return if (path == null) o.path == null else path == o.path + } + + override fun hashCode(): Int { + val prime = 31 + var hash = 1940604225 + hash = hash * prime + (path?.hashCode() ?: 0) + return hash + } + + companion object { + + fun toKey(folder: CryptoFolder): DirIdCacheKey { + return DirIdCacheKey(folder.path) + } + } + + init { + this.path = path + } + } + + companion object { + + private const val MAX_SIZE = 1024 + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/DirIdCacheFormatPre7.java b/data/src/main/java/org/cryptomator/data/cloud/crypto/DirIdCacheFormatPre7.java deleted file mode 100644 index 62337ba6..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/DirIdCacheFormatPre7.java +++ /dev/null @@ -1,88 +0,0 @@ -package org.cryptomator.data.cloud.crypto; - -import android.util.LruCache; - -import org.cryptomator.domain.CloudFile; - -import java.util.Date; - -class DirIdCacheFormatPre7 implements DirIdCache { - - private static final int MAX_SIZE = 1024; - - private final LruCache cache = new LruCache<>(MAX_SIZE); - - DirIdCacheFormatPre7() { - } - - public DirIdInfo get(CryptoFolder folder) { - return cache.get(DirIdCacheKey.toKey(folder)); - } - - public DirIdInfo put(CryptoFolder folder, DirIdInfo dirIdInfo) { - DirIdCacheKey key = DirIdCacheKey.toKey(folder); - cache.put(key, dirIdInfo); - cache.remove(key.withoutModified()); - return dirIdInfo; - } - - public void evict(CryptoFolder folder) { - DirIdCacheKey key = DirIdCacheKey.toKey(folder); - cache.remove(key); - cache.remove(key.withoutModified()); - } - - @Override - public void evictSubFoldersOf(CryptoFolder cryptoFolder) { - // no implementation needed - } - - private static class DirIdCacheKey { - - private final String path; - private final Date modified; - - private DirIdCacheKey(CloudFile dirFile) { - this.path = dirFile == null ? null : dirFile.getPath(); - this.modified = dirFile == null ? null : dirFile.getModified().orElse(null); - } - - private DirIdCacheKey(String path) { - this.path = path; - this.modified = null; - } - - static DirIdCacheKey toKey(CryptoFolder folder) { - return new DirIdCacheKey(folder.getDirFile()); - } - - DirIdCacheKey withoutModified() { - return new DirIdCacheKey(path); - } - - @Override - public boolean equals(Object obj) { - if (obj == this) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - return internalEquals((DirIdCacheKey) obj); - } - - private boolean internalEquals(DirIdCacheKey o) { - return (path == null ? o.path == null : path.equals(o.path)) // - && (modified == null ? o.modified == null : modified.equals(o.modified)); - } - - @Override - public int hashCode() { - final int prime = 31; - int hash = 1940604225; - hash = hash * prime + (path == null ? 0 : path.hashCode()); - hash = hash * prime + (modified == null ? 0 : modified.hashCode()); - return hash; - } - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/DirIdCacheFormatPre7.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/DirIdCacheFormatPre7.kt new file mode 100644 index 00000000..552dfed6 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/DirIdCacheFormatPre7.kt @@ -0,0 +1,86 @@ +package org.cryptomator.data.cloud.crypto + +import android.util.LruCache +import org.cryptomator.data.cloud.crypto.DirIdCache.DirIdInfo +import org.cryptomator.domain.CloudFile +import java.util.Date + +internal class DirIdCacheFormatPre7 : DirIdCache { + + private val cache = LruCache(MAX_SIZE) + + override fun get(folder: CryptoFolder): DirIdInfo? { + return cache[DirIdCacheKey.toKey(folder)] + } + + override fun put(folder: CryptoFolder, dirIdInfo: DirIdInfo): DirIdInfo { + val key = DirIdCacheKey.toKey(folder) + cache.put(key, dirIdInfo) + cache.remove(key.withoutModified()) + return dirIdInfo + } + + override fun evict(folder: CryptoFolder) { + val key = DirIdCacheKey.toKey(folder) + cache.remove(key) + cache.remove(key.withoutModified()) + } + + override fun evictSubFoldersOf(cryptoFolder: CryptoFolder) { + // no implementation needed + } + + private class DirIdCacheKey { + + private val path: String? + private val modified: Date? + + private constructor(dirFile: CloudFile?) { + path = dirFile?.path + modified = dirFile?.modified + } + + private constructor(path: String?) { + this.path = path + modified = null + } + + fun withoutModified(): DirIdCacheKey { + return DirIdCacheKey(path) + } + + override fun equals(other: Any?): Boolean { + if (other === this) { + return true + } + return if (other == null || javaClass != other.javaClass) { + false + } else internalEquals(other as DirIdCacheKey) + } + + private fun internalEquals(o: DirIdCacheKey): Boolean { + return ((if (path == null) o.path == null else path == o.path) // + && if (modified == null) o.modified == null else modified == o.modified) + } + + override fun hashCode(): Int { + val prime = 31 + var hash = 1940604225 + hash = hash * prime + (path?.hashCode() ?: 0) + hash = hash * prime + (modified?.hashCode() ?: 0) + return hash + } + + companion object { + + fun toKey(folder: CryptoFolder): DirIdCacheKey { + return DirIdCacheKey(folder.dirFile) + } + } + } + + companion object { + + private const val MAX_SIZE = 1024 + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProvider.java b/data/src/main/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProvider.java deleted file mode 100644 index 4e1508ae..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProvider.java +++ /dev/null @@ -1,324 +0,0 @@ -package org.cryptomator.data.cloud.crypto; - -import org.cryptomator.cryptolib.api.Cryptor; -import org.cryptomator.cryptolib.api.InvalidPassphraseException; -import org.cryptomator.cryptolib.api.Masterkey; -import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException; -import org.cryptomator.cryptolib.common.MasterkeyFileAccess; -import org.cryptomator.domain.CloudFile; -import org.cryptomator.domain.CloudFolder; -import org.cryptomator.domain.UnverifiedVaultConfig; -import org.cryptomator.domain.Vault; -import org.cryptomator.domain.exception.BackendException; -import org.cryptomator.domain.exception.CancellationException; -import org.cryptomator.domain.exception.FatalBackendException; -import org.cryptomator.domain.exception.vaultconfig.UnsupportedMasterkeyLocationException; -import org.cryptomator.domain.repository.CloudContentRepository; -import org.cryptomator.domain.usecases.cloud.ByteArrayDataSource; -import org.cryptomator.domain.usecases.cloud.Flag; -import org.cryptomator.domain.usecases.vault.UnlockToken; -import org.cryptomator.util.Optional; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.net.URI; -import java.security.SecureRandom; -import java.text.Normalizer; - -import static java.text.Normalizer.normalize; -import static org.cryptomator.data.cloud.crypto.CryptoConstants.DATA_DIR_NAME; -import static org.cryptomator.data.cloud.crypto.CryptoConstants.DEFAULT_CIPHER_COMBO; -import static org.cryptomator.data.cloud.crypto.CryptoConstants.DEFAULT_MASTERKEY_FILE_VERSION; -import static org.cryptomator.data.cloud.crypto.CryptoConstants.DEFAULT_MAX_FILE_NAME; -import static org.cryptomator.data.cloud.crypto.CryptoConstants.MASTERKEY_BACKUP_FILE_EXT; -import static org.cryptomator.data.cloud.crypto.CryptoConstants.MASTERKEY_FILE_NAME; -import static org.cryptomator.data.cloud.crypto.CryptoConstants.MASTERKEY_SCHEME; -import static org.cryptomator.data.cloud.crypto.CryptoConstants.MAX_VAULT_VERSION; -import static org.cryptomator.data.cloud.crypto.CryptoConstants.MAX_VAULT_VERSION_WITHOUT_VAULT_CONFIG; -import static org.cryptomator.data.cloud.crypto.CryptoConstants.MIN_VAULT_VERSION; -import static org.cryptomator.data.cloud.crypto.CryptoConstants.PEPPER; -import static org.cryptomator.data.cloud.crypto.CryptoConstants.ROOT_DIR_ID; -import static org.cryptomator.data.cloud.crypto.CryptoConstants.VAULT_FILE_NAME; -import static org.cryptomator.data.cloud.crypto.CryptoConstants.VERSION_WITH_NORMALIZED_PASSWORDS; -import static org.cryptomator.data.cloud.crypto.VaultCipherCombo.SIV_CTRMAC; -import static org.cryptomator.domain.Vault.aCopyOf; -import static org.cryptomator.domain.usecases.ProgressAware.NO_OP_PROGRESS_AWARE; -import static org.cryptomator.util.Encodings.UTF_8; - -public class MasterkeyCryptoCloudProvider implements CryptoCloudProvider { - - private final CloudContentRepository cloudContentRepository; - private final CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory; - private final SecureRandom secureRandom; - - public MasterkeyCryptoCloudProvider(CloudContentRepository cloudContentRepository, // - CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory, // - SecureRandom secureRandom) { - this.cloudContentRepository = cloudContentRepository; - this.cryptoCloudContentRepositoryFactory = cryptoCloudContentRepositoryFactory; - this.secureRandom = secureRandom; - } - - @Override - public void create(CloudFolder location, CharSequence password) throws BackendException { - // Just for testing (id in VaultConfig is auto generated which makes sense while creating a vault but not for testing) - create(location, password, VaultConfig.createVaultConfig()); - } - - // Visible for testing - void create(CloudFolder location, CharSequence password, VaultConfig.VaultConfigBuilder vaultConfigBuilder) throws BackendException { - // 1. write masterkey: - Masterkey masterkey = Masterkey.generate(secureRandom); - try (ByteArrayOutputStream data = new ByteArrayOutputStream()) { - new MasterkeyFileAccess(PEPPER, secureRandom).persist(masterkey, data, password, DEFAULT_MASTERKEY_FILE_VERSION); - cloudContentRepository.write(legacyMasterkeyFile(location), ByteArrayDataSource.from(data.toByteArray()), NO_OP_PROGRESS_AWARE, false, data.size()); - } catch (IOException e) { - throw new FatalBackendException("Failed to write masterkey", e); - } - - // 2. initialize vault: - VaultConfig vaultConfig = vaultConfigBuilder // - .vaultFormat(MAX_VAULT_VERSION) // - .cipherCombo(DEFAULT_CIPHER_COMBO) // - .keyId(URI.create(String.format("%s:%s", MASTERKEY_SCHEME, MASTERKEY_FILE_NAME))) // - .shorteningThreshold(DEFAULT_MAX_FILE_NAME) // - .build(); - - byte[] encodedVaultConfig = vaultConfig.toToken(masterkey.getEncoded()).getBytes(UTF_8); - CloudFile vaultFile = cloudContentRepository.file(location, VAULT_FILE_NAME); - cloudContentRepository.write(vaultFile, ByteArrayDataSource.from(encodedVaultConfig), NO_OP_PROGRESS_AWARE, false, encodedVaultConfig.length); - - // 3. create root folder: - createRootFolder(location, cryptorFor(masterkey, vaultConfig.getCipherCombo())); - } - - private void createRootFolder(CloudFolder location, Cryptor cryptor) throws BackendException { - CloudFolder dFolder = cloudContentRepository.folder(location, DATA_DIR_NAME); - dFolder = cloudContentRepository.create(dFolder); - String rootDirHash = cryptor.fileNameCryptor().hashDirectoryId(ROOT_DIR_ID); - CloudFolder lvl1Folder = cloudContentRepository.folder(dFolder, rootDirHash.substring(0, 2)); - lvl1Folder = cloudContentRepository.create(lvl1Folder); - CloudFolder lvl2Folder = cloudContentRepository.folder(lvl1Folder, rootDirHash.substring(2)); - cloudContentRepository.create(lvl2Folder); - } - - @Override - public Vault unlock(Vault vault, Optional unverifiedVaultConfig, CharSequence password, Flag cancelledFlag) throws BackendException { - return unlock(createUnlockToken(vault, unverifiedVaultConfig), unverifiedVaultConfig, password, cancelledFlag); - } - - @Override - public Vault unlock(UnlockToken token, Optional unverifiedVaultConfig, CharSequence password, Flag cancelledFlag) throws BackendException { - UnlockTokenImpl impl = (UnlockTokenImpl) token; - try { - Masterkey masterkey = impl.getKeyFile(password); - - int vaultFormat; - int shorteningThreshold; - Cryptor cryptor; - - if (unverifiedVaultConfig.isPresent()) { - VaultConfig vaultConfig = VaultConfig.verify(masterkey.getEncoded(), unverifiedVaultConfig.get()); - vaultFormat = vaultConfig.getVaultFormat(); - assertVaultVersionIsSupported(vaultConfig.getVaultFormat()); - shorteningThreshold = vaultConfig.getShorteningThreshold(); - cryptor = cryptorFor(masterkey, vaultConfig.getCipherCombo()); - } else { - vaultFormat = MasterkeyFileAccess.readAllegedVaultVersion(impl.keyFileData); - assertLegacyVaultVersionIsSupported(vaultFormat); - shorteningThreshold = vaultFormat > 6 ? CryptoConstants.DEFAULT_MAX_FILE_NAME : CryptoImplVaultFormatPre7.SHORTENING_THRESHOLD; - cryptor = cryptorFor(masterkey, SIV_CTRMAC); - } - - - if (cancelledFlag.get()) { - throw new CancellationException(); - } - - Vault vault = aCopyOf(token.getVault()) // - .withUnlocked(true) // - .withFormat(vaultFormat) // - .withShorteningThreshold(shorteningThreshold) // - .build(); - - cryptoCloudContentRepositoryFactory.registerCryptor(vault, cryptor); - - return vault; - } catch (IOException e) { - throw new FatalBackendException(e); - } - } - - @Override - public UnlockTokenImpl createUnlockToken(Vault vault, Optional unverifiedVaultConfig) throws BackendException { - CloudFolder vaultLocation = vaultLocation(vault); - if (unverifiedVaultConfig.isPresent()) { - return createUnlockToken(vault, masterkeyFile(vaultLocation, unverifiedVaultConfig.get())); - } else { - return createUnlockToken(vault, legacyMasterkeyFile(vaultLocation)); - } - } - - private CloudFile masterkeyFile(CloudFolder vaultLocation, UnverifiedVaultConfig unverifiedVaultConfig) throws BackendException { - String path = unverifiedVaultConfig.getKeyId().getSchemeSpecificPart(); - if (!path.equals(MASTERKEY_FILE_NAME)) { - throw new UnsupportedMasterkeyLocationException(unverifiedVaultConfig); - } - return cloudContentRepository.file(vaultLocation, path); - } - - private CloudFile legacyMasterkeyFile(CloudFolder location) throws BackendException { - return cloudContentRepository.file(location, MASTERKEY_FILE_NAME); - } - - private UnlockTokenImpl createUnlockToken(Vault vault, CloudFile location) throws BackendException { - byte[] keyFileData = readKeyFileData(location); - UnlockTokenImpl unlockToken = new UnlockTokenImpl(vault, keyFileData); - return unlockToken; - } - - private byte[] readKeyFileData(CloudFile masterkeyFile) throws BackendException { - ByteArrayOutputStream data = new ByteArrayOutputStream(); - cloudContentRepository.read(masterkeyFile, Optional.empty(), data, NO_OP_PROGRESS_AWARE); - return data.toByteArray(); - } - - // Visible for testing - Cryptor cryptorFor(Masterkey keyFile, VaultCipherCombo vaultCipherCombo) { - return vaultCipherCombo.getCryptorProvider(secureRandom).withKey(keyFile); - } - - @Override - public boolean isVaultPasswordValid(Vault vault, Optional unverifiedVaultConfig, CharSequence password) throws BackendException { - try { - // create a cryptor, which checks the password, then destroy it immediately - UnlockTokenImpl unlockToken = createUnlockToken(vault, unverifiedVaultConfig); - Masterkey masterkey = unlockToken.getKeyFile(password); - VaultCipherCombo vaultCipherCombo; - if (unverifiedVaultConfig.isPresent()) { - VaultConfig vaultConfig = VaultConfig.verify(masterkey.getEncoded(), unverifiedVaultConfig.get()); - assertVaultVersionIsSupported(vaultConfig.getVaultFormat()); - vaultCipherCombo = vaultConfig.getCipherCombo(); - } else { - int vaultVersion = MasterkeyFileAccess.readAllegedVaultVersion(unlockToken.keyFileData); - assertLegacyVaultVersionIsSupported(vaultVersion); - vaultCipherCombo = SIV_CTRMAC; - } - cryptorFor(masterkey, vaultCipherCombo).destroy(); - return true; - } catch (InvalidPassphraseException e) { - return false; - } catch (IOException e) { - throw new FatalBackendException(e); - } - } - - @Override - public void lock(Vault vault) { - cryptoCloudContentRepositoryFactory.deregisterCryptor(vault); - } - - private void assertVaultVersionIsSupported(int version) { - if (version < MIN_VAULT_VERSION) { - throw new UnsupportedVaultFormatException(version, MIN_VAULT_VERSION); - } else if (version > MAX_VAULT_VERSION) { - throw new UnsupportedVaultFormatException(version, MAX_VAULT_VERSION); - } - } - - private void assertLegacyVaultVersionIsSupported(int version) { - if (version < MIN_VAULT_VERSION) { - throw new UnsupportedVaultFormatException(version, MIN_VAULT_VERSION); - } else if (version > MAX_VAULT_VERSION_WITHOUT_VAULT_CONFIG) { - throw new UnsupportedVaultFormatException(version, MAX_VAULT_VERSION_WITHOUT_VAULT_CONFIG); - } - } - - @Override - public void changePassword(Vault vault, Optional unverifiedVaultConfig, String oldPassword, String newPassword) throws BackendException { - CloudFolder vaultLocation = vaultLocation(vault); - CloudFile masterkeyFile; - if (unverifiedVaultConfig.isPresent()) { - masterkeyFile = masterkeyFile(vaultLocation, unverifiedVaultConfig.get()); - } else { - masterkeyFile = legacyMasterkeyFile(vaultLocation); - } - - ByteArrayOutputStream dataOutputStream = new ByteArrayOutputStream(); - cloudContentRepository.read(masterkeyFile, Optional.empty(), dataOutputStream, NO_OP_PROGRESS_AWARE); - byte[] data = dataOutputStream.toByteArray(); - - int vaultVersion; - if (unverifiedVaultConfig.isPresent()) { - vaultVersion = unverifiedVaultConfig.get().getVaultFormat(); - assertVaultVersionIsSupported(vaultVersion); - } else { - try { - vaultVersion = MasterkeyFileAccess.readAllegedVaultVersion(data); - assertLegacyVaultVersionIsSupported(vaultVersion); - } catch (IOException e) { - throw new FatalBackendException("Failed to read legacy vault version", e); - } - } - - createBackupMasterKeyFile(data, masterkeyFile); - createNewMasterKeyFile(data, vaultVersion, oldPassword, newPassword, masterkeyFile); - } - - private CloudFolder vaultLocation(Vault vault) throws BackendException { - return cloudContentRepository.resolve(vault.getCloud(), vault.getPath()); - } - - private void createBackupMasterKeyFile(byte[] data, CloudFile masterkeyFile) throws BackendException { - cloudContentRepository.write(masterkeyBackupFile(masterkeyFile, data), ByteArrayDataSource.from(data), NO_OP_PROGRESS_AWARE, true, data.length); - } - - private CloudFile masterkeyBackupFile(CloudFile masterkeyFile, byte[] data) throws BackendException { - String fileName = masterkeyFile.getName() + BackupFileIdSuffixGenerator.generate(data) + MASTERKEY_BACKUP_FILE_EXT; - return cloudContentRepository.file(masterkeyFile.getParent(), fileName); - } - - private void createNewMasterKeyFile(byte[] data, int vaultVersion, String oldPassword, String newPassword, CloudFile masterkeyFile) throws BackendException { - try { - byte[] newMasterKeyFile = new MasterkeyFileAccess(PEPPER, secureRandom) // - .changePassphrase(data, normalizePassword(oldPassword, vaultVersion), normalizePassword(newPassword, vaultVersion)); - cloudContentRepository.write(masterkeyFile, // - ByteArrayDataSource.from(newMasterKeyFile), // - NO_OP_PROGRESS_AWARE, // - true, // - newMasterKeyFile.length); - } catch (IOException e) { - throw new FatalBackendException("Failed to read legacy vault version", e); - } - } - - private CharSequence normalizePassword(CharSequence password, int vaultVersion) { - if (vaultVersion >= VERSION_WITH_NORMALIZED_PASSWORDS) { - return normalize(password, Normalizer.Form.NFC); - } else { - return password; - } - } - - static class UnlockTokenImpl implements UnlockToken { - - private final Vault vault; - private final byte[] keyFileData; - - UnlockTokenImpl(Vault vault, byte[] keyFileData) { - this.vault = vault; - this.keyFileData = keyFileData; - } - - @Override - public Vault getVault() { - return vault; - } - - public Masterkey getKeyFile(CharSequence password) throws IOException { - return new MasterkeyFileAccess(PEPPER, new SecureRandom()).load(new ByteArrayInputStream(keyFileData), password); - } - } - -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProvider.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProvider.kt new file mode 100644 index 00000000..e7c214e2 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProvider.kt @@ -0,0 +1,293 @@ +package org.cryptomator.data.cloud.crypto + +import com.google.common.base.Optional +import org.cryptomator.cryptolib.api.Cryptor +import org.cryptomator.cryptolib.api.CryptorProvider +import org.cryptomator.cryptolib.api.InvalidPassphraseException +import org.cryptomator.cryptolib.api.Masterkey +import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException +import org.cryptomator.cryptolib.common.MasterkeyFileAccess +import org.cryptomator.data.cloud.crypto.VaultConfig.Companion.createVaultConfig +import org.cryptomator.data.cloud.crypto.VaultConfig.Companion.verify +import org.cryptomator.data.cloud.crypto.VaultConfig.VaultConfigBuilder +import org.cryptomator.domain.Cloud +import org.cryptomator.domain.CloudFile +import org.cryptomator.domain.CloudFolder +import org.cryptomator.domain.CloudNode +import org.cryptomator.domain.UnverifiedVaultConfig +import org.cryptomator.domain.Vault +import org.cryptomator.domain.exception.BackendException +import org.cryptomator.domain.exception.CancellationException +import org.cryptomator.domain.exception.FatalBackendException +import org.cryptomator.domain.exception.vaultconfig.UnsupportedMasterkeyLocationException +import org.cryptomator.domain.repository.CloudContentRepository +import org.cryptomator.domain.usecases.ProgressAware +import org.cryptomator.domain.usecases.cloud.ByteArrayDataSource.Companion.from +import org.cryptomator.domain.usecases.cloud.Flag +import org.cryptomator.domain.usecases.vault.UnlockToken +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.net.URI +import java.nio.charset.StandardCharsets +import java.security.SecureRandom +import java.text.Normalizer + +class MasterkeyCryptoCloudProvider( + private val cloudContentRepository: CloudContentRepository, // + private val cryptoCloudContentRepositoryFactory: CryptoCloudContentRepositoryFactory, // + private val secureRandom: SecureRandom +) : CryptoCloudProvider { + + @Throws(BackendException::class) + override fun create(location: CloudFolder, password: CharSequence) { + // Just for testing (id in VaultConfig is auto generated which makes sense while creating a vault but not for testing) + create(location, password, createVaultConfig()) + } + + // Visible for testing + @Throws(BackendException::class) + fun create(location: CloudFolder, password: CharSequence?, vaultConfigBuilder: VaultConfigBuilder) { + // 1. write masterkey: + val masterkey = Masterkey.generate(secureRandom) + try { + ByteArrayOutputStream().use { data -> + MasterkeyFileAccess(CryptoConstants.PEPPER, secureRandom).persist(masterkey, data, password, CryptoConstants.DEFAULT_MASTERKEY_FILE_VERSION) + cloudContentRepository.write(legacyMasterkeyFile(location), from(data.toByteArray()), ProgressAware.NO_OP_PROGRESS_AWARE_UPLOAD, false, data.size().toLong()) + } + } catch (e: IOException) { + throw FatalBackendException("Failed to write masterkey", e) + } + + // 2. initialize vault: + val vaultConfig = vaultConfigBuilder // + .vaultFormat(CryptoConstants.MAX_VAULT_VERSION) // + .cipherCombo(CryptoConstants.DEFAULT_CIPHER_COMBO) // + .keyId(URI.create(String.format("%s:%s", CryptoConstants.MASTERKEY_SCHEME, CryptoConstants.MASTERKEY_FILE_NAME))) // + .shorteningThreshold(CryptoConstants.DEFAULT_MAX_FILE_NAME) // + .build() + val encodedVaultConfig = vaultConfig.toToken(masterkey.encoded).toByteArray(StandardCharsets.UTF_8) + val vaultFile = cloudContentRepository.file(location, CryptoConstants.VAULT_FILE_NAME) + cloudContentRepository.write(vaultFile, from(encodedVaultConfig), ProgressAware.NO_OP_PROGRESS_AWARE_UPLOAD, false, encodedVaultConfig.size.toLong()) + + // 3. create root folder: + createRootFolder(location, cryptorFor(masterkey, vaultConfig.cipherCombo)) + } + + @Throws(BackendException::class) + private fun createRootFolder(location: CloudFolder, cryptor: Cryptor) { + var dFolder = cloudContentRepository.folder(location, CryptoConstants.DATA_DIR_NAME) + dFolder = cloudContentRepository.create(dFolder) + val rootDirHash = cryptor.fileNameCryptor().hashDirectoryId(CryptoConstants.ROOT_DIR_ID) + var lvl1Folder = cloudContentRepository.folder(dFolder, rootDirHash.substring(0, 2)) + lvl1Folder = cloudContentRepository.create(lvl1Folder) + val lvl2Folder = cloudContentRepository.folder(lvl1Folder, rootDirHash.substring(2)) + cloudContentRepository.create(lvl2Folder) + } + + @Throws(BackendException::class) + override fun unlock(vault: Vault, unverifiedVaultConfig: Optional, password: CharSequence, cancelledFlag: Flag): Vault { + return unlock(createUnlockToken(vault, unverifiedVaultConfig), unverifiedVaultConfig, password, cancelledFlag) + } + + @Throws(BackendException::class) + override fun unlock(token: UnlockToken, unverifiedVaultConfig: Optional, password: CharSequence, cancelledFlag: Flag): Vault { + val impl = token as UnlockTokenImpl + return try { + val masterkey = impl.getKeyFile(password) + val vaultFormat: Int + val shorteningThreshold: Int + val cryptor: Cryptor + if (unverifiedVaultConfig.isPresent) { + val vaultConfig = verify(masterkey.encoded, unverifiedVaultConfig.get()) + vaultFormat = vaultConfig.vaultFormat + assertVaultVersionIsSupported(vaultConfig.vaultFormat) + shorteningThreshold = vaultConfig.shorteningThreshold + cryptor = cryptorFor(masterkey, vaultConfig.cipherCombo) + } else { + vaultFormat = MasterkeyFileAccess.readAllegedVaultVersion(impl.keyFileData) + assertLegacyVaultVersionIsSupported(vaultFormat) + shorteningThreshold = if (vaultFormat > 6) CryptoConstants.DEFAULT_MAX_FILE_NAME else CryptoImplVaultFormatPre7.SHORTENING_THRESHOLD + cryptor = cryptorFor(masterkey, CryptorProvider.Scheme.SIV_CTRMAC) + } + if (cancelledFlag.get()) { + throw CancellationException() + } + val vault = Vault.aCopyOf(token.vault) // + .withUnlocked(true) // + .withFormat(vaultFormat) // + .withShorteningThreshold(shorteningThreshold) // + .build() + cryptoCloudContentRepositoryFactory.registerCryptor(vault, cryptor) + vault + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun createUnlockToken(vault: Vault, unverifiedVaultConfig: Optional): UnlockTokenImpl { + val vaultLocation = vaultLocation(vault) + return if (unverifiedVaultConfig.isPresent) { + createUnlockToken(vault, masterkeyFile(vaultLocation, unverifiedVaultConfig.get())) + } else { + createUnlockToken(vault, legacyMasterkeyFile(vaultLocation)) + } + } + + @Throws(BackendException::class) + private fun masterkeyFile(vaultLocation: CloudFolder, unverifiedVaultConfig: UnverifiedVaultConfig): CloudFile { + val path = unverifiedVaultConfig.keyId.schemeSpecificPart + if (path != CryptoConstants.MASTERKEY_FILE_NAME) { + throw UnsupportedMasterkeyLocationException(unverifiedVaultConfig) + } + return cloudContentRepository.file(vaultLocation, path) + } + + @Throws(BackendException::class) + private fun legacyMasterkeyFile(location: CloudFolder): CloudFile { + return cloudContentRepository.file(location, CryptoConstants.MASTERKEY_FILE_NAME) + } + + @Throws(BackendException::class) + private fun createUnlockToken(vault: Vault, location: CloudFile): UnlockTokenImpl { + val keyFileData = readKeyFileData(location) + return UnlockTokenImpl(vault, keyFileData) + } + + @Throws(BackendException::class) + private fun readKeyFileData(masterkeyFile: CloudFile): ByteArray { + val data = ByteArrayOutputStream() + cloudContentRepository.read(masterkeyFile, null, data, ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD) + return data.toByteArray() + } + + // Visible for testing + fun cryptorFor(keyFile: Masterkey?, vaultCipherCombo: CryptorProvider.Scheme): Cryptor { + return CryptorProvider.forScheme(vaultCipherCombo).provide(keyFile, secureRandom) + } + + @Throws(BackendException::class) + override fun isVaultPasswordValid(vault: Vault, unverifiedVaultConfig: Optional, password: CharSequence): Boolean { + return try { + // create a cryptor, which checks the password, then destroy it immediately + val unlockToken = createUnlockToken(vault, unverifiedVaultConfig) + val masterkey = unlockToken.getKeyFile(password) + val vaultCipherCombo = if (unverifiedVaultConfig.isPresent) { + val vaultConfig = verify(masterkey.encoded, unverifiedVaultConfig.get()) + assertVaultVersionIsSupported(vaultConfig.vaultFormat) + vaultConfig.cipherCombo + } else { + val vaultVersion = MasterkeyFileAccess.readAllegedVaultVersion(unlockToken.keyFileData) + assertLegacyVaultVersionIsSupported(vaultVersion) + CryptorProvider.Scheme.SIV_CTRMAC + } + cryptorFor(masterkey, vaultCipherCombo).destroy() + true + } catch (e: InvalidPassphraseException) { + false + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + + override fun lock(vault: Vault) { + cryptoCloudContentRepositoryFactory.deregisterCryptor(vault) + } + + private fun assertVaultVersionIsSupported(version: Int) { + if (version < CryptoConstants.MIN_VAULT_VERSION) { + throw UnsupportedVaultFormatException(version, CryptoConstants.MIN_VAULT_VERSION) + } else if (version > CryptoConstants.MAX_VAULT_VERSION) { + throw UnsupportedVaultFormatException(version, CryptoConstants.MAX_VAULT_VERSION) + } + } + + private fun assertLegacyVaultVersionIsSupported(version: Int) { + if (version < CryptoConstants.MIN_VAULT_VERSION) { + throw UnsupportedVaultFormatException(version, CryptoConstants.MIN_VAULT_VERSION) + } else if (version > CryptoConstants.MAX_VAULT_VERSION_WITHOUT_VAULT_CONFIG) { + throw UnsupportedVaultFormatException(version, CryptoConstants.MAX_VAULT_VERSION_WITHOUT_VAULT_CONFIG) + } + } + + @Throws(BackendException::class) + override fun changePassword(vault: Vault, unverifiedVaultConfig: Optional, oldPassword: String, newPassword: String) { + val vaultLocation = vaultLocation(vault) + val masterkeyFile = if (unverifiedVaultConfig.isPresent) { + masterkeyFile(vaultLocation, unverifiedVaultConfig.get()) + } else { + legacyMasterkeyFile(vaultLocation) + } + val dataOutputStream = ByteArrayOutputStream() + cloudContentRepository.read(masterkeyFile, null, dataOutputStream, ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD) + val data = dataOutputStream.toByteArray() + val vaultVersion: Int + if (unverifiedVaultConfig.isPresent) { + vaultVersion = unverifiedVaultConfig.get().vaultFormat + assertVaultVersionIsSupported(vaultVersion) + } else { + try { + vaultVersion = MasterkeyFileAccess.readAllegedVaultVersion(data) + assertLegacyVaultVersionIsSupported(vaultVersion) + } catch (e: IOException) { + throw FatalBackendException("Failed to read legacy vault version", e) + } + } + createBackupMasterKeyFile(data, masterkeyFile) + createNewMasterKeyFile(data, vaultVersion, oldPassword, newPassword, masterkeyFile) + } + + @Throws(BackendException::class) + private fun vaultLocation(vault: Vault): CloudFolder { + return cloudContentRepository.resolve(vault.cloud, vault.path) + } + + @Throws(BackendException::class) + private fun createBackupMasterKeyFile(data: ByteArray, masterkeyFile: CloudFile) { + cloudContentRepository.write(masterkeyBackupFile(masterkeyFile, data), from(data), ProgressAware.NO_OP_PROGRESS_AWARE_UPLOAD, true, data.size.toLong()) + } + + @Throws(BackendException::class) + private fun masterkeyBackupFile(masterkeyFile: CloudFile, data: ByteArray): CloudFile { + val fileName = masterkeyFile.name + BackupFileIdSuffixGenerator.generate(data) + CryptoConstants.MASTERKEY_BACKUP_FILE_EXT + return cloudContentRepository.file(masterkeyFile.parent, fileName) + } + + @Throws(BackendException::class) + private fun createNewMasterKeyFile(data: ByteArray, vaultVersion: Int, oldPassword: String, newPassword: String, masterkeyFile: CloudFile) { + try { + val newMasterKeyFile = MasterkeyFileAccess(CryptoConstants.PEPPER, secureRandom) // + .changePassphrase(data, normalizePassword(oldPassword, vaultVersion), normalizePassword(newPassword, vaultVersion)) + cloudContentRepository.write( + masterkeyFile, // + from(newMasterKeyFile), // + ProgressAware.NO_OP_PROGRESS_AWARE_UPLOAD, // + true, // + newMasterKeyFile.size.toLong() + ) + } catch (e: IOException) { + throw FatalBackendException("Failed to read legacy vault version", e) + } + } + + private fun normalizePassword(password: CharSequence, vaultVersion: Int): CharSequence { + return if (vaultVersion >= CryptoConstants.VERSION_WITH_NORMALIZED_PASSWORDS) { + Normalizer.normalize(password, Normalizer.Form.NFC) + } else { + password + } + } + + class UnlockTokenImpl(private val vault: Vault, val keyFileData: ByteArray) : UnlockToken { + + override fun getVault(): Vault { + return vault + } + + @Throws(IOException::class) + fun getKeyFile(password: CharSequence?): Masterkey { + return MasterkeyFileAccess(CryptoConstants.PEPPER, SecureRandom()).load(ByteArrayInputStream(keyFileData), password) + } + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/RootCryptoFolder.java b/data/src/main/java/org/cryptomator/data/cloud/crypto/RootCryptoFolder.java deleted file mode 100644 index f1fee965..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/RootCryptoFolder.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.cryptomator.data.cloud.crypto; - -import org.cryptomator.domain.Cloud; - -class RootCryptoFolder extends CryptoFolder { - - private final CryptoCloud cloud; - - public RootCryptoFolder(CryptoCloud cloud) { - super(null, "", "", null); - this.cloud = cloud; - } - - public static boolean isRoot(CryptoFolder folder) { - return folder instanceof RootCryptoFolder; - } - - @Override - public Cloud getCloud() { - return cloud; - } - - @Override - public CryptoFolder withCloud(Cloud cloud) { - return new RootCryptoFolder((CryptoCloud) cloud); - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/RootCryptoFolder.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/RootCryptoFolder.kt new file mode 100644 index 00000000..cbecd053 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/RootCryptoFolder.kt @@ -0,0 +1,18 @@ +package org.cryptomator.data.cloud.crypto + +import org.cryptomator.domain.Cloud + +class RootCryptoFolder(override val cloud: CryptoCloud) : CryptoFolder(null, "", "", null) { + + override fun withCloud(cloud: Cloud?): CryptoFolder { + return RootCryptoFolder(cloud as CryptoCloud) + } + + companion object { + + @JvmStatic + fun isRoot(folder: CryptoFolder): Boolean { + return folder is RootCryptoFolder + } + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/VaultCipherCombo.java b/data/src/main/java/org/cryptomator/data/cloud/crypto/VaultCipherCombo.java deleted file mode 100644 index 07096eba..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/VaultCipherCombo.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.cryptomator.data.cloud.crypto; - -import org.cryptomator.cryptolib.Cryptors; -import org.cryptomator.cryptolib.api.CryptorProvider; - -import java.security.SecureRandom; -import java.util.function.Function; - -/** - * A combination of different ciphers and/or cipher modes in a Cryptomator vault. - */ -public enum VaultCipherCombo { - /** - * AES-SIV for file name encryption - * AES-CTR + HMAC for content encryption - */ - SIV_CTRMAC(Cryptors::version1), - - /** - * AES-SIV for file name encryption - * AES-GCM for content encryption - */ - SIV_GCM(Cryptors::version2); - - private final Function cryptorProvider; - - VaultCipherCombo(Function cryptorProvider) { - this.cryptorProvider = cryptorProvider; - } - - public CryptorProvider getCryptorProvider(SecureRandom csprng) { - return cryptorProvider.apply(csprng); - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/VaultConfig.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/VaultConfig.kt index 993b7f2c..d361fdd7 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/VaultConfig.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/VaultConfig.kt @@ -1,5 +1,6 @@ package org.cryptomator.data.cloud.crypto +import org.cryptomator.cryptolib.api.CryptorProvider import org.cryptomator.domain.UnverifiedVaultConfig import org.cryptomator.domain.exception.vaultconfig.VaultConfigLoadException import org.cryptomator.domain.exception.vaultconfig.VaultKeyInvalidException @@ -23,7 +24,7 @@ class VaultConfig private constructor(builder: VaultConfigBuilder) { val keyId: URI val id: String val vaultFormat: Int - val cipherCombo: VaultCipherCombo + val cipherCombo: CryptorProvider.Scheme val shorteningThreshold: Int fun toToken(rawKey: ByteArray): String { @@ -41,7 +42,7 @@ class VaultConfig private constructor(builder: VaultConfigBuilder) { internal var id: String = UUID.randomUUID().toString() internal var vaultFormat = CryptoConstants.MAX_VAULT_VERSION; - internal var cipherCombo = VaultCipherCombo.SIV_CTRMAC + internal var cipherCombo = CryptoConstants.DEFAULT_CIPHER_COMBO internal var shorteningThreshold = CryptoConstants.DEFAULT_MAX_FILE_NAME; lateinit var keyId: URI @@ -50,7 +51,7 @@ class VaultConfig private constructor(builder: VaultConfigBuilder) { return this } - fun cipherCombo(cipherCombo: VaultCipherCombo): VaultConfigBuilder { + fun cipherCombo(cipherCombo: CryptorProvider.Scheme): VaultConfigBuilder { this.cipherCombo = cipherCombo return this } @@ -110,7 +111,7 @@ class VaultConfig private constructor(builder: VaultConfigBuilder) { val vaultConfigBuilder = createVaultConfig() // .keyId(unverifiedVaultConfig.keyId) .id(parser.header[JSON_KEY_ID] as String) // - .cipherCombo(VaultCipherCombo.valueOf(parser.body.get(JSON_KEY_CIPHERCONFIG, String::class.java))) // + .cipherCombo(CryptorProvider.Scheme.valueOf(parser.body.get(JSON_KEY_CIPHERCONFIG, String::class.java))) // .vaultFormat(unverifiedVaultConfig.vaultFormat) // .shorteningThreshold(parser.body[JSON_KEY_SHORTENING_THRESHOLD] as Int) diff --git a/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxClientFactory.java b/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxClientFactory.java deleted file mode 100644 index 78e403b6..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxClientFactory.java +++ /dev/null @@ -1,56 +0,0 @@ -package org.cryptomator.data.cloud.dropbox; - -import android.content.Context; - -import com.dropbox.core.DbxRequestConfig; -import com.dropbox.core.http.OkHttp3Requestor; -import com.dropbox.core.v2.DbxClientV2; - -import org.cryptomator.data.BuildConfig; -import org.cryptomator.data.cloud.okhttplogging.HttpLoggingInterceptor; - -import java.util.Locale; - -import okhttp3.Interceptor; -import okhttp3.OkHttpClient; -import timber.log.Timber; - -import static org.cryptomator.data.util.NetworkTimeout.CONNECTION; -import static org.cryptomator.data.util.NetworkTimeout.READ; -import static org.cryptomator.data.util.NetworkTimeout.WRITE; - -class DropboxClientFactory { - - private DbxClientV2 sDbxClient; - - private static Interceptor httpLoggingInterceptor(Context context) { - return new HttpLoggingInterceptor(message -> Timber.tag("OkHttp").d(message), context); - } - - public DbxClientV2 getClient(String accessToken, Context context) { - if (sDbxClient == null) { - sDbxClient = createDropboxClient(accessToken, context); - } - return sDbxClient; - } - - private DbxClientV2 createDropboxClient(String accessToken, Context context) { - String userLocale = Locale.getDefault().toString(); - - OkHttpClient okHttpClient = new OkHttpClient() // - .newBuilder() // - .connectTimeout(CONNECTION.getTimeout(), CONNECTION.getUnit()) // - .readTimeout(READ.getTimeout(), READ.getUnit()) // - .writeTimeout(WRITE.getTimeout(), WRITE.getUnit()) // - .addInterceptor(httpLoggingInterceptor(context)) // - .build(); - - DbxRequestConfig requestConfig = DbxRequestConfig // - .newBuilder("Cryptomator-Android/" + BuildConfig.VERSION_NAME) // - .withUserLocale(userLocale) // - .withHttpRequestor(new OkHttp3Requestor(okHttpClient)) // - .build(); - - return new DbxClientV2(requestConfig, accessToken); - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxClientFactory.kt b/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxClientFactory.kt new file mode 100644 index 00000000..6458d4da --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxClientFactory.kt @@ -0,0 +1,60 @@ +package org.cryptomator.data.cloud.dropbox + +import android.content.Context +import com.dropbox.core.DbxRequestConfig +import com.dropbox.core.http.OkHttp3Requestor +import com.dropbox.core.v2.DbxClientV2 +import org.cryptomator.data.BuildConfig +import org.cryptomator.data.cloud.okhttplogging.HttpLoggingInterceptor +import org.cryptomator.data.util.NetworkTimeout +import org.cryptomator.util.crypto.CredentialCryptor +import java.util.Locale +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import timber.log.Timber + +class DropboxClientFactory { + + companion object { + + @Volatile + private var instance: DbxClientV2? = null + + @Synchronized + fun getInstance(accessToken: String, context: Context): DbxClientV2 = instance ?: createDropboxClient(decrypt(accessToken, context), context).also { instance = it } + + private fun decrypt(password: String, context: Context): String { + return CredentialCryptor.getInstance(context).decrypt(password) + } + + private fun createDropboxClient(accessToken: String, context: Context): DbxClientV2 { + val userLocale = Locale.getDefault().toString() + + val okHttpClient = OkHttpClient() // + .newBuilder() // + .connectTimeout(NetworkTimeout.CONNECTION.timeout, NetworkTimeout.CONNECTION.unit) // + .readTimeout(NetworkTimeout.READ.timeout, NetworkTimeout.READ.unit) // + .writeTimeout(NetworkTimeout.WRITE.timeout, NetworkTimeout.WRITE.unit) // + .addInterceptor(httpLoggingInterceptor(context)) // + .build() + + val requestConfig = DbxRequestConfig // + .newBuilder("Cryptomator-Android/" + BuildConfig.VERSION_NAME) // + .withUserLocale(userLocale) // + .withHttpRequestor(OkHttp3Requestor(okHttpClient)) // + .build() + + return DbxClientV2(requestConfig, accessToken) + } + + private fun httpLoggingInterceptor(context: Context): Interceptor { + val logger = object : HttpLoggingInterceptor.Logger { + override fun log(message: String) { + Timber.tag("OkHttp").d(message) + } + } + return HttpLoggingInterceptor(logger, context) + } + + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxCloudContentRepository.java b/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxCloudContentRepository.java deleted file mode 100644 index 8bae83e9..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxCloudContentRepository.java +++ /dev/null @@ -1,213 +0,0 @@ -package org.cryptomator.data.cloud.dropbox; - -import android.content.Context; - -import com.dropbox.core.DbxException; -import com.dropbox.core.InvalidAccessTokenException; -import com.dropbox.core.NetworkIOException; -import com.dropbox.core.v2.files.CreateFolderErrorException; -import com.dropbox.core.v2.files.DeleteErrorException; -import com.dropbox.core.v2.files.DownloadErrorException; -import com.dropbox.core.v2.files.ListFolderErrorException; -import com.dropbox.core.v2.files.RelocationErrorException; - -import org.cryptomator.data.cloud.InterceptingCloudContentRepository; -import org.cryptomator.domain.DropboxCloud; -import org.cryptomator.domain.exception.BackendException; -import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException; -import org.cryptomator.domain.exception.FatalBackendException; -import org.cryptomator.domain.exception.NetworkConnectionException; -import org.cryptomator.domain.exception.NoSuchCloudFileException; -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; -import static org.cryptomator.util.ExceptionUtil.extract; - -class DropboxCloudContentRepository extends InterceptingCloudContentRepository { - - private final DropboxCloud cloud; - - public DropboxCloudContentRepository(DropboxCloud 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, NetworkIOException.class)) { - throw new NetworkConnectionException(e); - } - } - - private void throwWrongCredentialsExceptionIfRequired(Exception e) { - if (contains(e, InvalidAccessTokenException.class)) { - throw new WrongCredentialsException(cloud); - } - } - - private static class Intercepted implements CloudContentRepository { - - private final DropboxImpl cloud; - - public Intercepted(DropboxCloud cloud, Context context) { - this.cloud = new DropboxImpl(cloud, context); - } - - public DropboxFolder root(DropboxCloud cloud) { - return this.cloud.root(); - } - - @Override - public DropboxFolder resolve(DropboxCloud cloud, String path) { - return this.cloud.resolve(path); - } - - @Override - public DropboxFile file(DropboxFolder parent, String name) { - return cloud.file(parent, name); - } - - @Override - public DropboxFile file(DropboxFolder parent, String name, Optional size) throws BackendException { - return cloud.file(parent, name, size); - } - - @Override - public DropboxFolder folder(DropboxFolder parent, String name) { - return cloud.folder(parent, name); - } - - @Override - public boolean exists(DropboxNode node) throws BackendException { - try { - return cloud.exists(node); - } catch (DbxException e) { - throw new FatalBackendException(e); - } - } - - @Override - public List list(DropboxFolder folder) throws BackendException { - try { - return cloud.list(folder); - } catch (DbxException e) { - if (e instanceof ListFolderErrorException) { - if (((ListFolderErrorException) e).errorValue.getPathValue().isNotFound()) { - throw new NoSuchCloudFileException(); - } - } - throw new FatalBackendException(e); - } - } - - @Override - public DropboxFolder create(DropboxFolder folder) throws BackendException { - try { - return cloud.create(folder); - } catch (DbxException e) { - if (e instanceof CreateFolderErrorException) { - throw new CloudNodeAlreadyExistsException(folder.getName()); - } - throw new FatalBackendException(e); - } - } - - @Override - public DropboxFolder move(DropboxFolder source, DropboxFolder target) throws BackendException { - try { - return (DropboxFolder) cloud.move(source, target); - } catch (DbxException e) { - if (e instanceof RelocationErrorException) { - if (extract(e, RelocationErrorException.class).get().errorValue.isFromLookup()) { - throw new NoSuchCloudFileException(source.getName()); - } - throw new CloudNodeAlreadyExistsException(target.getName()); - } - throw new FatalBackendException(e); - } - } - - @Override - public DropboxFile move(DropboxFile source, DropboxFile target) throws BackendException { - try { - return (DropboxFile) cloud.move(source, target); - } catch (DbxException e) { - if (e instanceof RelocationErrorException) { - throw new CloudNodeAlreadyExistsException(target.getName()); - } - throw new FatalBackendException(e); - } - } - - @Override - public DropboxFile write(DropboxFile uploadFile, DataSource data, ProgressAware progressAware, boolean replace, long size) throws BackendException { - try { - return cloud.write(uploadFile, data, progressAware, replace, size); - } catch (IOException | DbxException e) { - if (contains(e, NoSuchCloudFileException.class)) { - throw new NoSuchCloudFileException(uploadFile.getName()); - } - throw new FatalBackendException(e); - } - } - - @Override - public void read(DropboxFile file, Optional encryptedTmpFile, OutputStream data, ProgressAware progressAware) throws BackendException { - try { - cloud.read(file, encryptedTmpFile, data, progressAware); - } catch (IOException | DbxException e) { - if (contains(e, DownloadErrorException.class)) { - if (extract(e, DownloadErrorException.class).get().errorValue.getPathValue().isNotFound()) { - throw new NoSuchCloudFileException(file.getName()); - } - } - throw new FatalBackendException(e); - } - } - - @Override - public void delete(DropboxNode node) throws BackendException { - try { - cloud.delete(node); - } catch (DbxException e) { - if (contains(e, DeleteErrorException.class)) { - if (extract(e, DeleteErrorException.class).get().errorValue.getPathLookupValue().isNotFound()) { - throw new NoSuchCloudFileException(node.getName()); - } - } - throw new FatalBackendException(e); - } - } - - @Override - public String checkAuthenticationAndRetrieveCurrentAccount(DropboxCloud cloud) throws BackendException { - try { - return this.cloud.currentAccount(); - } catch (DbxException e) { - throw new FatalBackendException(e); - } - } - - @Override - public void logout(DropboxCloud cloud) throws BackendException { - // empty - } - } - -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxCloudContentRepository.kt b/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxCloudContentRepository.kt new file mode 100644 index 00000000..3a66f108 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxCloudContentRepository.kt @@ -0,0 +1,205 @@ +package org.cryptomator.data.cloud.dropbox + +import android.content.Context +import com.dropbox.core.DbxException +import com.dropbox.core.InvalidAccessTokenException +import com.dropbox.core.NetworkIOException +import com.dropbox.core.v2.files.CreateFolderErrorException +import com.dropbox.core.v2.files.DeleteErrorException +import com.dropbox.core.v2.files.DownloadErrorException +import com.dropbox.core.v2.files.ListFolderErrorException +import com.dropbox.core.v2.files.RelocationErrorException +import org.cryptomator.data.cloud.InterceptingCloudContentRepository +import org.cryptomator.domain.DropboxCloud +import org.cryptomator.domain.exception.BackendException +import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException +import org.cryptomator.domain.exception.FatalBackendException +import org.cryptomator.domain.exception.NetworkConnectionException +import org.cryptomator.domain.exception.NoSuchCloudFileException +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.ExceptionUtil +import java.io.File +import java.io.IOException +import java.io.OutputStream + +internal class DropboxCloudContentRepository(private val cloud: DropboxCloud, context: Context) : InterceptingCloudContentRepository(Intercepted(cloud, context)){ + + @Throws(BackendException::class) + override fun throwWrappedIfRequired(e: Exception) { + throwConnectionErrorIfRequired(e) + throwWrongCredentialsExceptionIfRequired(e) + } + + @Throws(NetworkConnectionException::class) + private fun throwConnectionErrorIfRequired(e: Exception) { + if (ExceptionUtil.contains(e, NetworkIOException::class.java)) { + throw NetworkConnectionException(e) + } + } + + private fun throwWrongCredentialsExceptionIfRequired(e: Exception) { + if (ExceptionUtil.contains(e, InvalidAccessTokenException::class.java)) { + throw WrongCredentialsException(cloud) + } + } + + private class Intercepted(cloud: DropboxCloud, context: Context) : CloudContentRepository { + + private val cloud: DropboxImpl = DropboxImpl(cloud, context) + + override fun root(cloud: DropboxCloud): DropboxFolder { + return this.cloud.root() + } + + override fun resolve(cloud: DropboxCloud, path: String): DropboxFolder { + return this.cloud.resolve(path) + } + + override fun file(parent: DropboxFolder, name: String): DropboxFile { + return cloud.file(parent, name, null) + } + + @Throws(BackendException::class) + override fun file(parent: DropboxFolder, name: String, size: Long?): DropboxFile { + return cloud.file(parent, name, size) + } + + override fun folder(parent: DropboxFolder, name: String): DropboxFolder { + return cloud.folder(parent, name) + } + + @Throws(BackendException::class) + override fun exists(node: DropboxNode): Boolean { + return try { + cloud.exists(node) + } catch (e: DbxException) { + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun list(folder: DropboxFolder): List { + return try { + cloud.list(folder) + } catch (e: DbxException) { + if (e is ListFolderErrorException) { + if (e.errorValue.pathValue.isNotFound) { + throw NoSuchCloudFileException() + } + } + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun create(folder: DropboxFolder): DropboxFolder { + return try { + cloud.create(folder) + } catch (e: DbxException) { + if (e is CreateFolderErrorException) { + throw CloudNodeAlreadyExistsException(folder.name) + } + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun move(source: DropboxFolder, target: DropboxFolder): DropboxFolder { + return try { + cloud.move(source, target) as DropboxFolder + } catch (e: DbxException) { + if (e is RelocationErrorException) { + if (ExceptionUtil.extract(e, RelocationErrorException::class.java).get().errorValue.isFromLookup) { + throw NoSuchCloudFileException(source.name) + } + throw CloudNodeAlreadyExistsException(target.name) + } + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun move(source: DropboxFile, target: DropboxFile): DropboxFile { + return try { + cloud.move(source, target) as DropboxFile + } catch (e: DbxException) { + if (e is RelocationErrorException) { + throw CloudNodeAlreadyExistsException(target.name) + } + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun write(file: DropboxFile, data: DataSource, progressAware: ProgressAware, replace: Boolean, size: Long): DropboxFile { + return try { + cloud.write(file, data, progressAware, replace, size) + } catch (e: IOException) { + if (ExceptionUtil.contains(e, NoSuchCloudFileException::class.java)) { + throw NoSuchCloudFileException(file.name) + } + throw FatalBackendException(e) + } catch (e: DbxException) { + if (ExceptionUtil.contains(e, NoSuchCloudFileException::class.java)) { + throw NoSuchCloudFileException(file.name) + } + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun read(file: DropboxFile, encryptedTmpFile: File?, data: OutputStream, progressAware: ProgressAware) { + try { + cloud.read(file, encryptedTmpFile, data, progressAware) + } catch (e: IOException) { + if (ExceptionUtil.contains(e, DownloadErrorException::class.java)) { + if (ExceptionUtil.extract(e, DownloadErrorException::class.java).get().errorValue.pathValue.isNotFound) { + throw NoSuchCloudFileException(file.name) + } + } + throw FatalBackendException(e) + } catch (e: DbxException) { + if (ExceptionUtil.contains(e, DownloadErrorException::class.java)) { + if (ExceptionUtil.extract(e, DownloadErrorException::class.java).get().errorValue.pathValue.isNotFound) { + throw NoSuchCloudFileException(file.name) + } + } + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun delete(node: DropboxNode) { + try { + cloud.delete(node) + } catch (e: DbxException) { + if (ExceptionUtil.contains(e, DeleteErrorException::class.java)) { + if (ExceptionUtil.extract(e, DeleteErrorException::class.java).get().errorValue.pathLookupValue.isNotFound) { + throw NoSuchCloudFileException(node.name) + } + } + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun checkAuthenticationAndRetrieveCurrentAccount(cloud: DropboxCloud): String { + return try { + this.cloud.currentAccount() + } catch (e: DbxException) { + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun logout(cloud: DropboxCloud) { + // empty + } + + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxCloudContentRepositoryFactory.java b/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxCloudContentRepositoryFactory.java index bba187e9..c2abd0f6 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxCloudContentRepositoryFactory.java +++ b/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxCloudContentRepositoryFactory.java @@ -28,7 +28,7 @@ public class DropboxCloudContentRepositoryFactory implements CloudContentReposit } @Override - public CloudContentRepository cloudContentRepositoryFor(Cloud cloud) { + public CloudContentRepository cloudContentRepositoryFor(Cloud cloud) { return new DropboxCloudContentRepository((DropboxCloud) cloud, context); } diff --git a/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxCloudNodeFactory.java b/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxCloudNodeFactory.java deleted file mode 100644 index 6c931587..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxCloudNodeFactory.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.cryptomator.data.cloud.dropbox; - -import com.dropbox.core.v2.files.FileMetadata; -import com.dropbox.core.v2.files.FolderMetadata; -import com.dropbox.core.v2.files.Metadata; - -import org.cryptomator.util.Optional; - -class DropboxCloudNodeFactory { - - public static DropboxFile from(DropboxFolder parent, FileMetadata metadata) { - return new DropboxFile(parent, metadata.getName(), metadata.getPathDisplay(), Optional.ofNullable(metadata.getSize()), Optional.ofNullable(metadata.getServerModified())); - } - - public static DropboxFile file(DropboxFolder parent, String name, Optional size, String path) { - return new DropboxFile(parent, name, path, size, Optional.empty()); - } - - public static DropboxFolder from(DropboxFolder parent, FolderMetadata metadata) { - return new DropboxFolder(parent, metadata.getName(), getNodePath(parent, metadata.getName())); - } - - private static String getNodePath(DropboxFolder parent, String name) { - return parent.getPath() + "/" + name; - } - - public static DropboxFolder folder(DropboxFolder parent, String name, String path) { - return new DropboxFolder(parent, name, path); - } - - public static DropboxNode from(DropboxFolder parent, Metadata metadata) { - if (metadata instanceof FileMetadata) { - return from(parent, (FileMetadata) metadata); - } else { - return from(parent, (FolderMetadata) metadata); - } - } - -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxCloudNodeFactory.kt b/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxCloudNodeFactory.kt new file mode 100644 index 00000000..e7414081 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxCloudNodeFactory.kt @@ -0,0 +1,39 @@ +package org.cryptomator.data.cloud.dropbox + +import com.dropbox.core.v2.files.FileMetadata +import com.dropbox.core.v2.files.FolderMetadata +import com.dropbox.core.v2.files.Metadata + +internal object DropboxCloudNodeFactory { + + fun from(parent: DropboxFolder, metadata: FileMetadata): DropboxFile { + return DropboxFile(parent, metadata.name, metadata.pathDisplay, metadata.size, metadata.serverModified) + } + + @JvmStatic + fun file(parent: DropboxFolder, name: String, size: Long?, path: String): DropboxFile { + return DropboxFile(parent, name, path, size, null) + } + + fun from(parent: DropboxFolder, metadata: FolderMetadata): DropboxFolder { + return DropboxFolder(parent, metadata.name, getNodePath(parent, metadata.name)) + } + + private fun getNodePath(parent: DropboxFolder, name: String): String { + return parent.path + "/" + name + } + + @JvmStatic + fun folder(parent: DropboxFolder?, name: String, path: String): DropboxFolder { + return DropboxFolder(parent, name, path) + } + + @JvmStatic + fun from(parent: DropboxFolder, metadata: Metadata): DropboxNode { + return if (metadata is FileMetadata) { + from(parent, metadata) + } else { + from(parent, metadata as FolderMetadata) + } + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxFile.java b/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxFile.java deleted file mode 100644 index b1fa31b2..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxFile.java +++ /dev/null @@ -1,55 +0,0 @@ -package org.cryptomator.data.cloud.dropbox; - -import org.cryptomator.domain.Cloud; -import org.cryptomator.domain.CloudFile; -import org.cryptomator.util.Optional; - -import java.util.Date; - -class DropboxFile implements CloudFile, DropboxNode { - - private final DropboxFolder parent; - private final String name; - private final String path; - private final Optional size; - private final Optional modified; - - public DropboxFile(DropboxFolder 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 DropboxFolder 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/dropbox/DropboxFile.kt b/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxFile.kt new file mode 100644 index 00000000..037f4d1d --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxFile.kt @@ -0,0 +1,11 @@ +package org.cryptomator.data.cloud.dropbox + +import org.cryptomator.domain.Cloud +import org.cryptomator.domain.CloudFile +import java.util.Date + +internal class DropboxFile(override val parent: DropboxFolder, override val name: String, override val path: String, override val size: Long?, override val modified: Date?) : CloudFile, DropboxNode { + + override val cloud: Cloud? + get() = parent.cloud +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxFolder.java b/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxFolder.java deleted file mode 100644 index c762ff92..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxFolder.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.cryptomator.data.cloud.dropbox; - -import org.cryptomator.domain.Cloud; -import org.cryptomator.domain.CloudFolder; - -class DropboxFolder implements CloudFolder, DropboxNode { - - private final DropboxFolder parent; - private final String name; - private final String path; - - public DropboxFolder(DropboxFolder 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 DropboxFolder getParent() { - return parent; - } - - @Override - public DropboxFolder withCloud(Cloud cloud) { - return new DropboxFolder(parent.withCloud(cloud), name, path); - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxFolder.kt b/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxFolder.kt new file mode 100644 index 00000000..c024e4f2 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxFolder.kt @@ -0,0 +1,14 @@ +package org.cryptomator.data.cloud.dropbox + +import org.cryptomator.domain.Cloud +import org.cryptomator.domain.CloudFolder + +open class DropboxFolder(override val parent: DropboxFolder?, override val name: String, override val path: String) : CloudFolder, DropboxNode { + + override val cloud: Cloud? + get() = parent?.cloud + + override fun withCloud(cloud: Cloud?): DropboxFolder? { + return DropboxFolder(parent?.withCloud(cloud), name, path) + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxImpl.java b/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxImpl.java deleted file mode 100644 index ed9234e1..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxImpl.java +++ /dev/null @@ -1,466 +0,0 @@ -package org.cryptomator.data.cloud.dropbox; - -import android.content.Context; - -import com.dropbox.core.DbxException; -import com.dropbox.core.NetworkIOException; -import com.dropbox.core.RetryException; -import com.dropbox.core.v2.DbxClientV2; -import com.dropbox.core.v2.files.CommitInfo; -import com.dropbox.core.v2.files.CreateFolderResult; -import com.dropbox.core.v2.files.FileMetadata; -import com.dropbox.core.v2.files.FolderMetadata; -import com.dropbox.core.v2.files.GetMetadataErrorException; -import com.dropbox.core.v2.files.ListFolderResult; -import com.dropbox.core.v2.files.Metadata; -import com.dropbox.core.v2.files.RelocationResult; -import com.dropbox.core.v2.files.UploadSessionCursor; -import com.dropbox.core.v2.files.UploadSessionFinishErrorException; -import com.dropbox.core.v2.files.UploadSessionLookupErrorException; -import com.dropbox.core.v2.files.WriteMode; -import com.dropbox.core.v2.users.FullAccount; -import com.tomclaw.cache.DiskLruCache; - -import org.cryptomator.data.util.TransferredBytesAwareInputStream; -import org.cryptomator.data.util.TransferredBytesAwareOutputStream; -import org.cryptomator.domain.CloudFile; -import org.cryptomator.domain.CloudFolder; -import org.cryptomator.domain.CloudNode; -import org.cryptomator.domain.DropboxCloud; -import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException; -import org.cryptomator.domain.exception.FatalBackendException; -import org.cryptomator.domain.exception.authentication.AuthenticationException; -import org.cryptomator.domain.exception.authentication.NoAuthenticationProvidedException; -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.crypto.CredentialCryptor; -import org.cryptomator.util.file.LruFileCacheUtil; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -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.DROPBOX; -import static org.cryptomator.util.file.LruFileCacheUtil.retrieveFromLruCache; -import static org.cryptomator.util.file.LruFileCacheUtil.storeToLruCache; - -class DropboxImpl { - - private static final long CHUNKED_UPLOAD_CHUNK_SIZE = 8L << 20; - private static final int CHUNKED_UPLOAD_MAX_ATTEMPTS = 5; - - private final DropboxClientFactory clientFactory = new DropboxClientFactory(); - private final DropboxCloud cloud; - private final RootDropboxFolder root; - private final Context context; - private final SharedPreferencesHandler sharedPreferencesHandler; - - private DiskLruCache diskLruCache; - - DropboxImpl(DropboxCloud cloud, Context context) { - if (cloud.accessToken() == null) { - throw new NoAuthenticationProvidedException(cloud); - } - this.cloud = cloud; - this.root = new RootDropboxFolder(cloud); - this.context = context; - - sharedPreferencesHandler = new SharedPreferencesHandler(context); - } - - private static void sleepQuietly(long millis) { - try { - Thread.sleep(millis); - } catch (InterruptedException ex) { - throw new FatalBackendException("Error uploading to Dropbox: interrupted during backoff."); - } - } - - private DbxClientV2 client() throws AuthenticationException { - return clientFactory.getClient(decrypt(cloud.accessToken()), context); - } - - private String decrypt(String password) { - return CredentialCryptor // - .getInstance(context) // - .decrypt(password); - } - - public DropboxFolder root() { - return root; - } - - public DropboxFolder resolve(String path) { - if (path.startsWith("/")) { - path = path.substring(1); - } - String[] names = path.split("/"); - DropboxFolder folder = root; - for (String name : names) { - folder = folder(folder, name); - } - return folder; - } - - public DropboxFile file(CloudFolder folder, String name) { - return file(folder, name, Optional.empty()); - } - - public DropboxFile file(CloudFolder folder, String name, Optional size) { - return DropboxCloudNodeFactory.file( // - (DropboxFolder) folder, // - name, // - size, // - folder.getPath() + '/' + name); - } - - public DropboxFolder folder(CloudFolder folder, String name) { - return DropboxCloudNodeFactory.folder( // - (DropboxFolder) folder, // - name, // - folder.getPath() + '/' + name); - } - - public boolean exists(CloudNode node) throws AuthenticationException, DbxException { - try { - Metadata metadata = client() // - .files() // - .getMetadata(node.getPath()); - if (node instanceof CloudFolder) { - return metadata instanceof FolderMetadata; - } else { - return metadata instanceof FileMetadata; - } - } catch (GetMetadataErrorException e) { - if (e.errorValue.isPath()) { - return false; - } - throw e; - } - } - - public List list(CloudFolder folder) throws AuthenticationException, DbxException { - List result = new ArrayList<>(); - ListFolderResult listFolderResult = null; - do { - if (listFolderResult == null) { - listFolderResult = client() // - .files() // - .listFolder(folder.getPath()); - } else { - String cursor = listFolderResult.getCursor(); - listFolderResult = client() // - .files() // - .listFolderContinue(cursor); - } - List entryMetadata = listFolderResult.getEntries(); - for (Metadata metadata : entryMetadata) { - result.add(DropboxCloudNodeFactory.from( // - (DropboxFolder) folder, // - metadata)); - } - } while (listFolderResult.getHasMore()); - return result; - } - - public DropboxFolder create(CloudFolder folder) throws AuthenticationException, DbxException { - CreateFolderResult createFolderResult = client() // - .files() // - .createFolderV2(folder.getPath()); - - return DropboxCloudNodeFactory.from( // - (DropboxFolder) folder.getParent(), // - createFolderResult.getMetadata()); - } - - public CloudNode move(CloudNode source, CloudNode target) throws AuthenticationException, DbxException { - RelocationResult relocationResult = client() // - .files() // - .moveV2(source.getPath(), target.getPath()); - - return DropboxCloudNodeFactory.from( // - (DropboxFolder) target.getParent(), // - relocationResult.getMetadata()); - } - - public DropboxFile write(DropboxFile file, DataSource data, final ProgressAware progressAware, boolean replace, long size) throws AuthenticationException, DbxException, IOException, CloudNodeAlreadyExistsException { - if (!replace && exists(file)) { - throw new CloudNodeAlreadyExistsException("CloudNode already exists and replace is false"); - } - - progressAware.onProgress(Progress.started(UploadState.upload(file))); - WriteMode writeMode = WriteMode.ADD; - if (replace) { - writeMode = WriteMode.OVERWRITE; - } - // "Upload the file with simple upload API if it is small enough, otherwise use chunked - // upload API for better performance. Arbitrarily chose 2 times our chunk size as the - // deciding factor. This should really depend on your network." - // Source: https://github.com/dropbox/dropbox-sdk-java/blob/master/examples/upload-file/src/main/java/com/dropbox/core/examples/upload_file/Main.java - if (size <= (2 * CHUNKED_UPLOAD_CHUNK_SIZE)) { - uploadFile(file, data, progressAware, writeMode, size); - } else { - chunkedUploadFile(file, data, progressAware, writeMode, size); - } - FileMetadata metadata = (FileMetadata) client() // - .files() // - .getMetadata(file.getPath()); - - progressAware.onProgress(Progress.completed(UploadState.upload(file))); - - return DropboxCloudNodeFactory.from( // - file.getParent(), // - metadata); - } - - private void uploadFile(final DropboxFile file, DataSource data, final ProgressAware progressAware, WriteMode writeMode, final long size) // - throws AuthenticationException, DbxException, IOException { - try (TransferredBytesAwareInputStream in = new TransferredBytesAwareInputStream(data.open(context)) { - @Override - public void bytesTransferred(long transferred) { - progressAware.onProgress( // - progress(UploadState.upload(file)) // - .between(0) // - .and(size) // - .withValue(transferred)); - } - }) { - client() // - .files() // - .uploadBuilder(file.getPath()) // - .withMode(writeMode) // - .uploadAndFinish(in); - } - } - - private void chunkedUploadFile(final DropboxFile file, DataSource data, final ProgressAware progressAware, WriteMode writeMode, final long size) throws AuthenticationException, DbxException, IOException { - // Assert our file is at least the chunk upload size. We make this assumption in the code - // below to simplify the logic. - if (size < CHUNKED_UPLOAD_CHUNK_SIZE) { - throw new FatalBackendException("File too small, use uploadFile() instead."); - } - - long uploaded = 0L; - DbxException thrown = null; - - try (InputStream stream = data.open(context)) { - - // Chunked uploads have 3 phases, each of which can accept uploaded bytes: - // - // (1) Start: initiate the upload and get an upload session ID - // (2) Append: upload chunks of the file to append to our session - // (3) Finish: commit the upload and close the session - // - // We track how many bytes we uploaded to determine which phase we should be in. - String sessionId = null; - for (int i = 0; i < CHUNKED_UPLOAD_MAX_ATTEMPTS; i++) { - if (i > 0) { - Timber.v("Retrying chunked upload (" + (i + 1) + " / " + CHUNKED_UPLOAD_MAX_ATTEMPTS + " attempts)"); - } - - try { - // if this is a retry, make sure seek to the correct offset - stream.skip(uploaded); - - // (1) Start - if (sessionId == null) { - sessionId = client() // - .files() // - .uploadSessionStart() // - .uploadAndFinish(new TransferredBytesAwareInputStream(stream) { - @Override - public void bytesTransferred(long transferred) { - progressAware.onProgress( // - progress(UploadState.upload(file)) // - .between(0) // - .and(size) // - .withValue(transferred)); - } - }, CHUNKED_UPLOAD_CHUNK_SIZE).getSessionId(); - uploaded += CHUNKED_UPLOAD_CHUNK_SIZE; - - progressAware.onProgress( // - progress(UploadState.upload(file)) // - .between(0) // - .and(size) // - .withValue(uploaded)); - } - - UploadSessionCursor cursor = new UploadSessionCursor(sessionId, uploaded); - - // (2) Append - while ((size - uploaded) > CHUNKED_UPLOAD_CHUNK_SIZE) { - final long fullyUploaded = uploaded; - client() // - .files() // - .uploadSessionAppendV2(cursor) // - .uploadAndFinish(new TransferredBytesAwareInputStream(stream) { - @Override - public void bytesTransferred(long transferred) { - progressAware.onProgress( // - progress(UploadState.upload(file)) // - .between(0) // - .and(size) // - .withValue(fullyUploaded + transferred)); - } - }, CHUNKED_UPLOAD_CHUNK_SIZE); - uploaded += CHUNKED_UPLOAD_CHUNK_SIZE; - - progressAware.onProgress( // - progress(UploadState.upload(file)) // - .between(0) // - .and(size) // - .withValue(uploaded)); - - cursor = new UploadSessionCursor(sessionId, uploaded); - } - - // (3) Finish - long remaining = size - uploaded; - CommitInfo commitInfo = CommitInfo // - .newBuilder(file.getPath()) // - .withMode(writeMode) // - .build(); - - client() // - .files() // - .uploadSessionFinish(cursor, commitInfo) // - .uploadAndFinish(stream, remaining); - - return; - } catch (RetryException ex) { - thrown = ex; - // RetryExceptions are never automatically retried by the client for uploads. Must - // catch this exception even if DbxRequestConfig.getMaxRetries() > 0. - sleepQuietly(ex.getBackoffMillis()); - } catch (NetworkIOException ex) { - thrown = ex; - // Network issue with Dropbox (maybe a timeout?), try again. - } catch (UploadSessionLookupErrorException ex) { - if (ex.errorValue.isIncorrectOffset()) { - thrown = ex; - // Server offset into the stream doesn't match our offset (uploaded). Seek to - // the expected offset according to the server and try again. - uploaded = ex. // - errorValue. // - getIncorrectOffsetValue(). // - getCorrectOffset(); - } else { - throw new FatalBackendException(ex); - } - } catch (UploadSessionFinishErrorException ex) { - if (ex.errorValue.isLookupFailed() && ex.errorValue.getLookupFailedValue().isIncorrectOffset()) { - thrown = ex; - // Server offset into the stream doesn't match our offset (uploaded). Seek to - // the expected offset according to the server and try again. - uploaded = ex. // - errorValue. // - getLookupFailedValue(). // - getIncorrectOffsetValue(). // - getCorrectOffset(); - } else { - throw new FatalBackendException(ex); - } - } - } - } - - throw new FatalBackendException("Maxed out upload attempts to Dropbox.", thrown); - } - - public void read(CloudFile file, Optional encryptedTmpFile, OutputStream data, final ProgressAware progressAware) throws DbxException, IOException { - progressAware.onProgress(Progress.started(DownloadState.download(file))); - - Optional cacheKey = Optional.empty(); - Optional cacheFile = Optional.empty(); - - if (sharedPreferencesHandler.useLruCache() && createLruCache(sharedPreferencesHandler.lruCacheSize())) { - final FileMetadata fileMetadata = (FileMetadata) client() // - .files() // - .getMetadata(file.getPath()); - cacheKey = Optional.of(fileMetadata.getId() + fileMetadata.getRev()); - java.io.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("DropboxImpl").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 CloudFile file, // - final OutputStream data, // - final Optional encryptedTmpFile, // - final Optional cacheKey, // - final ProgressAware progressAware) throws DbxException, IOException { - try (TransferredBytesAwareOutputStream out = new TransferredBytesAwareOutputStream(data) { - @Override - public void bytesTransferred(long transferred) { - progressAware.onProgress( // - progress(DownloadState.download(file)) // - .between(0) // - .and(file.getSize().orElse(Long.MAX_VALUE)) // - .withValue(transferred)); - } - }) { - client() // - .files() // - .download(file.getPath()) // - .download(out); - } - - if (sharedPreferencesHandler.useLruCache() && encryptedTmpFile.isPresent() && cacheKey.isPresent()) { - try { - storeToLruCache(diskLruCache, cacheKey.get(), encryptedTmpFile.get()); - } catch (IOException e) { - Timber.tag("DropboxImpl").e(e, "Failed to write downloaded file in LRU cache"); - } - } - } - - private boolean createLruCache(int cacheSize) { - if (diskLruCache == null) { - try { - diskLruCache = DiskLruCache.create(new LruFileCacheUtil(context).resolve(DROPBOX), cacheSize); - } catch (IOException e) { - Timber.tag("DropboxImpl").e(e, "Failed to setup LRU cache"); - return false; - } - } - - return true; - } - - public void delete(CloudNode node) throws AuthenticationException, DbxException { - client() // - .files() // - .deleteV2(node.getPath()); - } - - public String currentAccount() throws AuthenticationException, DbxException { - FullAccount currentAccount = client() // - .users() // - .getCurrentAccount(); - return currentAccount.getName().getDisplayName(); - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxImpl.kt b/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxImpl.kt new file mode 100644 index 00000000..fb953649 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxImpl.kt @@ -0,0 +1,397 @@ +package org.cryptomator.data.cloud.dropbox + +import android.content.Context +import com.dropbox.core.DbxException +import com.dropbox.core.NetworkIOException +import com.dropbox.core.RetryException +import com.dropbox.core.v2.DbxClientV2 +import com.dropbox.core.v2.files.CommitInfo +import com.dropbox.core.v2.files.FileMetadata +import com.dropbox.core.v2.files.FolderMetadata +import com.dropbox.core.v2.files.GetMetadataErrorException +import com.dropbox.core.v2.files.ListFolderResult +import com.dropbox.core.v2.files.UploadSessionCursor +import com.dropbox.core.v2.files.UploadSessionFinishErrorException +import com.dropbox.core.v2.files.UploadSessionLookupErrorException +import com.dropbox.core.v2.files.WriteMode +import com.tomclaw.cache.DiskLruCache +import org.cryptomator.data.cloud.dropbox.DropboxCloudNodeFactory.file +import org.cryptomator.data.cloud.dropbox.DropboxCloudNodeFactory.folder +import org.cryptomator.data.cloud.dropbox.DropboxCloudNodeFactory.from +import org.cryptomator.data.util.TransferredBytesAwareInputStream +import org.cryptomator.data.util.TransferredBytesAwareOutputStream +import org.cryptomator.domain.CloudFile +import org.cryptomator.domain.CloudFolder +import org.cryptomator.domain.CloudNode +import org.cryptomator.domain.DropboxCloud +import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException +import org.cryptomator.domain.exception.FatalBackendException +import org.cryptomator.domain.exception.ParentFolderIsNullException +import org.cryptomator.domain.exception.authentication.AuthenticationException +import org.cryptomator.domain.exception.authentication.NoAuthenticationProvidedException +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.SharedPreferencesHandler +import org.cryptomator.util.file.LruFileCacheUtil +import org.cryptomator.util.file.LruFileCacheUtil.Companion.retrieveFromLruCache +import java.io.File +import java.io.IOException +import java.io.OutputStream +import java.util.ArrayList +import timber.log.Timber + +internal class DropboxImpl(cloud: DropboxCloud, context: Context) { + + private val cloud: DropboxCloud + private val root: RootDropboxFolder + private val context: Context + private val sharedPreferencesHandler: SharedPreferencesHandler + private var diskLruCache: DiskLruCache? = null + + @Throws(AuthenticationException::class) + private fun client(): DbxClientV2 { + return DropboxClientFactory.getInstance(cloud.accessToken(), context) + } + + fun root(): DropboxFolder { + return root + } + + fun resolve(path: String): DropboxFolder { + val names = path.removePrefix("/").split("/").toTypedArray() + var folder: DropboxFolder = root + for (name in names) { + folder = folder(folder, name) + } + return folder + } + + fun file(folder: DropboxFolder, name: String, size: Long?): DropboxFile { + return file(folder, name, size, folder.path + '/' + name) + } + + fun folder(folder: DropboxFolder, name: String): DropboxFolder { + return folder(folder, name, folder.path + '/' + name) + } + + @Throws(AuthenticationException::class, DbxException::class) + fun exists(node: CloudNode): Boolean { + return try { + val metadata = client() // + .files() // + .getMetadata(node.path) + if (node is CloudFolder) { + metadata is FolderMetadata + } else { + metadata is FileMetadata + } + } catch (e: GetMetadataErrorException) { + if (e.errorValue.isPath) { + return false + } + throw e + } + } + + @Throws(AuthenticationException::class, DbxException::class) + fun list(folder: DropboxFolder): List { + val result: MutableList = ArrayList() + var listFolderResult: ListFolderResult? = null + do { + listFolderResult = if (listFolderResult == null) { + client().files().listFolder(folder.path) + } else { + client().files().listFolderContinue(listFolderResult.cursor) + } + listFolderResult.entries.parallelStream().forEach { + result.add(from(folder, it)) + } + } while (listFolderResult?.hasMore == true) + return result + } + + @Throws(AuthenticationException::class, DbxException::class) + fun create(folder: DropboxFolder): DropboxFolder { + folder.parent?.let { + val createFolderResult = client().files().createFolderV2(folder.path) + return from(it, createFolderResult.metadata) + } ?: throw ParentFolderIsNullException(folder.name) + } + + @Throws(AuthenticationException::class, DbxException::class) + fun move(source: DropboxNode, target: DropboxNode): DropboxNode { + target.parent?.let { targetsParent -> + val relocationResult = client().files().moveV2(source.path, target.path) + return from(targetsParent, relocationResult.metadata) + } ?: throw ParentFolderIsNullException(target.name) + } + + @Throws(AuthenticationException::class, DbxException::class, IOException::class, CloudNodeAlreadyExistsException::class) + fun write(file: DropboxFile, data: DataSource, progressAware: ProgressAware, replace: Boolean, size: Long): DropboxFile { + if (!replace && exists(file)) { + throw CloudNodeAlreadyExistsException("CloudNode already exists and replace is false") + } + progressAware.onProgress(Progress.started(UploadState.upload(file))) + var writeMode = WriteMode.ADD + if (replace) { + writeMode = WriteMode.OVERWRITE + } + // "Upload the file with simple upload API if it is small enough, otherwise use chunked + // upload API for better performance. Arbitrarily chose 2 times our chunk size as the + // deciding factor. This should really depend on your network." + // Source: https://github.com/dropbox/dropbox-sdk-java/blob/master/examples/upload-file/src/main/java/com/dropbox/core/examples/upload_file/Main.java + if (size <= 2 * CHUNKED_UPLOAD_CHUNK_SIZE) { + uploadFile(file, data, progressAware, writeMode, size) + } else { + chunkedUploadFile(file, data, progressAware, writeMode, size) + } + val metadata = client().files().getMetadata(file.path) + progressAware.onProgress(Progress.completed(UploadState.upload(file))) + return from(file.parent, metadata) as DropboxFile + } + + @Throws(AuthenticationException::class, DbxException::class, IOException::class) + private fun uploadFile(file: DropboxFile, data: DataSource, progressAware: ProgressAware, writeMode: WriteMode, size: Long) { + data.open(context)?.use { inputStream -> + object : TransferredBytesAwareInputStream(inputStream) { + override fun bytesTransferred(transferred: Long) { + progressAware.onProgress( // + Progress.progress(UploadState.upload(file)) // + .between(0) // + .and(size) // + .withValue(transferred) + ) + } + }.use { + client() // + .files() // + .uploadBuilder(file.path) // + .withMode(writeMode) // + .uploadAndFinish(it) + } + } ?: Timber.tag("").e("InputStream shouldn't be null") + } + + @Throws(AuthenticationException::class, DbxException::class, IOException::class) + private fun chunkedUploadFile(file: DropboxFile, data: DataSource, progressAware: ProgressAware, writeMode: WriteMode, size: Long) { + // Assert our file is at least the chunk upload size. We make this assumption in the code + // below to simplify the logic. + if (size < CHUNKED_UPLOAD_CHUNK_SIZE) { + throw FatalBackendException("File too small, use uploadFile() instead.") + } + var uploaded = 0L + var thrown: DbxException? = null + data.open(context)?.use { + + // Chunked uploads have 3 phases, each of which can accept uploaded bytes: + // + // (1) Start: initiate the upload and get an upload session ID + // (2) Append: upload chunks of the file to append to our session + // (3) Finish: commit the upload and close the session + // + // We track how many bytes we uploaded to determine which phase we should be in. + var sessionId: String? = null + for (i in 0 until CHUNKED_UPLOAD_MAX_ATTEMPTS) { + if (i > 0) { + Timber.v("Retrying chunked upload (" + (i + 1) + " / " + CHUNKED_UPLOAD_MAX_ATTEMPTS + " attempts)") + } + try { + // if this is a retry, make sure seek to the correct offset + it.skip(uploaded) + + // (1) Start + if (sessionId == null) { + sessionId = client() // + .files() // + .uploadSessionStart() // + .uploadAndFinish(object : TransferredBytesAwareInputStream(it) { + override fun bytesTransferred(transferred: Long) { + progressAware.onProgress( // + Progress.progress(UploadState.upload(file)) // + .between(0) // + .and(size) // + .withValue(transferred) + ) + } + }, CHUNKED_UPLOAD_CHUNK_SIZE).sessionId + uploaded += CHUNKED_UPLOAD_CHUNK_SIZE + progressAware.onProgress( // + Progress.progress(UploadState.upload(file)) // + .between(0) // + .and(size) // + .withValue(uploaded) + ) + } + var cursor = UploadSessionCursor(sessionId, uploaded) + + // (2) Append + while (size - uploaded > CHUNKED_UPLOAD_CHUNK_SIZE) { + val fullyUploaded = uploaded + client() // + .files() // + .uploadSessionAppendV2(cursor) // + .uploadAndFinish(object : TransferredBytesAwareInputStream(it) { + override fun bytesTransferred(transferred: Long) { + progressAware.onProgress( // + Progress.progress(UploadState.upload(file)) // + .between(0) // + .and(size) // + .withValue(fullyUploaded + transferred) + ) + } + }, CHUNKED_UPLOAD_CHUNK_SIZE) + uploaded += CHUNKED_UPLOAD_CHUNK_SIZE + progressAware.onProgress( // + Progress.progress(UploadState.upload(file)) // + .between(0) // + .and(size) // + .withValue(uploaded) + ) + cursor = UploadSessionCursor(sessionId, uploaded) + } + + // (3) Finish + val remaining = size - uploaded + val commitInfo = CommitInfo // + .newBuilder(file.path) // + .withMode(writeMode) // + .build() + client() // + .files() // + .uploadSessionFinish(cursor, commitInfo) // + .uploadAndFinish(it, remaining) + return + } catch (ex: RetryException) { + thrown = ex + // RetryExceptions are never automatically retried by the client for uploads. Must + // catch this exception even if DbxRequestConfig.getMaxRetries() > 0. + sleepQuietly(ex.backoffMillis) + } catch (ex: NetworkIOException) { + thrown = ex + // Network issue with Dropbox (maybe a timeout?), try again. + } catch (ex: UploadSessionLookupErrorException) { + if (ex.errorValue.isIncorrectOffset) { + thrown = ex + // Server offset into the stream doesn't match our offset (uploaded). Seek to + // the expected offset according to the server and try again. + uploaded = ex.errorValue.incorrectOffsetValue.correctOffset + } else { + throw FatalBackendException(ex) + } + } catch (ex: UploadSessionFinishErrorException) { + if (ex.errorValue.isLookupFailed && ex.errorValue.lookupFailedValue.isIncorrectOffset) { + thrown = ex + // Server offset into the stream doesn't match our offset (uploaded). Seek to + // the expected offset according to the server and try again. + uploaded = ex.errorValue.lookupFailedValue.incorrectOffsetValue.correctOffset + } else { + throw FatalBackendException(ex) + } + } + } + } ?: throw FatalBackendException("InputStream is null") + throw FatalBackendException("Maxed out upload attempts to Dropbox.", thrown) + } + + @Throws(DbxException::class, IOException::class) + fun read(file: CloudFile, encryptedTmpFile: File?, data: OutputStream, progressAware: ProgressAware) { + progressAware.onProgress(Progress.started(DownloadState.download(file))) + var cacheKey: String? = null + var cacheFile: File? = null + if (sharedPreferencesHandler.useLruCache() && createLruCache(sharedPreferencesHandler.lruCacheSize())) { + val fileMetadata = client() // + .files() // + .getMetadata(file.path) as FileMetadata + cacheKey = fileMetadata.id + fileMetadata.rev + cacheFile = diskLruCache?.let { it[cacheKey] } + } + if (sharedPreferencesHandler.useLruCache() && cacheFile != null) { + try { + retrieveFromLruCache(cacheFile, data) + } catch (e: IOException) { + Timber.tag("DropboxImpl").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))) + } + + @Throws(DbxException::class, IOException::class) + private fun writeToData(file: CloudFile, data: OutputStream, encryptedTmpFile: File?, cacheKey: String?, progressAware: ProgressAware) { + object : TransferredBytesAwareOutputStream(data) { + override fun bytesTransferred(transferred: Long) { + progressAware.onProgress( // + Progress.progress(DownloadState.download(file)) // + .between(0) // + .and(file.size ?: Long.MAX_VALUE) // + .withValue(transferred) + ) + } + }.use { + client() // + .files() // + .download(file.path) // + .download(it) + } + if (sharedPreferencesHandler.useLruCache() && encryptedTmpFile != null && cacheKey != null) { + try { + diskLruCache?.let { + LruFileCacheUtil.storeToLruCache(it, cacheKey, encryptedTmpFile) + } ?: Timber.tag("DropboxImpl").e("Failed to store item in LRU cache") + } catch (e: IOException) { + Timber.tag("DropboxImpl").e(e, "Failed to write downloaded file in LRU cache") + } + } + } + + private fun createLruCache(cacheSize: Int): Boolean { + if (diskLruCache == null) { + diskLruCache = try { + DiskLruCache.create(LruFileCacheUtil(context).resolve(LruFileCacheUtil.Cache.DROPBOX), cacheSize.toLong()) + } catch (e: IOException) { + Timber.tag("DropboxImpl").e(e, "Failed to setup LRU cache") + return false + } + } + return true + } + + @Throws(AuthenticationException::class, DbxException::class) + fun delete(node: CloudNode) { + client().files().deleteV2(node.path) + } + + @Throws(AuthenticationException::class, DbxException::class) + fun currentAccount(): String { + val currentAccount = client().users().currentAccount + return currentAccount.name.displayName + } + + companion object { + + private const val CHUNKED_UPLOAD_CHUNK_SIZE = 8L shl 20 + private const val CHUNKED_UPLOAD_MAX_ATTEMPTS = 5 + private fun sleepQuietly(millis: Long) { + try { + Thread.sleep(millis) + } catch (ex: InterruptedException) { + throw FatalBackendException("Error uploading to Dropbox: interrupted during backoff.") + } + } + } + + init { + if (cloud.accessToken() == null) { + throw NoAuthenticationProvidedException(cloud) + } + this.cloud = cloud + this.root = RootDropboxFolder(cloud) + this.context = context + sharedPreferencesHandler = SharedPreferencesHandler(context) + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxNode.java b/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxNode.java deleted file mode 100644 index 9d6a6aa2..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxNode.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.cryptomator.data.cloud.dropbox; - -import org.cryptomator.domain.CloudNode; - -interface DropboxNode extends CloudNode { - -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxNode.kt b/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxNode.kt new file mode 100644 index 00000000..24dd6b65 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxNode.kt @@ -0,0 +1,9 @@ +package org.cryptomator.data.cloud.dropbox + +import org.cryptomator.domain.CloudNode + +interface DropboxNode : CloudNode { + + override val parent: DropboxFolder? + +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/dropbox/RootDropboxFolder.java b/data/src/main/java/org/cryptomator/data/cloud/dropbox/RootDropboxFolder.java deleted file mode 100644 index e0be4aa0..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/dropbox/RootDropboxFolder.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.cryptomator.data.cloud.dropbox; - -import org.cryptomator.domain.Cloud; -import org.cryptomator.domain.DropboxCloud; - -class RootDropboxFolder extends DropboxFolder { - - private final DropboxCloud cloud; - - public RootDropboxFolder(DropboxCloud cloud) { - super(null, "", ""); - this.cloud = cloud; - } - - @Override - public DropboxCloud getCloud() { - return cloud; - } - - @Override - public DropboxFolder withCloud(Cloud cloud) { - return new RootDropboxFolder((DropboxCloud) cloud); - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/dropbox/RootDropboxFolder.kt b/data/src/main/java/org/cryptomator/data/cloud/dropbox/RootDropboxFolder.kt new file mode 100644 index 00000000..528369d8 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/dropbox/RootDropboxFolder.kt @@ -0,0 +1,11 @@ +package org.cryptomator.data.cloud.dropbox + +import org.cryptomator.domain.Cloud +import org.cryptomator.domain.DropboxCloud + +internal class RootDropboxFolder(override val cloud: DropboxCloud) : DropboxFolder(null, "", "") { + + override fun withCloud(cloud: Cloud?): DropboxFolder { + return RootDropboxFolder(cloud as DropboxCloud) + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/local/file/LocalFile.java b/data/src/main/java/org/cryptomator/data/cloud/local/file/LocalFile.java deleted file mode 100644 index 9e9c17a4..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/local/file/LocalFile.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.cryptomator.data.cloud.local.file; - -import org.cryptomator.domain.Cloud; -import org.cryptomator.domain.CloudFile; -import org.cryptomator.util.Optional; - -import java.util.Date; - -class LocalFile implements CloudFile, LocalNode { - - private final LocalFolder parent; - private final String name; - private final String path; - private final Optional size; - private final Optional modified; - - LocalFile(LocalFolder 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 LocalFolder 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/local/file/LocalFile.kt b/data/src/main/java/org/cryptomator/data/cloud/local/file/LocalFile.kt new file mode 100644 index 00000000..43eb8ad6 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/local/file/LocalFile.kt @@ -0,0 +1,11 @@ +package org.cryptomator.data.cloud.local.file + +import org.cryptomator.domain.Cloud +import org.cryptomator.domain.CloudFile +import java.util.Date + +class LocalFile(override val parent: LocalFolder, override val name: String, override val path: String, override val size: Long?, override val modified: Date?) : CloudFile, LocalNode { + + override val cloud: Cloud? + get() = parent.cloud +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/local/file/LocalFolder.java b/data/src/main/java/org/cryptomator/data/cloud/local/file/LocalFolder.java deleted file mode 100644 index 490441dd..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/local/file/LocalFolder.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.cryptomator.data.cloud.local.file; - -import org.cryptomator.domain.Cloud; -import org.cryptomator.domain.CloudFolder; - -class LocalFolder implements CloudFolder, LocalNode { - - private final LocalFolder parent; - private final String name; - private final String path; - - LocalFolder(LocalFolder 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 LocalFolder getParent() { - return parent; - } - - @Override - public LocalFolder withCloud(Cloud cloud) { - return new LocalFolder(parent.withCloud(cloud), name, path); - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/local/file/LocalFolder.kt b/data/src/main/java/org/cryptomator/data/cloud/local/file/LocalFolder.kt new file mode 100644 index 00000000..40d60d43 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/local/file/LocalFolder.kt @@ -0,0 +1,14 @@ +package org.cryptomator.data.cloud.local.file + +import org.cryptomator.domain.Cloud +import org.cryptomator.domain.CloudFolder + +open class LocalFolder(override val parent: LocalFolder?, override val name: String, override val path: String) : CloudFolder, LocalNode { + + override val cloud: Cloud? + get() = parent?.cloud + + override fun withCloud(cloud: Cloud?): LocalFolder? { + return LocalFolder(parent?.withCloud(cloud), name, path) + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/local/file/LocalNode.java b/data/src/main/java/org/cryptomator/data/cloud/local/file/LocalNode.java deleted file mode 100644 index dc9e9d02..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/local/file/LocalNode.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.cryptomator.data.cloud.local.file; - -import org.cryptomator.domain.CloudNode; - -interface LocalNode extends CloudNode { - -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/local/file/LocalNode.kt b/data/src/main/java/org/cryptomator/data/cloud/local/file/LocalNode.kt new file mode 100644 index 00000000..313b8859 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/local/file/LocalNode.kt @@ -0,0 +1,9 @@ +package org.cryptomator.data.cloud.local.file + +import org.cryptomator.domain.CloudNode + +interface LocalNode : CloudNode { + + override val parent: LocalFolder? + +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/local/file/LocalStorageContentRepository.java b/data/src/main/java/org/cryptomator/data/cloud/local/file/LocalStorageContentRepository.java deleted file mode 100644 index 38ad0722..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/local/file/LocalStorageContentRepository.java +++ /dev/null @@ -1,121 +0,0 @@ -package org.cryptomator.data.cloud.local.file; - -import android.content.Context; - -import org.cryptomator.domain.CloudNode; -import org.cryptomator.domain.LocalStorageCloud; -import org.cryptomator.domain.exception.BackendException; -import org.cryptomator.domain.exception.FatalBackendException; -import org.cryptomator.domain.exception.NoSuchCloudFileException; -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.FileNotFoundException; -import java.io.IOException; -import java.io.OutputStream; -import java.util.List; - -import static org.cryptomator.util.ExceptionUtil.contains; - -public class LocalStorageContentRepository implements CloudContentRepository { - - private final LocalStorageImpl localStorageImpl; - - public LocalStorageContentRepository(Context context, LocalStorageCloud localStorageCloud) { - this.localStorageImpl = new LocalStorageImpl(context, localStorageCloud); - } - - @Override - public LocalFolder root(LocalStorageCloud cloud) throws BackendException { - return localStorageImpl.root(); - } - - @Override - public LocalFolder resolve(LocalStorageCloud cloud, String path) throws BackendException { - return localStorageImpl.resolve(path); - } - - @Override - public LocalFile file(LocalFolder parent, String name) throws BackendException { - return localStorageImpl.file(parent, name); - } - - @Override - public LocalFile file(LocalFolder parent, String name, Optional size) throws BackendException { - return localStorageImpl.file(parent, name, size); - } - - @Override - public LocalFolder folder(LocalFolder parent, String name) throws BackendException { - return localStorageImpl.folder(parent, name); - } - - @Override - public boolean exists(LocalNode node) throws BackendException { - return localStorageImpl.exists(node); - } - - @Override - public List list(LocalFolder folder) throws BackendException { - return localStorageImpl.list(folder); - } - - @Override - public LocalFolder create(LocalFolder folder) throws BackendException { - return localStorageImpl.create(folder); - } - - @Override - public LocalFolder move(LocalFolder source, LocalFolder target) throws BackendException { - return (LocalFolder) localStorageImpl.move(source, target); - } - - @Override - public LocalFile move(LocalFile source, LocalFile target) throws BackendException { - return (LocalFile) localStorageImpl.move(source, target); - } - - @Override - public LocalFile write(LocalFile file, DataSource data, ProgressAware progressAware, boolean replace, long size) throws BackendException { - try { - return localStorageImpl.write(file, data, progressAware, replace, size); - } catch (IOException e) { - if (contains(e, FileNotFoundException.class)) { - throw new NoSuchCloudFileException(file.getName()); - } - throw new FatalBackendException(e); - } - } - - @Override - public void read(LocalFile file, Optional tmpEncryptedFile, OutputStream data, ProgressAware progressAware) throws BackendException { - try { - localStorageImpl.read(file, data, progressAware); - } catch (IOException e) { - if (contains(e, FileNotFoundException.class)) { - throw new NoSuchCloudFileException(file.getName()); - } - throw new FatalBackendException(e); - } - } - - @Override - public void delete(LocalNode node) throws BackendException { - localStorageImpl.delete(node); - } - - @Override - public String checkAuthenticationAndRetrieveCurrentAccount(LocalStorageCloud cloud) throws BackendException { - return null; - } - - @Override - public void logout(LocalStorageCloud cloud) throws BackendException { - // empty - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/local/file/LocalStorageContentRepository.kt b/data/src/main/java/org/cryptomator/data/cloud/local/file/LocalStorageContentRepository.kt new file mode 100644 index 00000000..46c225f1 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/local/file/LocalStorageContentRepository.kt @@ -0,0 +1,111 @@ +package org.cryptomator.data.cloud.local.file + +import android.content.Context +import org.cryptomator.domain.LocalStorageCloud +import org.cryptomator.domain.exception.BackendException +import org.cryptomator.domain.exception.FatalBackendException +import org.cryptomator.domain.exception.NoSuchCloudFileException +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.ExceptionUtil +import java.io.File +import java.io.FileNotFoundException +import java.io.IOException +import java.io.OutputStream + +class LocalStorageContentRepository(context: Context, localStorageCloud: LocalStorageCloud) : CloudContentRepository { + + private val localStorageImpl: LocalStorageImpl = LocalStorageImpl(context, localStorageCloud) + + @Throws(BackendException::class) + override fun root(cloud: LocalStorageCloud): LocalFolder { + return localStorageImpl.root() + } + + override fun resolve(cloud: LocalStorageCloud, path: String): LocalFolder { + return localStorageImpl.resolve(path) + } + + @Throws(BackendException::class) + override fun file(parent: LocalFolder, name: String): LocalFile { + return localStorageImpl.file(parent, name, null) + } + + @Throws(BackendException::class) + override fun file(parent: LocalFolder, name: String, size: Long?): LocalFile { + return localStorageImpl.file(parent, name, size) + } + + @Throws(BackendException::class) + override fun folder(parent: LocalFolder, name: String): LocalFolder { + return localStorageImpl.folder(parent, name) + } + + @Throws(BackendException::class) + override fun exists(node: LocalNode): Boolean { + return localStorageImpl.exists(node) + } + + @Throws(BackendException::class) + override fun list(folder: LocalFolder): List { + return localStorageImpl.list(folder) + } + + @Throws(BackendException::class) + override fun create(folder: LocalFolder): LocalFolder { + return localStorageImpl.create(folder) + } + + @Throws(BackendException::class) + override fun move(source: LocalFolder, target: LocalFolder): LocalFolder { + return localStorageImpl.move(source, target) as LocalFolder + } + + @Throws(BackendException::class) + override fun move(source: LocalFile, target: LocalFile): LocalFile { + return localStorageImpl.move(source, target) as LocalFile + } + + @Throws(BackendException::class) + override fun write(file: LocalFile, data: DataSource, progressAware: ProgressAware, replace: Boolean, size: Long): LocalFile { + return try { + localStorageImpl.write(file, data, progressAware, replace, size) + } catch (e: IOException) { + if (ExceptionUtil.contains(e, FileNotFoundException::class.java)) { + throw NoSuchCloudFileException(file.name) + } + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun read(file: LocalFile, encryptedTmpFile: File?, data: OutputStream, progressAware: ProgressAware) { + try { + localStorageImpl.read(file, data, progressAware) + } catch (e: IOException) { + if (ExceptionUtil.contains(e, FileNotFoundException::class.java)) { + throw NoSuchCloudFileException(file.name) + } + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun delete(node: LocalNode) { + localStorageImpl.delete(node) + } + + @Throws(BackendException::class) + override fun checkAuthenticationAndRetrieveCurrentAccount(cloud: LocalStorageCloud): String { + return "" + } + + @Throws(BackendException::class) + override fun logout(cloud: LocalStorageCloud) { + // empty + } + +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/local/file/LocalStorageImpl.java b/data/src/main/java/org/cryptomator/data/cloud/local/file/LocalStorageImpl.java deleted file mode 100644 index ba973ebb..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/local/file/LocalStorageImpl.java +++ /dev/null @@ -1,193 +0,0 @@ -package org.cryptomator.data.cloud.local.file; - -import android.content.Context; - -import org.cryptomator.data.util.TransferredBytesAwareInputStream; -import org.cryptomator.data.util.TransferredBytesAwareOutputStream; -import org.cryptomator.domain.CloudFile; -import org.cryptomator.domain.CloudFolder; -import org.cryptomator.domain.CloudNode; -import org.cryptomator.domain.LocalStorageCloud; -import org.cryptomator.domain.exception.BackendException; -import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException; -import org.cryptomator.domain.exception.FatalBackendException; -import org.cryptomator.domain.exception.NoSuchCloudFileException; -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 java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -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 static org.cryptomator.data.util.CopyStream.copyStreamToStream; -import static org.cryptomator.domain.usecases.cloud.Progress.progress; - -class LocalStorageImpl { - - private final Context context; - private final RootLocalFolder root; - - LocalStorageImpl(Context context, LocalStorageCloud localStorageCloud) { - this.context = context; - this.root = new RootLocalFolder(localStorageCloud); - } - - public LocalFolder root() { - return root; - } - - public LocalFolder resolve(String path) { - if (path.startsWith(root.getPath())) { - path = path.substring(root.getPath().length() + 1); - } - String[] names = path.split("/"); - LocalFolder folder = root; - for (String name : names) { - folder = folder(folder, name); - } - return folder; - } - - public LocalFile file(CloudFolder folder, String name) { - return file(folder, name, Optional.empty()); - } - - public LocalFile file(CloudFolder folder, String name, Optional size) { - return LocalStorageNodeFactory.file( // - (LocalFolder) folder, // - name, // - folder.getPath() + '/' + name, // - size, // - Optional.empty()); - } - - public LocalFolder folder(CloudFolder folder, String name) { - return LocalStorageNodeFactory.folder( // - (LocalFolder) folder, // - name, // - folder.getPath() + '/' + name); - } - - public boolean exists(CloudNode node) { - return new File(node.getPath()).exists(); - } - - public List list(LocalFolder folder) throws BackendException { - List result = new ArrayList<>(); - File localDirectory = new File(folder.getPath()); - if (!exists(folder)) { - throw new NoSuchCloudFileException(); - } - for (File file : localDirectory.listFiles()) { - result.add(LocalStorageNodeFactory.from(folder, file)); - } - return result; - } - - public LocalFolder create(LocalFolder folder) throws BackendException { - File createFolder = new File(folder.getPath()); - if (createFolder.exists()) { - throw new CloudNodeAlreadyExistsException(folder.getName()); - } - if (!createFolder.mkdirs()) { - throw new FatalBackendException("Couldn't create a local folder at " + folder.getPath()); - } - - return LocalStorageNodeFactory.folder( // - folder.getParent(), // - createFolder); - } - - public LocalNode move(CloudNode source, CloudNode target) throws BackendException { - File sourceFile = new File(source.getPath()); - File targetFile = new File(target.getPath()); - if (targetFile.exists()) { - throw new CloudNodeAlreadyExistsException(target.getName()); - } - if (!sourceFile.exists()) { - throw new NoSuchCloudFileException(source.getName()); - } - if (!sourceFile.renameTo(targetFile)) { - throw new FatalBackendException("Couldn't move " + source.getPath() + " to " + target.getPath()); - } - return LocalStorageNodeFactory.from((LocalFolder) target.getParent(), targetFile); - } - - public void delete(CloudNode node) { - File fileOrDirectory = new File(node.getPath()); - if (!deleteRecursive(fileOrDirectory)) { - throw new FatalBackendException("Couldn't delete local CloudNode " + fileOrDirectory); - } - } - - private boolean deleteRecursive(File fileOrDirectory) { - if (fileOrDirectory.isDirectory()) { - for (File child : fileOrDirectory.listFiles()) { - deleteRecursive(child); - } - } - return fileOrDirectory.delete(); - } - - public LocalFile write(final CloudFile file, DataSource data, final ProgressAware progressAware, boolean replace, final 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))); - File localFile = new File(file.getPath()); - - try (OutputStream out = new FileOutputStream(localFile); TransferredBytesAwareInputStream in = new TransferredBytesAwareInputStream(data.open(context)) { - @Override - public void bytesTransferred(long transferred) { - progressAware.onProgress( // - progress(UploadState.upload(file)) // - .between(0) // - .and(size) // - .withValue(transferred)); - } - }) { - copyStreamToStream(in, out); - } - - progressAware.onProgress(Progress.completed(UploadState.upload(file))); - - return LocalStorageNodeFactory.file( // - (LocalFolder) file.getParent(), // - file.getName(), // - localFile.getPath(), // - Optional.of(localFile.length()), // - Optional.of(new Date(localFile.lastModified()))); - } - - public void read(final LocalFile file, OutputStream data, final ProgressAware progressAware) throws IOException { - progressAware.onProgress(Progress.started(DownloadState.download(file))); - File localFile = new File(file.getPath()); - - try (InputStream in = new FileInputStream(localFile); TransferredBytesAwareOutputStream out = new TransferredBytesAwareOutputStream(data) { - @Override - public void bytesTransferred(long transferred) { - progressAware // - .onProgress(progress(DownloadState.download(file)) // - .between(0) // - .and(localFile.length()) // - .withValue(transferred)); - } - }) { - copyStreamToStream(in, out); - } - - progressAware.onProgress(Progress.completed(DownloadState.download(file))); - } - -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/local/file/LocalStorageImpl.kt b/data/src/main/java/org/cryptomator/data/cloud/local/file/LocalStorageImpl.kt new file mode 100644 index 00000000..18547d8e --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/local/file/LocalStorageImpl.kt @@ -0,0 +1,165 @@ +package org.cryptomator.data.cloud.local.file + +import android.content.Context +import org.cryptomator.data.util.CopyStream +import org.cryptomator.data.util.TransferredBytesAwareInputStream +import org.cryptomator.data.util.TransferredBytesAwareOutputStream +import org.cryptomator.domain.CloudNode +import org.cryptomator.domain.LocalStorageCloud +import org.cryptomator.domain.exception.BackendException +import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException +import org.cryptomator.domain.exception.FatalBackendException +import org.cryptomator.domain.exception.NoSuchCloudFileException +import org.cryptomator.domain.exception.ParentFolderIsNullException +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 java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.IOException +import java.io.OutputStream +import java.util.Date + +internal class LocalStorageImpl(private val context: Context, localStorageCloud: LocalStorageCloud) { + + private val root: RootLocalFolder = RootLocalFolder(localStorageCloud) + + fun root(): LocalFolder { + return root + } + + fun resolve(path: String): LocalFolder { + val names = path.substring(root.path.length + 1).split("/").toTypedArray() + var folder: LocalFolder = root + for (name in names) { + folder = folder(folder, name) + } + return folder + } + + fun file(folder: LocalFolder, name: String, size: Long?): LocalFile { + return LocalStorageNodeFactory.file(folder, name, folder.path + '/' + name, size, null) + } + + fun folder(folder: LocalFolder, name: String): LocalFolder { + return LocalStorageNodeFactory.folder(folder, name, folder.path + '/' + name) + } + + fun exists(node: CloudNode): Boolean { + return File(node.path).exists() + } + + @Throws(BackendException::class) + fun list(folder: LocalFolder): List { + val localDirectory = File(folder.path) + if (!exists(folder)) { + throw NoSuchCloudFileException() + } + return localDirectory.listFiles()?.map { file -> LocalStorageNodeFactory.from(folder, file) } + ?: throw FatalBackendException("listFiles() shouldn't return null") + } + + @Throws(BackendException::class) + fun create(folder: LocalFolder): LocalFolder { + folder.parent?.let { parentFolder -> + val createFolder = File(folder.path) + if (createFolder.exists()) { + throw CloudNodeAlreadyExistsException(folder.name) + } + if (!createFolder.mkdirs()) { + throw FatalBackendException("Couldn't create a local folder at " + folder.path) + } + return LocalStorageNodeFactory.folder(parentFolder, createFolder) + } ?: throw ParentFolderIsNullException(folder.name) + } + + @Throws(BackendException::class) + fun move(source: LocalNode, target: LocalNode): LocalNode { + target.parent?.let { + val sourceFile = File(source.path) + val targetFile = File(target.path) + if (targetFile.exists()) { + throw CloudNodeAlreadyExistsException(target.name) + } + if (!sourceFile.exists()) { + throw NoSuchCloudFileException(source.name) + } + if (!sourceFile.renameTo(targetFile)) { + throw FatalBackendException("Couldn't move " + source.path + " to " + target.path) + } + + return LocalStorageNodeFactory.from(it, targetFile) + } ?: throw ParentFolderIsNullException(target.name) + } + + fun delete(node: CloudNode) { + val fileOrDirectory = File(node.path) + if (!deleteRecursive(fileOrDirectory)) { + throw FatalBackendException("Couldn't delete local CloudNode $fileOrDirectory") + } + } + + private fun deleteRecursive(fileOrDirectory: File): Boolean { + if (fileOrDirectory.isDirectory) { + fileOrDirectory.listFiles()?.forEach { + deleteRecursive(it) + } + } + return fileOrDirectory.delete() + } + + @Throws(IOException::class, BackendException::class) + fun write(file: LocalFile, data: DataSource, progressAware: ProgressAware, replace: Boolean, size: Long): LocalFile { + if (!replace && exists(file)) { + throw CloudNodeAlreadyExistsException("CloudNode already exists and replace is false") + } + progressAware.onProgress(Progress.started(UploadState.upload(file))) + val localFile = File(file.path) + FileOutputStream(localFile).use { out -> + data.open(context)?.use { inputStream -> + object : TransferredBytesAwareInputStream(inputStream) { + override fun bytesTransferred(transferred: Long) { + progressAware.onProgress( // + Progress.progress(UploadState.upload(file)) // + .between(0) // + .and(size) // + .withValue(transferred) + ) + } + }.use { CopyStream.copyStreamToStream(it, out) } + } ?: throw FatalBackendException("InputStream shouldn't be null") + } + progressAware.onProgress(Progress.completed(UploadState.upload(file))) + return LocalStorageNodeFactory.file( // + file.parent, // + file.name, // + localFile.path, // + localFile.length(), // + Date(localFile.lastModified()) + ) + } + + @Throws(IOException::class) + fun read(file: LocalFile, data: OutputStream, progressAware: ProgressAware) { + progressAware.onProgress(Progress.started(DownloadState.download(file))) + val localFile = File(file.path) + FileInputStream(localFile).use { inputStream -> + object : TransferredBytesAwareOutputStream(data) { + override fun bytesTransferred(transferred: Long) { + progressAware // + .onProgress( + Progress.progress(DownloadState.download(file)) // + .between(0) // + .and(localFile.length()) // + .withValue(transferred) + ) + } + }.use { out -> CopyStream.copyStreamToStream(inputStream, out) } + } + progressAware.onProgress(Progress.completed(DownloadState.download(file))) + } + +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/local/file/LocalStorageNodeFactory.java b/data/src/main/java/org/cryptomator/data/cloud/local/file/LocalStorageNodeFactory.java deleted file mode 100644 index 30cc58eb..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/local/file/LocalStorageNodeFactory.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.cryptomator.data.cloud.local.file; - -import org.cryptomator.util.Optional; - -import java.io.File; -import java.util.Date; - -class LocalStorageNodeFactory { - - public static LocalNode from(LocalFolder parent, File file) { - if (file.isDirectory()) { - return folder(parent, file); - } else { - return file( // - parent, // - file.getName(), // - file.getPath(), // - Optional.of(file.length()), // - Optional.of(new Date(file.lastModified()))); - } - } - - public static LocalFolder folder(LocalFolder parent, File file) { - return folder(parent, file.getName(), file.getPath()); - } - - public static LocalFolder folder(LocalFolder parent, String name, String path) { - return new LocalFolder(parent, name, path); - } - - public static LocalFile file(LocalFolder folder, String name, String path, Optional size, Optional modified) { - return new LocalFile(folder, name, path, size, modified); - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/local/file/LocalStorageNodeFactory.kt b/data/src/main/java/org/cryptomator/data/cloud/local/file/LocalStorageNodeFactory.kt new file mode 100644 index 00000000..e6788644 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/local/file/LocalStorageNodeFactory.kt @@ -0,0 +1,36 @@ +package org.cryptomator.data.cloud.local.file + +import java.io.File +import java.util.Date + +internal object LocalStorageNodeFactory { + + @JvmStatic + fun from(parent: LocalFolder, file: File): LocalNode { + return if (file.isDirectory) { + folder(parent, file) + } else { + file( // + parent, // + file.name, // + file.path, // + file.length(), // + Date(file.lastModified()) + ) + } + } + + fun folder(parent: LocalFolder, file: File): LocalFolder { + return folder(parent, file.name, file.path) + } + + @JvmStatic + fun folder(parent: LocalFolder, name: String, path: String): LocalFolder { + return LocalFolder(parent, name, path) + } + + @JvmStatic + fun file(folder: LocalFolder, name: String, path: String, size: Long?, modified: Date?): LocalFile { + return LocalFile(folder, name, path, size, modified) + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/local/file/RootLocalFolder.java b/data/src/main/java/org/cryptomator/data/cloud/local/file/RootLocalFolder.java deleted file mode 100644 index 19e71595..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/local/file/RootLocalFolder.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.cryptomator.data.cloud.local.file; - -import android.os.Environment; - -import org.cryptomator.domain.Cloud; -import org.cryptomator.domain.LocalStorageCloud; - -public class RootLocalFolder extends LocalFolder { - - private final LocalStorageCloud localStorageCloud; - - public RootLocalFolder(LocalStorageCloud localStorageCloud) { - super(null, "", Environment.getExternalStorageDirectory().getPath()); - this.localStorageCloud = localStorageCloud; - } - - @Override - public Cloud getCloud() { - return localStorageCloud; - } - - @Override - public RootLocalFolder withCloud(Cloud cloud) { - return new RootLocalFolder((LocalStorageCloud) cloud); - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/local/file/RootLocalFolder.kt b/data/src/main/java/org/cryptomator/data/cloud/local/file/RootLocalFolder.kt new file mode 100644 index 00000000..fde9d439 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/local/file/RootLocalFolder.kt @@ -0,0 +1,15 @@ +package org.cryptomator.data.cloud.local.file + +import android.os.Environment +import org.cryptomator.domain.Cloud +import org.cryptomator.domain.LocalStorageCloud + +class RootLocalFolder(private val localStorageCloud: LocalStorageCloud) : LocalFolder(null, "", Environment.getExternalStorageDirectory().path) { + + override val cloud: Cloud + get() = localStorageCloud + + override fun withCloud(cloud: Cloud?): RootLocalFolder { + return RootLocalFolder(cloud as LocalStorageCloud) + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/DocumentIdCache.java b/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/DocumentIdCache.java deleted file mode 100644 index aefe70d9..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/DocumentIdCache.java +++ /dev/null @@ -1,74 +0,0 @@ -package org.cryptomator.data.cloud.local.storageaccessframework; - -import android.util.LruCache; - -import org.cryptomator.domain.CloudFolder; - -class DocumentIdCache { - - private final LruCache cache; - - DocumentIdCache() { - cache = new LruCache<>(1000); - } - - public NodeInfo get(String path) { - return cache.get(path); - } - - T cache(T value) { - add(value); - return value; - } - - public void add(LocalStorageAccessNode node) { - add(node.getPath(), new NodeInfo(node)); - } - - private void add(String path, NodeInfo info) { - cache.put(path, info); - } - - public void remove(LocalStorageAccessNode node) { - remove(node.getPath()); - } - - private void remove(String path) { - removeChildren(path); - cache.remove(path); - } - - private void removeChildren(String path) { - String prefix = path + '/'; - for (String key : cache.snapshot().keySet()) { - if (key.startsWith(prefix)) { - cache.remove(key); - } - } - } - - static class NodeInfo { - - private final String id; - private final boolean isFolder; - - private NodeInfo(LocalStorageAccessNode node) { - this(node.getDocumentId(), node instanceof CloudFolder); - } - - NodeInfo(String id, boolean isFolder) { - this.id = id; - this.isFolder = isFolder; - } - - public String getId() { - return id; - } - - public boolean isFolder() { - return isFolder; - } - - } - -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/DocumentIdCache.kt b/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/DocumentIdCache.kt new file mode 100644 index 00000000..b4c9698d --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/DocumentIdCache.kt @@ -0,0 +1,51 @@ +package org.cryptomator.data.cloud.local.storageaccessframework + +import android.util.LruCache +import org.cryptomator.domain.CloudFolder + +internal class DocumentIdCache { + + private val cache: LruCache = LruCache(1000) + + operator fun get(path: String): NodeInfo? { + return cache[path] + } + + fun cache(value: T): T { + add(value) + return value + } + + fun add(node: LocalStorageAccessNode) { + add(node.path, NodeInfo(node)) + } + + private fun add(path: String, info: NodeInfo) { + cache.put(path, info) + } + + fun remove(node: LocalStorageAccessNode) { + remove(node.path) + } + + private fun remove(path: String) { + removeChildren(path) + cache.remove(path) + } + + private fun removeChildren(path: String) { + val prefix = "$path/" + for (key in cache.snapshot().keys) { + if (key.startsWith(prefix)) { + cache.remove(key) + } + } + } + + internal class NodeInfo(val id: String?, val isFolder: Boolean) { + + constructor(node: LocalStorageAccessNode) : this(node.documentId, node is CloudFolder) + + } + +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFile.java b/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFile.java deleted file mode 100644 index a65a5de8..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFile.java +++ /dev/null @@ -1,96 +0,0 @@ -package org.cryptomator.data.cloud.local.storageaccessframework; - -import android.net.Uri; - -import org.cryptomator.domain.Cloud; -import org.cryptomator.domain.CloudFile; -import org.cryptomator.util.Optional; - -import java.util.Date; - -import static android.net.Uri.parse; - -class LocalStorageAccessFile implements CloudFile, LocalStorageAccessNode { - - private final LocalStorageAccessFolder parent; - private final String name; - private final String path; - private final Optional size; - private final Optional modified; - private final String documentId; - private final String documentUri; - - LocalStorageAccessFile(LocalStorageAccessFolder parent, String name, String path, Optional size, Optional modified, String documentId, String documentUri) { - this.parent = parent; - this.name = name; - this.path = path; - this.size = size; - this.modified = modified; - this.documentId = documentId; - this.documentUri = documentUri; - } - - @Override - public Cloud getCloud() { - return parent.getCloud(); - } - - @Override - public String getName() { - return name; - } - - @Override - public String getPath() { - return path; - } - - @Override - public Uri getUri() { - return parse(documentUri); - } - - @Override - public LocalStorageAccessFolder getParent() { - return parent; - } - - @Override - public String getDocumentId() { - return documentId; - } - - @Override - public Optional getSize() { - return size; - } - - @Override - public Optional getModified() { - return modified; - } - - @Override - public boolean equals(Object obj) { - if (obj == this) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - return internalEquals((LocalStorageAccessFile) obj); - } - - private boolean internalEquals(LocalStorageAccessFile o) { - return path.equals(o.path); - } - - @Override - public int hashCode() { - final int prime = 31; - int hash = 56127034; - hash = hash * prime + path.hashCode(); - return hash; - } - -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFile.kt b/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFile.kt new file mode 100644 index 00000000..cffbb02c --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFile.kt @@ -0,0 +1,42 @@ +package org.cryptomator.data.cloud.local.storageaccessframework + +import android.net.Uri +import org.cryptomator.domain.Cloud +import org.cryptomator.domain.CloudFile +import java.util.Date + +class LocalStorageAccessFile( + override val parent: LocalStorageAccessFolder, + override val name: String, + override val path: String, + override val size: Long?, + override val modified: Date?, + override val documentId: String?, + private val documentUri: String? +) : CloudFile, LocalStorageAccessNode { + + override val cloud: Cloud? + get() = parent.cloud + override val uri: Uri + get() = Uri.parse(documentUri) + + override fun equals(other: Any?): Boolean { + if (other === this) { + return true + } + return if (other == null || javaClass != other.javaClass) { + false + } else internalEquals(other as LocalStorageAccessFile) + } + + private fun internalEquals(o: LocalStorageAccessFile): Boolean { + return path == o.path + } + + override fun hashCode(): Int { + val prime = 31 + var hash = 56127034 + hash = hash * prime + path.hashCode() + return hash + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFolder.java b/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFolder.java deleted file mode 100644 index a392f618..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFolder.java +++ /dev/null @@ -1,87 +0,0 @@ -package org.cryptomator.data.cloud.local.storageaccessframework; - -import android.net.Uri; - -import org.cryptomator.domain.Cloud; -import org.cryptomator.domain.CloudFolder; - -import static android.net.Uri.parse; - -class LocalStorageAccessFolder implements CloudFolder, LocalStorageAccessNode { - - private final LocalStorageAccessFolder parent; - private final String name; - private final String path; - private final String documentId; - private final String documentUri; - - LocalStorageAccessFolder(LocalStorageAccessFolder parent, String name, String path, String documentId, String documentUri) { - this.parent = parent; - this.name = name; - this.path = path; - this.documentId = documentId; - this.documentUri = documentUri; - } - - @Override - public Cloud getCloud() { - return parent.getCloud(); - } - - @Override - public String getName() { - return name; - } - - @Override - public String getPath() { - return path; - } - - @Override - public Uri getUri() { - if (documentUri == null) { - return null; - } - - return parse(documentUri); - } - - @Override - public LocalStorageAccessFolder getParent() { - return parent; - } - - @Override - public String getDocumentId() { - return documentId; - } - - @Override - public boolean equals(Object obj) { - if (obj == this) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - return internalEquals((LocalStorageAccessFolder) obj); - } - - private boolean internalEquals(LocalStorageAccessFolder o) { - return path.equals(o.path); - } - - @Override - public int hashCode() { - final int prime = 31; - int hash = 341797327; - hash = hash * prime + path.hashCode(); - return hash; - } - - @Override - public LocalStorageAccessFolder withCloud(Cloud cloud) { - return new LocalStorageAccessFolder(parent.withCloud(cloud), name, path, documentId, documentUri); - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFolder.kt b/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFolder.kt new file mode 100644 index 00000000..2416f61b --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFolder.kt @@ -0,0 +1,40 @@ +package org.cryptomator.data.cloud.local.storageaccessframework + +import android.net.Uri +import org.cryptomator.domain.Cloud +import org.cryptomator.domain.CloudFolder + +open class LocalStorageAccessFolder(override val parent: LocalStorageAccessFolder?, override val name: String, override val path: String, override val documentId: String?, private val documentUri: String?) : + CloudFolder, LocalStorageAccessNode { + + override val cloud: Cloud? + get() = parent?.cloud + override val uri: Uri? + get() = if (documentUri == null) { + null + } else Uri.parse(documentUri) + + override fun equals(other: Any?): Boolean { + if (other === this) { + return true + } + return if (other == null || javaClass != other.javaClass) { + false + } else internalEquals(other as LocalStorageAccessFolder) + } + + private fun internalEquals(o: LocalStorageAccessFolder): Boolean { + return path == o.path + } + + override fun hashCode(): Int { + val prime = 31 + var hash = 341797327 + hash = hash * prime + path.hashCode() + return hash + } + + override fun withCloud(cloud: Cloud?): LocalStorageAccessFolder? { + return LocalStorageAccessFolder(parent!!.withCloud(cloud), name, path, documentId, documentUri) + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFrameworkContentRepository.java b/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFrameworkContentRepository.java deleted file mode 100644 index a880b696..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFrameworkContentRepository.java +++ /dev/null @@ -1,123 +0,0 @@ -package org.cryptomator.data.cloud.local.storageaccessframework; - -import android.content.Context; -import android.os.Build; - -import androidx.annotation.RequiresApi; - -import org.cryptomator.domain.CloudNode; -import org.cryptomator.domain.LocalStorageCloud; -import org.cryptomator.domain.exception.BackendException; -import org.cryptomator.domain.exception.FatalBackendException; -import org.cryptomator.domain.exception.NoSuchCloudFileException; -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 org.cryptomator.util.file.MimeTypes; - -import java.io.File; -import java.io.IOException; -import java.io.OutputStream; -import java.util.List; - -@RequiresApi(Build.VERSION_CODES.LOLLIPOP) -public class LocalStorageAccessFrameworkContentRepository implements CloudContentRepository { - - private final LocalStorageAccessFrameworkImpl localStorageAccessFramework; - - public LocalStorageAccessFrameworkContentRepository(Context context, MimeTypes mimeTypes, LocalStorageCloud localStorageCloud) { - this.localStorageAccessFramework = new LocalStorageAccessFrameworkImpl(context, mimeTypes, localStorageCloud, new DocumentIdCache()); - } - - @Override - public LocalStorageAccessFolder root(LocalStorageCloud cloud) throws BackendException { - return localStorageAccessFramework.root(); - } - - @Override - public LocalStorageAccessFolder resolve(LocalStorageCloud cloud, String path) throws BackendException { - return localStorageAccessFramework.resolve(path); - } - - @Override - public LocalStorageAccessFile file(LocalStorageAccessFolder parent, String name) throws BackendException { - return localStorageAccessFramework.file(parent, name); - } - - @Override - public LocalStorageAccessFile file(LocalStorageAccessFolder parent, String name, Optional size) throws BackendException { - return localStorageAccessFramework.file(parent, name, size); - } - - @Override - public LocalStorageAccessFolder folder(LocalStorageAccessFolder parent, String name) throws BackendException { - return localStorageAccessFramework.folder(parent, name); - } - - @Override - public boolean exists(LocalStorageAccessNode node) throws BackendException { - return localStorageAccessFramework.exists(node); - } - - @Override - public List list(LocalStorageAccessFolder folder) throws BackendException { - return localStorageAccessFramework.list(folder); - } - - @Override - public LocalStorageAccessFolder create(LocalStorageAccessFolder folder) throws BackendException { - return localStorageAccessFramework.create(folder); - } - - @Override - public LocalStorageAccessFolder move(LocalStorageAccessFolder source, LocalStorageAccessFolder target) throws BackendException { - if (source.getDocumentId() == null) { - throw new NoSuchCloudFileException(source.getName()); - } - return (LocalStorageAccessFolder) localStorageAccessFramework.move(source, target); - } - - @Override - public LocalStorageAccessFile move(LocalStorageAccessFile source, LocalStorageAccessFile target) throws BackendException { - return (LocalStorageAccessFile) localStorageAccessFramework.move(source, target); - } - - @Override - public LocalStorageAccessFile write(LocalStorageAccessFile file, DataSource data, ProgressAware progressAware, boolean replace, long size) throws BackendException { - try { - return localStorageAccessFramework.write(file, data, progressAware, replace, size); - } catch (IOException e) { - throw new FatalBackendException(e); - } - } - - @Override - public void read(LocalStorageAccessFile file, Optional tmpEnctypted, OutputStream data, ProgressAware progressAware) throws BackendException { - try { - if (file.getDocumentId() == null) { - throw new NoSuchCloudFileException(file.getName()); - } - localStorageAccessFramework.read(file, data, progressAware); - } catch (IOException e) { - throw new FatalBackendException(e); - } - } - - @Override - public void delete(LocalStorageAccessNode node) throws BackendException { - localStorageAccessFramework.delete(node); - } - - @Override - public String checkAuthenticationAndRetrieveCurrentAccount(LocalStorageCloud cloud) throws BackendException { - return null; - } - - @Override - public void logout(LocalStorageCloud cloud) throws BackendException { - // empty - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFrameworkContentRepository.kt b/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFrameworkContentRepository.kt new file mode 100644 index 00000000..0f21ca53 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFrameworkContentRepository.kt @@ -0,0 +1,111 @@ +package org.cryptomator.data.cloud.local.storageaccessframework + +import android.content.Context +import org.cryptomator.domain.LocalStorageCloud +import org.cryptomator.domain.exception.BackendException +import org.cryptomator.domain.exception.FatalBackendException +import org.cryptomator.domain.exception.NoSuchCloudFileException +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.file.MimeTypes +import java.io.File +import java.io.IOException +import java.io.OutputStream + +class LocalStorageAccessFrameworkContentRepository(context: Context, mimeTypes: MimeTypes, localStorageCloud: LocalStorageCloud) : + CloudContentRepository { + + private val localStorageAccessFramework: LocalStorageAccessFrameworkImpl = LocalStorageAccessFrameworkImpl(context, mimeTypes, localStorageCloud, DocumentIdCache()) + + @Throws(BackendException::class) + override fun root(cloud: LocalStorageCloud): LocalStorageAccessFolder { + return localStorageAccessFramework.root() + } + + override fun resolve(cloud: LocalStorageCloud, path: String): LocalStorageAccessFolder { + return localStorageAccessFramework.resolve(path) + } + + @Throws(BackendException::class) + override fun file(parent: LocalStorageAccessFolder, name: String): LocalStorageAccessFile { + return localStorageAccessFramework.file(parent, name, null) + } + + @Throws(BackendException::class) + override fun file(parent: LocalStorageAccessFolder, name: String, size: Long?): LocalStorageAccessFile { + return localStorageAccessFramework.file(parent, name, size) + } + + @Throws(BackendException::class) + override fun folder(parent: LocalStorageAccessFolder, name: String): LocalStorageAccessFolder { + return localStorageAccessFramework.folder(parent, name) + } + + @Throws(BackendException::class) + override fun exists(node: LocalStorageAccessNode): Boolean { + return localStorageAccessFramework.exists(node) + } + + @Throws(BackendException::class) + override fun list(folder: LocalStorageAccessFolder): List { + return localStorageAccessFramework.list(folder) + } + + @Throws(BackendException::class) + override fun create(folder: LocalStorageAccessFolder): LocalStorageAccessFolder { + return localStorageAccessFramework.create(folder) + } + + @Throws(BackendException::class) + override fun move(source: LocalStorageAccessFolder, target: LocalStorageAccessFolder): LocalStorageAccessFolder { + if (source.documentId == null) { + throw NoSuchCloudFileException(source.name) + } + return localStorageAccessFramework.move(source, target) as LocalStorageAccessFolder + } + + @Throws(BackendException::class) + override fun move(source: LocalStorageAccessFile, target: LocalStorageAccessFile): LocalStorageAccessFile { + return localStorageAccessFramework.move(source, target) as LocalStorageAccessFile + } + + @Throws(BackendException::class) + override fun write(file: LocalStorageAccessFile, data: DataSource, progressAware: ProgressAware, replace: Boolean, size: Long): LocalStorageAccessFile { + return try { + localStorageAccessFramework.write(file, data, progressAware, replace, size) + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun read(file: LocalStorageAccessFile, encryptedTmpFile: File?, data: OutputStream, progressAware: ProgressAware) { + try { + if (file.documentId == null) { + throw NoSuchCloudFileException(file.name) + } + localStorageAccessFramework.read(file, data, progressAware) + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun delete(node: LocalStorageAccessNode) { + localStorageAccessFramework.delete(node) + } + + @Throws(BackendException::class) + override fun checkAuthenticationAndRetrieveCurrentAccount(cloud: LocalStorageCloud): String { + return "" + } + + @Throws(BackendException::class) + override fun logout(cloud: LocalStorageCloud) { + // empty + } + +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFrameworkImpl.java b/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFrameworkImpl.java deleted file mode 100644 index 1143a51e..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFrameworkImpl.java +++ /dev/null @@ -1,536 +0,0 @@ -package org.cryptomator.data.cloud.local.storageaccessframework; - -import android.content.ContentResolver; -import android.content.Context; -import android.content.UriPermission; -import android.database.Cursor; -import android.net.Uri; -import android.os.Build; -import android.provider.DocumentsContract; -import android.provider.DocumentsContract.Document; - -import androidx.annotation.RequiresApi; -import androidx.documentfile.provider.DocumentFile; - -import org.cryptomator.data.util.TransferredBytesAwareInputStream; -import org.cryptomator.data.util.TransferredBytesAwareOutputStream; -import org.cryptomator.domain.CloudFolder; -import org.cryptomator.domain.CloudNode; -import org.cryptomator.domain.LocalStorageCloud; -import org.cryptomator.domain.exception.BackendException; -import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException; -import org.cryptomator.domain.exception.FatalBackendException; -import org.cryptomator.domain.exception.NoSuchCloudFileException; -import org.cryptomator.domain.exception.NotFoundException; -import org.cryptomator.domain.exception.authentication.NoAuthenticationProvidedException; -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.Supplier; -import org.cryptomator.util.file.MimeType; -import org.cryptomator.util.file.MimeTypes; - -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; - -import timber.log.Timber; - -import static org.cryptomator.data.cloud.local.storageaccessframework.LocalStorageAccessFrameworkNodeFactory.from; -import static org.cryptomator.data.util.CopyStream.closeQuietly; -import static org.cryptomator.data.util.CopyStream.copyStreamToStream; -import static org.cryptomator.domain.usecases.ProgressAware.NO_OP_PROGRESS_AWARE; -import static org.cryptomator.domain.usecases.cloud.Progress.progress; - -@RequiresApi(Build.VERSION_CODES.LOLLIPOP) -class LocalStorageAccessFrameworkImpl { - - private final Context context; - private final RootLocalStorageAccessFolder root; - private final DocumentIdCache idCache; - private final MimeTypes mimeTypes; - - LocalStorageAccessFrameworkImpl(Context context, MimeTypes mimeTypes, LocalStorageCloud cloud, DocumentIdCache documentIdCache) { - this.mimeTypes = mimeTypes; - if (!hasUriPermissions(context, cloud.rootUri())) { - throw new NoAuthenticationProvidedException(cloud); - } - this.context = context; - this.root = new RootLocalStorageAccessFolder(cloud); - this.idCache = documentIdCache; - } - - private boolean hasUriPermissions(Context context, String uri) { - Optional uriPermission = uriPermissionFor(context, uri); - return uriPermission.isPresent() // - && uriPermission.get().isReadPermission() // - && uriPermission.get().isWritePermission(); - } - - private Optional uriPermissionFor(Context context, String uri) { - for (UriPermission uriPermission : context.getContentResolver().getPersistedUriPermissions()) { - if (uri.equals(uriPermission.getUri().toString())) { - return Optional.of(uriPermission); - } - } - return Optional.empty(); - } - - public LocalStorageAccessFolder root() { - return root; - } - - public LocalStorageAccessFolder resolve(String path) throws BackendException { - if (path.startsWith("/")) { - path = path.substring(1); - } - - String[] names = path.split("/"); - LocalStorageAccessFolder folder = root; - for (String name : names) { - folder = folder(folder, name); - } - return folder; - } - - public LocalStorageAccessFile file(LocalStorageAccessFolder parent, String name) throws BackendException { - return file( // - parent, // - name, // - Optional.empty()); - } - - public LocalStorageAccessFile file(LocalStorageAccessFolder parent, String name, Optional size) throws BackendException { - if (parent.getDocumentId() == null) { - return LocalStorageAccessFrameworkNodeFactory.file( // - parent, // - name, // - size); - } - String path = LocalStorageAccessFrameworkNodeFactory.getNodePath(parent, name); - DocumentIdCache.NodeInfo nodeInfo = idCache.get(path); - if (nodeInfo != null && !nodeInfo.isFolder()) { - return LocalStorageAccessFrameworkNodeFactory.file( // - parent, // - name, // - path, // - size, // - nodeInfo.getId()); - } - List cloudNodes = listFilesWithNameFilter(parent, name); - if (cloudNodes.size() > 0) { - LocalStorageAccessNode cloudNode = cloudNodes.get(0); - if (cloudNode instanceof LocalStorageAccessFile) { - return idCache.cache((LocalStorageAccessFile) cloudNode); - } - } - return LocalStorageAccessFrameworkNodeFactory.file( // - parent, // - name, // - size); - } - - public LocalStorageAccessFolder folder(LocalStorageAccessFolder parent, String name) throws BackendException { - if (parent.getDocumentId() == null) { - return LocalStorageAccessFrameworkNodeFactory.folder( // - parent, // - name); - } - String path = LocalStorageAccessFrameworkNodeFactory.getNodePath(parent, name); - DocumentIdCache.NodeInfo nodeInfo = idCache.get(path); - if (nodeInfo != null && nodeInfo.isFolder()) { - return LocalStorageAccessFrameworkNodeFactory.folder( // - parent, // - name, // - nodeInfo.getId()); - } - List cloudNodes = listFilesWithNameFilter(parent, name); - if (cloudNodes.size() > 0) { - LocalStorageAccessNode cloudNode = cloudNodes.get(0); - if (cloudNode instanceof LocalStorageAccessFolder) { - return idCache.cache((LocalStorageAccessFolder) cloudNode); - } - } - - return LocalStorageAccessFrameworkNodeFactory.folder( // - parent, // - name); - } - - private List listFilesWithNameFilter(LocalStorageAccessFolder parent, String name) throws BackendException { - if (parent.getUri() == null) { - List parents = listFilesWithNameFilter(parent.getParent(), parent.getName()); - if (parents.isEmpty() || !(parents.get(0) instanceof LocalStorageAccessFolder)) { - throw new NoSuchCloudFileException(name); - } - parent = (LocalStorageAccessFolder) parents.get(0); - } - Cursor childCursor = null; - try { - childCursor = contentResolver() // - .query( // - DocumentsContract.buildChildDocumentsUriUsingTree( // - parent.getUri(), // - parent.getDocumentId()), // - new String[] {Document.COLUMN_DISPLAY_NAME, // cursor position 0 - Document.COLUMN_MIME_TYPE, // cursor position 1 - Document.COLUMN_SIZE, // cursor position 2 - Document.COLUMN_LAST_MODIFIED, // cursor position 3 - Document.COLUMN_DOCUMENT_ID // cursor position 4 - }, // - null, // - null, // - null); - - List result = new ArrayList<>(); - while (childCursor != null && childCursor.moveToNext()) { - if (childCursor.getString(0).equals(name)) { - result.add(idCache.cache(from(parent, childCursor))); - } - } - return result; - } catch (IllegalArgumentException e) { - if (e.getMessage().contains(FileNotFoundException.class.getCanonicalName())) { - throw new NoSuchCloudFileException(name); - } - throw new FatalBackendException(e); - } finally { - closeQuietly(childCursor); - } - } - - public boolean exists(LocalStorageAccessNode node) throws BackendException { - try { - - List cloudNodes = listFilesWithNameFilter( // - node.getParent(), // - node.getName()); - - boolean documentExists = cloudNodes.size() > 0; - - if (documentExists) { - idCache.add(cloudNodes.get(0)); - } - - return documentExists; - } catch (NoSuchCloudFileException e) { - return false; - } - } - - public List list(LocalStorageAccessFolder folder) throws BackendException { - Cursor childCursor = contentResolver() // - .query( // - DocumentsContract.buildChildDocumentsUriUsingTree( // - folder.getUri(), // - folder.getDocumentId()), // - new String[] { // - Document.COLUMN_DISPLAY_NAME, // cursor position 0 - Document.COLUMN_MIME_TYPE, // cursor position 1 - Document.COLUMN_SIZE, // cursor position 2 - Document.COLUMN_LAST_MODIFIED, // cursor position 3 - Document.COLUMN_DOCUMENT_ID // cursor position 4 - }, null, null, null); - - try { - List result = new ArrayList<>(); - while (childCursor != null && childCursor.moveToNext()) { - result.add(idCache.cache(from(folder, childCursor))); - } - return result; - } finally { - closeQuietly(childCursor); - } - } - - public LocalStorageAccessFolder create(LocalStorageAccessFolder folder) throws BackendException { - if (folder // - .getParent() // - .getDocumentId() == null) { - folder = new LocalStorageAccessFolder( // - create(folder.getParent()), // - folder.getName(), // - folder.getPath(), // - null, // - null); - } - Uri createdDocument; - try { - createdDocument = DocumentsContract.createDocument( // - contentResolver(), // - folder.getParent().getUri(), // - Document.MIME_TYPE_DIR, // - folder.getName()); - } catch (FileNotFoundException e) { - throw new NoSuchCloudFileException(folder.getName()); - } - return idCache.cache( // - LocalStorageAccessFrameworkNodeFactory.folder( // - folder.getParent(), // - buildDocumentFile(createdDocument))); - } - - public LocalStorageAccessNode move(LocalStorageAccessNode source, LocalStorageAccessNode target) throws BackendException { - if (exists(target)) { - throw new CloudNodeAlreadyExistsException(target.getName()); - } - - idCache.remove(source); - idCache.remove(target); - boolean isRename = !source // - .getName() // - .equals(target.getName()); - boolean isMove = !source // - .getParent() // - .equals(target.getParent()); - LocalStorageAccessNode renamedSource = source; - if (isRename) { - renamedSource = rename(source, target.getName()); - } - if (isMove) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - return idCache.cache( // - moveForApiStartingFrom24(renamedSource, target)); - } else { - return idCache.cache( // - moveForApiBelow24(renamedSource, target)); - } - } - return renamedSource; - } - - private LocalStorageAccessNode rename(LocalStorageAccessNode source, String name) throws NoSuchCloudFileException { - Uri newUri = null; - try { - newUri = DocumentsContract.renameDocument( // - contentResolver(), // - source.getUri(), // - name); - } catch (FileNotFoundException e) { - // Bug in Android 9 see #460 - if (Build.VERSION.SDK_INT != Build.VERSION_CODES.P) { - throw new NoSuchCloudFileException(source.getName()); - } - } - - // Bug in Android 9 see #460 - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) { - try { - List cloudNodes = listFilesWithNameFilter( // - source.getParent(), // - name); - - newUri = cloudNodes.get(0).getUri(); - } catch (BackendException e) { - Timber.tag("LocalStgeAccessFrkImpl").e(e); - } - } - - return LocalStorageAccessFrameworkNodeFactory.from( // - source.getParent(), // - buildDocumentFile(newUri)); - } - - @RequiresApi(api = Build.VERSION_CODES.N) - private LocalStorageAccessNode moveForApiStartingFrom24(LocalStorageAccessNode source, LocalStorageAccessNode target) throws NoSuchCloudFileException { - Uri movedTargetUri; - try { - movedTargetUri = DocumentsContract.moveDocument( // - contentResolver(), // - source.getUri(), // - source.getParent().getUri(), // - target.getParent().getUri()); - } catch (FileNotFoundException e) { - throw new NoSuchCloudFileException(source.getName()); - } - return from( // - target.getParent(), // - buildDocumentFile(movedTargetUri)); - } - - private LocalStorageAccessNode moveForApiBelow24(LocalStorageAccessNode source, LocalStorageAccessNode target) throws BackendException { - try { - LocalStorageAccessNode result; - if (source instanceof CloudFolder) { - result = moveForApiBelow24( // - (LocalStorageAccessFolder) source, // - (LocalStorageAccessFolder) target); - } else { - result = moveForApiBelow24( // - (LocalStorageAccessFile) source, // - (LocalStorageAccessFile) target); - } - delete(source); - return result; - } catch (IOException e) { - throw new FatalBackendException(e); - } - } - - private LocalStorageAccessFolder moveForApiBelow24(LocalStorageAccessFolder source, LocalStorageAccessFolder target) throws IOException, BackendException { - if (!exists(target.getParent())) { - throw new NoSuchCloudFileException(target.getParent().getPath()); - } - LocalStorageAccessFolder createdFolder = create(target); - for (CloudNode child : list(source)) { - if (child instanceof CloudFolder) { - moveForApiBelow24( // - (LocalStorageAccessFolder) child, // - folder(target, child.getName())); - } else { - moveForApiBelow24( // - (LocalStorageAccessFile) child, // - file(target, child.getName())); - } - } - return createdFolder; - } - - private LocalStorageAccessFile moveForApiBelow24(final LocalStorageAccessFile source, LocalStorageAccessFile target) throws IOException, BackendException { - DataSource dataSource = new DataSource() { - @Override - public void close() throws IOException { - // do nothing - } - - @Override - public Optional size(Context context) { - return source.getSize(); - } - - @Override - public InputStream open(Context context) throws IOException { - return contentResolver().openInputStream(source.getUri()); - } - - @Override - public DataSource decorate(DataSource delegate) { - return delegate; - } - }; - return write(target, dataSource, NO_OP_PROGRESS_AWARE, true, source.getSize().get()); - } - - public LocalStorageAccessFile write( // - LocalStorageAccessFile file, // - final DataSource data, // - final ProgressAware progressAware, // - final boolean replace, // - final long size) throws IOException, BackendException { - - progressAware.onProgress(Progress.started(UploadState.upload(file))); - Optional fileUri = existingFileUri(file); - if (!replace && fileUri.isPresent()) { - throw new CloudNodeAlreadyExistsException("CloudNode already exists and replace is false"); - } - - if (file.getParent().getUri() == null) { - LocalStorageAccessFolder parent = (LocalStorageAccessFolder) listFilesWithNameFilter(file.getParent().getParent(), file.getParent().getName()).get(0); - String tmpFileUri = fileUri.isPresent() ? fileUri.get().toString() : ""; - file = new LocalStorageAccessFile(parent, file.getName(), file.getPath(), file.getSize(), file.getModified(), file.getDocumentId(), tmpFileUri); - } - - final LocalStorageAccessFile tmpFile = file; - - Uri uploadUri = fileUri.orElseGet(createNewDocumentSupplier(tmpFile)); - if (uploadUri == null) { - throw new NotFoundException(tmpFile.getName()); - } - - try (OutputStream out = contentResolver().openOutputStream(uploadUri); // - TransferredBytesAwareInputStream in = new TransferredBytesAwareInputStream(data.open(context)) { - @Override - public void bytesTransferred(long transferred) { - progressAware // - .onProgress(progress(UploadState.upload(tmpFile)) // - .between(0) // - .and(size) // - .withValue(transferred)); - } - }) { - if (out instanceof FileOutputStream) { - ((FileOutputStream) out).getChannel().truncate(0); - } - - copyStreamToStream(in, out); - } - - progressAware.onProgress(Progress.completed(UploadState.upload(file))); - - return LocalStorageAccessFrameworkNodeFactory.file( // - file.getParent(), // - buildDocumentFile(uploadUri)); - } - - private Supplier createNewDocumentSupplier(final LocalStorageAccessFile file) { - return () -> { - MimeType mimeType = mimeTypes.fromFilename(file.getName()) // - .orElse(MimeType.APPLICATION_OCTET_STREAM); - try { - return DocumentsContract.createDocument( // - contentResolver(), // - file.getParent().getUri(), // - mimeType.toString(), // - file.getName()); - } catch (FileNotFoundException e) { - return null; - } - }; - } - - private Optional existingFileUri(LocalStorageAccessFile file) throws BackendException { - List nodes = listFilesWithNameFilter( // - file.getParent(), // - file.getName()); - if (nodes.size() > 0) { - return Optional.of(nodes.get(0).getUri()); - } else { - return Optional.empty(); - } - } - - public void read(final LocalStorageAccessFile file, final OutputStream data, final ProgressAware progressAware) throws IOException { - progressAware.onProgress(Progress.started(DownloadState.download(file))); - - try (InputStream in = contentResolver().openInputStream(file.getUri()); // - TransferredBytesAwareOutputStream out = new TransferredBytesAwareOutputStream(data) { - @Override - public void bytesTransferred(long transferred) { - progressAware.onProgress(progress(DownloadState.download(file)) // - .between(0) // - .and(file.getSize().orElse(Long.MAX_VALUE)) // - .withValue(transferred)); - } - }) { - copyStreamToStream(in, out); - } - - progressAware.onProgress(Progress.completed(DownloadState.download(file))); - } - - public void delete(LocalStorageAccessNode node) throws NoSuchCloudFileException { - try { - DocumentsContract.deleteDocument( // - contentResolver(), // - node.getUri()); - } catch (FileNotFoundException e) { - throw new NoSuchCloudFileException(node.getName()); - } - idCache.remove(node); - } - - private DocumentFile buildDocumentFile(Uri fileUri) { - return DocumentFile.fromSingleUri(context, fileUri); - } - - private ContentResolver contentResolver() { - return context.getContentResolver(); - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFrameworkImpl.kt b/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFrameworkImpl.kt new file mode 100644 index 00000000..bf93abcd --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFrameworkImpl.kt @@ -0,0 +1,399 @@ +package org.cryptomator.data.cloud.local.storageaccessframework + +import android.content.ContentResolver +import android.content.Context +import android.content.UriPermission +import android.net.Uri +import android.os.Build +import android.provider.DocumentsContract +import androidx.documentfile.provider.DocumentFile +import org.cryptomator.data.cloud.local.storageaccessframework.LocalStorageAccessFrameworkNodeFactory.file +import org.cryptomator.data.cloud.local.storageaccessframework.LocalStorageAccessFrameworkNodeFactory.folder +import org.cryptomator.data.cloud.local.storageaccessframework.LocalStorageAccessFrameworkNodeFactory.from +import org.cryptomator.data.cloud.local.storageaccessframework.LocalStorageAccessFrameworkNodeFactory.getNodePath +import org.cryptomator.data.util.CopyStream +import org.cryptomator.data.util.TransferredBytesAwareInputStream +import org.cryptomator.data.util.TransferredBytesAwareOutputStream +import org.cryptomator.domain.LocalStorageCloud +import org.cryptomator.domain.exception.BackendException +import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException +import org.cryptomator.domain.exception.FatalBackendException +import org.cryptomator.domain.exception.NoSuchCloudFileException +import org.cryptomator.domain.exception.NotFoundException +import org.cryptomator.domain.exception.ParentFolderIsNullException +import org.cryptomator.domain.exception.authentication.NoAuthenticationProvidedException +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.file.MimeType +import org.cryptomator.util.file.MimeTypes +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.io.IOException +import java.io.OutputStream +import java.util.ArrayList +import java.util.function.Supplier + +internal class LocalStorageAccessFrameworkImpl(context: Context, private val mimeTypes: MimeTypes, cloud: LocalStorageCloud, documentIdCache: DocumentIdCache) { + + private val context: Context + private val root: RootLocalStorageAccessFolder + private val idCache: DocumentIdCache + + private fun hasUriPermissions(context: Context, uri: String): Boolean { + val uriPermission = uriPermissionFor(context, uri) + return uriPermission != null && uriPermission.isReadPermission && uriPermission.isWritePermission + } + + private fun uriPermissionFor(context: Context, uri: String): UriPermission? { + return context + .contentResolver + .persistedUriPermissions + .find { uriPermission -> uriPermission.uri.toString() == uri } + } + + fun root(): LocalStorageAccessFolder { + return root + } + + @Throws(BackendException::class) + fun resolve(path: String): LocalStorageAccessFolder { + val names = path.removePrefix("/").split("/").toTypedArray() + var folder: LocalStorageAccessFolder = root + for (name in names) { + folder = folder(folder, name) + } + return folder + } + + @Throws(BackendException::class) + fun file(parent: LocalStorageAccessFolder, name: String, size: Long?): LocalStorageAccessFile { + if (parent.documentId == null) { + return LocalStorageAccessFrameworkNodeFactory.file(parent, name, size) + } + val path = getNodePath(parent, name) + val nodeInfo = idCache[path] + if (nodeInfo != null && !nodeInfo.isFolder && nodeInfo.id != null) { + return file(parent, name, path, size, nodeInfo.id) + } + listFilesWithNameFilter(parent, name).getOrNull(0)?.let { + if(it is LocalStorageAccessFile) { + return idCache.cache(it) + } + } + return LocalStorageAccessFrameworkNodeFactory.file(parent, name, size) + } + + @Throws(BackendException::class) + fun folder(parent: LocalStorageAccessFolder, name: String): LocalStorageAccessFolder { + if (parent.documentId == null) { + return LocalStorageAccessFrameworkNodeFactory.folder(parent, name) + } + val path = getNodePath(parent, name) + val nodeInfo = idCache[path] + if (nodeInfo != null && nodeInfo.isFolder && nodeInfo.id != null) { + return folder(parent, name, nodeInfo.id) + } + listFilesWithNameFilter(parent, name).getOrNull(0)?.let { + if(it is LocalStorageAccessFolder) { + return idCache.cache(it) + } + } + return LocalStorageAccessFrameworkNodeFactory.folder(parent, name) + } + + @Throws(BackendException::class) + private fun listFilesWithNameFilter(parent: LocalStorageAccessFolder, name: String): List { + var parent = parent + if (parent.uri == null) { + parent.parent?.let { + val parents = listFilesWithNameFilter(it, parent.name) + if (parents.isEmpty() || parents[0] !is LocalStorageAccessFolder) { + throw NoSuchCloudFileException(name) + } + parent = parents[0] as LocalStorageAccessFolder + } ?: throw ParentFolderIsNullException(parent.name) + } + + val result: MutableList = ArrayList() + try { + contentResolver() // + .query( // + DocumentsContract.buildChildDocumentsUriUsingTree( // + parent.uri, // + parent.documentId + ), arrayOf( + DocumentsContract.Document.COLUMN_DISPLAY_NAME, // cursor position 0 + DocumentsContract.Document.COLUMN_MIME_TYPE, // cursor position 1 + DocumentsContract.Document.COLUMN_SIZE, // cursor position 2 + DocumentsContract.Document.COLUMN_LAST_MODIFIED, // cursor position 3 + DocumentsContract.Document.COLUMN_DOCUMENT_ID // cursor position 4 + ), // + null, // + null, // + null + )?.use { + while (it.moveToNext()) { + if (it.getString(0) == name) { + result.add(idCache.cache(from(parent, it))) + } + } + } + return result + } catch (e: IllegalArgumentException) { + if (e.message?.contains(FileNotFoundException::class.java.canonicalName!!) == true) { + throw NoSuchCloudFileException(name) + } + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + fun exists(node: LocalStorageAccessNode): Boolean { + node.parent?.let { + return try { + return listFilesWithNameFilter(it, node.name).getOrNull(0)?.also { + idCache.add(it) + } != null + } catch (e: NoSuchCloudFileException) { + false + } + } ?: throw ParentFolderIsNullException(node.name) + } + + @Throws(BackendException::class) + fun list(folder: LocalStorageAccessFolder): List { + val result: MutableList = ArrayList() + contentResolver() // + .query( // + DocumentsContract.buildChildDocumentsUriUsingTree( // + folder.uri, // + folder.documentId + ), arrayOf( // + DocumentsContract.Document.COLUMN_DISPLAY_NAME, // cursor position 0 + DocumentsContract.Document.COLUMN_MIME_TYPE, // cursor position 1 + DocumentsContract.Document.COLUMN_SIZE, // cursor position 2 + DocumentsContract.Document.COLUMN_LAST_MODIFIED, // cursor position 3 + DocumentsContract.Document.COLUMN_DOCUMENT_ID // cursor position 4 + ), null, null, null + )?.use { + while (it.moveToNext()) { + result.add(idCache.cache(from(folder, it))) + } + } + return result + } + + @Throws(BackendException::class) + fun create(folder: LocalStorageAccessFolder): LocalStorageAccessFolder { + var folder = folder + folder.parent?.let { foldersParent -> + if (foldersParent.documentId == null) { + folder = LocalStorageAccessFolder( // + create(foldersParent), + folder.name, // + folder.path, // + null, // + null + ) + } + } ?: throw ParentFolderIsNullException(folder.name) + + folder.parent?.let { foldersParent -> + foldersParent.uri?.let { foldersParentUri -> + val createdDocument = try { + DocumentsContract.createDocument( // + contentResolver(), // + foldersParentUri, + DocumentsContract.Document.MIME_TYPE_DIR, // + folder.name + ) + } catch (e: FileNotFoundException) { + throw NoSuchCloudFileException(folder.name) + } ?: throw FatalBackendException("Failed to create document for unknown reason") + + return idCache.cache(folder(foldersParent, buildDocumentFile(createdDocument))) + } ?: throw FatalBackendException("FoldersParentsUri shouldn't be null") + } ?: throw ParentFolderIsNullException(folder.name) + } + + @Throws(BackendException::class) + fun move(source: LocalStorageAccessNode, target: LocalStorageAccessNode): LocalStorageAccessNode { + source.parent?.let { sourcesParent -> + if (exists(target)) { + throw CloudNodeAlreadyExistsException(target.name) + } + idCache.remove(source) + idCache.remove(target) + val isRename = source.name != target.name + val isMove = sourcesParent != target.parent + var renamedSource = source + if (isRename) { + renamedSource = rename(source, target.name) + } + return if (isMove) { + idCache.cache(internalMove(renamedSource, target)) + } else renamedSource + } ?: throw ParentFolderIsNullException(source.name) + } + + @Throws(NoSuchCloudFileException::class) + private fun rename(source: LocalStorageAccessNode, name: String): LocalStorageAccessNode { + source.parent?.let { parent -> + var newUri = try { + DocumentsContract.renameDocument(contentResolver(), source.uri, name) + } catch (e: FileNotFoundException) { + /* Bug in Android 9 see #460 TLDR; In this renameDocument-method, Android 9 throws + a `FileNotFoundException` although the file exists and is also renamed. */ + if (Build.VERSION.SDK_INT != Build.VERSION_CODES.P) { + throw NoSuchCloudFileException(source.name) + } + null + } + + /* Bug in Android 9 see #460 TLDR; In this renameDocument-method, Android 9 throws + a `FileNotFoundException` although the file exists and is also renamed. */ + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) { + newUri = try { + listFilesWithNameFilter(parent, name).getOrNull(0)?.uri + } catch (e: BackendException) { + throw FatalBackendException("Failed to list file while move of ${source.name}", e) + } ?: throw FatalBackendException("Failed to list file while move of ${source.name} for unkown reason") + } + + requireNotNull(newUri) + + return from(parent, buildDocumentFile(newUri)) + } ?: throw ParentFolderIsNullException(source.name) + } + + @Throws(NoSuchCloudFileException::class) + private fun internalMove(source: LocalStorageAccessNode, target: LocalStorageAccessNode): LocalStorageAccessNode { + source.uri?.let { sourceUri -> + source.parent?.uri?.let { sourcesParentUri -> + target.parent?.let { targetsParent -> + target.parent?.uri?.let { targetsParentUri -> + val movedTargetUri = try { + DocumentsContract.moveDocument(contentResolver(), sourceUri, sourcesParentUri, targetsParentUri) + } catch (e: FileNotFoundException) { + throw NoSuchCloudFileException(source.name) + } ?: throw FatalBackendException("Move failed for unknown reason") + return from(targetsParent, buildDocumentFile(movedTargetUri)) + } ?: throw FatalBackendException("Target parents uri shouldn't be null") + } ?: throw FatalBackendException("Targets parent shouldn't be null") + } ?: throw FatalBackendException("Source parents uri shouldn't be null") + } ?: throw FatalBackendException("Source uri shouldn't be null") + + + } + + @Throws(IOException::class, BackendException::class) + fun write(file: LocalStorageAccessFile, data: DataSource, progressAware: ProgressAware, replace: Boolean, size: Long): LocalStorageAccessFile { + var file = file + progressAware.onProgress(Progress.started(UploadState.upload(file))) + val fileUri = existingFileUri(file) + + if (!replace && fileUri != null) { + throw CloudNodeAlreadyExistsException("CloudNode already exists and replace is false") + } + + if (file.parent.uri == null) { + file.parent.parent?.let { + val parent = listFilesWithNameFilter(it, file.parent.name)[0] as LocalStorageAccessFolder + val tmpFileUri = fileUri?.toString() ?: "" + file = LocalStorageAccessFile(parent, file.name, file.path, file.size, file.modified, file.documentId, tmpFileUri) + } ?: throw ParentFolderIsNullException(file.parent.name) + } + val tmpFile = file + val uploadUri: Uri = (fileUri ?: createNewDocumentSupplier(tmpFile).get()) ?: throw NotFoundException(tmpFile.name) + + data.open(context)?.use { inputStream -> + contentResolver().openOutputStream(uploadUri)?.use { out -> + object : TransferredBytesAwareInputStream(inputStream) { + override fun bytesTransferred(transferred: Long) { + progressAware // + .onProgress( + Progress.progress(UploadState.upload(tmpFile)) // + .between(0) // + .and(size) // + .withValue(transferred) + ) + } + }.use { inputStream -> + if (out is FileOutputStream) { + out.channel.truncate(0) + } + CopyStream.copyStreamToStream(inputStream, out) + } + } ?: throw FatalBackendException("OutputStream shouldn't bee null") + } ?: throw FatalBackendException("InputStream shouldn't bee null") + + progressAware.onProgress(Progress.completed(UploadState.upload(file))) + return file(file.parent, buildDocumentFile(uploadUri)) + } + + private fun createNewDocumentSupplier(file: LocalStorageAccessFile): Supplier { + return Supplier { + val mimeType = if (mimeTypes.fromFilename(file.name) == null) MimeType.APPLICATION_OCTET_STREAM else mimeTypes.fromFilename(file.name) + try { + DocumentsContract.createDocument(contentResolver(), file.parent.uri, mimeType.toString(), file.name) // FIXME + } catch (e: FileNotFoundException) { + null + } + } + } + + @Throws(BackendException::class) + private fun existingFileUri(file: LocalStorageAccessFile): Uri? { + return listFilesWithNameFilter(file.parent, file.name).getOrNull(0)?.uri + } + + @Throws(IOException::class) + fun read(file: LocalStorageAccessFile, data: OutputStream, progressAware: ProgressAware) { + progressAware.onProgress(Progress.started(DownloadState.download(file))) + contentResolver().openInputStream(file.uri)?.use { inputStream -> + object : TransferredBytesAwareOutputStream(data) { + override fun bytesTransferred(transferred: Long) { + progressAware.onProgress( + Progress.progress(DownloadState.download(file)) // + .between(0) // + .and(file.size ?: Long.MAX_VALUE) // + .withValue(transferred) + ) + } + }.use { out -> CopyStream.copyStreamToStream(inputStream, out) } + } ?: throw FatalBackendException("InputStream shouldn't bee null") + progressAware.onProgress(Progress.completed(DownloadState.download(file))) + } + + @Throws(NoSuchCloudFileException::class) + fun delete(node: LocalStorageAccessNode) { + requireNotNull(node.uri) + try { + DocumentsContract.deleteDocument(contentResolver(), node.uri) + } catch (e: FileNotFoundException) { + throw NoSuchCloudFileException(node.name) + } + idCache.remove(node) + } + + private fun buildDocumentFile(fileUri: Uri): DocumentFile { + // can only be zero on devices with pre-Kitkat, which is excluded by the minSDK + return DocumentFile.fromSingleUri(context, fileUri)!! + } + + private fun contentResolver(): ContentResolver { + return context.contentResolver + } + + init { + if (!hasUriPermissions(context, cloud.rootUri())) { + throw NoAuthenticationProvidedException(cloud) + } + this.context = context + this.root = RootLocalStorageAccessFolder(cloud) + idCache = documentIdCache + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFrameworkNodeFactory.java b/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFrameworkNodeFactory.java deleted file mode 100644 index 6964e315..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFrameworkNodeFactory.java +++ /dev/null @@ -1,124 +0,0 @@ -package org.cryptomator.data.cloud.local.storageaccessframework; - -import android.database.Cursor; -import android.os.Build; -import android.provider.DocumentsContract; - -import androidx.annotation.RequiresApi; -import androidx.documentfile.provider.DocumentFile; - -import org.cryptomator.util.Optional; - -import java.util.Date; - -@RequiresApi(Build.VERSION_CODES.LOLLIPOP) -class LocalStorageAccessFrameworkNodeFactory { - - public static LocalStorageAccessNode from(LocalStorageAccessFolder parent, Cursor cursor) { - if (isFolder(cursor)) { - return folder(parent, cursor); - } else { - return file(parent, cursor); - } - } - - private static LocalStorageAccessFile file(LocalStorageAccessFolder parent, Cursor cursor) { - return new LocalStorageAccessFile( // - parent, // - cursor.getString(0), // - getNodePath(parent, cursor.getString(0)), // - Optional.of(cursor.getLong(2)), // - Optional.of(new Date(cursor.getLong(3))), // - cursor.getString(4), // - getDocumentUri(parent, cursor.getString(4))); - } - - private static LocalStorageAccessFolder folder(LocalStorageAccessFolder parent, Cursor cursor) { - return new LocalStorageAccessFolder(parent, // - cursor.getString(0), // - getNodePath(parent, cursor.getString(0)), // - cursor.getString(4), // - getDocumentUri(parent, cursor.getString(4))); - } - - public static LocalStorageAccessNode from(LocalStorageAccessFolder parent, DocumentFile documentFile) { - if (isFolder(documentFile)) { - return folder(parent, documentFile); - } else { - return file(parent, documentFile); - } - } - - public static LocalStorageAccessFolder folder(LocalStorageAccessFolder parent, DocumentFile directory) { - return new LocalStorageAccessFolder(parent, // - directory.getName(), // - getNodePath(parent, directory.getName()), // - DocumentsContract.getDocumentId(directory.getUri()), // - directory.getUri().toString()); - } - - public static LocalStorageAccessFile file(LocalStorageAccessFolder parent, DocumentFile documentFile) { - return new LocalStorageAccessFile( // - parent, // - documentFile.getName(), // - getNodePath(parent, documentFile.getName()), // - Optional.of(documentFile.length()), // - Optional.of(new Date(documentFile.lastModified())), // - DocumentsContract.getTreeDocumentId(documentFile.getUri()), // - documentFile.getUri().toString()); - } - - public static LocalStorageAccessFile file(LocalStorageAccessFolder parent, String name, Optional size) { - return new LocalStorageAccessFile(// - parent, // - name, // - getNodePath(parent, name), // - size, // - Optional.empty(), // - null, // - null); - } - - public static LocalStorageAccessFile file(LocalStorageAccessFolder parent, String name, String path, Optional size, String documentId) { - return new LocalStorageAccessFile(parent, // - name, // - path, // - size, // - Optional.empty(), // - documentId, // - getDocumentUri(parent, documentId)); - } - - public static LocalStorageAccessFolder folder(LocalStorageAccessFolder parent, String name) { - return new LocalStorageAccessFolder(parent, // - name, // - getNodePath(parent, name), // - null, // - null); - } - - public static LocalStorageAccessFolder folder(LocalStorageAccessFolder parent, String name, String documentId) { - return new LocalStorageAccessFolder(parent, // - name, // - getNodePath(parent, name), // - documentId, // - getDocumentUri(parent, documentId)); - } - - private static String getDocumentUri(LocalStorageAccessFolder parent, String documentId) { - return DocumentsContract.buildDocumentUriUsingTree(parent.getUri(), documentId).toString(); - } - - private static boolean isFolder(DocumentFile file) { - return file.isDirectory(); - } - - private static boolean isFolder(Cursor cursor) { - return cursor.getString(1).equals(DocumentsContract.Document.MIME_TYPE_DIR); - } - - public static String getNodePath(LocalStorageAccessFolder parent, String name) { - return parent.getPath() + "/" + name; - } - -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFrameworkNodeFactory.kt b/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFrameworkNodeFactory.kt new file mode 100644 index 00000000..f352c4bb --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFrameworkNodeFactory.kt @@ -0,0 +1,133 @@ +package org.cryptomator.data.cloud.local.storageaccessframework + +import android.database.Cursor +import android.provider.DocumentsContract +import androidx.documentfile.provider.DocumentFile +import java.util.Date + +internal object LocalStorageAccessFrameworkNodeFactory { + + fun from(parent: LocalStorageAccessFolder, cursor: Cursor): LocalStorageAccessNode { + return if (isFolder(cursor)) { + folder(parent, cursor) + } else { + file(parent, cursor) + } + } + + private fun file(parent: LocalStorageAccessFolder, cursor: Cursor): LocalStorageAccessFile { + return LocalStorageAccessFile( // + parent, // + cursor.getString(0), // + getNodePath(parent, cursor.getString(0)), // + cursor.getLong(2), // + Date(cursor.getLong(3)), // + cursor.getString(4), // + getDocumentUri(parent, cursor.getString(4)) + ) + } + + private fun folder(parent: LocalStorageAccessFolder, cursor: Cursor): LocalStorageAccessFolder { + return LocalStorageAccessFolder( + parent, // + cursor.getString(0), // + getNodePath(parent, cursor.getString(0)), // + cursor.getString(4), // + getDocumentUri(parent, cursor.getString(4)) + ) + } + + @JvmStatic + fun from(parent: LocalStorageAccessFolder, documentFile: DocumentFile): LocalStorageAccessNode { + return if (isFolder(documentFile)) { + folder(parent, documentFile) + } else { + file(parent, documentFile) + } + } + + fun folder(parent: LocalStorageAccessFolder, directory: DocumentFile): LocalStorageAccessFolder { + return LocalStorageAccessFolder( + parent, // + directory.name!!, // FIXME + getNodePath(parent, directory.name), // + DocumentsContract.getDocumentId(directory.uri), // + directory.uri.toString() + ) + } + + fun file(parent: LocalStorageAccessFolder, documentFile: DocumentFile): LocalStorageAccessFile { + return LocalStorageAccessFile( // + parent, // + documentFile.name!!, // FIXME + getNodePath(parent, documentFile.name), // + documentFile.length(), // + Date(documentFile.lastModified()), // + DocumentsContract.getTreeDocumentId(documentFile.uri), // + documentFile.uri.toString() + ) + } + + fun file(parent: LocalStorageAccessFolder, name: String, size: Long?): LocalStorageAccessFile { + return LocalStorageAccessFile( // + parent, // + name, // + getNodePath(parent, name), // + size, // + null, // + null, // + null + ) + } + + @JvmStatic + fun file(parent: LocalStorageAccessFolder, name: String, path: String, size: Long?, documentId: String): LocalStorageAccessFile { + return LocalStorageAccessFile( + parent, // + name, // + path, // + size, // + null, // + documentId, // + getDocumentUri(parent, documentId) + ) + } + + fun folder(parent: LocalStorageAccessFolder, name: String): LocalStorageAccessFolder { + return LocalStorageAccessFolder( + parent, // + name, // + getNodePath(parent, name), // + null, // + null + ) + } + + @JvmStatic + fun folder(parent: LocalStorageAccessFolder, name: String, documentId: String): LocalStorageAccessFolder { + return LocalStorageAccessFolder( + parent, // + name, // + getNodePath(parent, name), // + documentId, // + getDocumentUri(parent, documentId) + ) + } + + private fun getDocumentUri(parent: LocalStorageAccessFolder, documentId: String): String { + return DocumentsContract.buildDocumentUriUsingTree(parent.uri, documentId).toString() + } + + private fun isFolder(file: DocumentFile): Boolean { + return file.isDirectory + } + + private fun isFolder(cursor: Cursor): Boolean { + return cursor.getString(1) == DocumentsContract.Document.MIME_TYPE_DIR + } + + @JvmStatic + fun getNodePath(parent: LocalStorageAccessFolder, name: String?): String { + return parent.path + "/" + name + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessNode.java b/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessNode.java deleted file mode 100644 index d4254dc7..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessNode.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.cryptomator.data.cloud.local.storageaccessframework; - -import android.net.Uri; - -import org.cryptomator.domain.CloudNode; - -public interface LocalStorageAccessNode extends CloudNode { - - Uri getUri(); - - LocalStorageAccessFolder getParent(); - - String getDocumentId(); - -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessNode.kt b/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessNode.kt new file mode 100644 index 00000000..12ffd6df --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessNode.kt @@ -0,0 +1,11 @@ +package org.cryptomator.data.cloud.local.storageaccessframework + +import android.net.Uri +import org.cryptomator.domain.CloudNode + +interface LocalStorageAccessNode : CloudNode { + + val uri: Uri? + override val parent: LocalStorageAccessFolder? + val documentId: String? +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/RootLocalStorageAccessFolder.java b/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/RootLocalStorageAccessFolder.java deleted file mode 100644 index ccc021a7..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/RootLocalStorageAccessFolder.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.cryptomator.data.cloud.local.storageaccessframework; - -import android.os.Build; -import android.provider.DocumentsContract; - -import androidx.annotation.RequiresApi; - -import org.cryptomator.domain.Cloud; -import org.cryptomator.domain.LocalStorageCloud; - -import static android.net.Uri.parse; - -@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) -public class RootLocalStorageAccessFolder extends LocalStorageAccessFolder { - - private final LocalStorageCloud localStorageCloud; - - public RootLocalStorageAccessFolder(LocalStorageCloud localStorageCloud) { - super(null, // - "", // - "", // - DocumentsContract.getTreeDocumentId( // - parse(localStorageCloud.rootUri())), // - DocumentsContract.buildChildDocumentsUriUsingTree( // - parse(localStorageCloud.rootUri()), // - DocumentsContract.getTreeDocumentId(parse(localStorageCloud.rootUri()))).toString()); - this.localStorageCloud = localStorageCloud; - } - - @Override - public Cloud getCloud() { - return localStorageCloud; - } - - @Override - public LocalStorageAccessFolder withCloud(Cloud cloud) { - return new RootLocalStorageAccessFolder((LocalStorageCloud) cloud); - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/RootLocalStorageAccessFolder.kt b/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/RootLocalStorageAccessFolder.kt new file mode 100644 index 00000000..bc59d39b --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/RootLocalStorageAccessFolder.kt @@ -0,0 +1,25 @@ +package org.cryptomator.data.cloud.local.storageaccessframework + +import android.net.Uri +import android.provider.DocumentsContract +import org.cryptomator.domain.Cloud +import org.cryptomator.domain.LocalStorageCloud + +class RootLocalStorageAccessFolder(private val localStorageCloud: LocalStorageCloud) : LocalStorageAccessFolder( + null, // + "", // + "", // + DocumentsContract.getTreeDocumentId(Uri.parse(localStorageCloud.rootUri())), // + DocumentsContract.buildChildDocumentsUriUsingTree( // + Uri.parse(localStorageCloud.rootUri()), // + DocumentsContract.getTreeDocumentId(Uri.parse(localStorageCloud.rootUri())) + ).toString() +) { + + override val cloud: Cloud + get() = localStorageCloud + + override fun withCloud(cloud: Cloud?): RootLocalStorageAccessFolder { + return RootLocalStorageAccessFolder(cloud as LocalStorageCloud) + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/okhttplogging/HeaderNames.java b/data/src/main/java/org/cryptomator/data/cloud/okhttplogging/HeaderNames.java deleted file mode 100644 index 58295d84..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/okhttplogging/HeaderNames.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.cryptomator.data.cloud.okhttplogging; - -import java.util.HashSet; -import java.util.Set; - -class HeaderNames { - - private final Set lowercaseNames = new HashSet<>(); - - public HeaderNames(String... headerNames) { - for (String headerName : headerNames) { - lowercaseNames.add(headerName.toLowerCase()); - } - } - - public boolean contains(String headerName) { - return lowercaseNames.contains(headerName.toLowerCase()); - } - -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/okhttplogging/HeaderNames.kt b/data/src/main/java/org/cryptomator/data/cloud/okhttplogging/HeaderNames.kt new file mode 100644 index 00000000..885b854e --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/okhttplogging/HeaderNames.kt @@ -0,0 +1,16 @@ +package org.cryptomator.data.cloud.okhttplogging + +import java.util.HashSet + +internal class HeaderNames(vararg headerNames: String) { + + private val lowercaseNames: MutableSet = HashSet() + + operator fun contains(headerName: String): Boolean { + return lowercaseNames.contains(headerName.lowercase()) + } + + init { + headerNames.mapTo(lowercaseNames) { it.lowercase() } + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/okhttplogging/HttpLoggingInterceptor.java b/data/src/main/java/org/cryptomator/data/cloud/okhttplogging/HttpLoggingInterceptor.java deleted file mode 100644 index 0e1b71f9..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/okhttplogging/HttpLoggingInterceptor.java +++ /dev/null @@ -1,147 +0,0 @@ -package org.cryptomator.data.cloud.okhttplogging; - -import android.content.Context; - -import org.jetbrains.annotations.NotNull; - -import java.io.IOException; - -import okhttp3.Connection; -import okhttp3.Headers; -import okhttp3.Interceptor; -import okhttp3.Protocol; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; - -import static android.preference.PreferenceManager.getDefaultSharedPreferences; -import static java.lang.String.format; -import static java.util.concurrent.TimeUnit.NANOSECONDS; - -public final class HttpLoggingInterceptor implements Interceptor { - - private static final HeaderNames EXCLUDED_HEADERS = new HeaderNames(// - // headers excluded because they are logged separately: - "Content-Type", "Content-Length", - // headers excluded because they contain sensitive information: - "Authorization", // - "WWW-Authenticate", // - "Cookie", // - "Set-Cookie" // - ); - private final Logger logger; - private final Context context; - - public HttpLoggingInterceptor(Logger logger, Context context) { - this.logger = logger; - this.context = context; - } - - private static boolean debugModeEnabled(Context context) { - return getDefaultSharedPreferences(context).getBoolean("debugMode", false); - } - - @NotNull - @Override - public Response intercept(@NotNull Chain chain) throws IOException { - if (debugModeEnabled(context)) { - return proceedWithLogging(chain); - } else { - return chain.proceed(chain.request()); - } - } - - private Response proceedWithLogging(Chain chain) throws IOException { - Request request = chain.request(); - logRequest(request, chain); - return getAndLogResponse(request, chain); - } - - private void logRequest(Request request, Chain chain) throws IOException { - logRequestStart(request, chain); - logContentTypeAndLength(request); - logHeaders(request.headers()); - logRequestEnd(request); - } - - private Response getAndLogResponse(Request request, Chain chain) throws IOException { - long startOfRequestMs = System.nanoTime(); - Response response = getResponseLoggingExceptions(request, chain); - long requestDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startOfRequestMs); - logResponse(response, requestDurationMs); - return response; - } - - private Response getResponseLoggingExceptions(Request request, Chain chain) throws IOException { - try { - return chain.proceed(request); - } catch (Exception e) { - logger.log("<-- HTTP FAILED: " + e); - throw e; - } - } - - private void logResponse(Response response, long requestDurationMs) { - logResponseStart(response, requestDurationMs); - logHeaders(response.headers()); - logger.log("<-- END HTTP"); - } - - private void logRequestStart(Request request, Chain chain) throws IOException { - Connection connection = chain.connection(); - Protocol protocol = connection != null ? connection.protocol() : Protocol.HTTP_1_1; - String bodyLength = hasBody(request) ? request.body().contentLength() + "-byte body" : "unknown length"; - - logger.log(format("--> %s %s %s (%s)", // - request.method(), // - request.url(), // - protocol, // - bodyLength // - )); - } - - private void logContentTypeAndLength(Request request) throws IOException { - // Request body headers are only present when installed as a network interceptor. Force - // them to be included (when available) so there values are known. - if (hasBody(request)) { - RequestBody body = request.body(); - if (body.contentType() != null) { - logger.log("Content-Type: " + body.contentType()); - } - if (body.contentLength() != -1) { - logger.log("Content-Length: " + body.contentLength()); - } - } - } - - private void logRequestEnd(Request request) { - logger.log("--> END " + request.method()); - } - - private void logResponseStart(Response response, long requestDurationMs) { - logger.log("<-- " + response.code() + ' ' + response.message() + ' ' + response.request().url() + " (" + requestDurationMs + "ms" + ')'); - } - - private boolean hasBody(Request request) { - return request.body() != null; - } - - private void logHeaders(Headers headers) { - for (int i = 0, count = headers.size(); i < count; i++) { - String name = headers.name(i); - if (isExcludedHeader(name)) { - continue; - } - logger.log(name + ": " + headers.value(i)); - } - } - - private boolean isExcludedHeader(String name) { - return EXCLUDED_HEADERS.contains(name); - } - - public interface Logger { - - void log(String message); - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/okhttplogging/HttpLoggingInterceptor.kt b/data/src/main/java/org/cryptomator/data/cloud/okhttplogging/HttpLoggingInterceptor.kt new file mode 100644 index 00000000..558eb6f0 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/okhttplogging/HttpLoggingInterceptor.kt @@ -0,0 +1,135 @@ +package org.cryptomator.data.cloud.okhttplogging + +import android.content.Context +import org.cryptomator.util.SharedPreferencesHandler +import java.io.IOException +import java.util.concurrent.TimeUnit +import okhttp3.Headers +import okhttp3.Interceptor +import okhttp3.Protocol +import okhttp3.Request +import okhttp3.Response + +class HttpLoggingInterceptor(private val logger: Logger, private val context: Context) : Interceptor { + + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + return if (debugModeEnabled(context)) { + proceedWithLogging(chain) + } else { + chain.proceed(chain.request()) + } + } + + @Throws(IOException::class) + private fun proceedWithLogging(chain: Interceptor.Chain): Response { + val request: Request = chain.request() + logRequest(request, chain) + return getAndLogResponse(request, chain) + } + + @Throws(IOException::class) + private fun logRequest(request: Request, chain: Interceptor.Chain) { + logRequestStart(request, chain) + logContentTypeAndLength(request) + logHeaders(request.headers) + logRequestEnd(request) + } + + @Throws(IOException::class) + private fun getAndLogResponse(request: Request, chain: Interceptor.Chain): Response { + val startOfRequestMs = System.nanoTime() + val response = getResponseLoggingExceptions(request, chain) + val requestDurationMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startOfRequestMs) + logResponse(response, requestDurationMs) + return response + } + + @Throws(IOException::class) + private fun getResponseLoggingExceptions(request: Request, chain: Interceptor.Chain): Response { + return try { + chain.proceed(request) + } catch (e: Exception) { + logger.log("<-- HTTP FAILED: $e") + throw e + } + } + + private fun logResponse(response: Response, requestDurationMs: Long) { + logResponseStart(response, requestDurationMs) + logHeaders(response.headers) + logger.log("<-- END HTTP") + } + + @Throws(IOException::class) + private fun logRequestStart(request: Request, chain: Interceptor.Chain) { + val connection = chain.connection() + val protocol = connection?.protocol() ?: Protocol.HTTP_1_1 + val bodyLength = if (hasBody(request)) request.body?.contentLength().toString() + "-byte body" else "unknown length" + logger.log(String.format("--> %s %s %s (%s)", request.method, request.url, protocol, bodyLength)) + } + + @Throws(IOException::class) + private fun logContentTypeAndLength(request: Request) { + // Request body headers are only present when installed as a network interceptor. Force + // them to be included (when available) so there values are known. + if (hasBody(request)) { + val body = request.body + if (body?.contentType() != null) { + logger.log("Content-Type: " + body.contentType()) + } + if (body?.contentLength() != -1L) { + logger.log("Content-Length: " + body?.contentLength()) + } + } + } + + private fun logRequestEnd(request: Request) { + logger.log("--> END " + request.method) + } + + private fun logResponseStart(response: Response, requestDurationMs: Long) { + logger.log("<-- " + response.code + ' ' + response.message + ' ' + response.request.url + " (" + requestDurationMs + "ms" + ')') + } + + private fun hasBody(request: Request): Boolean { + return request.body != null + } + + private fun logHeaders(headers: Headers) { + var i = 0 + while (i < headers.size) { + val name = headers.name(i) + if (isExcludedHeader(name)) { + i++ + continue + } + logger.log(name + ": " + headers.value(i)) + i++ + } + } + + private fun isExcludedHeader(name: String): Boolean { + return EXCLUDED_HEADERS.contains(name) + } + + interface Logger { + fun log(message: String) + } + + companion object { + + private val EXCLUDED_HEADERS = HeaderNames( // + // headers excluded because they are logged separately: + "Content-Type", "Content-Length", // headers excluded because they contain sensitive information: + "Authorization", // + "WWW-Authenticate", // + "Cookie", // + "Set-Cookie" // + ) + + private fun debugModeEnabled(context: Context): Boolean { + return SharedPreferencesHandler(context).debugMode() + } + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveClientFactory.java b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveClientFactory.java deleted file mode 100644 index 01956cc9..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveClientFactory.java +++ /dev/null @@ -1,76 +0,0 @@ -package org.cryptomator.data.cloud.onedrive; - -import android.content.Context; - -import com.microsoft.graph.authentication.IAuthenticationProvider; -import com.microsoft.graph.core.DefaultClientConfig; -import com.microsoft.graph.models.extensions.IGraphServiceClient; -import com.microsoft.graph.requests.extensions.GraphServiceClient; - -import org.cryptomator.data.cloud.okhttplogging.HttpLoggingInterceptor; -import org.cryptomator.data.cloud.onedrive.graph.IAuthenticationAdapter; - -import java.util.concurrent.atomic.AtomicReference; - -import okhttp3.Interceptor; -import okhttp3.OkHttpClient; -import timber.log.Timber; - -import static org.cryptomator.data.util.NetworkTimeout.CONNECTION; -import static org.cryptomator.data.util.NetworkTimeout.READ; -import static org.cryptomator.data.util.NetworkTimeout.WRITE; - -public class OnedriveClientFactory { - - private static OnedriveClientFactory instance; - private final AtomicReference graphServiceClient = new AtomicReference<>(); - private final IAuthenticationAdapter authenticationAdapter; - private final Context context; - - private OnedriveClientFactory(Context context, String refreshToken) { - this.context = context; - this.authenticationAdapter = new MSAAuthAndroidAdapterImpl(context, refreshToken); - } - - public static OnedriveClientFactory instance(Context context, String accessToken) { - if (instance == null) { - instance = new OnedriveClientFactory(context, accessToken); - } - return instance; - } - - private static Interceptor httpLoggingInterceptor(Context context) { - return new HttpLoggingInterceptor(message -> Timber.tag("OkHttp").d(message), context); - } - - public IGraphServiceClient client() { - if (graphServiceClient.get() == null) { - - OkHttpClient.Builder builder = new OkHttpClient() // - .newBuilder() // - .connectTimeout(CONNECTION.getTimeout(), CONNECTION.getUnit()) // - .readTimeout(READ.getTimeout(), READ.getUnit()) // - .writeTimeout(WRITE.getTimeout(), WRITE.getUnit()) // - .addInterceptor(httpLoggingInterceptor(context)); - - OnedriveHttpProvider onedriveHttpProvider = new OnedriveHttpProvider(new DefaultClientConfig() { - @Override - public IAuthenticationProvider getAuthenticationProvider() { - return getAuthenticationAdapter(); - } - }, builder.build()); - - graphServiceClient.set(GraphServiceClient // - .builder() // - .authenticationProvider(getAuthenticationAdapter()) // - .httpProvider(onedriveHttpProvider) // - .buildClient()); - } - return graphServiceClient.get(); - } - - public synchronized IAuthenticationAdapter getAuthenticationAdapter() { - return authenticationAdapter; - } - -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveClientFactory.kt b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveClientFactory.kt new file mode 100644 index 00000000..684571c9 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveClientFactory.kt @@ -0,0 +1,63 @@ +package org.cryptomator.data.cloud.onedrive + +import android.content.Context +import com.microsoft.graph.authentication.IAuthenticationProvider +import com.microsoft.graph.core.DefaultClientConfig +import com.microsoft.graph.models.extensions.IGraphServiceClient +import com.microsoft.graph.requests.extensions.GraphServiceClient +import org.cryptomator.data.cloud.okhttplogging.HttpLoggingInterceptor +import org.cryptomator.data.cloud.onedrive.graph.MSAAuthAndroidAdapter +import org.cryptomator.data.util.NetworkTimeout +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import timber.log.Timber + +class OnedriveClientFactory private constructor() { + + companion object { + + @Volatile + private var instance: IGraphServiceClient? = null + + @Volatile + private var authenticationAdapter: MSAAuthAndroidAdapter? = null + + @Synchronized + fun getInstance(context: Context, refreshToken: String?): IGraphServiceClient = instance ?: createClient(context, refreshToken).also { instance = it } + + @Synchronized + fun getAuthAdapter(context: Context, refreshToken: String?): MSAAuthAndroidAdapter = authenticationAdapter ?: MSAAuthAndroidAdapterImpl(context, refreshToken).also { authenticationAdapter = it } + + private fun createClient(context: Context, refreshToken: String?): IGraphServiceClient { + val builder = OkHttpClient() // + .newBuilder() // + .connectTimeout(NetworkTimeout.CONNECTION.timeout, NetworkTimeout.CONNECTION.unit) // + .readTimeout(NetworkTimeout.READ.timeout, NetworkTimeout.READ.unit) // + .writeTimeout(NetworkTimeout.WRITE.timeout, NetworkTimeout.WRITE.unit) // + .addInterceptor(httpLoggingInterceptor(context)) + + val onedriveHttpProvider = OnedriveHttpProvider(object : DefaultClientConfig() { + override fun getAuthenticationProvider(): IAuthenticationProvider { + return getAuthAdapter(context, refreshToken) + } + }, builder.build()) + + return GraphServiceClient // + .builder() // + .authenticationProvider(authenticationAdapter) // + .httpProvider(onedriveHttpProvider) // + .buildClient() + } + + + private fun httpLoggingInterceptor(context: Context): Interceptor { + val logger = object : HttpLoggingInterceptor.Logger { + override fun log(message: String) { + Timber.tag("OkHttp").d(message) + } + } + + return HttpLoggingInterceptor(logger, context) + } + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveCloudContentRepository.java b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveCloudContentRepository.java deleted file mode 100644 index 4da67edc..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveCloudContentRepository.java +++ /dev/null @@ -1,165 +0,0 @@ -package org.cryptomator.data.cloud.onedrive; - -import android.content.Context; - -import com.microsoft.graph.core.GraphErrorCodes; - -import org.cryptomator.data.cloud.InterceptingCloudContentRepository; -import org.cryptomator.data.cloud.onedrive.graph.ClientException; -import org.cryptomator.domain.CloudNode; -import org.cryptomator.domain.OnedriveCloud; -import org.cryptomator.domain.exception.BackendException; -import org.cryptomator.domain.exception.FatalBackendException; -import org.cryptomator.domain.exception.NetworkConnectionException; -import org.cryptomator.domain.exception.NoSuchCloudFileException; -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.net.SocketTimeoutException; -import java.util.List; - -import static org.cryptomator.util.ExceptionUtil.contains; - -class OnedriveCloudContentRepository extends InterceptingCloudContentRepository { - - private final OnedriveCloud cloud; - - public OnedriveCloudContentRepository(OnedriveCloud cloud, Context context) { - super(new Intercepted(cloud, context)); - this.cloud = cloud; - } - - @Override - protected void throwWrappedIfRequired(Exception e) throws BackendException { - throwNetworkConnectionExceptionIfRequired(e); - throwWrongCredentialsExceptionIfRequired(e); - } - - private void throwNetworkConnectionExceptionIfRequired(Exception e) throws NetworkConnectionException { - if (contains(e, SocketTimeoutException.class)) { - throw new NetworkConnectionException(e); - } - } - - private void throwWrongCredentialsExceptionIfRequired(Exception e) { - if (isAuthenticationError(e)) { - throw new WrongCredentialsException(cloud); - } - } - - private boolean isAuthenticationError(Throwable e) { - return e != null // - && ((e instanceof ClientException && ((ClientException) e).errorCode().equals(GraphErrorCodes.AUTHENTICATION_FAILURE)) // - || isAuthenticationError(e.getCause())); - } - - private static class Intercepted implements CloudContentRepository { - - private final OnedriveImpl oneDriveImpl; - - public Intercepted(OnedriveCloud cloud, Context context) { - this.oneDriveImpl = new OnedriveImpl(cloud, context, new OnedriveIdCache()); - } - - @Override - public OnedriveFolder root(OnedriveCloud cloud) { - return oneDriveImpl.root(); - } - - @Override - public OnedriveFolder resolve(OnedriveCloud cloud, String path) { - return oneDriveImpl.resolve(path); - } - - @Override - public OnedriveFile file(OnedriveFolder parent, String name) { - return oneDriveImpl.file(parent, name); - } - - @Override - public OnedriveFile file(OnedriveFolder parent, String name, Optional size) { - return oneDriveImpl.file(parent, name, size); - } - - @Override - public OnedriveFolder folder(OnedriveFolder parent, String name) { - return oneDriveImpl.folder(parent, name); - } - - @Override - public boolean exists(OnedriveNode node) throws BackendException { - return oneDriveImpl.exists(node); - } - - @Override - public List list(OnedriveFolder folder) throws BackendException { - return oneDriveImpl.list(folder); - } - - @Override - public OnedriveFolder create(OnedriveFolder folder) throws BackendException { - return oneDriveImpl.create(folder); - } - - @Override - public OnedriveFolder move(OnedriveFolder source, OnedriveFolder target) throws BackendException { - return (OnedriveFolder) oneDriveImpl.move(source, target); - } - - @Override - public OnedriveFile move(OnedriveFile source, OnedriveFile target) throws BackendException { - return (OnedriveFile) oneDriveImpl.move(source, target); - } - - @Override - public OnedriveFile write(OnedriveFile file, DataSource data, ProgressAware progressAware, boolean replace, long size) throws BackendException { - try { - return oneDriveImpl.write(file, data, progressAware, replace, size); - } catch (BackendException e) { - if (contains(e, NoSuchCloudFileException.class)) { - throw new NoSuchCloudFileException(file.getName()); - } - throw e; - } - } - - @Override - public void read(OnedriveFile file, Optional tmpEncryptedFile, OutputStream data, ProgressAware progressAware) throws BackendException { - try { - oneDriveImpl.read(file, tmpEncryptedFile, data, progressAware); - } catch (IOException | BackendException e) { - if (contains(e, NoSuchCloudFileException.class)) { - throw new NoSuchCloudFileException(file.getName()); - } else if (e instanceof IOException) { - throw new FatalBackendException(e); - } else if (e instanceof BackendException) { - throw (BackendException) e; - } - } - } - - @Override - public void delete(OnedriveNode node) throws BackendException { - oneDriveImpl.delete(node); - } - - @Override - public String checkAuthenticationAndRetrieveCurrentAccount(OnedriveCloud cloud) throws BackendException { - return oneDriveImpl.currentAccount(); - } - - @Override - public void logout(OnedriveCloud cloud) { - oneDriveImpl.logout(); - } - } - -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveCloudContentRepository.kt b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveCloudContentRepository.kt new file mode 100644 index 00000000..850324f0 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveCloudContentRepository.kt @@ -0,0 +1,152 @@ +package org.cryptomator.data.cloud.onedrive + +import android.content.Context +import com.microsoft.graph.core.GraphErrorCodes +import org.cryptomator.data.cloud.InterceptingCloudContentRepository +import org.cryptomator.data.cloud.onedrive.graph.ClientException +import org.cryptomator.domain.OnedriveCloud +import org.cryptomator.domain.exception.BackendException +import org.cryptomator.domain.exception.FatalBackendException +import org.cryptomator.domain.exception.NetworkConnectionException +import org.cryptomator.domain.exception.NoSuchCloudFileException +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.ExceptionUtil +import java.io.File +import java.io.IOException +import java.io.OutputStream +import java.net.SocketTimeoutException + +internal class OnedriveCloudContentRepository(private val cloud: OnedriveCloud, context: Context) : InterceptingCloudContentRepository(Intercepted(cloud, context)) { + + @Throws(BackendException::class) + override fun throwWrappedIfRequired(e: Exception) { + throwNetworkConnectionExceptionIfRequired(e) + throwWrongCredentialsExceptionIfRequired(e) + } + + @Throws(NetworkConnectionException::class) + private fun throwNetworkConnectionExceptionIfRequired(e: Exception) { + if (ExceptionUtil.contains(e, SocketTimeoutException::class.java)) { + throw NetworkConnectionException(e) + } + } + + private fun throwWrongCredentialsExceptionIfRequired(e: Exception) { + if (isAuthenticationError(e)) { + throw WrongCredentialsException(cloud) + } + } + + private fun isAuthenticationError(e: Throwable?): Boolean { + return (e != null // + && (e is ClientException && e.errorCode() == GraphErrorCodes.AUTHENTICATION_FAILURE // + || isAuthenticationError(e.cause))) + } + + private class Intercepted(cloud: OnedriveCloud, context: Context) : CloudContentRepository { + + private val oneDriveImpl: OnedriveImpl = OnedriveImpl(cloud, context, OnedriveIdCache()) + + override fun root(cloud: OnedriveCloud): OnedriveFolder { + return oneDriveImpl.root() + } + + override fun resolve(cloud: OnedriveCloud, path: String): OnedriveFolder { + return oneDriveImpl.resolve(path) + } + + override fun file(parent: OnedriveFolder, name: String): OnedriveFile { + return oneDriveImpl.file(parent, name) + } + + override fun file(parent: OnedriveFolder, name: String, size: Long?): OnedriveFile { + return oneDriveImpl.file(parent, name, size) + } + + override fun folder(parent: OnedriveFolder, name: String): OnedriveFolder { + return oneDriveImpl.folder(parent, name) + } + + @Throws(BackendException::class) + override fun exists(node: OnedriveNode): Boolean { + return oneDriveImpl.exists(node) + } + + @Throws(BackendException::class) + override fun list(folder: OnedriveFolder): List { + return oneDriveImpl.list(folder) + } + + @Throws(BackendException::class) + override fun create(folder: OnedriveFolder): OnedriveFolder { + return oneDriveImpl.create(folder) + } + + @Throws(BackendException::class) + override fun move(source: OnedriveFolder, target: OnedriveFolder): OnedriveFolder { + return oneDriveImpl.move(source, target) as OnedriveFolder + } + + @Throws(BackendException::class) + override fun move(source: OnedriveFile, target: OnedriveFile): OnedriveFile { + return oneDriveImpl.move(source, target) as OnedriveFile + } + + @Throws(BackendException::class) + override fun write(file: OnedriveFile, data: DataSource, progressAware: ProgressAware, replace: Boolean, size: Long): OnedriveFile { + return try { + oneDriveImpl.write(file, data, progressAware, replace, size) + } catch (e: BackendException) { + if (ExceptionUtil.contains(e, NoSuchCloudFileException::class.java)) { + throw NoSuchCloudFileException(file.name) + } + throw e + } + } + + @Throws(BackendException::class) + override fun read(file: OnedriveFile, tmpEncryptedFile: File?, data: OutputStream, progressAware: ProgressAware) { + try { + oneDriveImpl.read(file, tmpEncryptedFile, data, progressAware) + } catch (e: IOException) { + when { + ExceptionUtil.contains(e, NoSuchCloudFileException::class.java) -> { + throw NoSuchCloudFileException(file.name) + } + else -> { + throw FatalBackendException(e) + } + } + } catch (e: BackendException) { + when { + ExceptionUtil.contains(e, NoSuchCloudFileException::class.java) -> { + throw NoSuchCloudFileException(file.name) + } + else -> { + throw e + } + } + } + } + + @Throws(BackendException::class) + override fun delete(node: OnedriveNode) { + oneDriveImpl.delete(node) + } + + @Throws(BackendException::class) + override fun checkAuthenticationAndRetrieveCurrentAccount(cloud: OnedriveCloud): String { + return oneDriveImpl.currentAccount() + } + + override fun logout(cloud: OnedriveCloud) { + oneDriveImpl.logout() + } + + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveCloudContentRepositoryFactory.java b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveCloudContentRepositoryFactory.java index 7dff2b35..243f8039 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveCloudContentRepositoryFactory.java +++ b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveCloudContentRepositoryFactory.java @@ -28,7 +28,7 @@ public class OnedriveCloudContentRepositoryFactory implements CloudContentReposi } @Override - public CloudContentRepository cloudContentRepositoryFor(Cloud cloud) { + public CloudContentRepository cloudContentRepositoryFor(Cloud cloud) { return new OnedriveCloudContentRepository((OnedriveCloud) cloud, context); } } diff --git a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveCloudNodeFactory.java b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveCloudNodeFactory.java deleted file mode 100644 index d9968b65..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveCloudNodeFactory.java +++ /dev/null @@ -1,77 +0,0 @@ -package org.cryptomator.data.cloud.onedrive; - -import com.microsoft.graph.models.extensions.DriveItem; - -import org.cryptomator.util.Optional; - -import java.util.Date; - -class OnedriveCloudNodeFactory { - - public static OnedriveNode from(OnedriveFolder parent, DriveItem item) { - if (isFolder(item)) { - return folder(parent, item); - } else { - return file(parent, item); - } - } - - private static OnedriveFile file(OnedriveFolder parent, DriveItem item) { - return new OnedriveFile(parent, item.name, getNodePath(parent, item.name), Optional.ofNullable(item.size), lastModified(item)); - } - - public static OnedriveFile file(OnedriveFolder parent, DriveItem item, Optional lastModified) { - return new OnedriveFile(parent, item.name, getNodePath(parent, item.name), Optional.ofNullable(item.size), lastModified); - } - - public static OnedriveFile file(OnedriveFolder parent, String name, Optional size) { - return new OnedriveFile(parent, name, getNodePath(parent, name), size, Optional.empty()); - } - - public static OnedriveFile file(OnedriveFolder parent, String name, Optional size, String path) { - return new OnedriveFile(parent, name, path, size, Optional.empty()); - } - - public static OnedriveFolder folder(OnedriveFolder parent, DriveItem item) { - return new OnedriveFolder(parent, item.name, getNodePath(parent, item.name)); - } - - public static OnedriveFolder folder(OnedriveFolder parent, String name) { - return new OnedriveFolder(parent, name, getNodePath(parent, name)); - } - - public static OnedriveFolder folder(OnedriveFolder parent, String name, String path) { - return new OnedriveFolder(parent, name, path); - } - - private static String getNodePath(OnedriveFolder parent, String name) { - return parent.getPath() + "/" + name; - } - - public static String getId(DriveItem item) { - return item.remoteItem != null // - ? item.remoteItem.id // - : item.id; - } - - public static String getDriveId(DriveItem item) { - return item.remoteItem != null // - ? item.remoteItem.parentReference.driveId // - : item.parentReference != null // - ? item.parentReference.driveId // - : null; - } - - public static boolean isFolder(DriveItem item) { - return item.folder != null || (item.remoteItem != null && item.remoteItem.folder != null); - } - - private static Optional lastModified(DriveItem item) { - if (item.lastModifiedDateTime == null) { - return Optional.empty(); - } else { - return Optional.of(item.lastModifiedDateTime.getTime()); - } - } - -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveCloudNodeFactory.kt b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveCloudNodeFactory.kt new file mode 100644 index 00000000..c2bab99e --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveCloudNodeFactory.kt @@ -0,0 +1,69 @@ +package org.cryptomator.data.cloud.onedrive + +import com.microsoft.graph.models.extensions.DriveItem +import java.util.Date + +internal object OnedriveCloudNodeFactory { + + @JvmStatic + fun from(parent: OnedriveFolder, item: DriveItem): OnedriveNode { + return if (isFolder(item)) { + folder(parent, item) + } else { + file(parent, item) + } + } + + private fun file(parent: OnedriveFolder, item: DriveItem): OnedriveFile { + return OnedriveFile(parent, item.name, getNodePath(parent, item.name), item.size, lastModified(item)) + } + + fun file(parent: OnedriveFolder, item: DriveItem, lastModified: Date?): OnedriveFile { + return OnedriveFile(parent, item.name, getNodePath(parent, item.name), item.size, lastModified) + } + + fun file(parent: OnedriveFolder, name: String, size: Long?): OnedriveFile { + return OnedriveFile(parent, name, getNodePath(parent, name), size, null) + } + + fun file(parent: OnedriveFolder, name: String, size: Long?, path: String): OnedriveFile { + return OnedriveFile(parent, name, path, size, null) + } + + fun folder(parent: OnedriveFolder, item: DriveItem): OnedriveFolder { + return OnedriveFolder(parent, item.name, getNodePath(parent, item.name)) + } + + fun folder(parent: OnedriveFolder, name: String): OnedriveFolder { + return OnedriveFolder(parent, name, getNodePath(parent, name)) + } + + fun folder(parent: OnedriveFolder, name: String, path: String): OnedriveFolder { + return OnedriveFolder(parent, name, path) + } + + private fun getNodePath(parent: OnedriveFolder, name: String): String { + return parent.path + "/" + name + } + + @JvmStatic + fun getId(item: DriveItem): String { + return if (item.remoteItem != null) item.remoteItem.id + else item.id + } + + @JvmStatic + fun getDriveId(item: DriveItem): String { + return if (item.remoteItem != null) item.remoteItem.parentReference.driveId + else (if (item.parentReference != null) item.parentReference.driveId else null)!! + } + + @JvmStatic + fun isFolder(item: DriveItem): Boolean { + return item.folder != null || item.remoteItem != null && item.remoteItem.folder != null + } + + private fun lastModified(item: DriveItem): Date? { + return item.lastModifiedDateTime?.time + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveFile.java b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveFile.java deleted file mode 100644 index d1bd6cbc..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveFile.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.cryptomator.data.cloud.onedrive; - -import org.cryptomator.domain.Cloud; -import org.cryptomator.domain.CloudFile; -import org.cryptomator.util.Optional; - -import java.util.Date; - -class OnedriveFile implements CloudFile, OnedriveNode { - - private final OnedriveFolder parent; - private final String name; - private final String path; - private final Optional size; - private final Optional modified; - - public OnedriveFile(OnedriveFolder 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 boolean isFolder() { - return false; - } - - @Override - public Cloud getCloud() { - return parent.getCloud(); - } - - @Override - public String getName() { - return name; - } - - @Override - public String getPath() { - return path; - } - - @Override - public OnedriveFolder 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/onedrive/OnedriveFile.kt b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveFile.kt new file mode 100644 index 00000000..8cd2f836 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveFile.kt @@ -0,0 +1,13 @@ +package org.cryptomator.data.cloud.onedrive + +import org.cryptomator.domain.Cloud +import org.cryptomator.domain.CloudFile +import java.util.Date + +internal class OnedriveFile(override val parent: OnedriveFolder, override val name: String, override val path: String, override val size: Long?, override val modified: Date?) : CloudFile, OnedriveNode { + + override val isFolder: Boolean = false + + override val cloud: Cloud? + get() = parent.cloud +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveFolder.java b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveFolder.java deleted file mode 100644 index c51a22fa..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveFolder.java +++ /dev/null @@ -1,47 +0,0 @@ -package org.cryptomator.data.cloud.onedrive; - -import org.cryptomator.domain.Cloud; -import org.cryptomator.domain.CloudFolder; - -class OnedriveFolder implements CloudFolder, OnedriveNode { - - private final OnedriveFolder parent; - private final String name; - private final String path; - - public OnedriveFolder(OnedriveFolder parent, String name, String path) { - this.parent = parent; - this.name = name; - this.path = path; - } - - @Override - public boolean isFolder() { - return true; - } - - @Override - public Cloud getCloud() { - return parent.getCloud(); - } - - @Override - public String getName() { - return name; - } - - @Override - public String getPath() { - return path; - } - - @Override - public OnedriveFolder getParent() { - return parent; - } - - @Override - public OnedriveFolder withCloud(Cloud cloud) { - return new OnedriveFolder(parent.withCloud(cloud), name, path); - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveFolder.kt b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveFolder.kt new file mode 100644 index 00000000..1f6bcd12 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveFolder.kt @@ -0,0 +1,16 @@ +package org.cryptomator.data.cloud.onedrive + +import org.cryptomator.domain.Cloud +import org.cryptomator.domain.CloudFolder + +open class OnedriveFolder(override val parent: OnedriveFolder?, override val name: String, override val path: String) : CloudFolder, OnedriveNode { + + override val isFolder: Boolean = true + + override val cloud: Cloud? + get() = parent?.cloud + + override fun withCloud(cloud: Cloud?): OnedriveFolder? { + return OnedriveFolder(parent?.withCloud(cloud), name, path) + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveIdCache.java b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveIdCache.java deleted file mode 100644 index 4fde4e63..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveIdCache.java +++ /dev/null @@ -1,89 +0,0 @@ -package org.cryptomator.data.cloud.onedrive; - -import android.util.LruCache; - -import org.cryptomator.domain.CloudFolder; - -import javax.inject.Inject; - -class OnedriveIdCache { - - private final LruCache cache; - - @Inject - OnedriveIdCache() { - cache = new LruCache<>(1000); - } - - public NodeInfo get(String path) { - return cache.get(path); - } - - public T cache(T value) { - add(value); - return value; - } - - private void add(OnedriveIdCloudNode node) { - add(node.getPath(), new NodeInfo(node)); - } - - public void add(String path, NodeInfo info) { - cache.put(path, info); - } - - public void remove(OnedriveIdCloudNode node) { - remove(node.getPath()); - } - - public void remove(String path) { - removeChildren(path); - cache.remove(path); - } - - void removeChildren(String path) { - String prefix = path + '/'; - for (String key : cache.snapshot().keySet()) { - if (key.startsWith(prefix)) { - cache.remove(key); - } - } - } - - static class NodeInfo { - - private final String id; - private final String driveId; - private final boolean isFolder; - private final String cTag; - - private NodeInfo(OnedriveIdCloudNode node) { - this(node.getId(), node.getDriveId(), node instanceof CloudFolder, ""); - } - - NodeInfo(String id, String driveId, boolean isFolder, String cTag) { - this.id = id; - this.driveId = driveId; - this.isFolder = isFolder; - this.cTag = cTag; - } - - public String getId() { - return id; - } - - public String getDriveId() { - return driveId; - } - - public boolean isFolder() { - return isFolder; - } - - public String getcTag() { - return cTag; - } - - } - -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveIdCache.kt b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveIdCache.kt new file mode 100644 index 00000000..38ac5eff --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveIdCache.kt @@ -0,0 +1,54 @@ +package org.cryptomator.data.cloud.onedrive + +import android.util.LruCache +import org.cryptomator.domain.CloudFolder +import javax.inject.Inject + +internal class OnedriveIdCache @Inject constructor() { + + private val cache: LruCache = LruCache(1000) + + operator fun get(path: String): NodeInfo? { + return cache[path] + } + + fun cache(value: T): T { + add(value) + return value + } + + private fun add(node: OnedriveIdCloudNode) { + add(node.path, NodeInfo(node)) + } + + fun add(path: String, info: NodeInfo) { + cache.put(path, info) + } + + fun remove(node: OnedriveIdCloudNode) { + remove(node.path) + } + + fun remove(path: String) { + removeChildren(path) + cache.remove(path) + } + + fun removeChildren(path: String) { + val prefix = "$path/" + for (key in cache.snapshot().keys) { + if (key.startsWith(prefix)) { + cache.remove(key) + } + } + } + + internal class NodeInfo(val id: String, val driveId: String, val isFolder: Boolean, private val cTag: String?) { + constructor(node: OnedriveIdCloudNode) : this(node.id, node.driveId, node is CloudFolder, "") + + fun getcTag(): String? { + return cTag + } + } + +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveIdCloudNode.java b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveIdCloudNode.java deleted file mode 100644 index 87292872..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveIdCloudNode.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.cryptomator.data.cloud.onedrive; - -import org.cryptomator.domain.CloudNode; - -public interface OnedriveIdCloudNode extends CloudNode { - - String getId(); - - String getDriveId(); - -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveIdCloudNode.kt b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveIdCloudNode.kt new file mode 100644 index 00000000..c39a0279 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveIdCloudNode.kt @@ -0,0 +1,10 @@ +package org.cryptomator.data.cloud.onedrive + +import org.cryptomator.domain.CloudNode + +interface OnedriveIdCloudNode : CloudNode { + + val id: String + val driveId: String + +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveImpl.java b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveImpl.java deleted file mode 100644 index f6c7ef62..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveImpl.java +++ /dev/null @@ -1,548 +0,0 @@ -package org.cryptomator.data.cloud.onedrive; - -import android.content.Context; -import android.net.Uri; - -import androidx.annotation.Nullable; - -import com.microsoft.graph.concurrency.ChunkedUploadProvider; -import com.microsoft.graph.http.GraphServiceException; -import com.microsoft.graph.models.extensions.DriveItem; -import com.microsoft.graph.models.extensions.DriveItemUploadableProperties; -import com.microsoft.graph.models.extensions.Folder; -import com.microsoft.graph.models.extensions.IGraphServiceClient; -import com.microsoft.graph.models.extensions.ItemReference; -import com.microsoft.graph.models.extensions.UploadSession; -import com.microsoft.graph.options.Option; -import com.microsoft.graph.options.QueryOption; -import com.microsoft.graph.requests.extensions.IDriveItemCollectionPage; -import com.microsoft.graph.requests.extensions.IDriveItemContentStreamRequest; -import com.microsoft.graph.requests.extensions.IDriveRequestBuilder; -import com.tomclaw.cache.DiskLruCache; - -import org.cryptomator.data.cloud.onedrive.graph.ClientException; -import org.cryptomator.data.cloud.onedrive.graph.ICallback; -import org.cryptomator.data.cloud.onedrive.graph.IProgressCallback; -import org.cryptomator.data.util.TransferredBytesAwareOutputStream; -import org.cryptomator.domain.CloudNode; -import org.cryptomator.domain.OnedriveCloud; -import org.cryptomator.domain.exception.BackendException; -import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException; -import org.cryptomator.domain.exception.FatalBackendException; -import org.cryptomator.domain.exception.NoSuchCloudFileException; -import org.cryptomator.domain.exception.authentication.NoAuthenticationProvidedException; -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.ExceptionUtil; -import org.cryptomator.util.Optional; -import org.cryptomator.util.SharedPreferencesHandler; -import org.cryptomator.util.concurrent.CompletableFuture; -import org.cryptomator.util.file.LruFileCacheUtil; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.lang.reflect.Field; -import java.net.SocketTimeoutException; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.concurrent.ExecutionException; - -import timber.log.Timber; - -import static java.util.Collections.singletonList; -import static org.cryptomator.data.util.CopyStream.copyStreamToStream; -import static org.cryptomator.data.util.CopyStream.toByteArray; -import static org.cryptomator.domain.usecases.cloud.Progress.progress; -import static org.cryptomator.util.file.LruFileCacheUtil.Cache.ONEDRIVE; -import static org.cryptomator.util.file.LruFileCacheUtil.retrieveFromLruCache; -import static org.cryptomator.util.file.LruFileCacheUtil.storeToLruCache; - -class OnedriveImpl { - - private static final long CHUNKED_UPLOAD_MAX_SIZE = 4L << 20; - private static final int CHUNKED_UPLOAD_CHUNK_SIZE = 327680 * 32; - private static final int CHUNKED_UPLOAD_MAX_ATTEMPTS = 5; - private static final String REPLACE_MODE = "replace"; - private static final String NON_REPLACING_MODE = "rename"; - private final OnedriveCloud cloud; - private final Context context; - private final OnedriveIdCache nodeInfoCache; - private final OnedriveClientFactory clientFactory; - private final SharedPreferencesHandler sharedPreferencesHandler; - - private DiskLruCache diskLruCache; - - OnedriveImpl(OnedriveCloud cloud, Context context, OnedriveIdCache nodeInfoCache) { - if (cloud.accessToken() == null) { - throw new NoAuthenticationProvidedException(cloud); - } - this.cloud = cloud; - this.context = context; - this.nodeInfoCache = nodeInfoCache; - this.clientFactory = OnedriveClientFactory.instance(context, cloud.accessToken()); - - sharedPreferencesHandler = new SharedPreferencesHandler(context); - } - - private IGraphServiceClient client() { - return clientFactory.client(); - } - - private IDriveRequestBuilder drive(String driveId) { - return driveId == null ? client().me().drive() : client().drives(driveId); - } - - public OnedriveFolder root() { - return new RootOnedriveFolder(cloud); - } - - public OnedriveFolder resolve(String path) { - if (path.startsWith("/")) { - path = path.substring(1); - } - String[] names = path.split("/"); - OnedriveFolder folder = root(); - for (String name : names) { - folder = folder(folder, name); - } - return folder; - } - - public OnedriveFile file(OnedriveFolder parent, String name) { - return file(parent, name, Optional.empty()); - } - - public OnedriveFile file(OnedriveFolder parent, String name, Optional size) { - return OnedriveCloudNodeFactory.file(parent, name, size); - } - - public OnedriveFolder folder(OnedriveFolder parent, String name) { - return OnedriveCloudNodeFactory.folder(parent, name); - } - - private DriveItem childByName(String parentId, String parentDriveId, String name) { - try { - return drive(parentDriveId) // - .items(parentId) // - .itemWithPath(Uri.encode(name)) // - .buildRequest() // - .get(); - } catch (GraphServiceException e) { - if (isNotFoundError(e)) { - return null; - } else { - throw e; - } - } - } - - private boolean isNotFoundError(GraphServiceException error) { - try { - Field responseCodeField = GraphServiceException.class.getDeclaredField("responseCode"); - responseCodeField.setAccessible(true); - Integer responseCode = (Integer) responseCodeField.get(error); - return responseCode == 404; - } catch (NoSuchFieldException e) { - throw new IllegalStateException(e); - } catch (IllegalAccessException e) { - throw new IllegalStateException(e); - } - } - - public boolean exists(OnedriveNode node) { - try { - OnedriveIdCache.NodeInfo parentNodeInfo = nodeInfo(node.getParent()); - if (parentNodeInfo == null) { - removeNodeInfo(node); - return false; - } - DriveItem item = childByName(parentNodeInfo.getId(), parentNodeInfo.getDriveId(), node.getName()); - if (item == null) { - removeNodeInfo(node); - return false; - } - cacheNodeInfo(node, item); - return true; - } catch (ClientException e) { - if (ExceptionUtil.contains(e, SocketTimeoutException.class)) { - throw e; - } - return false; - } - } - - public List list(OnedriveFolder folder) throws BackendException { - List result = new ArrayList<>(); - OnedriveIdCache.NodeInfo nodeInfo = requireNodeInfo(folder); - IDriveItemCollectionPage page = drive(nodeInfo.getDriveId()) // - .items(nodeInfo.getId()) // - .children() // - .buildRequest() // - .get(); - do { - removeChildNodeInfo(folder); - for (DriveItem item : page.getCurrentPage()) { - result.add(cacheNodeInfo(OnedriveCloudNodeFactory.from(folder, item), item)); - } - if (page.getNextPage() != null) { - page = page.getNextPage() // - .buildRequest() // - .get(); - } else { - page = null; - } - } while (page != null); - return result; - } - - public OnedriveFolder create(OnedriveFolder folder) throws NoSuchCloudFileException { - OnedriveFolder parent = folder.getParent(); - if (nodeInfo(parent) == null) { - parent = create(folder.getParent()); - } - - final DriveItem folderToCreate = new DriveItem(); - folderToCreate.name = folder.getName(); - folderToCreate.folder = new Folder(); - - OnedriveIdCache.NodeInfo parentNodeInfo = requireNodeInfo(parent); - DriveItem createdFolder = drive(parentNodeInfo.getDriveId()) // - .items(parentNodeInfo.getId()).children() // - .buildRequest() // - .post(folderToCreate); - return cacheNodeInfo(OnedriveCloudNodeFactory.folder(parent, createdFolder), createdFolder); - } - - public OnedriveNode move(OnedriveNode source, OnedriveNode target) throws NoSuchCloudFileException, CloudNodeAlreadyExistsException { - if (exists(target)) { - throw new CloudNodeAlreadyExistsException(target.getName()); - } - - final DriveItem targetItem = new DriveItem(); - targetItem.name = target.getName(); - ItemReference targetParentReference = new ItemReference(); - OnedriveIdCache.NodeInfo targetNodeInfo = nodeInfo(target.getParent()); - targetParentReference.id = targetNodeInfo == null ? null : targetNodeInfo.getId(); - targetParentReference.driveId = targetNodeInfo == null ? null : targetNodeInfo.getDriveId(); - targetItem.parentReference = targetParentReference; - - OnedriveIdCache.NodeInfo sourceNodeInfo = requireNodeInfo(source); - DriveItem movedItem = drive(sourceNodeInfo.getDriveId())// - .items(sourceNodeInfo.getId()) // - .buildRequest() // - .patch(targetItem); - removeNodeInfo(source); - return cacheNodeInfo(OnedriveCloudNodeFactory.from(target.getParent(), movedItem), movedItem); - } - - public OnedriveFile write(final OnedriveFile file, DataSource data, final ProgressAware progressAware, boolean replace, final long size) throws BackendException { - if (!replace && exists(file)) { - throw new CloudNodeAlreadyExistsException("CloudNode already exists and replace is false"); - } - - progressAware.onProgress(Progress.started(UploadState.upload(file))); - String uploadMode = NON_REPLACING_MODE; - if (replace) { - uploadMode = REPLACE_MODE; - } - final Option conflictBehaviorOption = new QueryOption("@name.conflictBehavior", uploadMode); - final CompletableFuture result = new CompletableFuture<>(); - if (size <= CHUNKED_UPLOAD_MAX_SIZE) { - uploadFile(file, data, progressAware, result, conflictBehaviorOption); - } else { - try { - chunkedUploadFile(file, data, progressAware, result, conflictBehaviorOption, size); - } catch (IOException e) { - throw new FatalBackendException(e); - } - } - - progressAware.onProgress(Progress.completed(UploadState.upload(file))); - try { - return OnedriveCloudNodeFactory.file(file.getParent(), result.get(), Optional.of(new Date())); - } catch (ExecutionException | InterruptedException e) { - throw new FatalBackendException(e); - } - } - - private void uploadFile( // - final OnedriveFile file, // - DataSource data, // - final ProgressAware progressAware, // - final CompletableFuture result, // - Option conflictBehaviorOption) throws NoSuchCloudFileException { - OnedriveIdCache.NodeInfo parentNodeInfo = requireNodeInfo(file.getParent()); - try (InputStream in = data.open(context)) { - drive(parentNodeInfo.getDriveId()) // - .items(parentNodeInfo.getId())// - .itemWithPath(file.getName()) // - .content() // - .buildRequest(singletonList(conflictBehaviorOption)) // - .put(toByteArray(in), new IProgressCallback() { - @Override - public void progress(long current, long max) { - progressAware // - .onProgress(Progress.progress(UploadState.upload(file)) // - .between(0) // - .and(max) // - .withValue(current)); - } - - @Override - public void success(DriveItem item) { - progressAware.onProgress(Progress.completed(UploadState.upload(file))); - result.complete(item); - cacheNodeInfo(file, item); - } - - @Override - public void failure(com.microsoft.graph.core.ClientException ex) { - result.fail(ex); - } - }); - } catch (IOException e) { - throw new FatalBackendException(e); - } - } - - private void chunkedUploadFile( // - final OnedriveFile file, // - DataSource data, // - final ProgressAware progressAware, // - final CompletableFuture result, // - Option conflictBehaviorOption, // - long size) throws IOException, NoSuchCloudFileException { - OnedriveIdCache.NodeInfo parentNodeInfo = requireNodeInfo(file.getParent()); - UploadSession uploadSession = drive(parentNodeInfo.getDriveId()) // - .items(parentNodeInfo.getId()) // - .itemWithPath(file.getName()) // - .createUploadSession(new DriveItemUploadableProperties()) // - .buildRequest() // - .post(); - - try (InputStream in = data.open(context)) { - new ChunkedUploadProvider<>(uploadSession, client(), in, size, DriveItem.class) // - .upload(singletonList(conflictBehaviorOption), new IProgressCallback() { - @Override - public void progress(long current, long max) { - progressAware.onProgress(Progress // - .progress(UploadState.upload(file)) // - .between(0) // - .and(max) // - .withValue(current)); - } - - @Override - public void success(DriveItem item) { - progressAware.onProgress(Progress.completed(UploadState.upload(file))); - result.complete(item); - cacheNodeInfo(file, item); - } - - @Override - public void failure(com.microsoft.graph.core.ClientException ex) { - result.fail(ex); - } - }, CHUNKED_UPLOAD_CHUNK_SIZE, CHUNKED_UPLOAD_MAX_ATTEMPTS); - } - } - - public void read(final OnedriveFile file, final Optional encryptedTmpFile, final OutputStream data, final ProgressAware progressAware) throws BackendException, IOException { - progressAware.onProgress(Progress.started(DownloadState.download(file))); - - Optional cacheKey = Optional.empty(); - Optional cacheFile = Optional.empty(); - - OnedriveIdCache.NodeInfo nodeInfo = requireNodeInfo(file); - - if (sharedPreferencesHandler.useLruCache() && createLruCache(sharedPreferencesHandler.lruCacheSize())) { - cacheKey = Optional.of(nodeInfo.getId() + nodeInfo.getcTag()); - java.io.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("OnedriveImpl").w(e, "Error while retrieving content from Cache, get from web request"); - writeToData(file, nodeInfo, data, encryptedTmpFile, cacheKey, progressAware); - } - } else { - writeToData(file, nodeInfo, data, encryptedTmpFile, cacheKey, progressAware); - } - } - - private void writeToData(final OnedriveFile file, // - final OnedriveIdCache.NodeInfo nodeInfo, // - final OutputStream data, // - final Optional encryptedTmpFile, // - final Optional cacheKey, // - final ProgressAware progressAware) throws IOException { - - final IDriveItemContentStreamRequest request = drive(nodeInfo.getDriveId()) // - .items(nodeInfo.getId()) // - .content() // - .buildRequest(); - - try (InputStream in = request.get(); // - TransferredBytesAwareOutputStream out = new TransferredBytesAwareOutputStream(data) { - @Override - public void bytesTransferred(long transferred) { - progressAware.onProgress( // - progress(DownloadState.download(file)) // - .between(0) // - .and(file.getSize().orElse(Long.MAX_VALUE)) // - .withValue(transferred)); - } - }) { - copyStreamToStream(in, out); - } - - if (sharedPreferencesHandler.useLruCache() && encryptedTmpFile.isPresent() && cacheKey.isPresent()) { - try { - storeToLruCache(diskLruCache, cacheKey.get(), encryptedTmpFile.get()); - } catch (IOException e) { - Timber.tag("OnedriveImpl").e(e, "Failed to write downloaded file in LRU cache"); - } - } - - progressAware.onProgress(Progress.completed(DownloadState.download(file))); - } - - private boolean createLruCache(int cacheSize) { - if (diskLruCache == null) { - try { - diskLruCache = DiskLruCache.create(new LruFileCacheUtil(context).resolve(ONEDRIVE), cacheSize); - } catch (IOException e) { - Timber.tag("OnedriveImpl").e(e, "Failed to setup LRU cache"); - return false; - } - } - - return true; - } - - public void delete(OnedriveNode node) throws NoSuchCloudFileException { - OnedriveIdCache.NodeInfo nodeInfo = requireNodeInfo(node); - drive(nodeInfo.getDriveId()) // - .items(nodeInfo.getId()) // - .buildRequest() // - .delete(); - removeNodeInfo(node); - } - - private OnedriveIdCache.NodeInfo requireNodeInfo(OnedriveNode node) throws NoSuchCloudFileException { - OnedriveIdCache.NodeInfo result = nodeInfo(node); - if (result == null) { - throw new NoSuchCloudFileException(node.getPath()); - } - return result; - } - - @Nullable - private OnedriveIdCache.NodeInfo nodeInfo(OnedriveNode node) { - OnedriveIdCache.NodeInfo result = nodeInfoCache.get(node.getPath()); - if (result == null) { - result = loadNodeInfo(node); - if (result == null) { - return null; - } else { - nodeInfoCache.add(node.getPath(), result); - } - } - if (result.isFolder() != node.isFolder()) { - return null; - } - return result; - } - - private T cacheNodeInfo(T node, DriveItem item) { - nodeInfoCache.add( // - node.getPath(), new OnedriveIdCache.NodeInfo( // - OnedriveCloudNodeFactory.getId(item), // - OnedriveCloudNodeFactory.getDriveId(item), // - OnedriveCloudNodeFactory.isFolder(item), // - item.cTag // - ) // - ); - return node; - } - - private void removeNodeInfo(OnedriveNode node) { - nodeInfoCache.remove(node.getPath()); - } - - private void removeChildNodeInfo(OnedriveFolder folder) { - nodeInfoCache.removeChildren(folder.getPath()); - } - - private OnedriveIdCache.NodeInfo loadNodeInfo(OnedriveNode node) { - if (node.getParent() == null) { - return loadRootNodeInfo(); - } else { - return loadNonRootNodeInfo(node); - } - } - - private OnedriveIdCache.NodeInfo loadRootNodeInfo() { - DriveItem item = drive(null).root().buildRequest().get(); - return new OnedriveIdCache.NodeInfo( // - OnedriveCloudNodeFactory.getId(item), // - OnedriveCloudNodeFactory.getDriveId(item), // - true, // - item.cTag // - ); - } - - private OnedriveIdCache.NodeInfo loadNonRootNodeInfo(OnedriveNode node) { - OnedriveIdCache.NodeInfo parentNodeInfo = nodeInfo(node.getParent()); - if (parentNodeInfo == null) { - return null; - } - DriveItem item = childByName(parentNodeInfo.getId(), parentNodeInfo.getDriveId(), node.getName()); - - if (item == null) { - return null; - } else { - String cTag = item.cTag; - - return new OnedriveIdCache.NodeInfo( // - OnedriveCloudNodeFactory.getId(item), // - OnedriveCloudNodeFactory.getDriveId(item), // - OnedriveCloudNodeFactory.isFolder(item), // - cTag // - ); - } - } - - public String currentAccount() { - return client().me().drive().buildRequest().get().owner.user.displayName; - } - - public void logout() { - final CompletableFuture result = new CompletableFuture<>(); - clientFactory.getAuthenticationAdapter().logout(new ICallback() { - @Override - public void success(Void aVoid) { - result.complete(null); - } - - @Override - public void failure(ClientException e) { - result.fail(e); - } - }); - try { - result.get(); - } catch (InterruptedException | ExecutionException e) { - throw new FatalBackendException(e); - } - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveImpl.kt b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveImpl.kt new file mode 100644 index 00000000..ba1a9cee --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveImpl.kt @@ -0,0 +1,509 @@ +package org.cryptomator.data.cloud.onedrive + +import android.content.Context +import android.net.Uri +import com.microsoft.graph.concurrency.ChunkedUploadProvider +import com.microsoft.graph.http.GraphServiceException +import com.microsoft.graph.models.extensions.DriveItem +import com.microsoft.graph.models.extensions.DriveItemUploadableProperties +import com.microsoft.graph.models.extensions.Folder +import com.microsoft.graph.models.extensions.IGraphServiceClient +import com.microsoft.graph.models.extensions.ItemReference +import com.microsoft.graph.options.Option +import com.microsoft.graph.options.QueryOption +import com.microsoft.graph.requests.extensions.IDriveRequestBuilder +import com.tomclaw.cache.DiskLruCache +import org.cryptomator.data.cloud.onedrive.OnedriveCloudNodeFactory.folder +import org.cryptomator.data.cloud.onedrive.OnedriveCloudNodeFactory.from +import org.cryptomator.data.cloud.onedrive.OnedriveCloudNodeFactory.getDriveId +import org.cryptomator.data.cloud.onedrive.OnedriveCloudNodeFactory.getId +import org.cryptomator.data.cloud.onedrive.OnedriveCloudNodeFactory.isFolder +import org.cryptomator.data.cloud.onedrive.graph.ClientException +import org.cryptomator.data.cloud.onedrive.graph.ICallback +import org.cryptomator.data.cloud.onedrive.graph.IProgressCallback +import org.cryptomator.data.util.CopyStream +import org.cryptomator.data.util.TransferredBytesAwareOutputStream +import org.cryptomator.domain.OnedriveCloud +import org.cryptomator.domain.exception.BackendException +import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException +import org.cryptomator.domain.exception.FatalBackendException +import org.cryptomator.domain.exception.NoSuchCloudFileException +import org.cryptomator.domain.exception.ParentFolderDoesNotExistException +import org.cryptomator.domain.exception.ParentFolderIsNullException +import org.cryptomator.domain.exception.authentication.NoAuthenticationProvidedException +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.SharedPreferencesHandler +import org.cryptomator.util.file.LruFileCacheUtil +import org.cryptomator.util.file.LruFileCacheUtil.Companion.retrieveFromLruCache +import java.io.File +import java.io.IOException +import java.io.OutputStream +import java.util.ArrayList +import java.util.Date +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ExecutionException +import timber.log.Timber + +internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, nodeInfoCache: OnedriveIdCache) { + + private val cloud: OnedriveCloud + private val context: Context + private val nodeInfoCache: OnedriveIdCache + private val sharedPreferencesHandler: SharedPreferencesHandler + private var diskLruCache: DiskLruCache? = null + + private fun client(): IGraphServiceClient { + return OnedriveClientFactory.getInstance(context, cloud.accessToken()) + } + + private fun drive(driveId: String?): IDriveRequestBuilder { + return if (driveId == null) client().me().drive() else client().drives(driveId) + } + + fun root(): OnedriveFolder { + return RootOnedriveFolder(cloud) + } + + fun resolve(path: String): OnedriveFolder { + val names = path.removePrefix("/").split("/").toTypedArray() + var folder = root() + for (name in names) { + folder = folder(folder, name) + } + return folder + } + + fun file(parent: OnedriveFolder, name: String): OnedriveFile { + return OnedriveCloudNodeFactory.file(parent, name, null) + } + + fun file(parent: OnedriveFolder, name: String, size: Long?): OnedriveFile { + return OnedriveCloudNodeFactory.file(parent, name, size) + } + + fun folder(parent: OnedriveFolder, name: String): OnedriveFolder { + return OnedriveCloudNodeFactory.folder(parent, name) + } + + private fun childByName(parentId: String, parentDriveId: String, name: String): DriveItem? { + return try { + drive(parentDriveId) // + .items(parentId) // + .itemWithPath(Uri.encode(name)) // + .buildRequest() // + .get() + } catch (e: GraphServiceException) { + if (isNotFoundError(e)) { + null + } else { + throw e + } + } + } + + private fun isNotFoundError(error: GraphServiceException): Boolean { + return try { + val responseCodeField = GraphServiceException::class.java.getDeclaredField("responseCode") + responseCodeField.isAccessible = true + val responseCode = responseCodeField[error] as Int + responseCode == 404 + } catch (e: NoSuchFieldException) { + throw IllegalStateException(e) + } catch (e: IllegalAccessException) { + throw IllegalStateException(e) + } + } + + fun exists(node: OnedriveNode): Boolean { + node.parent?.let { + val parentNodeInfo = nodeInfo(it) + if (parentNodeInfo == null) { + removeNodeInfo(node) + return false + } + val item = childByName(parentNodeInfo.id, parentNodeInfo.driveId, node.name) + if (item == null) { + removeNodeInfo(node) + return false + } + cacheNodeInfo(node, item) + return true + } ?: throw ParentFolderIsNullException(node.name) + } + + @Throws(BackendException::class) + fun list(folder: OnedriveFolder): List { + val result: MutableList = ArrayList() + val nodeInfo = requireNodeInfo(folder) + var page = drive(nodeInfo.driveId) // + .items(nodeInfo.id) // + .children() // + .buildRequest() // + .get() + do { + removeChildNodeInfo(folder) + page.currentPage?.forEach { + result.add(cacheNodeInfo(from(folder, it), it)) + } + page = if (page.nextPage != null) { + page.nextPage.buildRequest().get() + } else { + null + } + } while (page != null) + return result + } + + @Throws(NoSuchCloudFileException::class) + fun create(folder: OnedriveFolder): OnedriveFolder { + var parent = folder.parent + if (nodeInfo(parent!!) == null) { //FIXME + if (parent == null) { + throw ParentFolderDoesNotExistException() + } else { + parent = create(parent) + } + } + val folderToCreate = DriveItem() + folderToCreate.name = folder.name + folderToCreate.folder = Folder() + val parentNodeInfo = requireNodeInfo(parent) + val createdFolder = drive(parentNodeInfo.driveId) // + .items(parentNodeInfo.id).children() // + .buildRequest() // + .post(folderToCreate) + return cacheNodeInfo(folder(parent, createdFolder), createdFolder) + } + + @Throws(NoSuchCloudFileException::class, CloudNodeAlreadyExistsException::class) + fun move(source: OnedriveNode, target: OnedriveNode): OnedriveNode { + target.parent?.let { targetsParent -> + if (exists(target)) { + throw CloudNodeAlreadyExistsException(target.name) + } + val targetItem = DriveItem() + targetItem.name = target.name + val targetParentReference = ItemReference() + val targetNodeInfo = nodeInfo(targetsParent) + targetParentReference.id = targetNodeInfo?.id + targetParentReference.driveId = targetNodeInfo?.driveId + targetItem.parentReference = targetParentReference + val sourceNodeInfo = requireNodeInfo(source) + val movedItem = drive(sourceNodeInfo.driveId) // + .items(sourceNodeInfo.id) // + .buildRequest() // + .patch(targetItem) + removeNodeInfo(source) + return cacheNodeInfo(from(targetsParent, movedItem), movedItem) + } ?: throw ParentFolderIsNullException(target.name) + } + + @Throws(BackendException::class) + fun write(file: OnedriveFile, data: DataSource, progressAware: ProgressAware, replace: Boolean, size: Long): OnedriveFile { + if (!replace && exists(file)) { + throw CloudNodeAlreadyExistsException("CloudNode already exists and replace is false") + } + progressAware.onProgress(Progress.started(UploadState.upload(file))) + var uploadMode = NON_REPLACING_MODE + if (replace) { + uploadMode = REPLACE_MODE + } + val conflictBehaviorOption: Option = QueryOption("@name.conflictBehavior", uploadMode) + val result = CompletableFuture() + if (size <= CHUNKED_UPLOAD_MAX_SIZE) { + uploadFile(file, data, progressAware, result, conflictBehaviorOption) + } else { + try { + chunkedUploadFile(file, data, progressAware, result, conflictBehaviorOption, size) + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + progressAware.onProgress(Progress.completed(UploadState.upload(file))) + return try { + OnedriveCloudNodeFactory.file(file.parent, result.get(), Date()) + } catch (e: ExecutionException) { + throw FatalBackendException(e) + } catch (e: InterruptedException) { + throw FatalBackendException(e) + } + } + + @Throws(NoSuchCloudFileException::class) + private fun uploadFile( // + file: OnedriveFile, // + data: DataSource, // + progressAware: ProgressAware, // + result: CompletableFuture, // + conflictBehaviorOption: Option + ) { + val parentNodeInfo = requireNodeInfo(file.parent) + try { + data.open(context)?.use { inputStream -> + drive(parentNodeInfo.driveId) // + .items(parentNodeInfo.id) // + .itemWithPath(file.name) // + .content() // + .buildRequest(listOf(conflictBehaviorOption)) // + .put(CopyStream.toByteArray(inputStream), object : IProgressCallback { + override fun progress(current: Long, max: Long) { + progressAware // + .onProgress( + Progress.progress(UploadState.upload(file)) // + .between(0) // + .and(max) // + .withValue(current) + ) + } + + override fun success(item: DriveItem) { + progressAware.onProgress(Progress.completed(UploadState.upload(file))) + result.complete(item) + cacheNodeInfo(file, item) + } + + override fun failure(ex: com.microsoft.graph.core.ClientException) { + result.completeExceptionally(ex) + } + }) + } ?: throw FatalBackendException("InputStream shouldn't be null") + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + + @Throws(IOException::class, NoSuchCloudFileException::class) + private fun chunkedUploadFile( // + file: OnedriveFile, // + data: DataSource, // + progressAware: ProgressAware, // + result: CompletableFuture, // + conflictBehaviorOption: Option, // + size: Long + ) { + val parentNodeInfo = requireNodeInfo(file.parent) + val uploadSession = drive(parentNodeInfo.driveId) // + .items(parentNodeInfo.id) // + .itemWithPath(file.name) // + .createUploadSession(DriveItemUploadableProperties()) // + .buildRequest() // + .post() + data.open(context).use { inputStream -> + ChunkedUploadProvider(uploadSession, client(), inputStream, size, DriveItem::class.java) // + .upload(listOf(conflictBehaviorOption), object : IProgressCallback { + override fun progress(current: Long, max: Long) { + progressAware.onProgress( + Progress // + .progress(UploadState.upload(file)) // + .between(0) // + .and(max) // + .withValue(current) + ) + } + + override fun success(item: DriveItem) { + progressAware.onProgress(Progress.completed(UploadState.upload(file))) + result.complete(item) + cacheNodeInfo(file, item) + } + + override fun failure(ex: com.microsoft.graph.core.ClientException) { + result.completeExceptionally(ex) + } + }, CHUNKED_UPLOAD_CHUNK_SIZE, CHUNKED_UPLOAD_MAX_ATTEMPTS) + } + } + + @Throws(BackendException::class, IOException::class) + fun read(file: OnedriveFile, encryptedTmpFile: File?, data: OutputStream, progressAware: ProgressAware) { + progressAware.onProgress(Progress.started(DownloadState.download(file))) + var cacheKey: String? = null + var cacheFile: File? = null + val nodeInfo = requireNodeInfo(file) + if (sharedPreferencesHandler.useLruCache() && createLruCache(sharedPreferencesHandler.lruCacheSize())) { + cacheKey = nodeInfo.id + nodeInfo.getcTag() + cacheFile = diskLruCache?.let { it[cacheKey] } + } + if (sharedPreferencesHandler.useLruCache() && cacheFile != null) { + try { + retrieveFromLruCache(cacheFile, data) + } catch (e: IOException) { + Timber.tag("OnedriveImpl").w(e, "Error while retrieving content from Cache, get from web request") + writeToData(file, nodeInfo, data, encryptedTmpFile, cacheKey, progressAware) + } + } else { + writeToData(file, nodeInfo, data, encryptedTmpFile, cacheKey, progressAware) + } + } + + @Throws(IOException::class) + private fun writeToData( + file: OnedriveFile, // + nodeInfo: OnedriveIdCache.NodeInfo, // + data: OutputStream, // + encryptedTmpFile: File?, // + cacheKey: String?, // + progressAware: ProgressAware + ) { + val request = drive(nodeInfo.driveId) // + .items(nodeInfo.id) // + .content() // + .buildRequest() + request.get().use { inputStream -> + object : TransferredBytesAwareOutputStream(data) { + override fun bytesTransferred(transferred: Long) { + progressAware.onProgress( // + Progress.progress(DownloadState.download(file)) // + .between(0) // + .and(file.size ?: Long.MAX_VALUE) // + .withValue(transferred) + ) + } + }.use { out -> CopyStream.copyStreamToStream(inputStream, out) } + } + if (sharedPreferencesHandler.useLruCache() && encryptedTmpFile != null && cacheKey != null) { + try { + diskLruCache?.let { + LruFileCacheUtil.storeToLruCache(it, cacheKey, encryptedTmpFile) + } ?: Timber.tag("OnedriveImpl").e("Failed to store item in LRU cache") + } catch (e: IOException) { + Timber.tag("OnedriveImpl").e(e, "Failed to write downloaded file in LRU cache") + } + } + progressAware.onProgress(Progress.completed(DownloadState.download(file))) + } + + private fun createLruCache(cacheSize: Int): Boolean { + if (diskLruCache == null) { + diskLruCache = try { + DiskLruCache.create(LruFileCacheUtil(context).resolve(LruFileCacheUtil.Cache.ONEDRIVE), cacheSize.toLong()) + } catch (e: IOException) { + Timber.tag("OnedriveImpl").e(e, "Failed to setup LRU cache") + return false + } + } + return true + } + + @Throws(NoSuchCloudFileException::class) + fun delete(node: OnedriveNode) { + val nodeInfo = requireNodeInfo(node) + drive(nodeInfo.driveId) // + .items(nodeInfo.id) // + .buildRequest() // + .delete() + removeNodeInfo(node) + } + + @Throws(NoSuchCloudFileException::class) + private fun requireNodeInfo(node: OnedriveNode): OnedriveIdCache.NodeInfo { + return nodeInfo(node) ?: throw NoSuchCloudFileException(node.path) + } + + private fun nodeInfo(node: OnedriveNode): OnedriveIdCache.NodeInfo? { + var result: OnedriveIdCache.NodeInfo? = nodeInfoCache[node.path] + if (result == null) { + result = loadNodeInfo(node) + if (result == null) { + return null + } else { + nodeInfoCache.add(node.path, result) + } + } + return if (result.isFolder != node.isFolder) { + null + } else result + } + + private fun cacheNodeInfo(node: T, item: DriveItem): T { + nodeInfoCache.add( // + node?.path!!, OnedriveIdCache.NodeInfo( // + getId(item), // + getDriveId(item), // + isFolder(item), // + item.cTag // + ) // + ) + return node + } + + private fun removeNodeInfo(node: OnedriveNode) { + nodeInfoCache.remove(node.path) + } + + private fun removeChildNodeInfo(folder: OnedriveFolder) { + nodeInfoCache.removeChildren(folder.path) + } + + private fun loadNodeInfo(node: OnedriveNode): OnedriveIdCache.NodeInfo? { + return if (node.parent == null) { + loadRootNodeInfo() + } else { + loadNonRootNodeInfo(node) + } + } + + private fun loadRootNodeInfo(): OnedriveIdCache.NodeInfo { + val item = drive(null).root().buildRequest().get() + return OnedriveIdCache.NodeInfo(getId(item), getDriveId(item), true, item.cTag) + } + + private fun loadNonRootNodeInfo(node: OnedriveNode): OnedriveIdCache.NodeInfo? { + node.parent?.let { targetsParent -> + val parentNodeInfo = nodeInfo(targetsParent) ?: return null + val item = childByName(parentNodeInfo.id, parentNodeInfo.driveId, node.name) + return if (item == null) { + null + } else { + OnedriveIdCache.NodeInfo(getId(item), getDriveId(item), isFolder(item), item.cTag) + } + } ?: throw ParentFolderIsNullException(node.name) + } + + fun currentAccount(): String { + return client().me().drive().buildRequest().get().owner.user.displayName + } + + fun logout() { + val result = CompletableFuture() + OnedriveClientFactory.getAuthAdapter(context, cloud.accessToken()).logout(object : ICallback { + override fun success(aVoid: Void?) { + result.complete(null) + } + + override fun failure(e: ClientException) { + result.completeExceptionally(e) + } + }) + try { + result.get() + } catch (e: InterruptedException) { + throw FatalBackendException(e) + } catch (e: ExecutionException) { + throw FatalBackendException(e) + } + } + + companion object { + + private const val CHUNKED_UPLOAD_MAX_SIZE = 4L shl 20 + private const val CHUNKED_UPLOAD_CHUNK_SIZE = 327680 * 32 + private const val CHUNKED_UPLOAD_MAX_ATTEMPTS = 5 + private const val REPLACE_MODE = "replace" + private const val NON_REPLACING_MODE = "rename" + } + + init { + if (cloud.accessToken() == null) { + throw NoAuthenticationProvidedException(cloud) + } + this.cloud = cloud + this.context = context + this.nodeInfoCache = nodeInfoCache + sharedPreferencesHandler = SharedPreferencesHandler(context) + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveNode.java b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveNode.java deleted file mode 100644 index c39225ba..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveNode.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.cryptomator.data.cloud.onedrive; - -import org.cryptomator.domain.CloudNode; - -public interface OnedriveNode extends CloudNode { - - boolean isFolder(); - - String getName(); - - String getPath(); - - OnedriveFolder getParent(); - -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveNode.kt b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveNode.kt new file mode 100644 index 00000000..08755430 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveNode.kt @@ -0,0 +1,12 @@ +package org.cryptomator.data.cloud.onedrive + +import org.cryptomator.domain.CloudNode + +interface OnedriveNode : CloudNode { + + val isFolder: Boolean + override val name: String + override val path: String + override val parent: OnedriveFolder? + +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/onedrive/RootOnedriveFolder.java b/data/src/main/java/org/cryptomator/data/cloud/onedrive/RootOnedriveFolder.java deleted file mode 100644 index 4b61380a..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/onedrive/RootOnedriveFolder.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.cryptomator.data.cloud.onedrive; - -import org.cryptomator.domain.Cloud; -import org.cryptomator.domain.OnedriveCloud; - -class RootOnedriveFolder extends OnedriveFolder { - - private final OnedriveCloud oneDriveCloud; - - public RootOnedriveFolder(OnedriveCloud oneDriveCloud) { - super(null, "", ""); - this.oneDriveCloud = oneDriveCloud; - } - - @Override - public OnedriveCloud getCloud() { - return oneDriveCloud; - } - - @Override - public RootOnedriveFolder withCloud(Cloud cloud) { - return new RootOnedriveFolder((OnedriveCloud) cloud); - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/onedrive/RootOnedriveFolder.kt b/data/src/main/java/org/cryptomator/data/cloud/onedrive/RootOnedriveFolder.kt new file mode 100644 index 00000000..8d84e4db --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/onedrive/RootOnedriveFolder.kt @@ -0,0 +1,11 @@ +package org.cryptomator.data.cloud.onedrive + +import org.cryptomator.domain.Cloud +import org.cryptomator.domain.OnedriveCloud + +internal class RootOnedriveFolder(override val cloud: OnedriveCloud) : OnedriveFolder(null, "", "") { + + override fun withCloud(cloud: Cloud?): RootOnedriveFolder { + return RootOnedriveFolder(cloud as OnedriveCloud) + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudApiError.java b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudApiError.java deleted file mode 100644 index d502576d..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudApiError.java +++ /dev/null @@ -1,112 +0,0 @@ -package org.cryptomator.data.cloud.pcloud; - -import java.util.Arrays; -import java.util.HashSet; - -public class PCloudApiError { - - public static final HashSet ignoreExistsSet = new HashSet<>( // - Arrays.asList( // - PCloudApiErrorCodes.COMPONENT_OF_PARENT_DIRECTORY_DOES_NOT_EXIST.getValue(), // - PCloudApiErrorCodes.FILE_NOT_FOUND.getValue(), // - PCloudApiErrorCodes.FILE_OR_FOLDER_NOT_FOUND.getValue(), // - PCloudApiErrorCodes.DIRECTORY_DOES_NOT_EXIST.getValue(), // - PCloudApiErrorCodes.INVALID_FILE_OR_FOLDER_NAME.getValue() // - )); - public static final HashSet ignoreMoveSet = new HashSet<>( // - Arrays.asList( // - PCloudApiErrorCodes.FILE_OR_FOLDER_ALREADY_EXISTS.getValue(), // - PCloudApiErrorCodes.COMPONENT_OF_PARENT_DIRECTORY_DOES_NOT_EXIST.getValue(), // - PCloudApiErrorCodes.FILE_NOT_FOUND.getValue(), // - PCloudApiErrorCodes.FILE_OR_FOLDER_NOT_FOUND.getValue(), // - PCloudApiErrorCodes.DIRECTORY_DOES_NOT_EXIST.getValue() // - ) // - ); - - public static boolean isCloudNodeAlreadyExistsException(int errorCode) { - return errorCode == PCloudApiErrorCodes.FILE_OR_FOLDER_ALREADY_EXISTS.getValue(); - } - - public static boolean isFatalBackendException(int errorCode) { - return errorCode == PCloudApiErrorCodes.INTERNAL_UPLOAD_ERROR.getValue() // - || errorCode == PCloudApiErrorCodes.INTERNAL_UPLOAD_ERROR.getValue() // - || errorCode == PCloudApiErrorCodes.UPLOAD_NOT_FOUND.getValue() // - || errorCode == PCloudApiErrorCodes.TRANSFER_NOT_FOUND.getValue(); - } - - public static boolean isForbiddenException(int errorCode) { - return errorCode == PCloudApiErrorCodes.ACCESS_DENIED.getValue(); - } - - public static boolean isNetworkConnectionException(int errorCode) { - return errorCode == PCloudApiErrorCodes.CONNECTION_BROKE.getValue(); - } - - public static boolean isNoSuchCloudFileException(int errorCode) { - return errorCode == PCloudApiErrorCodes.COMPONENT_OF_PARENT_DIRECTORY_DOES_NOT_EXIST.getValue() // - || errorCode == PCloudApiErrorCodes.FILE_NOT_FOUND.getValue() // - || errorCode == PCloudApiErrorCodes.FILE_OR_FOLDER_NOT_FOUND.getValue() // - || errorCode == PCloudApiErrorCodes.DIRECTORY_DOES_NOT_EXIST.getValue(); - } - - public static boolean isWrongCredentialsException(int errorCode) { - return errorCode == PCloudApiErrorCodes.INVALID_ACCESS_TOKEN.getValue() // - || errorCode == PCloudApiErrorCodes.ACCESS_TOKEN_REVOKED.getValue(); - } - - public static boolean isUnauthorizedException(int errorCode) { - return errorCode == PCloudApiErrorCodes.LOGIN_FAILED.getValue() // - || errorCode == PCloudApiErrorCodes.LOGIN_REQUIRED.getValue() // - || errorCode == PCloudApiErrorCodes.TOO_MANY_LOGIN_TRIES_FROM_IP.getValue(); - } - - public enum PCloudApiErrorCodes { - LOGIN_REQUIRED(1000), // - NO_FULL_PATH_OR_NAME_FOLDER_ID_PROVIDED(1001), // - NO_FULL_PATH_OR_FOLDER_ID_PROVIDED(1002), // - NO_FILE_ID_OR_PATH_PROVIDED(1004), // - INVALID_DATE_TIME_FORMAT(1013), // - NO_DESTINATION_PROVIDED(1016), // - INVALID_FOLDER_ID(1017), // - INVALID_DESTINATION(1037), // - PROVIDE_URL(1040), // - UPLOAD_NOT_FOUND(1900), // - TRANSFER_NOT_FOUND(1902), // - LOGIN_FAILED(2000), // - INVALID_FILE_OR_FOLDER_NAME(2001), // - COMPONENT_OF_PARENT_DIRECTORY_DOES_NOT_EXIST(2002), // - ACCESS_DENIED(2003), // - FILE_OR_FOLDER_ALREADY_EXISTS(2004), // - DIRECTORY_DOES_NOT_EXIST(2005), // - FOLDER_NOT_EMPTY(2006), // - CANNOT_DELETE_ROOT_FOLDER(2007), // - USER_OVER_QUOTA(2008), // - FILE_NOT_FOUND(2009), // - INVALID_PATH(2010), // - SHARED_FOLDER_IN_SHARED_FOLDER(2023), // - ACTIVE_SHARES_OR_SHAREREQUESTS_PRESENT(2028), // - CONNECTION_BROKE(2041), // - CANNOT_RENAME_ROOT_FOLDER(2042), // - CANNOT_MOVE_FOLDER_INTO_SUBFOLDER_OF_ITSELF(2043), // - FILE_OR_FOLDER_NOT_FOUND(2055), // - NO_FILE_UPLOAD_DETECTED(2088), // - INVALID_ACCESS_TOKEN(2094), // - ACCESS_TOKEN_REVOKED(2095), // - TRANSFER_OVER_QUOTA(2097), // - TARGET_FOLDER_DOES_NOT_EXIST(2208), // - TOO_MANY_LOGIN_TRIES_FROM_IP(4000), // - INTERNAL_ERROR(5000), // - INTERNAL_UPLOAD_ERROR(5001); - - private final int value; - - PCloudApiErrorCodes(final int newValue) { - value = newValue; - } - - public int getValue() { - return value; - } - } - -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudApiError.kt b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudApiError.kt new file mode 100644 index 00000000..11a9407e --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudApiError.kt @@ -0,0 +1,110 @@ +package org.cryptomator.data.cloud.pcloud + +import java.util.HashSet + +object PCloudApiError { + + @JvmField + val ignoreExistsSet = HashSet( // + listOf( // + PCloudApiErrorCodes.COMPONENT_OF_PARENT_DIRECTORY_DOES_NOT_EXIST.value, // + PCloudApiErrorCodes.FILE_NOT_FOUND.value, // + PCloudApiErrorCodes.FILE_OR_FOLDER_NOT_FOUND.value, // + PCloudApiErrorCodes.DIRECTORY_DOES_NOT_EXIST.value, // + PCloudApiErrorCodes.INVALID_FILE_OR_FOLDER_NAME.value // + ) + ) + @JvmField + val ignoreMoveSet = HashSet( // + listOf( // + PCloudApiErrorCodes.FILE_OR_FOLDER_ALREADY_EXISTS.value, // + PCloudApiErrorCodes.COMPONENT_OF_PARENT_DIRECTORY_DOES_NOT_EXIST.value, // + PCloudApiErrorCodes.FILE_NOT_FOUND.value, // + PCloudApiErrorCodes.FILE_OR_FOLDER_NOT_FOUND.value, // + PCloudApiErrorCodes.DIRECTORY_DOES_NOT_EXIST.value // + ) // + ) + + @JvmStatic + fun isCloudNodeAlreadyExistsException(errorCode: Int): Boolean { + return errorCode == PCloudApiErrorCodes.FILE_OR_FOLDER_ALREADY_EXISTS.value + } + + fun isFatalBackendException(errorCode: Int): Boolean { + return errorCode == PCloudApiErrorCodes.INTERNAL_UPLOAD_ERROR.value // + || errorCode == PCloudApiErrorCodes.INTERNAL_UPLOAD_ERROR.value // + || errorCode == PCloudApiErrorCodes.UPLOAD_NOT_FOUND.value // + || errorCode == PCloudApiErrorCodes.TRANSFER_NOT_FOUND.value + } + + @JvmStatic + fun isForbiddenException(errorCode: Int): Boolean { + return errorCode == PCloudApiErrorCodes.ACCESS_DENIED.value + } + + @JvmStatic + fun isNetworkConnectionException(errorCode: Int): Boolean { + return errorCode == PCloudApiErrorCodes.CONNECTION_BROKE.value + } + + @JvmStatic + fun isNoSuchCloudFileException(errorCode: Int): Boolean { + return errorCode == PCloudApiErrorCodes.COMPONENT_OF_PARENT_DIRECTORY_DOES_NOT_EXIST.value // + || errorCode == PCloudApiErrorCodes.FILE_NOT_FOUND.value // + || errorCode == PCloudApiErrorCodes.FILE_OR_FOLDER_NOT_FOUND.value // + || errorCode == PCloudApiErrorCodes.DIRECTORY_DOES_NOT_EXIST.value + } + + @JvmStatic + fun isWrongCredentialsException(errorCode: Int): Boolean { + return (errorCode == PCloudApiErrorCodes.INVALID_ACCESS_TOKEN.value // + || errorCode == PCloudApiErrorCodes.ACCESS_TOKEN_REVOKED.value) + } + + @JvmStatic + fun isUnauthorizedException(errorCode: Int): Boolean { + return errorCode == PCloudApiErrorCodes.LOGIN_FAILED.value // + || errorCode == PCloudApiErrorCodes.LOGIN_REQUIRED.value // + || errorCode == PCloudApiErrorCodes.TOO_MANY_LOGIN_TRIES_FROM_IP.value + } + + enum class PCloudApiErrorCodes(val value: Int) { + LOGIN_REQUIRED(1000), // + NO_FULL_PATH_OR_NAME_FOLDER_ID_PROVIDED(1001), // + NO_FULL_PATH_OR_FOLDER_ID_PROVIDED(1002), // + NO_FILE_ID_OR_PATH_PROVIDED(1004), // + INVALID_DATE_TIME_FORMAT(1013), // + NO_DESTINATION_PROVIDED(1016), // + INVALID_FOLDER_ID(1017), // + INVALID_DESTINATION(1037), // + PROVIDE_URL(1040), // + UPLOAD_NOT_FOUND(1900), // + TRANSFER_NOT_FOUND(1902), // + LOGIN_FAILED(2000), // + INVALID_FILE_OR_FOLDER_NAME(2001), // + COMPONENT_OF_PARENT_DIRECTORY_DOES_NOT_EXIST(2002), // + ACCESS_DENIED(2003), // + FILE_OR_FOLDER_ALREADY_EXISTS(2004), // + DIRECTORY_DOES_NOT_EXIST(2005), // + FOLDER_NOT_EMPTY(2006), // + CANNOT_DELETE_ROOT_FOLDER(2007), // + USER_OVER_QUOTA(2008), // + FILE_NOT_FOUND(2009), // + INVALID_PATH(2010), // + SHARED_FOLDER_IN_SHARED_FOLDER(2023), // + ACTIVE_SHARES_OR_SHAREREQUESTS_PRESENT(2028), // + CONNECTION_BROKE(2041), // + CANNOT_RENAME_ROOT_FOLDER(2042), // + CANNOT_MOVE_FOLDER_INTO_SUBFOLDER_OF_ITSELF(2043), // + FILE_OR_FOLDER_NOT_FOUND(2055), // + NO_FILE_UPLOAD_DETECTED(2088), // + INVALID_ACCESS_TOKEN(2094), // + ACCESS_TOKEN_REVOKED(2095), // + TRANSFER_OVER_QUOTA(2097), // + TARGET_FOLDER_DOES_NOT_EXIST(2208), // + TOO_MANY_LOGIN_TRIES_FROM_IP(4000), // + INTERNAL_ERROR(5000), // + INTERNAL_UPLOAD_ERROR(5001); + + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudClientFactory.java b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudClientFactory.java deleted file mode 100644 index f0a0b535..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudClientFactory.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.cryptomator.data.cloud.pcloud; - -import android.content.Context; - -import com.pcloud.sdk.ApiClient; -import com.pcloud.sdk.Authenticators; -import com.pcloud.sdk.PCloudSdk; - -import org.cryptomator.data.cloud.okhttplogging.HttpLoggingInterceptor; -import org.cryptomator.util.crypto.CredentialCryptor; - -import okhttp3.Interceptor; -import okhttp3.OkHttpClient; -import timber.log.Timber; - -import static org.cryptomator.data.util.NetworkTimeout.CONNECTION; -import static org.cryptomator.data.util.NetworkTimeout.READ; -import static org.cryptomator.data.util.NetworkTimeout.WRITE; - -class PCloudClientFactory { - - private ApiClient apiClient; - - private static Interceptor httpLoggingInterceptor(Context context) { - return new HttpLoggingInterceptor(message -> Timber.tag("OkHttp").d(message), context); - } - - public ApiClient getClient(String accessToken, String url, Context context) { - if (apiClient == null) { - apiClient = createApiClient(accessToken, url, context); - } - return apiClient; - } - - private ApiClient createApiClient(String accessToken, String url, Context context) { - OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient() // - .newBuilder() // - .connectTimeout(CONNECTION.getTimeout(), CONNECTION.getUnit()) // - .readTimeout(READ.getTimeout(), READ.getUnit()) // - .writeTimeout(WRITE.getTimeout(), WRITE.getUnit()) // - .addInterceptor(httpLoggingInterceptor(context)); //; - - OkHttpClient okHttpClient = okHttpClientBuilder.build(); - - return PCloudSdk.newClientBuilder().authenticator(Authenticators.newOAuthAuthenticator(decrypt(accessToken, context))).withClient(okHttpClient).apiHost(url).create(); - } - - private String decrypt(String password, Context context) { - return CredentialCryptor // - .getInstance(context) // - .decrypt(password); - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudClientFactory.kt b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudClientFactory.kt new file mode 100644 index 00000000..c69d2f3b --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudClientFactory.kt @@ -0,0 +1,57 @@ +package org.cryptomator.data.cloud.pcloud + +import android.content.Context +import com.pcloud.sdk.ApiClient +import com.pcloud.sdk.Authenticators +import com.pcloud.sdk.PCloudSdk +import org.cryptomator.data.cloud.okhttplogging.HttpLoggingInterceptor +import org.cryptomator.data.util.NetworkTimeout +import org.cryptomator.util.crypto.CredentialCryptor +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import timber.log.Timber + +class PCloudClientFactory { + + companion object { + + @Volatile + private var instance: ApiClient? = null + + @Synchronized + fun getInstance(accessToken: String, url: String, context: Context): ApiClient = instance ?: createApiClient(accessToken, url, context).also { instance = it } + + private fun createApiClient(accessToken: String, url: String, context: Context): ApiClient { + val okHttpClient = OkHttpClient() // + .newBuilder() // + .connectTimeout(NetworkTimeout.CONNECTION.timeout, NetworkTimeout.CONNECTION.unit) // + .readTimeout(NetworkTimeout.READ.timeout, NetworkTimeout.READ.unit) // + .writeTimeout(NetworkTimeout.WRITE.timeout, NetworkTimeout.WRITE.unit) // + .addInterceptor(httpLoggingInterceptor(context)) // + .build() + + return PCloudSdk // + .newClientBuilder() // + .authenticator(Authenticators.newOAuthAuthenticator(decrypt(accessToken, context))) // + .withClient(okHttpClient) // + .apiHost(url) // + .create() + } + + private fun decrypt(password: String, context: Context): String { + return CredentialCryptor // + .getInstance(context) // + .decrypt(password) + } + + private fun httpLoggingInterceptor(context: Context): Interceptor { + val logger = object : HttpLoggingInterceptor.Logger { + override fun log(message: String) { + Timber.tag("OkHttp").d(message) + } + } + + return HttpLoggingInterceptor(logger, context) + } + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudContentRepository.java b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudContentRepository.java deleted file mode 100644 index 20d1d62a..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudContentRepository.java +++ /dev/null @@ -1,193 +0,0 @@ -package org.cryptomator.data.cloud.pcloud; - -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 PCloudContentRepository extends InterceptingCloudContentRepository { - - private final PCloud cloud; - - public PCloudContentRepository(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 PCloudImpl cloud; - - public Intercepted(PCloud cloud, Context context) { - this.cloud = new PCloudImpl(context, cloud); - } - - public PCloudFolder root(PCloud cloud) { - return this.cloud.root(); - } - - @Override - public PCloudFolder resolve(PCloud cloud, String path) throws BackendException { - try { - return this.cloud.resolve(path); - } catch (IOException ex) { - throw new FatalBackendException(ex); - } - } - - @Override - public PCloudFile file(PCloudFolder parent, String name) throws BackendException { - try { - return cloud.file(parent, name); - } catch (IOException ex) { - throw new FatalBackendException(ex); - } - } - - @Override - public PCloudFile file(PCloudFolder parent, String name, Optional size) throws BackendException { - try { - return cloud.file(parent, name, size); - } catch (IOException ex) { - throw new FatalBackendException(ex); - } - } - - @Override - public PCloudFolder folder(PCloudFolder parent, String name) throws BackendException { - try { - return cloud.folder(parent, name); - } catch (IOException ex) { - throw new FatalBackendException(ex); - } - } - - @Override - public boolean exists(PCloudNode node) throws BackendException { - try { - return cloud.exists(node); - } catch (IOException e) { - throw new FatalBackendException(e); - } - } - - @Override - public List list(PCloudFolder folder) throws BackendException { - try { - return cloud.list(folder); - } catch (IOException e) { - throw new FatalBackendException(e); - } - } - - @Override - public PCloudFolder create(PCloudFolder folder) throws BackendException { - try { - return cloud.create(folder); - } catch (IOException e) { - throw new FatalBackendException(e); - } - } - - @Override - public PCloudFolder move(PCloudFolder source, PCloudFolder target) throws BackendException { - try { - return (PCloudFolder) cloud.move(source, target); - } catch (IOException e) { - throw new FatalBackendException(e); - } - } - - @Override - public PCloudFile move(PCloudFile source, PCloudFile target) throws BackendException { - try { - return (PCloudFile) cloud.move(source, target); - } catch (IOException e) { - throw new FatalBackendException(e); - } - } - - @Override - public PCloudFile write(PCloudFile 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(PCloudFile 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(PCloudNode 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/pcloud/PCloudContentRepository.kt b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudContentRepository.kt new file mode 100644 index 00000000..4b23cf25 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudContentRepository.kt @@ -0,0 +1,178 @@ +package org.cryptomator.data.cloud.pcloud + +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.ExceptionUtil +import java.io.File +import java.io.IOException +import java.io.OutputStream + +internal class PCloudContentRepository(private val cloud: PCloud, context: Context) : InterceptingCloudContentRepository(Intercepted(cloud, context)) { + + @Throws(BackendException::class) + override fun throwWrappedIfRequired(e: Exception) { + throwConnectionErrorIfRequired(e) + throwWrongCredentialsExceptionIfRequired(e) + } + + @Throws(NetworkConnectionException::class) + private fun throwConnectionErrorIfRequired(e: Exception) { + if (ExceptionUtil.contains(e, IOException::class.java)) { + throw NetworkConnectionException(e) + } + } + + private fun throwWrongCredentialsExceptionIfRequired(e: Exception) { + if (e is ApiError) { + val errorCode = e.errorCode() + if (errorCode == PCloudApiError.PCloudApiErrorCodes.INVALID_ACCESS_TOKEN.value // + || errorCode == PCloudApiError.PCloudApiErrorCodes.ACCESS_TOKEN_REVOKED.value + ) { + throw WrongCredentialsException(cloud) + } + } + } + + private class Intercepted(cloud: PCloud, context: Context) : CloudContentRepository { + + private val cloud: PCloudImpl = PCloudImpl(context, cloud) + + override fun root(cloud: PCloud): PCloudFolder { + return this.cloud.root() + } + + @Throws(BackendException::class) + override fun resolve(cloud: PCloud, path: String): PCloudFolder { + return try { + this.cloud.resolve(path) + } catch (ex: IOException) { + throw FatalBackendException(ex) + } + } + + @Throws(BackendException::class) + override fun file(parent: PCloudFolder, name: String): PCloudFile { + return try { + cloud.file(parent, name, null) + } catch (ex: IOException) { + throw FatalBackendException(ex) + } + } + + @Throws(BackendException::class) + override fun file(parent: PCloudFolder, name: String, size: Long?): PCloudFile { + return try { + cloud.file(parent, name, size) + } catch (ex: IOException) { + throw FatalBackendException(ex) + } + } + + @Throws(BackendException::class) + override fun folder(parent: PCloudFolder, name: String): PCloudFolder { + return try { + cloud.folder(parent, name) + } catch (ex: IOException) { + throw FatalBackendException(ex) + } + } + + @Throws(BackendException::class) + override fun exists(node: PCloudNode): Boolean { + return try { + cloud.exists(node) + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun list(folder: PCloudFolder): List { + return try { + cloud.list(folder) + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun create(folder: PCloudFolder): PCloudFolder { + return try { + cloud.create(folder) + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun move(source: PCloudFolder, target: PCloudFolder): PCloudFolder { + return try { + cloud.move(source, target) as PCloudFolder + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun move(source: PCloudFile, target: PCloudFile): PCloudFile { + return try { + cloud.move(source, target) as PCloudFile + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun write(file: PCloudFile, data: DataSource, progressAware: ProgressAware, replace: Boolean, size: Long): PCloudFile { + return try { + cloud.write(file, data, progressAware, replace, size) + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun read(file: PCloudFile, encryptedTmpFile: File?, data: OutputStream, progressAware: ProgressAware) { + try { + cloud.read(file, encryptedTmpFile, data, progressAware) + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun delete(node: PCloudNode) { + try { + cloud.delete(node) + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun checkAuthenticationAndRetrieveCurrentAccount(cloud: PCloud): String { + return try { + this.cloud.currentAccount() + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun logout(cloud: PCloud) { + // empty + } + + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudContentRepositoryFactory.java b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudContentRepositoryFactory.java index d3d1515d..6899b6af 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudContentRepositoryFactory.java +++ b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudContentRepositoryFactory.java @@ -28,7 +28,7 @@ public class PCloudContentRepositoryFactory implements CloudContentRepositoryFac } @Override - public CloudContentRepository cloudContentRepositoryFor(Cloud cloud) { + public CloudContentRepository cloudContentRepositoryFor(Cloud cloud) { return new PCloudContentRepository((PCloud) cloud, context); } diff --git a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudFile.java b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudFile.java deleted file mode 100644 index b245a4e7..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudFile.java +++ /dev/null @@ -1,55 +0,0 @@ -package org.cryptomator.data.cloud.pcloud; - -import org.cryptomator.domain.Cloud; -import org.cryptomator.domain.CloudFile; -import org.cryptomator.util.Optional; - -import java.util.Date; - -class PCloudFile implements CloudFile, PCloudNode { - - private final PCloudFolder parent; - private final String name; - private final String path; - private final Optional size; - private final Optional modified; - - public PCloudFile(PCloudFolder 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 PCloudFolder 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/pcloud/PCloudFile.kt b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudFile.kt new file mode 100644 index 00000000..d1f5dc0d --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudFile.kt @@ -0,0 +1,11 @@ +package org.cryptomator.data.cloud.pcloud + +import org.cryptomator.domain.Cloud +import org.cryptomator.domain.CloudFile +import java.util.Date + +internal class PCloudFile(override val parent: PCloudFolder, override val name: String, override val path: String, override val size: Long?, override val modified: Date?) : CloudFile, PCloudNode { + + override val cloud: Cloud? + get() = parent.cloud +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudFolder.java b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudFolder.java deleted file mode 100644 index 2674ffd6..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudFolder.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.cryptomator.data.cloud.pcloud; - -import org.cryptomator.domain.Cloud; -import org.cryptomator.domain.CloudFolder; - -class PCloudFolder implements CloudFolder, PCloudNode { - - private final PCloudFolder parent; - private final String name; - private final String path; - - public PCloudFolder(PCloudFolder 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 PCloudFolder getParent() { - return parent; - } - - @Override - public PCloudFolder withCloud(Cloud cloud) { - return new PCloudFolder(parent.withCloud(cloud), name, path); - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudFolder.kt b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudFolder.kt new file mode 100644 index 00000000..43ccc6cf --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudFolder.kt @@ -0,0 +1,14 @@ +package org.cryptomator.data.cloud.pcloud + +import org.cryptomator.domain.Cloud +import org.cryptomator.domain.CloudFolder + +open class PCloudFolder(override val parent: PCloudFolder?, override val name: String, override val path: String) : CloudFolder, PCloudNode { + + override val cloud: Cloud? + get() = parent?.cloud + + override fun withCloud(cloud: Cloud?): PCloudFolder? { + return PCloudFolder(parent?.withCloud(cloud), name, path) + } +} 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 deleted file mode 100644 index 1bee544f..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudImpl.java +++ /dev/null @@ -1,377 +0,0 @@ -package org.cryptomator.data.cloud.pcloud; - -import android.content.Context; - -import com.pcloud.sdk.ApiClient; -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.PCloud; -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 PCloudImpl { - - private final PCloudClientFactory clientFactory = new PCloudClientFactory(); - private final PCloud cloud; - private final RootPCloudFolder root; - private final Context context; - - private final SharedPreferencesHandler sharedPreferencesHandler; - private DiskLruCache diskLruCache; - - PCloudImpl(Context context, PCloud cloud) { - if (cloud.accessToken() == null) { - throw new NoAuthenticationProvidedException(cloud); - } - - this.context = context; - this.cloud = cloud; - this.root = new RootPCloudFolder(cloud); - this.sharedPreferencesHandler = new SharedPreferencesHandler(context); - } - - private ApiClient client() { - return clientFactory.getClient(cloud.accessToken(), cloud.url(), context); - } - - public PCloudFolder root() { - return root; - } - - public PCloudFolder resolve(String path) throws IOException, BackendException { - if (path.startsWith("/")) { - path = path.substring(1); - } - String[] names = path.split("/"); - PCloudFolder folder = root; - for (String name : names) { - folder = folder(folder, name); - } - return folder; - } - - public PCloudFile file(PCloudFolder parent, String name) throws BackendException, IOException { - return file(parent, name, Optional.empty()); - } - - public PCloudFile file(PCloudFolder parent, String name, Optional size) throws BackendException, IOException { - return PCloudNodeFactory.file(parent, name, size, parent.getPath() + "/" + name); - } - - public PCloudFolder folder(PCloudFolder parent, String name) throws IOException, BackendException { - return PCloudNodeFactory.folder(parent, name, parent.getPath() + "/" + name); - } - - public boolean exists(PCloudNode node) throws IOException, BackendException { - try { - if (node instanceof RootPCloudFolder) { - client().loadFolder("/").execute(); - } else if (node instanceof PCloudFolder) { - 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(PCloudFolder folder) throws IOException, BackendException { - List result = new ArrayList<>(); - - String path = folder.getPath(); - if (folder instanceof RootPCloudFolder) { - path = "/"; - } - - try { - RemoteFolder listFolderResult = client().listFolder(path).execute(); - List entryMetadata = listFolderResult.children(); - for (RemoteEntry metadata : entryMetadata) { - result.add(PCloudNodeFactory.from(folder, metadata)); - } - return result; - } catch (ApiError ex) { - handleApiError(ex, folder.getName()); - throw new FatalBackendException(ex); - } - } - - public PCloudFolder create(PCloudFolder folder) throws IOException, BackendException { - if (!exists(folder.getParent())) { - folder = new PCloudFolder( // - create(folder.getParent()), // - folder.getName(), folder.getPath() // - ); - } - - try { - RemoteFolder createdFolder = client() // - .createFolder(folder.getPath()) // - .execute(); - return PCloudNodeFactory.folder(folder.getParent(), createdFolder); - } catch (ApiError ex) { - handleApiError(ex, folder.getName()); - throw new FatalBackendException(ex); - } - } - - public PCloudNode move(PCloudNode source, PCloudNode target) throws IOException, BackendException { - if (exists(target)) { - throw new CloudNodeAlreadyExistsException(target.getName()); - } - - try { - if (source instanceof PCloudFolder) { - return PCloudNodeFactory.from(target.getParent(), client().moveFolder(source.getPath(), target.getPath()).execute()); - } else { - return PCloudNodeFactory.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 PCloudFile write(PCloudFile 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 PCloudNodeFactory.file(file.getParent(), uploadedFile); - - } - - private RemoteFile uploadFile(final PCloudFile 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(PCloudFile 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 PCloudFile 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(PCloudNode node) throws IOException, BackendException { - try { - if (node instanceof PCloudFolder) { - 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/pcloud/PCloudImpl.kt b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudImpl.kt new file mode 100644 index 00000000..63ca8de8 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudImpl.kt @@ -0,0 +1,374 @@ +package org.cryptomator.data.cloud.pcloud + +import android.content.Context +import com.pcloud.sdk.ApiClient +import com.pcloud.sdk.ApiError +import com.pcloud.sdk.DataSink +import com.pcloud.sdk.DownloadOptions +import com.pcloud.sdk.ProgressListener +import com.pcloud.sdk.RemoteFile +import com.pcloud.sdk.UploadOptions +import com.tomclaw.cache.DiskLruCache +import org.cryptomator.data.cloud.pcloud.PCloudApiError.isCloudNodeAlreadyExistsException +import org.cryptomator.data.cloud.pcloud.PCloudApiError.isForbiddenException +import org.cryptomator.data.cloud.pcloud.PCloudApiError.isNetworkConnectionException +import org.cryptomator.data.cloud.pcloud.PCloudApiError.isNoSuchCloudFileException +import org.cryptomator.data.cloud.pcloud.PCloudApiError.isUnauthorizedException +import org.cryptomator.data.cloud.pcloud.PCloudApiError.isWrongCredentialsException +import org.cryptomator.data.util.CopyStream +import org.cryptomator.domain.PCloud +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.ParentFolderIsNullException +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.SharedPreferencesHandler +import org.cryptomator.util.file.LruFileCacheUtil +import org.cryptomator.util.file.LruFileCacheUtil.Companion.retrieveFromLruCache +import java.io.File +import java.io.IOException +import java.io.OutputStream +import java.util.Date +import java.util.stream.Collectors +import okio.BufferedSink +import okio.BufferedSource +import okio.source +import timber.log.Timber + +internal class PCloudImpl(context: Context, cloud: PCloud) { + + private val cloud: PCloud + private val root: RootPCloudFolder + private val context: Context + private val sharedPreferencesHandler: SharedPreferencesHandler + private var diskLruCache: DiskLruCache? = null + + private fun client(): ApiClient { + return PCloudClientFactory.getInstance(cloud.accessToken(), cloud.url(), context) + } + + fun root(): PCloudFolder { + return root + } + + @Throws(IOException::class, BackendException::class) + fun resolve(path: String): PCloudFolder { + val names = path.removePrefix("/").split("/").toTypedArray() + var folder: PCloudFolder = root + for (name in names) { + folder = folder(folder, name) + } + return folder + } + + @Throws(BackendException::class, IOException::class) + fun file(parent: PCloudFolder, name: String, size: Long?): PCloudFile { + return PCloudNodeFactory.file(parent, name, size, parent.path + "/" + name) + } + + @Throws(IOException::class, BackendException::class) + fun folder(parent: PCloudFolder, name: String): PCloudFolder { + return PCloudNodeFactory.folder(parent, name, parent.path + "/" + name) + } + + @Throws(IOException::class, BackendException::class) + fun exists(node: PCloudNode): Boolean { + return try { + when (node) { + is RootPCloudFolder -> { + client().loadFolder("/").execute() + } + is PCloudFolder -> { + client().loadFolder(node.path).execute() + } + else -> { + client().loadFile(node.path).execute() + } + } + true + } catch (ex: ApiError) { + handleApiError(ex, PCloudApiError.ignoreExistsSet, node.name) + false + } + } + + @Throws(IOException::class, BackendException::class) + fun list(folder: PCloudFolder): List { + val path = if (folder !is RootPCloudFolder) { + folder.path + } else { + "/" + } + + return try { + client() + .listFolder(path) + .execute() + .children() + .parallelStream() + .map { node -> PCloudNodeFactory.from(folder, node) } + .collect(Collectors.toList()) + } catch (ex: ApiError) { + handleApiError(ex, folder.name) + throw FatalBackendException(ex) + } + } + + @Throws(IOException::class, BackendException::class) + fun create(folder: PCloudFolder): PCloudFolder { + var folder = folder + folder.parent?.let { parentFolder -> + if (!exists(parentFolder)) { + folder = PCloudFolder(create(parentFolder), folder.name, folder.path) + } + } ?: throw ParentFolderIsNullException(folder.name) + + folder.parent?.let { parentFolder -> + return try { + val createdFolder = client() // + .createFolder(folder.path) // + .execute() + PCloudNodeFactory.folder(parentFolder, createdFolder) + } catch (ex: ApiError) { + handleApiError(ex, folder.name) + throw FatalBackendException(ex) + } + } ?: throw ParentFolderIsNullException(folder.name) + } + + @Throws(IOException::class, BackendException::class) + fun move(source: PCloudNode, target: PCloudNode): PCloudNode { + target.parent?.let { targetsParent -> + if (exists(target)) { + throw CloudNodeAlreadyExistsException(target.name) + } + return try { + if (source is PCloudFolder) { + PCloudNodeFactory.from(targetsParent, client().moveFolder(source.path, target.path).execute()) + } else { + PCloudNodeFactory.from(targetsParent, client().moveFile(source.path, target.path).execute()) + } + } catch (ex: ApiError) { + when { + isCloudNodeAlreadyExistsException(ex.errorCode()) -> { + throw CloudNodeAlreadyExistsException(target.name) + } + isNoSuchCloudFileException(ex.errorCode()) -> { + throw NoSuchCloudFileException(source.name) + } + else -> { + handleApiError(ex, PCloudApiError.ignoreMoveSet, null) + } + } + throw FatalBackendException(ex) + } + } ?: throw ParentFolderIsNullException(target.name) + } + + @Throws(IOException::class, BackendException::class) + fun write(file: PCloudFile, data: DataSource, progressAware: ProgressAware, replace: Boolean, size: Long): PCloudFile { + if (!replace && exists(file)) { + throw CloudNodeAlreadyExistsException("CloudNode already exists and replace is false") + } + progressAware.onProgress(Progress.started(UploadState.upload(file))) + + val uploadOptions = if (replace) { + UploadOptions.OVERRIDE_FILE + } else { + UploadOptions.DEFAULT + } + + val uploadedFile = uploadFile(file, data, progressAware, uploadOptions, size) + progressAware.onProgress(Progress.completed(UploadState.upload(file))) + return PCloudNodeFactory.file(file.parent, uploadedFile) + } + + @Throws(IOException::class, BackendException::class) + private fun uploadFile(file: PCloudFile, data: DataSource, progressAware: ProgressAware, uploadOptions: UploadOptions, size: Long): RemoteFile { + val listener = ProgressListener { done: Long, _: Long -> + progressAware.onProgress( // + Progress.progress(UploadState.upload(file)) // + .between(0) // + .and(size) // + .withValue(done) + ) + } + val pCloudDataSource: com.pcloud.sdk.DataSource = object : com.pcloud.sdk.DataSource() { + override fun contentLength(): Long { + return data.size(context) ?: Long.MAX_VALUE + } + + @Throws(IOException::class) + override fun writeTo(sink: BufferedSink) { + data.open(context)?.source()?.use { source -> sink.writeAll(source) } + } + } + return try { + client() // + .createFile(file.parent.path, file.name, pCloudDataSource, Date(), listener, uploadOptions) // + .execute() + } catch (ex: ApiError) { + handleApiError(ex, file.name) + throw FatalBackendException(ex) + } + } + + @Throws(IOException::class, BackendException::class) + fun read(file: PCloudFile, encryptedTmpFile: File?, data: OutputStream, progressAware: ProgressAware) { + progressAware.onProgress(Progress.started(DownloadState.download(file))) + var cacheKey: String? = null + var cacheFile: File? = null + val remoteFile: RemoteFile + if (sharedPreferencesHandler.useLruCache() && createLruCache(sharedPreferencesHandler.lruCacheSize())) { + try { + remoteFile = client().loadFile(file.path).execute().asFile() + cacheKey = remoteFile.fileId().toString() + remoteFile.hash() + } catch (ex: ApiError) { + handleApiError(ex, file.name) + } + cacheFile = diskLruCache?.let { it[cacheKey] } + } + if (sharedPreferencesHandler.useLruCache() && cacheFile != null) { + try { + retrieveFromLruCache(cacheFile, data) + } catch (e: IOException) { + 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))) + } + + @Throws(IOException::class, BackendException::class) + private fun writeToData( + file: PCloudFile, // + data: OutputStream, // + encryptedTmpFile: File?, // + cacheKey: String?, // + progressAware: ProgressAware + ) { + try { + val fileLink = client().createFileLink(file.path, DownloadOptions.DEFAULT).execute() + val listener = ProgressListener { done: Long, _: Long -> + progressAware.onProgress( // + Progress.progress(DownloadState.download(file)) // + .between(0) // + .and(file.size ?: Long.MAX_VALUE) // + .withValue(done) + ) + } + val sink: DataSink = object : DataSink() { + override fun readAll(source: BufferedSource) { + CopyStream.copyStreamToStream(source.inputStream(), data) + } + } + client().download(fileLink, sink, listener).execute() + } catch (ex: ApiError) { + handleApiError(ex, file.name) + } + if (sharedPreferencesHandler.useLruCache() && encryptedTmpFile != null && cacheKey != null) { + try { + diskLruCache?.let { + LruFileCacheUtil.storeToLruCache(it, cacheKey, encryptedTmpFile) + } ?: Timber.tag("PCloudImpl").e("Failed to store item in LRU cache") + } catch (e: IOException) { + Timber.tag("PCloudImpl").e(e, "Failed to write downloaded file in LRU cache") + } + } + } + + @Throws(IOException::class, BackendException::class) + fun delete(node: PCloudNode) { + try { + if (node is PCloudFolder) { + client().deleteFolder(node.path, true).execute() + } else { + client().deleteFile(node.path).execute() + } + } catch (ex: ApiError) { + handleApiError(ex, node.name) + } + } + + @Throws(IOException::class, BackendException::class) + fun currentAccount(): String { + return try { + client() // + .userInfo // + .execute() // + .email() + } catch (ex: ApiError) { + handleApiError(ex) + throw FatalBackendException(ex) + } + } + + private fun createLruCache(cacheSize: Int): Boolean { + if (diskLruCache == null) { + diskLruCache = try { + DiskLruCache.create(LruFileCacheUtil(context).resolve(LruFileCacheUtil.Cache.PCLOUD), cacheSize.toLong()) + } catch (e: IOException) { + Timber.tag("PCloudImpl").e(e, "Failed to setup LRU cache") + return false + } + } + return true + } + + @Throws(BackendException::class) + private fun handleApiError(ex: ApiError, name: String) { + handleApiError(ex, null, name) + } + + @Throws(BackendException::class) + private fun handleApiError(ex: ApiError, errorCodes: Set? = null, name: String? = null) { + if (errorCodes == null || !errorCodes.contains(ex.errorCode())) { + val errorCode = ex.errorCode() + when { + isCloudNodeAlreadyExistsException(errorCode) -> { + throw CloudNodeAlreadyExistsException(name) + } + isForbiddenException(errorCode) -> { + throw ForbiddenException() + } + isNetworkConnectionException(errorCode) -> { + throw NetworkConnectionException(ex) + } + isNoSuchCloudFileException(errorCode) -> { + throw NoSuchCloudFileException(name) + } + isWrongCredentialsException(errorCode) -> { + throw WrongCredentialsException(cloud) + } + isUnauthorizedException(errorCode) -> { + throw UnauthorizedException() + } + else -> { + throw FatalBackendException(ex) + } + } + } + } + + init { + if (cloud.accessToken() == null) { + throw NoAuthenticationProvidedException(cloud) + } + this.context = context + this.cloud = cloud + this.root = RootPCloudFolder(cloud) + sharedPreferencesHandler = SharedPreferencesHandler(context) + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudNode.java b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudNode.java deleted file mode 100644 index e460ae2c..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudNode.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.cryptomator.data.cloud.pcloud; - -import org.cryptomator.domain.CloudNode; - -interface PCloudNode extends CloudNode { - - @Override - PCloudFolder getParent(); - -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudNode.kt b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudNode.kt new file mode 100644 index 00000000..ec0b2127 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudNode.kt @@ -0,0 +1,8 @@ +package org.cryptomator.data.cloud.pcloud + +import org.cryptomator.domain.CloudNode + +interface PCloudNode : CloudNode { + + override val parent: PCloudFolder? +} 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 deleted file mode 100644 index 2b91ca70..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudNodeFactory.java +++ /dev/null @@ -1,47 +0,0 @@ -package org.cryptomator.data.cloud.pcloud; - -import com.pcloud.sdk.RemoteEntry; -import com.pcloud.sdk.RemoteFile; -import com.pcloud.sdk.RemoteFolder; - -import org.cryptomator.util.Optional; - -class PCloudNodeFactory { - - public static PCloudFile file(PCloudFolder parent, RemoteFile file) { - return new PCloudFile(parent, file.name(), getNodePath(parent, file.name()), Optional.ofNullable(file.size()), Optional.ofNullable(file.lastModified())); - } - - public static PCloudFile file(PCloudFolder parent, String name, Optional size) { - return new PCloudFile(parent, name, getNodePath(parent, name), 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) { - return new PCloudFolder(parent, folder.name(), getNodePath(parent, folder.name())); - } - - public static PCloudFolder folder(PCloudFolder parent, String name) { - return new PCloudFolder(parent, name, getNodePath(parent, name)); - } - - public static PCloudFolder folder(PCloudFolder parent, String name, String path) { - return new PCloudFolder(parent, name, path); - } - - public static String getNodePath(PCloudFolder parent, String name) { - return parent.getPath() + "/" + name; - } - - public static PCloudNode from(PCloudFolder 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/pcloud/PCloudNodeFactory.kt b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudNodeFactory.kt new file mode 100644 index 00000000..4a718ba6 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudNodeFactory.kt @@ -0,0 +1,47 @@ +package org.cryptomator.data.cloud.pcloud + +import com.pcloud.sdk.RemoteEntry +import com.pcloud.sdk.RemoteFile +import com.pcloud.sdk.RemoteFolder + +internal object PCloudNodeFactory { + + fun file(parent: PCloudFolder, file: RemoteFile): PCloudFile { + return PCloudFile(parent, file.name(), getNodePath(parent, file.name()), file.size(), file.lastModified()) + } + + fun file(parent: PCloudFolder, name: String, size: Long?): PCloudFile { + return PCloudFile(parent, name, getNodePath(parent, name), size, null) + } + + @JvmStatic + fun file(parent: PCloudFolder, name: String, size: Long?, path: String): PCloudFile { + return PCloudFile(parent, name, path, size, null) + } + + fun folder(parent: PCloudFolder, folder: RemoteFolder): PCloudFolder { + return PCloudFolder(parent, folder.name(), getNodePath(parent, folder.name())) + } + + fun folder(parent: PCloudFolder, name: String): PCloudFolder { + return PCloudFolder(parent, name, getNodePath(parent, name)) + } + + @JvmStatic + fun folder(parent: PCloudFolder?, name: String, path: String?): PCloudFolder { + return PCloudFolder(parent, name, path!!) + } + + fun getNodePath(parent: PCloudFolder, name: String): String { + return parent.path + "/" + name + } + + @JvmStatic + fun from(parent: PCloudFolder, remoteEntry: RemoteEntry): PCloudNode { + return if (remoteEntry is RemoteFile) { + file(parent, remoteEntry.asFile()) + } else { + folder(parent, remoteEntry.asFolder()) + } + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/pcloud/RootPCloudFolder.java b/data/src/main/java/org/cryptomator/data/cloud/pcloud/RootPCloudFolder.java deleted file mode 100644 index fd819a92..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/pcloud/RootPCloudFolder.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.cryptomator.data.cloud.pcloud; - -import org.cryptomator.domain.Cloud; -import org.cryptomator.domain.PCloud; - -class RootPCloudFolder extends PCloudFolder { - - private final PCloud cloud; - - public RootPCloudFolder(PCloud cloud) { - super(null, "", ""); - this.cloud = cloud; - } - - @Override - public PCloud getCloud() { - return cloud; - } - - @Override - public PCloudFolder withCloud(Cloud cloud) { - return new RootPCloudFolder((PCloud) cloud); - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/pcloud/RootPCloudFolder.kt b/data/src/main/java/org/cryptomator/data/cloud/pcloud/RootPCloudFolder.kt new file mode 100644 index 00000000..190a8566 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/pcloud/RootPCloudFolder.kt @@ -0,0 +1,11 @@ +package org.cryptomator.data.cloud.pcloud + +import org.cryptomator.domain.Cloud +import org.cryptomator.domain.PCloud + +internal class RootPCloudFolder(override val cloud: PCloud) : PCloudFolder(null, "", "") { + + override fun withCloud(cloud: Cloud?): PCloudFolder { + return RootPCloudFolder(cloud as PCloud) + } +} 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 deleted file mode 100644 index 25116de8..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/s3/RootS3Folder.java +++ /dev/null @@ -1,29 +0,0 @@ -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 String getKey() { - return ""; - } - - @Override - public S3Folder withCloud(Cloud cloud) { - return new RootS3Folder((S3Cloud) cloud); - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/s3/RootS3Folder.kt b/data/src/main/java/org/cryptomator/data/cloud/s3/RootS3Folder.kt new file mode 100644 index 00000000..a0cae516 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/s3/RootS3Folder.kt @@ -0,0 +1,14 @@ +package org.cryptomator.data.cloud.s3 + +import org.cryptomator.domain.Cloud +import org.cryptomator.domain.S3Cloud + +internal class RootS3Folder(override val cloud: S3Cloud) : S3Folder(null, "", "") { + + override val key: String + get() = "" + + override fun withCloud(cloud: Cloud?): S3Folder { + return RootS3Folder(cloud as S3Cloud) + } +} 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 deleted file mode 100644 index 532e2812..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/s3/S3ClientFactory.java +++ /dev/null @@ -1,98 +0,0 @@ -package org.cryptomator.data.cloud.s3; - -import android.content.Context; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; - -import org.cryptomator.data.cloud.okhttplogging.HttpLoggingInterceptor; -import org.cryptomator.domain.S3Cloud; -import org.cryptomator.util.SharedPreferencesHandler; -import org.cryptomator.util.crypto.CredentialCryptor; -import org.cryptomator.util.file.LruFileCacheUtil; - -import java.util.concurrent.TimeUnit; - -import io.minio.MinioClient; -import okhttp3.Cache; -import okhttp3.CacheControl; -import okhttp3.Interceptor; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import timber.log.Timber; - -import static org.cryptomator.data.util.NetworkTimeout.CONNECTION; -import static org.cryptomator.data.util.NetworkTimeout.READ; -import static org.cryptomator.data.util.NetworkTimeout.WRITE; -import static org.cryptomator.util.file.LruFileCacheUtil.Cache.S3; - -class S3ClientFactory { - - private MinioClient apiClient; - - private static Interceptor httpLoggingInterceptor(Context context) { - return new HttpLoggingInterceptor(message -> Timber.tag("OkHttp").d(message), context); - } - - private static Interceptor provideOfflineCacheInterceptor(final Context context) { - return chain -> { - Request request = chain.request(); - - if (isNetworkAvailable(context)) { - final CacheControl cacheControl = new CacheControl.Builder() // - .maxAge(0, TimeUnit.DAYS) // - .build(); - - request = request.newBuilder() // - .cacheControl(cacheControl) // - .build(); - } - - return chain.proceed(request); - }; - } - - private static boolean isNetworkAvailable(final Context context) { - ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo(); - return activeNetworkInfo != null && activeNetworkInfo.isConnected(); - } - - public MinioClient getClient(S3Cloud cloud, Context context) { - if (apiClient == null) { - apiClient = createApiClient(cloud, context); - } - return apiClient; - } - - private MinioClient createApiClient(S3Cloud cloud, Context context) { - final SharedPreferencesHandler sharedPreferencesHandler = new SharedPreferencesHandler(context); - - MinioClient.Builder minioClientBuilder = MinioClient.builder(); - - minioClientBuilder.endpoint(cloud.s3Endpoint()); - minioClientBuilder.region(cloud.s3Region()); - - OkHttpClient.Builder httpClientBuilder = new OkHttpClient() // - .newBuilder() // - .connectTimeout(CONNECTION.getTimeout(), CONNECTION.getUnit()) // - .readTimeout(READ.getTimeout(), READ.getUnit()) // - .writeTimeout(WRITE.getTimeout(), WRITE.getUnit()) // - .addInterceptor(httpLoggingInterceptor(context)); - - if (sharedPreferencesHandler.useLruCache()) { - final Cache cache = new Cache(new LruFileCacheUtil(context).resolve(S3), sharedPreferencesHandler.lruCacheSize()); - httpClientBuilder.cache(cache).addInterceptor(provideOfflineCacheInterceptor(context)); - } - - return minioClientBuilder // - .credentials(decrypt(cloud.accessKey(), context), decrypt(cloud.secretKey(), context)) // - .httpClient(httpClientBuilder.build()) // - .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/S3ClientFactory.kt b/data/src/main/java/org/cryptomator/data/cloud/s3/S3ClientFactory.kt new file mode 100644 index 00000000..501fc6ea --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/s3/S3ClientFactory.kt @@ -0,0 +1,89 @@ +package org.cryptomator.data.cloud.s3 + +import android.content.Context +import android.net.ConnectivityManager +import org.cryptomator.data.cloud.okhttplogging.HttpLoggingInterceptor +import org.cryptomator.data.util.NetworkTimeout +import org.cryptomator.domain.S3Cloud +import org.cryptomator.util.SharedPreferencesHandler +import org.cryptomator.util.crypto.CredentialCryptor +import org.cryptomator.util.file.LruFileCacheUtil +import java.util.concurrent.TimeUnit +import io.minio.MinioClient +import okhttp3.Cache +import okhttp3.CacheControl +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import timber.log.Timber + +class S3ClientFactory private constructor() { + + companion object { + + @Volatile + private var instance: MinioClient? = null + + @Synchronized + fun getInstance(context: Context, cloud: S3Cloud): MinioClient = instance ?: createClient(context, cloud).also { instance = it } + + private fun createClient(context: Context, cloud: S3Cloud): MinioClient { + val sharedPreferencesHandler = SharedPreferencesHandler(context) + val minioClientBuilder = MinioClient.builder() + + minioClientBuilder.endpoint(cloud.s3Endpoint()) + minioClientBuilder.region(cloud.s3Region()) + + val httpClientBuilder = OkHttpClient() // + .newBuilder() // + .connectTimeout(NetworkTimeout.CONNECTION.timeout, NetworkTimeout.CONNECTION.unit) // + .readTimeout(NetworkTimeout.READ.timeout, NetworkTimeout.READ.unit) // + .writeTimeout(NetworkTimeout.WRITE.timeout, NetworkTimeout.WRITE.unit) // + .addInterceptor(httpLoggingInterceptor(context)) + + if (sharedPreferencesHandler.useLruCache()) { + val cache = Cache(LruFileCacheUtil(context).resolve(LruFileCacheUtil.Cache.S3), sharedPreferencesHandler.lruCacheSize().toLong()) + httpClientBuilder.cache(cache).addInterceptor(provideOfflineCacheInterceptor(context)) + } + + return minioClientBuilder // + .credentials(decrypt(cloud.accessKey(), context), decrypt(cloud.secretKey(), context)) // + .httpClient(httpClientBuilder.build()) // + .build() + } + + private fun decrypt(password: String, context: Context): String { + return CredentialCryptor.getInstance(context).decrypt(password) + } + + private fun httpLoggingInterceptor(context: Context): Interceptor { + val logger = object : HttpLoggingInterceptor.Logger { + override fun log(message: String) { + Timber.tag("OkHttp").d(message) + } + } + + return HttpLoggingInterceptor(logger, context) + } + + private fun provideOfflineCacheInterceptor(context: Context): Interceptor { + return Interceptor { chain: Interceptor.Chain -> + var request = chain.request() + if (isNetworkAvailable(context)) { + val cacheControl = CacheControl.Builder() // + .maxAge(0, TimeUnit.DAYS) // + .build() + request = request.newBuilder() // + .cacheControl(cacheControl) // + .build() + } + chain.proceed(request) + } + } + + private fun isNetworkAvailable(context: Context): Boolean { + val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val activeNetworkInfo = connectivityManager.activeNetworkInfo + return activeNetworkInfo != null && activeNetworkInfo.isConnected + } + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudApiErrorCodes.java b/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudApiErrorCodes.java deleted file mode 100644 index 9292d71f..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudApiErrorCodes.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.cryptomator.data.cloud.s3; - -public enum S3CloudApiErrorCodes { - ACCESS_DENIED("AccessDenied"), ACCOUNT_PROBLEM("AccountProblem"), INTERNAL_ERROR("InternalError"), INVALID_ACCESS_KEY_ID("InvalidAccessKeyId"), INVALID_BUCKET_NAME("InvalidBucketName"), INVALID_OBJECT_STATE("InvalidObjectState"), NO_SUCH_BUCKET("NoSuchBucket"), NO_SUCH_KEY("NoSuchKey"); - - private final String value; - - S3CloudApiErrorCodes(final String newValue) { - value = newValue; - } - - public String getValue() { - return value; - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudApiErrorCodes.kt b/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudApiErrorCodes.kt new file mode 100644 index 00000000..c88e8f2c --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudApiErrorCodes.kt @@ -0,0 +1,12 @@ +package org.cryptomator.data.cloud.s3 + +enum class S3CloudApiErrorCodes(val value: String) { + ACCESS_DENIED("AccessDenied"), // + ACCOUNT_PROBLEM("AccountProblem"), // + INTERNAL_ERROR("InternalError"), // + INVALID_ACCESS_KEY_ID("InvalidAccessKeyId"), // + INVALID_BUCKET_NAME("InvalidBucketName"), // + INVALID_OBJECT_STATE("InvalidObjectState"), // + NO_SUCH_BUCKET("NoSuchBucket"), // + NO_SUCH_KEY("NoSuchKey") +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudApiExceptions.java b/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudApiExceptions.java deleted file mode 100644 index 2a557e7e..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudApiExceptions.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.cryptomator.data.cloud.s3; - -public class S3CloudApiExceptions { - - public static boolean isAccessProblem(String errorCode) { - return errorCode.equals(S3CloudApiErrorCodes.ACCESS_DENIED.getValue()) || errorCode.equals(S3CloudApiErrorCodes.ACCOUNT_PROBLEM.getValue()) || errorCode.equals(S3CloudApiErrorCodes.INVALID_ACCESS_KEY_ID.getValue()); - } - - public static boolean isNoSuchBucketException(String errorCode) { - return errorCode.equals(S3CloudApiErrorCodes.NO_SUCH_BUCKET.getValue()); - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudApiExceptions.kt b/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudApiExceptions.kt new file mode 100644 index 00000000..8e4c95e5 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudApiExceptions.kt @@ -0,0 +1,16 @@ +package org.cryptomator.data.cloud.s3 + +object S3CloudApiExceptions { + + @JvmStatic + fun isAccessProblem(errorCode: String): Boolean { + return errorCode == S3CloudApiErrorCodes.ACCESS_DENIED.value // + || errorCode == S3CloudApiErrorCodes.ACCOUNT_PROBLEM.value // + || errorCode == S3CloudApiErrorCodes.INVALID_ACCESS_KEY_ID.value + } + + @JvmStatic + fun isNoSuchBucketException(errorCode: String): Boolean { + return errorCode == S3CloudApiErrorCodes.NO_SUCH_BUCKET.value + } +} 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 deleted file mode 100644 index 9565975e..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudContentRepository.java +++ /dev/null @@ -1,190 +0,0 @@ -package org.cryptomator.data.cloud.s3; - -import android.content.Context; - -import org.cryptomator.data.cloud.InterceptingCloudContentRepository; -import org.cryptomator.domain.S3Cloud; -import org.cryptomator.domain.exception.BackendException; -import org.cryptomator.domain.exception.FatalBackendException; -import org.cryptomator.domain.exception.ForbiddenException; -import org.cryptomator.domain.exception.NetworkConnectionException; -import org.cryptomator.domain.exception.NoSuchBucketException; -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 io.minio.errors.ErrorResponseException; - -import static org.cryptomator.util.ExceptionUtil.contains; - -class S3CloudContentRepository extends InterceptingCloudContentRepository { - - private final S3Cloud cloud; - - public S3CloudContentRepository(S3Cloud cloud, Context context) { - super(new Intercepted(cloud, context)); - this.cloud = cloud; - } - - @Override - protected void throwWrappedIfRequired(Exception e) throws BackendException { - throwNoSuchBucketExceptionIfRequired(e); - throwConnectionErrorIfRequired(e); - throwWrongCredentialsExceptionIfRequired(e); - } - - private void throwNoSuchBucketExceptionIfRequired(Exception e) throws NoSuchBucketException { - if (e instanceof ErrorResponseException) { - String errorCode = ((ErrorResponseException) e).errorResponse().code(); - if (S3CloudApiExceptions.isNoSuchBucketException(errorCode)) { - throw new NoSuchBucketException(cloud.s3Bucket()); - } - } - } - - private void throwConnectionErrorIfRequired(Exception e) throws NetworkConnectionException { - if (contains(e, IOException.class)) { - throw new NetworkConnectionException(e); - } - } - - private void throwWrongCredentialsExceptionIfRequired(Exception e) { - if (e instanceof ErrorResponseException) { - String errorCode = ((ErrorResponseException) e).errorResponse().code(); - if (S3CloudApiExceptions.isAccessProblem(errorCode)) { - throw new WrongCredentialsException(cloud); - } - } else if (e instanceof ForbiddenException) { - throw new WrongCredentialsException(cloud); - } - } - - private static class Intercepted implements CloudContentRepository { - - private final S3Impl cloud; - - public Intercepted(S3Cloud cloud, Context context) { - this.cloud = new S3Impl(context, cloud); - } - - public S3Folder root(S3Cloud cloud) { - return this.cloud.root(); - } - - @Override - public S3Folder resolve(S3Cloud cloud, String path) throws BackendException { - return this.cloud.resolve(path); - } - - @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 { - return cloud.folder(parent, name); - } - - @Override - public boolean exists(S3Node node) throws BackendException { - return cloud.exists(node); - } - - @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, 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(S3Cloud cloud) throws BackendException { - return this.cloud.checkAuthentication(); - } - - @Override - public void logout(S3Cloud cloud) throws BackendException { - // empty - } - } - -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudContentRepository.kt b/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudContentRepository.kt new file mode 100644 index 00000000..ce9e038f --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudContentRepository.kt @@ -0,0 +1,176 @@ +package org.cryptomator.data.cloud.s3 + +import android.content.Context +import org.cryptomator.data.cloud.InterceptingCloudContentRepository +import org.cryptomator.data.cloud.s3.S3CloudApiExceptions.isAccessProblem +import org.cryptomator.data.cloud.s3.S3CloudApiExceptions.isNoSuchBucketException +import org.cryptomator.domain.S3Cloud +import org.cryptomator.domain.exception.BackendException +import org.cryptomator.domain.exception.FatalBackendException +import org.cryptomator.domain.exception.ForbiddenException +import org.cryptomator.domain.exception.NetworkConnectionException +import org.cryptomator.domain.exception.NoSuchBucketException +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.ExceptionUtil +import java.io.File +import java.io.IOException +import java.io.OutputStream +import io.minio.errors.ErrorResponseException + +internal class S3CloudContentRepository(private val cloud: S3Cloud, context: Context) : InterceptingCloudContentRepository(Intercepted(cloud, context)) { + + @Throws(BackendException::class) + override fun throwWrappedIfRequired(e: Exception) { + throwNoSuchBucketExceptionIfRequired(e) + throwConnectionErrorIfRequired(e) + throwWrongCredentialsExceptionIfRequired(e) + } + + @Throws(NoSuchBucketException::class) + private fun throwNoSuchBucketExceptionIfRequired(e: Exception) { + if (e is ErrorResponseException) { + val errorCode = e.errorResponse().code() + if (isNoSuchBucketException(errorCode)) { + throw NoSuchBucketException(cloud.s3Bucket()) + } + } + } + + @Throws(NetworkConnectionException::class) + private fun throwConnectionErrorIfRequired(e: Exception) { + if (ExceptionUtil.contains(e, IOException::class.java)) { + throw NetworkConnectionException(e) + } + } + + private fun throwWrongCredentialsExceptionIfRequired(e: Exception) { + if (e is ErrorResponseException) { + val errorCode = e.errorResponse().code() + if (isAccessProblem(errorCode)) { + throw WrongCredentialsException(cloud) + } + } else if (e is ForbiddenException) { + throw WrongCredentialsException(cloud) + } + } + + private class Intercepted(cloud: S3Cloud, context: Context) : CloudContentRepository { + + private val cloud: S3Impl = S3Impl(context, cloud) + + override fun root(cloud: S3Cloud): S3Folder { + return this.cloud.root() + } + + override fun resolve(cloud: S3Cloud, path: String): S3Folder { + return this.cloud.resolve(path) + } + + @Throws(BackendException::class) + override fun file(parent: S3Folder, name: String): S3File { + return try { + cloud.file(parent, name, null) + } catch (ex: IOException) { + throw FatalBackendException(ex) + } + } + + @Throws(BackendException::class) + override fun file(parent: S3Folder, name: String, size: Long?): S3File { + return try { + cloud.file(parent, name, size) + } catch (ex: IOException) { + throw FatalBackendException(ex) + } + } + + @Throws(BackendException::class) + override fun folder(parent: S3Folder, name: String): S3Folder { + return cloud.folder(parent, name) + } + + @Throws(BackendException::class) + override fun exists(node: S3Node): Boolean { + return cloud.exists(node) + } + + @Throws(BackendException::class) + override fun list(folder: S3Folder): List { + return try { + cloud.list(folder) + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun create(folder: S3Folder): S3Folder { + return try { + cloud.create(folder) + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun move(source: S3Folder, target: S3Folder): S3Folder { + return try { + cloud.move(source, target) as S3Folder + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun move(source: S3File, target: S3File): S3File { + return try { + cloud.move(source, target) as S3File + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun write(file: S3File, data: DataSource, progressAware: ProgressAware, replace: Boolean, size: Long): S3File { + return try { + cloud.write(file, data, progressAware, replace, size) + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun read(file: S3File, encryptedTmpFile: File?, data: OutputStream, progressAware: ProgressAware) { + try { + cloud.read(file, data, progressAware) + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun delete(node: S3Node) { + try { + cloud.delete(node) + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun checkAuthenticationAndRetrieveCurrentAccount(cloud: S3Cloud): String { + return this.cloud.checkAuthentication() + } + + @Throws(BackendException::class) + override fun logout(cloud: S3Cloud) { + // 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 index da7177e4..4386b437 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 @@ -28,7 +28,7 @@ public class S3CloudContentRepositoryFactory implements CloudContentRepositoryFa } @Override - public CloudContentRepository cloudContentRepositoryFor(Cloud cloud) { + public CloudContentRepository cloudContentRepositoryFor(Cloud cloud) { return new S3CloudContentRepository((S3Cloud) 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 deleted file mode 100644 index 12d8f712..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudNodeFactory.java +++ /dev/null @@ -1,47 +0,0 @@ -package org.cryptomator.data.cloud.s3; - -import org.cryptomator.util.Optional; - -import java.util.Date; - -class S3CloudNodeFactory { - - private static final String DELIMITER = "/"; - - public static S3File file(S3Folder parent, String name) { - return new S3File(parent, name, getNodePath(parent, name), Optional.empty(), Optional.empty()); - } - - 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 parent, String name, Optional size, String path) { - 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, 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); - } - - private static String getNodePath(S3Folder parent, String name) { - return parent.getPath() + "/" + name; - } - - public static String getNameFromKey(String key) { - String name = key; - if (key.endsWith(DELIMITER)) { - name = key.substring(0, key.length() - 1); - } - return name.contains(DELIMITER) ? name.substring(name.lastIndexOf(DELIMITER) + 1) : name; - } - -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudNodeFactory.kt b/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudNodeFactory.kt new file mode 100644 index 00000000..1c6432b6 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudNodeFactory.kt @@ -0,0 +1,44 @@ +package org.cryptomator.data.cloud.s3 + +import java.util.Date + +internal object S3CloudNodeFactory { + + private const val DELIMITER = "/" + + fun file(parent: S3Folder, name: String): S3File { + return S3File(parent, name, getNodePath(parent, name), null, null) + } + + fun file(parent: S3Folder, name: String, size: Long?): S3File { + return S3File(parent, name, getNodePath(parent, name), size, null) + } + + fun file(parent: S3Folder, name: String, size: Long?, path: String): S3File { + return S3File(parent, name, path, size, null) + } + + fun file(parent: S3Folder, name: String, size: Long?, lastModified: Date?): S3File { + return S3File(parent, name, getNodePath(parent, name), size, lastModified) + } + + fun folder(parent: S3Folder, name: String): S3Folder { + return S3Folder(parent, name, getNodePath(parent, name)) + } + + fun folder(parent: S3Folder?, name: String, path: String): S3Folder { + return S3Folder(parent, name, path) + } + + private fun getNodePath(parent: S3Folder, name: String): String { + return parent.path + "/" + name + } + + fun getNameFromKey(key: String): String { + var name = key + if (key.endsWith(DELIMITER)) { + name = key.substring(0, key.length - 1) + } + return if (name.contains(DELIMITER)) name.substring(name.lastIndexOf(DELIMITER) + 1) else name + } +} 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 deleted file mode 100644 index 99b45ea9..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/s3/S3File.java +++ /dev/null @@ -1,65 +0,0 @@ -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 static final String DELIMITER = "/"; - - 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 String getKey() { - if (path.startsWith(DELIMITER)) { - return path.substring(DELIMITER.length()); - } - 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/S3File.kt b/data/src/main/java/org/cryptomator/data/cloud/s3/S3File.kt new file mode 100644 index 00000000..6cf3f6fe --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/s3/S3File.kt @@ -0,0 +1,22 @@ +package org.cryptomator.data.cloud.s3 + +import org.cryptomator.domain.Cloud +import org.cryptomator.domain.CloudFile +import java.util.Date + +internal class S3File(override val parent: S3Folder, override val name: String, override val path: String, override val size: Long?, override val modified: Date?) : CloudFile, S3Node { + + override val cloud: Cloud? + get() = parent.cloud + + override val key: String + get() = if (path.startsWith(DELIMITER)) { + path.substring(DELIMITER.length) + } else path + + companion object { + + private const val DELIMITER = "/" + + } +} 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 deleted file mode 100644 index bdfd8fc8..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/s3/S3Folder.java +++ /dev/null @@ -1,52 +0,0 @@ -package org.cryptomator.data.cloud.s3; - -import org.cryptomator.domain.Cloud; -import org.cryptomator.domain.CloudFolder; - -class S3Folder implements CloudFolder, S3Node { - - private static final String DELIMITER = "/"; - - 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 String getKey() { - if (path.startsWith(DELIMITER)) { - return path.substring(DELIMITER.length()) + DELIMITER; - } - return path + DELIMITER; - } - - @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/S3Folder.kt b/data/src/main/java/org/cryptomator/data/cloud/s3/S3Folder.kt new file mode 100644 index 00000000..9a787546 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/s3/S3Folder.kt @@ -0,0 +1,23 @@ +package org.cryptomator.data.cloud.s3 + +import org.cryptomator.domain.Cloud +import org.cryptomator.domain.CloudFolder + +internal open class S3Folder(override val parent: S3Folder?, override val name: String, override val path: String) : CloudFolder, S3Node { + + override val cloud: Cloud? + get() = parent?.cloud + + override val key: String + get() = if (path.startsWith(DELIMITER)) { + path.substring(DELIMITER.length) + DELIMITER + } else path + DELIMITER + + override fun withCloud(cloud: Cloud?): S3Folder? { + return S3Folder(parent?.withCloud(cloud), name, path) + } + + companion object { + private const val DELIMITER = "/" + } +} 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 deleted file mode 100644 index c9e91572..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java +++ /dev/null @@ -1,404 +0,0 @@ -package org.cryptomator.data.cloud.s3; - -import android.content.Context; - -import org.cryptomator.data.util.CopyStream; -import org.cryptomator.data.util.TransferredBytesAwareInputStream; -import org.cryptomator.data.util.TransferredBytesAwareOutputStream; -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.NoSuchBucketException; -import org.cryptomator.domain.exception.NoSuchCloudFileException; -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 java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Date; -import java.util.LinkedList; -import java.util.List; - -import io.minio.BucketExistsArgs; -import io.minio.CopyObjectArgs; -import io.minio.CopySource; -import io.minio.GetObjectArgs; -import io.minio.GetObjectResponse; -import io.minio.ListObjectsArgs; -import io.minio.MinioClient; -import io.minio.ObjectWriteResponse; -import io.minio.PutObjectArgs; -import io.minio.RemoveObjectArgs; -import io.minio.RemoveObjectsArgs; -import io.minio.Result; -import io.minio.StatObjectArgs; -import io.minio.StatObjectResponse; -import io.minio.errors.ErrorResponseException; -import io.minio.messages.DeleteError; -import io.minio.messages.DeleteObject; -import io.minio.messages.Item; -import timber.log.Timber; - -import static org.cryptomator.domain.usecases.cloud.Progress.progress; - -class S3Impl { - - private static final String DELIMITER = "/"; - - private final S3ClientFactory clientFactory = new S3ClientFactory(); - private final S3Cloud cloud; - private final RootS3Folder root; - private final Context context; - - S3Impl(Context context, S3Cloud cloud) { - if (cloud.accessKey() == null || cloud.secretKey() == null) { - throw new WrongCredentialsException(cloud); - } - - this.context = context; - this.cloud = cloud; - this.root = new RootS3Folder(cloud); - } - - private MinioClient client() { - return clientFactory.getClient(cloud, context); - } - - public S3Folder root() { - return root; - } - - public S3Folder resolve(String path) { - if (path.startsWith(DELIMITER)) { - path = path.substring(1); - } - String[] names = path.split(DELIMITER); - S3Folder folder = root; - for (String name : names) { - if (!name.isEmpty()) { - 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.getKey() + name); - } - - public S3Folder folder(S3Folder parent, String name) { - return S3CloudNodeFactory.folder(parent, name, parent.getKey() + name); - } - - public boolean exists(S3Node node) throws BackendException { - String key = node.getKey(); - try { - if (!(node instanceof RootS3Folder)) { - client().statObject(StatObjectArgs.builder().bucket(cloud.s3Bucket()).object(key).build()); - return true; - } else { - // stat requests throws an IllegalStateException if key is empty string - ListObjectsArgs request = ListObjectsArgs.builder().bucket(cloud.s3Bucket()).prefix(key).delimiter(DELIMITER).build(); - return client().listObjects(request).iterator().hasNext(); - } - } catch (ErrorResponseException e) { - if (S3CloudApiErrorCodes.NO_SUCH_KEY.getValue().equals(e.errorResponse().code())) { - return false; - } - throw new FatalBackendException(e); - } catch (Exception ex) { - handleApiError(ex, node.getPath()); - } - - throw new FatalBackendException(new IllegalStateException("Exception thrown but not handled?")); - } - - public List list(S3Folder folder) throws IOException, BackendException { - List result = new ArrayList<>(); - - ListObjectsArgs request = ListObjectsArgs.builder().bucket(cloud.s3Bucket()).prefix(folder.getKey()).delimiter(DELIMITER).build(); - Iterable> listObjects = client().listObjects(request); - for (Result object : listObjects) { - try { - Item item = object.get(); - if (item.isDir()) { - result.add(S3CloudNodeFactory.folder(folder, S3CloudNodeFactory.getNameFromKey(item.objectName()))); - } else { - S3File file = S3CloudNodeFactory.file(folder, S3CloudNodeFactory.getNameFromKey(item.objectName()), Optional.of(item.size()), Optional.of(Date.from(item.lastModified().toInstant()))); - result.add(file); - } - } catch (Exception ex) { - handleApiError(ex, folder.getPath()); - } - } - - return result; - } - - public S3Folder create(S3Folder folder) throws IOException, BackendException { - if (!exists(folder.getParent())) { - folder = new S3Folder( // - create(folder.getParent()), // - folder.getName(), folder.getPath() // - ); - } - - try { - PutObjectArgs putObjectArgs = PutObjectArgs // - .builder() // - .bucket(cloud.s3Bucket()) // - .object(folder.getKey()) // - .stream(new ByteArrayInputStream(new byte[0]), 0, -1) // - .build(); - - client().putObject(putObjectArgs); - } catch (Exception ex) { - handleApiError(ex, folder.getPath()); - } - - return S3CloudNodeFactory.folder(folder.getParent(), folder.getName()); - } - - public S3Node move(S3Node source, S3Node target) throws IOException, BackendException { - if (exists(target)) { - throw new CloudNodeAlreadyExistsException(target.getName()); - } - - if (source instanceof S3Folder) { - List nodes = list((S3Folder) source); - - List objectsToDelete = new LinkedList<>(); - - for (S3Node node : nodes) { - objectsToDelete.add(new DeleteObject(node.getKey())); - - String targetKey; - if (node instanceof S3Folder) { - targetKey = S3CloudNodeFactory.folder((S3Folder) target, node.getName()).getKey(); - } else { - targetKey = S3CloudNodeFactory.file((S3Folder) target, node.getName()).getKey(); - } - - CopySource copySource = CopySource.builder().bucket(cloud.s3Bucket()).object(node.getKey()).build(); - - CopyObjectArgs copyObjectArgs = CopyObjectArgs.builder().bucket(cloud.s3Bucket()).object(targetKey).source(copySource).build(); - try { - client().copyObject(copyObjectArgs); - } catch (Exception ex) { - handleApiError(ex, source.getPath()); - } - } - - RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder().bucket(cloud.s3Bucket()).objects(objectsToDelete).build(); - - for (Result result : client().removeObjects(removeObjectsArgs)) { - try { - result.get(); - } catch (Exception ex) { - handleApiError(ex, source.getPath()); - } - } - - return S3CloudNodeFactory.folder(target.getParent(), target.getName()); - } else { - CopySource copySource = CopySource.builder().bucket(cloud.s3Bucket()).object(source.getKey()).build(); - CopyObjectArgs copyObjectArgs = CopyObjectArgs.builder().bucket(cloud.s3Bucket()).object(target.getKey()).source(copySource).build(); - try { - ObjectWriteResponse result = client().copyObject(copyObjectArgs); - - delete(source); - - Date lastModified = result.headers().getDate("Last-Modified"); - - return S3CloudNodeFactory.file(target.getParent(), target.getName(), ((S3File) source).getSize(), Optional.ofNullable(lastModified)); - } catch (Exception ex) { - handleApiError(ex, source.getPath()); - } - } - - throw new FatalBackendException(new IllegalStateException("Exception thrown but not handled?")); - } - - 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))); - - try (TransferredBytesAwareDataSource out = new TransferredBytesAwareDataSource(data) { - @Override - public void bytesTransferred(long transferred) { - progressAware.onProgress( // - progress(UploadState.upload(file)) // - .between(0) // - .and(size) // - .withValue(transferred)); - } - }) { - try { - PutObjectArgs putObjectArgs = PutObjectArgs.builder().bucket(cloud.s3Bucket()).object(file.getKey()).stream(out.open(context), data.size(context).get(), -1).build(); - ObjectWriteResponse objectWriteResponse = client().putObject(putObjectArgs); - - Date lastModified = objectWriteResponse.headers().getDate("Last-Modified"); - - if (lastModified == null) { - StatObjectResponse statObjectResponse = client().statObject(StatObjectArgs // - .builder() // - .bucket(cloud.s3Bucket()) // - .object(file.getKey()) // - .build()); - - lastModified = Date.from(statObjectResponse.lastModified().toInstant()); - } - - progressAware.onProgress(Progress.completed(UploadState.upload(file))); - return S3CloudNodeFactory.file(file.getParent(), file.getName(), Optional.of(size), Optional.of(lastModified)); - } catch (Exception ex) { - handleApiError(ex, file.getPath()); - } - } - - throw new FatalBackendException(new IllegalStateException("Exception thrown but not handled?")); - } - - public void read(S3File file, OutputStream data, final ProgressAware progressAware) throws IOException, BackendException { - progressAware.onProgress(Progress.started(DownloadState.download(file))); - - GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket(cloud.s3Bucket()).object(file.getKey()).build(); - - try (GetObjectResponse response = client().getObject(getObjectArgs); // - TransferredBytesAwareOutputStream out = new TransferredBytesAwareOutputStream(data) { - @Override - public void bytesTransferred(long transferred) { - progressAware.onProgress( // - progress(DownloadState.download(file)) // - .between(0) // - .and(file.getSize().orElse(Long.MAX_VALUE)) // - .withValue(transferred)); - } - }) { - CopyStream.copyStreamToStream(response, out); - } catch (Exception ex) { - handleApiError(ex, file.getPath()); - } - - progressAware.onProgress(Progress.completed(DownloadState.download(file))); - } - - public void delete(S3Node node) throws IOException, BackendException { - if (node instanceof S3Folder) { - - List objectsToDelete = new LinkedList<>(); - - ListObjectsArgs request = ListObjectsArgs.builder().bucket(cloud.s3Bucket()).prefix(node.getKey()).recursive(true).delimiter(DELIMITER).build(); - Iterable> listObjects = client().listObjects(request); - for (Result object : listObjects) { - try { - Item item = object.get(); - objectsToDelete.add(new DeleteObject(item.objectName())); - } catch (Exception e) { - handleApiError(e, node.getPath()); - } - } - - RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder().bucket(cloud.s3Bucket()).objects(objectsToDelete).build(); - Iterable> results = client().removeObjects(removeObjectsArgs); - for (Result result : results) { - try { - DeleteError error = result.get(); - Timber.tag("S3Impl").e("Error in deleting object " + error.objectName() + "; " + error.message()); - } catch (Exception e) { - handleApiError(e, node.getPath()); - } - } - - } else { - RemoveObjectArgs removeObjectArgs = RemoveObjectArgs.builder().bucket(cloud.s3Bucket()).object(node.getKey()).build(); - try { - client().removeObject(removeObjectArgs); - } catch (Exception e) { - handleApiError(e, ""); - } - } - } - - public String checkAuthentication() throws NoSuchBucketException, BackendException { - try { - if (!client().bucketExists(BucketExistsArgs.builder().bucket(cloud.s3Bucket()).build())) { - throw new NoSuchBucketException(cloud.s3Bucket()); - } - } catch (Exception e) { - handleApiError(e, ""); - } - - return ""; - } - - private void handleApiError(Exception ex, String name) throws BackendException { - if (ex instanceof ErrorResponseException) { - String errorCode = ((ErrorResponseException) ex).errorResponse().code(); - if (S3CloudApiExceptions.isAccessProblem(errorCode)) { - throw new ForbiddenException(); - } else if (S3CloudApiErrorCodes.NO_SUCH_BUCKET.getValue().equals(errorCode)) { - throw new NoSuchBucketException(name); - } else if (S3CloudApiErrorCodes.NO_SUCH_KEY.getValue().equals(errorCode)) { - throw new NoSuchCloudFileException(name); - } else { - throw new FatalBackendException(ex); - } - } else { - throw new FatalBackendException(ex); - } - } - - private static abstract class TransferredBytesAwareDataSource implements DataSource { - - private final DataSource data; - - TransferredBytesAwareDataSource(DataSource data) { - this.data = data; - } - - @Override - public Optional size(Context context) { - return data.size(context); - } - - @Override - public InputStream open(Context context) throws IOException { - return new TransferredBytesAwareInputStream(data.open(context)) { - @Override - public void bytesTransferred(long transferred) { - S3Impl.TransferredBytesAwareDataSource.this.bytesTransferred(transferred); - } - }; - } - - @Override - public void close() throws IOException { - data.close(); - } - - public abstract void bytesTransferred(long transferred); - - @Override - public DataSource decorate(DataSource delegate) { - return delegate; - } - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.kt b/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.kt new file mode 100644 index 00000000..fbd13777 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.kt @@ -0,0 +1,383 @@ +package org.cryptomator.data.cloud.s3 + +import android.content.Context +import org.cryptomator.data.cloud.s3.S3CloudApiExceptions.isAccessProblem +import org.cryptomator.data.util.CopyStream +import org.cryptomator.data.util.TransferredBytesAwareInputStream +import org.cryptomator.data.util.TransferredBytesAwareOutputStream +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.NoSuchBucketException +import org.cryptomator.domain.exception.NoSuchCloudFileException +import org.cryptomator.domain.exception.ParentFolderIsNullException +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 java.io.ByteArrayInputStream +import java.io.IOException +import java.io.OutputStream +import java.util.Date +import java.util.LinkedList +import io.minio.BucketExistsArgs +import io.minio.CopyObjectArgs +import io.minio.CopySource +import io.minio.GetObjectArgs +import io.minio.ListObjectsArgs +import io.minio.MinioClient +import io.minio.PutObjectArgs +import io.minio.RemoveObjectArgs +import io.minio.RemoveObjectsArgs +import io.minio.StatObjectArgs +import io.minio.errors.ErrorResponseException +import io.minio.messages.DeleteObject +import timber.log.Timber + +internal class S3Impl(context: Context, cloud: S3Cloud) { + + private val cloud: S3Cloud + private val root: RootS3Folder + private val context: Context + + private fun client(): MinioClient { + return S3ClientFactory.getInstance(context, cloud) + } + + fun root(): S3Folder { + return root + } + + fun resolve(path: String): S3Folder { + val names = path.removePrefix("/").split("/").toTypedArray() + var folder: S3Folder = root + for (name in names) { + if (name.isEmpty().not()) { + folder = folder(folder, name) + } + } + return folder + } + + @Throws(BackendException::class, IOException::class) + fun file(parent: S3Folder, name: String, size: Long?): S3File { + return S3CloudNodeFactory.file(parent, name, size, parent.key + name) + } + + fun folder(parent: S3Folder, name: String): S3Folder { + return S3CloudNodeFactory.folder(parent, name, parent.key + name) + } + + @Throws(BackendException::class) + fun exists(node: S3Node): Boolean { + try { + return if (node !is RootS3Folder) { + client().statObject(StatObjectArgs.builder().bucket(cloud.s3Bucket()).`object`(node.key).build()) + true + } else { + // stat requests throws an IllegalStateException if key is empty string + val request = ListObjectsArgs.builder().bucket(cloud.s3Bucket()).prefix(node.key).delimiter(DELIMITER).build() + client().listObjects(request).iterator().hasNext() + } + } catch (e: ErrorResponseException) { + if (S3CloudApiErrorCodes.NO_SUCH_KEY.value == e.errorResponse().code()) { + return false + } + throw FatalBackendException(e) + } catch (ex: Exception) { + throw handleApiError(ex, node.path) + } + } + + @Throws(IOException::class, BackendException::class) + fun list(folder: S3Folder): List { + val request = ListObjectsArgs.builder().bucket(cloud.s3Bucket()).prefix(folder.key).delimiter(DELIMITER).build() + + val listObjects = client().listObjects(request) + return try { + listObjects.mapNotNull { + run { + val item = it.get() + if (item.isDir) { + S3CloudNodeFactory.folder(folder, S3CloudNodeFactory.getNameFromKey(item.objectName())) + } else { + if (item.objectName() != folder.key) { + S3CloudNodeFactory.file(folder, S3CloudNodeFactory.getNameFromKey(item.objectName()), item.size(), Date.from(item.lastModified().toInstant())) + } else { + // skip listed folder itself + null + } + } + } + } + } catch (ex: Exception) { + throw handleApiError(ex, folder.path) + } + } + + @Throws(IOException::class, BackendException::class) + fun create(folder: S3Folder): S3Folder { + var folder = folder + + folder.parent?.let { parentFolder -> + if (!exists(parentFolder)) { + folder = S3Folder(create(parentFolder), folder.name, folder.path) + } + } ?: throw ParentFolderIsNullException(folder.name) + + + folder.parent?.let { parentFolder -> + try { + val putObjectArgs = PutObjectArgs // + .builder() // + .bucket(cloud.s3Bucket()) // + .`object`(folder.key) // + .stream(ByteArrayInputStream(ByteArray(0)), 0, -1) // + .build() + client().putObject(putObjectArgs) + + } catch (ex: Exception) { + throw handleApiError(ex, folder.path) + } + + return S3CloudNodeFactory.folder(parentFolder, folder.name) + } ?: throw ParentFolderIsNullException(folder.name) + } + + @Throws(IOException::class, BackendException::class) + fun move(source: S3Node, target: S3Node): S3Node { + if (exists(target)) { + throw CloudNodeAlreadyExistsException(target.name) + } + return if (source is S3Folder && target is S3Folder) { + moveFolder(source, target) + } else if (source is S3File && target is S3File) { + moveFile(source, target) + } else { + throw FatalBackendException("Can't move file to folder or folder to file") + } + } + + @Throws(IOException::class, BackendException::class) + private fun moveFolder(source: S3Folder, target: S3Folder): S3Folder { + target.parent?.let { targetsParent -> + val nodes = list(source) + val objectsToDelete: MutableList = LinkedList() + + for (node in nodes) { + objectsToDelete.add(DeleteObject(node.key)) + + val targetKey = if (node is S3Folder) { + S3CloudNodeFactory.folder(target, node.name).key + } else { + S3CloudNodeFactory.file(target, node.name).key + } + + val copySource = CopySource.builder().bucket(cloud.s3Bucket()).`object`(node.key).build() + val copyObjectArgs = CopyObjectArgs.builder().bucket(cloud.s3Bucket()).`object`(targetKey).source(copySource).build() + try { + client().copyObject(copyObjectArgs) + } catch (ex: Exception) { + throw handleApiError(ex, source.path) + } + } + + val removeObjectsArgs = RemoveObjectsArgs.builder().bucket(cloud.s3Bucket()).objects(objectsToDelete).build() + + for (result in client().removeObjects(removeObjectsArgs)) { + try { + result.get() + } catch (ex: Exception) { + throw handleApiError(ex, source.path) + } + } + + return S3CloudNodeFactory.folder(targetsParent, target.name) + } ?: throw ParentFolderIsNullException(target.name) + } + + @Throws(IOException::class, BackendException::class) + private fun moveFile(source: S3File, target: S3File): S3File { + val copySource = CopySource.builder().bucket(cloud.s3Bucket()).`object`(source.key).build() + val copyObjectArgs = CopyObjectArgs.builder().bucket(cloud.s3Bucket()).`object`(target.key).source(copySource).build() + try { + val result = client().copyObject(copyObjectArgs) + delete(source) + val lastModified = result.headers().getDate("Last-Modified") + return S3CloudNodeFactory.file(target.parent, target.name, source.size, lastModified) + } catch (ex: Exception) { + throw handleApiError(ex, source.path) + } + } + + @Throws(IOException::class, BackendException::class) + fun write(file: S3File, data: DataSource, progressAware: ProgressAware, replace: Boolean, size: Long): S3File { + if (!replace && exists(file)) { + throw CloudNodeAlreadyExistsException("CloudNode already exists and replace is false") + } + + progressAware.onProgress(Progress.started(UploadState.upload(file))) + + data.open(context)?.use { inputStream -> + object : TransferredBytesAwareInputStream(inputStream) { + override fun bytesTransferred(transferred: Long) { + progressAware.onProgress( // + Progress.progress(UploadState.upload(file)) // + .between(0) // + .and(size) // + .withValue(transferred) + ) + } + }.use { + try { + val putObjectArgs = PutObjectArgs // + .builder() // + .bucket(cloud.s3Bucket()) // + .`object`(file.key) // + .stream(it, data.size(context) ?: Long.MAX_VALUE, -1) // + .build() + + val objectWriteResponse = client().putObject(putObjectArgs) + + val lastModified = objectWriteResponse.headers().getDate("Last-Modified") ?: run { + val statObjectResponse = client().statObject( + StatObjectArgs // + .builder() // + .bucket(cloud.s3Bucket()) // + .`object`(file.key) // + .build() + ) + Date.from(statObjectResponse.lastModified().toInstant()) + } + + progressAware.onProgress(Progress.completed(UploadState.upload(file))) + + return S3CloudNodeFactory.file(file.parent, file.name, size, lastModified) + } catch (ex: Exception) { + throw handleApiError(ex, file.path) + } + } + } ?: throw FatalBackendException("InputStream shouldn't bee null") + } + + @Throws(IOException::class, BackendException::class) + fun read(file: S3File, data: OutputStream, progressAware: ProgressAware) { + progressAware.onProgress(Progress.started(DownloadState.download(file))) + val getObjectArgs = GetObjectArgs.builder().bucket(cloud.s3Bucket()).`object`(file.key).build() + try { + client().getObject(getObjectArgs).use { response -> + object : TransferredBytesAwareOutputStream(data) { + override fun bytesTransferred(transferred: Long) { + progressAware.onProgress( // + Progress.progress(DownloadState.download(file)) // + .between(0) // + .and(file.size ?: Long.MAX_VALUE) // + .withValue(transferred) + ) + } + }.use { out -> CopyStream.copyStreamToStream(response, out) } + } + } catch (ex: Exception) { + throw handleApiError(ex, file.path) + } + progressAware.onProgress(Progress.completed(DownloadState.download(file))) + } + + @Throws(IOException::class, BackendException::class) + fun delete(node: S3Node) = if (node is S3Folder) { + deleteFolder(node) + } else { + deleteFile(node as S3File) + } + + @Throws(IOException::class, BackendException::class) + private fun deleteFolder(node: S3Folder) { + val request = ListObjectsArgs.builder().bucket(cloud.s3Bucket()).prefix(node.key).recursive(true).delimiter(DELIMITER).build() + + val listObjects = client().listObjects(request) + + val objectsToDelete = try { + listObjects.map { + run { + val item = it.get() + DeleteObject(item.objectName()) + } + } + } catch (e: Exception) { + throw handleApiError(e, node.path) + } + + val removeObjectsArgs = RemoveObjectsArgs.builder().bucket(cloud.s3Bucket()).objects(objectsToDelete).build() + val results = client().removeObjects(removeObjectsArgs) + results.forEach { result -> + try { + val error = result.get() + Timber.tag("S3Impl").e("Error in deleting object " + error.objectName() + "; " + error.message()) + } catch (e: Exception) { + throw handleApiError(e, node.path) + } + } + } + + @Throws(IOException::class, BackendException::class) + private fun deleteFile(node: S3File) { + val removeObjectArgs = RemoveObjectArgs.builder().bucket(cloud.s3Bucket()).`object`(node.key).build() + try { + client().removeObject(removeObjectArgs) + } catch (e: Exception) { + throw handleApiError(e, "") + } + } + + @Throws(NoSuchBucketException::class, BackendException::class) + fun checkAuthentication(): String { + return try { + if (!client().bucketExists(BucketExistsArgs.builder().bucket(cloud.s3Bucket()).build())) { + throw NoSuchBucketException(cloud.s3Bucket()) + } + "" + } catch (e: Exception) { + throw handleApiError(e, "") + } + } + + private fun handleApiError(ex: Exception, name: String): Exception { + return if (ex is ErrorResponseException) { + val errorCode = ex.errorResponse().code() + when { + isAccessProblem(errorCode) -> { + ForbiddenException() + } + S3CloudApiErrorCodes.NO_SUCH_BUCKET.value == errorCode -> { + NoSuchBucketException(name) + } + S3CloudApiErrorCodes.NO_SUCH_KEY.value == errorCode -> { + NoSuchCloudFileException(name) + } + else -> { + FatalBackendException(ex) + } + } + } else { + FatalBackendException(ex) + } + } + + companion object { + + private const val DELIMITER = "/" + } + + init { + if (cloud.accessKey() == null || cloud.secretKey() == null) { + throw WrongCredentialsException(cloud) + } + this.context = context + this.cloud = cloud + this.root = RootS3Folder(cloud) + } +} 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 deleted file mode 100644 index 570d338b..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/s3/S3Node.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.cryptomator.data.cloud.s3; - -import org.cryptomator.domain.CloudNode; - -interface S3Node extends CloudNode { - - @Override - S3Folder getParent(); - - String getKey(); - -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/s3/S3Node.kt b/data/src/main/java/org/cryptomator/data/cloud/s3/S3Node.kt new file mode 100644 index 00000000..37453cdb --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/s3/S3Node.kt @@ -0,0 +1,10 @@ +package org.cryptomator.data.cloud.s3 + +import org.cryptomator.domain.CloudNode + +internal interface S3Node : CloudNode { + + override val parent: S3Folder? + val key: String? + +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/webdav/RootWebDavFolder.java b/data/src/main/java/org/cryptomator/data/cloud/webdav/RootWebDavFolder.java deleted file mode 100644 index 069c32e2..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/webdav/RootWebDavFolder.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.cryptomator.data.cloud.webdav; - -import org.cryptomator.domain.Cloud; -import org.cryptomator.domain.WebDavCloud; - -public class RootWebDavFolder extends WebDavFolder { - - private final WebDavCloud cloud; - - public RootWebDavFolder(WebDavCloud cloud) { - super(null, "", ""); - this.cloud = cloud; - } - - @Override - public Cloud getCloud() { - return cloud; - } - - @Override - public WebDavFolder withCloud(Cloud cloud) { - return new RootWebDavFolder((WebDavCloud) cloud); - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/webdav/RootWebDavFolder.kt b/data/src/main/java/org/cryptomator/data/cloud/webdav/RootWebDavFolder.kt new file mode 100644 index 00000000..16c8b2c3 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/webdav/RootWebDavFolder.kt @@ -0,0 +1,11 @@ +package org.cryptomator.data.cloud.webdav + +import org.cryptomator.domain.Cloud +import org.cryptomator.domain.WebDavCloud + +class RootWebDavFolder(override val cloud: WebDavCloud) : WebDavFolder(null, "", "") { + + override fun withCloud(cloud: Cloud?): WebDavFolder { + return RootWebDavFolder(cloud as WebDavCloud) + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavCloudContentRepository.java b/data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavCloudContentRepository.java deleted file mode 100644 index 43c645f2..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavCloudContentRepository.java +++ /dev/null @@ -1,235 +0,0 @@ -package org.cryptomator.data.cloud.webdav; - -import org.cryptomator.data.cloud.InterceptingCloudContentRepository; -import org.cryptomator.data.cloud.webdav.network.ConnectionHandlerHandlerImpl; -import org.cryptomator.domain.CloudNode; -import org.cryptomator.domain.WebDavCloud; -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.NotFoundException; -import org.cryptomator.domain.exception.NotImplementedException; -import org.cryptomator.domain.exception.NotTrustableCertificateException; -import org.cryptomator.domain.exception.UnauthorizedException; -import org.cryptomator.domain.exception.authentication.WebDavCertificateUntrustedAuthenticationException; -import org.cryptomator.domain.exception.authentication.WebDavNotSupportedException; -import org.cryptomator.domain.exception.authentication.WebDavServerNotFoundException; -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.net.SocketTimeoutException; -import java.net.UnknownHostException; -import java.util.List; - -import javax.inject.Singleton; -import javax.net.ssl.SSLHandshakeException; - -import static org.cryptomator.util.ExceptionUtil.contains; -import static org.cryptomator.util.ExceptionUtil.extract; - -@Singleton -class WebDavCloudContentRepository extends InterceptingCloudContentRepository { - - private static final CharSequence START_OF_CERTIFICATE = "-----BEGIN CERTIFICATE-----"; - private final WebDavCloud cloud; - - WebDavCloudContentRepository(WebDavCloud cloud, ConnectionHandlerHandlerImpl connectionHandlerHandler) { - super(new Intercepted(cloud, connectionHandlerHandler)); - this.cloud = cloud; - } - - @Override - protected void throwWrappedIfRequired(Exception e) throws BackendException { - throwNetworkConnectionExceptionIfRequired(e); - throwCertificateUntrustedExceptionIfRequired(e); - throwForbiddenExceptionIfRequired(e); - throwUnauthorizedExceptionIfRequired(e); - throwNotImplementedExceptionIfRequired(e); - throwServerNotFoundExceptionIfRequired(e); - } - - private void throwServerNotFoundExceptionIfRequired(Exception e) { - if (contains(e, UnknownHostException.class)) { - throw new WebDavServerNotFoundException(cloud); - } - } - - private void throwNotImplementedExceptionIfRequired(Exception e) { - if (contains(e, NotImplementedException.class)) { - throw new WebDavNotSupportedException(cloud); - } - } - - private void throwUnauthorizedExceptionIfRequired(Exception e) { - if (contains(e, UnauthorizedException.class)) { - throw new WrongCredentialsException(cloud); - } - } - - private void throwForbiddenExceptionIfRequired(Exception e) { - if (contains(e, ForbiddenException.class)) { - throw new WrongCredentialsException(cloud); - } - } - - private void throwCertificateUntrustedExceptionIfRequired(Exception e) { - Optional notTrustableCertificateException = extract(e, NotTrustableCertificateException.class); - if (notTrustableCertificateException.isPresent()) { - throw new WebDavCertificateUntrustedAuthenticationException(cloud, notTrustableCertificateException.get().getMessage()); - } - Optional sslHandshakeException = extract(e, SSLHandshakeException.class); - if (sslHandshakeException.isPresent() && containsCertificate(e.getMessage())) { - throw new WebDavCertificateUntrustedAuthenticationException(cloud, sslHandshakeException.get().getMessage()); - } - } - - private boolean containsCertificate(String message) { - return message != null && message.contains(START_OF_CERTIFICATE); - } - - private void throwNetworkConnectionExceptionIfRequired(Exception e) throws NetworkConnectionException { - if (contains(e, SocketTimeoutException.class)) { - throw new NetworkConnectionException(e); - } - } - - private static class Intercepted implements CloudContentRepository { - - private final WebDavImpl webDavImpl; - - Intercepted(WebDavCloud cloud, ConnectionHandlerHandlerImpl connectionHandler) { - this.webDavImpl = new WebDavImpl(cloud, connectionHandler); - } - - public WebDavFolder root(WebDavCloud cloud) { - return webDavImpl.root(); - } - - @Override - public WebDavFolder resolve(WebDavCloud cloud, String path) throws BackendException { - return webDavImpl.resolve(path); - } - - @Override - public WebDavFile file(WebDavFolder parent, String name) throws BackendException { - return webDavImpl.file(parent, name); - } - - @Override - public WebDavFile file(WebDavFolder parent, String name, Optional size) throws BackendException { - return webDavImpl.file(parent, name, size); - } - - @Override - public WebDavFolder folder(WebDavFolder parent, String name) { - return webDavImpl.folder(parent, name); - } - - @Override - public boolean exists(WebDavNode node) throws BackendException { - return webDavImpl.exists(node); - } - - @Override - public List list(WebDavFolder folder) throws BackendException { - try { - return webDavImpl.list(folder); - } catch (BackendException e) { - if (contains(e, NotFoundException.class)) { - throw new NoSuchCloudFileException(); - } - throw e; - } - } - - @Override - public WebDavFolder create(WebDavFolder folder) throws BackendException { - return webDavImpl.create(folder); - } - - @Override - public WebDavFolder move(WebDavFolder source, WebDavFolder target) throws BackendException { - try { - return webDavImpl.move(source, target); - } catch (BackendException e) { - if (contains(e, NotFoundException.class)) { - throw new NoSuchCloudFileException(source.getName()); - } else if (contains(e, CloudNodeAlreadyExistsException.class)) { - throw new CloudNodeAlreadyExistsException(target.getName()); - } - throw e; - } - } - - @Override - public WebDavFile move(WebDavFile source, WebDavFile target) throws BackendException { - return webDavImpl.move(source, target); - } - - @Override - public WebDavFile write(WebDavFile uploadFile, DataSource data, ProgressAware progressAware, boolean replace, long size) throws BackendException { - try { - return webDavImpl.write(uploadFile, data, progressAware, replace, size); - } catch (BackendException | IOException e) { - if (contains(e, NotFoundException.class)) { - throw new NoSuchCloudFileException(uploadFile.getName()); - } else if (e instanceof IOException) { - throw new FatalBackendException(e); - } else if (e instanceof FatalBackendException) { - throw (FatalBackendException) e; - } else { - throw new FatalBackendException(e); - } - } - } - - @Override - public void read(WebDavFile file, Optional tmpEncryptedFile, OutputStream data, ProgressAware progressAware) throws BackendException { - try { - webDavImpl.read(file, data, progressAware); - } catch (BackendException | IOException e) { - if (contains(e, NotFoundException.class)) { - throw new NoSuchCloudFileException(file.getName()); - } else if (e instanceof IOException) { - throw new FatalBackendException(e); - } else if (e instanceof FatalBackendException) { - throw (FatalBackendException) e; - } - } - } - - @Override - public void delete(WebDavNode node) throws BackendException { - try { - webDavImpl.delete(node); - } catch (BackendException e) { - if (contains(e, NotFoundException.class)) { - throw new NoSuchCloudFileException(node.getName()); - } - throw e; - } - } - - @Override - public String checkAuthenticationAndRetrieveCurrentAccount(WebDavCloud cloud) throws BackendException { - return webDavImpl.currentAccount(); - } - - @Override - public void logout(WebDavCloud cloud) { - // empty - } - } - -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavCloudContentRepository.kt b/data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavCloudContentRepository.kt new file mode 100644 index 00000000..304ffd3d --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavCloudContentRepository.kt @@ -0,0 +1,244 @@ +package org.cryptomator.data.cloud.webdav + +import android.content.Context +import org.cryptomator.data.cloud.InterceptingCloudContentRepository +import org.cryptomator.data.cloud.webdav.network.ConnectionHandlerHandlerImpl +import org.cryptomator.domain.WebDavCloud +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.NotFoundException +import org.cryptomator.domain.exception.NotImplementedException +import org.cryptomator.domain.exception.NotTrustableCertificateException +import org.cryptomator.domain.exception.UnauthorizedException +import org.cryptomator.domain.exception.authentication.WebDavCertificateUntrustedAuthenticationException +import org.cryptomator.domain.exception.authentication.WebDavNotSupportedException +import org.cryptomator.domain.exception.authentication.WebDavServerNotFoundException +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.ExceptionUtil +import java.io.File +import java.io.IOException +import java.io.OutputStream +import java.net.SocketTimeoutException +import java.net.UnknownHostException +import javax.inject.Singleton +import javax.net.ssl.SSLHandshakeException + +@Singleton +internal class WebDavCloudContentRepository(private val cloud: WebDavCloud, connectionHandlerHandler: ConnectionHandlerHandlerImpl, context: Context) : + InterceptingCloudContentRepository(Intercepted(cloud, connectionHandlerHandler, context)) { + + @Throws(BackendException::class) + override fun throwWrappedIfRequired(e: Exception) { + throwNetworkConnectionExceptionIfRequired(e) + throwCertificateUntrustedExceptionIfRequired(e) + throwForbiddenExceptionIfRequired(e) + throwUnauthorizedExceptionIfRequired(e) + throwNotImplementedExceptionIfRequired(e) + throwServerNotFoundExceptionIfRequired(e) + } + + private fun throwServerNotFoundExceptionIfRequired(e: Exception) { + if (ExceptionUtil.contains(e, UnknownHostException::class.java)) { + throw WebDavServerNotFoundException(cloud) + } + } + + private fun throwNotImplementedExceptionIfRequired(e: Exception) { + if (ExceptionUtil.contains(e, NotImplementedException::class.java)) { + throw WebDavNotSupportedException(cloud) + } + } + + private fun throwUnauthorizedExceptionIfRequired(e: Exception) { + if (ExceptionUtil.contains(e, UnauthorizedException::class.java)) { + throw WrongCredentialsException(cloud) + } + } + + private fun throwForbiddenExceptionIfRequired(e: Exception) { + if (ExceptionUtil.contains(e, ForbiddenException::class.java)) { + throw WrongCredentialsException(cloud) + } + } + + private fun throwCertificateUntrustedExceptionIfRequired(e: Exception) { + val notTrustableCertificateException = ExceptionUtil.extract(e, NotTrustableCertificateException::class.java) + if (notTrustableCertificateException.isPresent) { + throw WebDavCertificateUntrustedAuthenticationException(cloud, notTrustableCertificateException.get().message) + } + val sslHandshakeException = ExceptionUtil.extract(e, SSLHandshakeException::class.java) + if (sslHandshakeException.isPresent && containsCertificate(e.message)) { + throw WebDavCertificateUntrustedAuthenticationException(cloud, sslHandshakeException.get().message) + } + } + + private fun containsCertificate(message: String?): Boolean { + return message != null && message.contains(START_OF_CERTIFICATE) + } + + @Throws(NetworkConnectionException::class) + private fun throwNetworkConnectionExceptionIfRequired(e: Exception) { + if (ExceptionUtil.contains(e, SocketTimeoutException::class.java)) { + throw NetworkConnectionException(e) + } + } + + private class Intercepted constructor(cloud: WebDavCloud, connectionHandler: ConnectionHandlerHandlerImpl, context: Context) : CloudContentRepository { + + private val webDavImpl: WebDavImpl = WebDavImpl(cloud, connectionHandler, context) + + override fun root(cloud: WebDavCloud): WebDavFolder { + return webDavImpl.root() + } + + override fun resolve(cloud: WebDavCloud, path: String): WebDavFolder { + return webDavImpl.resolve(path) + } + + @Throws(BackendException::class) + override fun file(parent: WebDavFolder, name: String): WebDavFile { + return webDavImpl.file(parent, name, null) + } + + @Throws(BackendException::class) + override fun file(parent: WebDavFolder, name: String, size: Long?): WebDavFile { + return webDavImpl.file(parent, name, size) + } + + override fun folder(parent: WebDavFolder, name: String): WebDavFolder { + return webDavImpl.folder(parent, name) + } + + @Throws(BackendException::class) + override fun exists(node: WebDavNode): Boolean { + return webDavImpl.exists(node) + } + + @Throws(BackendException::class) + override fun list(folder: WebDavFolder): List { + return try { + webDavImpl.list(folder) + } catch (e: BackendException) { + if (ExceptionUtil.contains(e, NotFoundException::class.java)) { + throw NoSuchCloudFileException() + } + throw e + } + } + + @Throws(BackendException::class) + override fun create(folder: WebDavFolder): WebDavFolder { + return webDavImpl.create(folder) + } + + @Throws(BackendException::class) + override fun move(source: WebDavFolder, target: WebDavFolder): WebDavFolder { + return try { + webDavImpl.move(source, target) + } catch (e: BackendException) { + if (ExceptionUtil.contains(e, NotFoundException::class.java)) { + throw NoSuchCloudFileException(source.name) + } else if (ExceptionUtil.contains(e, CloudNodeAlreadyExistsException::class.java)) { + throw CloudNodeAlreadyExistsException(target.name) + } + throw e + } + } + + @Throws(BackendException::class) + override fun move(source: WebDavFile, target: WebDavFile): WebDavFile { + return webDavImpl.move(source, target) + } + + @Throws(BackendException::class) + override fun write(file: WebDavFile, data: DataSource, progressAware: ProgressAware, replace: Boolean, size: Long): WebDavFile { + return try { + webDavImpl.write(file, data, progressAware, replace, size) + } catch (e: BackendException) { + when { + ExceptionUtil.contains(e, NotFoundException::class.java) -> { + throw NoSuchCloudFileException(file.name) + } + e is IOException -> { + throw FatalBackendException(e) + } + e is FatalBackendException -> { + throw e + } + else -> { + throw FatalBackendException(e) + } + } + } catch (e: IOException) { + when { + ExceptionUtil.contains(e, NotFoundException::class.java) -> { + throw NoSuchCloudFileException(file.name) + } + e is FatalBackendException -> { + throw e + } + else -> { + throw FatalBackendException(e) + } + } + } + } + + @Throws(BackendException::class) + override fun read(file: WebDavFile, encryptedTmpFile: File?, data: OutputStream, progressAware: ProgressAware) { + try { + webDavImpl.read(file, data, progressAware) + } catch (e: BackendException) { + if (ExceptionUtil.contains(e, NotFoundException::class.java)) { + throw NoSuchCloudFileException(file.name) + } else if (e is IOException) { + throw FatalBackendException(e) + } else if (e is FatalBackendException) { + throw e + } + } catch (e: IOException) { + if (ExceptionUtil.contains(e, NotFoundException::class.java)) { + throw NoSuchCloudFileException(file.name) + } else if (e is FatalBackendException) { + throw e + } + } + } + + @Throws(BackendException::class) + override fun delete(node: WebDavNode) { + try { + webDavImpl.delete(node) + } catch (e: BackendException) { + if (ExceptionUtil.contains(e, NotFoundException::class.java)) { + throw NoSuchCloudFileException(node.name) + } + throw e + } + } + + @Throws(BackendException::class) + override fun checkAuthenticationAndRetrieveCurrentAccount(cloud: WebDavCloud): String { + return webDavImpl.currentAccount() + } + + override fun logout(cloud: WebDavCloud) { + // empty + } + + } + + companion object { + + private val START_OF_CERTIFICATE: CharSequence = "-----BEGIN CERTIFICATE-----" + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavCloudContentRepositoryFactory.java b/data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavCloudContentRepositoryFactory.java index 824da150..9231e828 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavCloudContentRepositoryFactory.java +++ b/data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavCloudContentRepositoryFactory.java @@ -1,5 +1,7 @@ package org.cryptomator.data.cloud.webdav; +import android.content.Context; + import org.cryptomator.data.cloud.webdav.network.ConnectionHandlerFactory; import org.cryptomator.data.repository.CloudContentRepositoryFactory; import org.cryptomator.domain.Cloud; @@ -14,10 +16,12 @@ import static org.cryptomator.domain.CloudType.WEBDAV; public class WebDavCloudContentRepositoryFactory implements CloudContentRepositoryFactory { private final ConnectionHandlerFactory connectionHandlerFactory; + private final Context context; @Inject - WebDavCloudContentRepositoryFactory(ConnectionHandlerFactory connectionHandlerFactory) { + WebDavCloudContentRepositoryFactory(ConnectionHandlerFactory connectionHandlerFactory, Context context) { this.connectionHandlerFactory = connectionHandlerFactory; + this.context = context; } @Override @@ -26,11 +30,11 @@ public class WebDavCloudContentRepositoryFactory implements CloudContentReposito } @Override - public CloudContentRepository cloudContentRepositoryFor(Cloud cloud) { + public CloudContentRepository cloudContentRepositoryFor(Cloud cloud) { WebDavCloud webDavCloud = (WebDavCloud) cloud; if (webDavCloud.username() == null || webDavCloud.password() == null) { throw new NoAuthenticationProvidedException(webDavCloud); } - return new WebDavCloudContentRepository(webDavCloud, connectionHandlerFactory.createConnectionHandler(webDavCloud)); + return new WebDavCloudContentRepository(webDavCloud, connectionHandlerFactory.createConnectionHandler(webDavCloud), context); } } diff --git a/data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavFile.java b/data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavFile.java deleted file mode 100644 index 48b342a7..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavFile.java +++ /dev/null @@ -1,58 +0,0 @@ -package org.cryptomator.data.cloud.webdav; - -import org.cryptomator.domain.Cloud; -import org.cryptomator.domain.CloudFile; -import org.cryptomator.util.Optional; - -import java.util.Date; - -public class WebDavFile implements CloudFile, WebDavNode { - - private final WebDavFolder parent; - private final String name; - private final String path; - private final Optional size; - private final Optional modified; - - public WebDavFile(WebDavFolder parent, String name, Optional size, Optional modified) { - this(parent, name, parent.getPath() + "/" + name, size, modified); - } - - public WebDavFile(WebDavFolder 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 WebDavFolder 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/webdav/WebDavFile.kt b/data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavFile.kt new file mode 100644 index 00000000..48be601a --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavFile.kt @@ -0,0 +1,13 @@ +package org.cryptomator.data.cloud.webdav + +import org.cryptomator.domain.Cloud +import org.cryptomator.domain.CloudFile +import java.util.Date + +class WebDavFile(override val parent: WebDavFolder, override val name: String, override val path: String, override val size: Long?, override val modified: Date?) : CloudFile, WebDavNode { + + constructor(parent: WebDavFolder, name: String, size: Long?, modified: Date?) : this(parent, name, parent.path + "/" + name, size, modified) + + override val cloud: Cloud? + get() = parent.cloud +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavFolder.java b/data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavFolder.java deleted file mode 100644 index 9d62c998..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavFolder.java +++ /dev/null @@ -1,55 +0,0 @@ -package org.cryptomator.data.cloud.webdav; - -import org.cryptomator.domain.Cloud; -import org.cryptomator.domain.CloudFolder; -import org.jetbrains.annotations.NotNull; - -import static java.lang.String.format; - -public class WebDavFolder implements CloudFolder, WebDavNode { - - private final WebDavFolder parent; - private final String name; - private final String path; - - public WebDavFolder(WebDavFolder parent, String name) { - this(parent, name, parent.getPath() + "/" + name); - } - - public WebDavFolder(WebDavFolder 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 WebDavFolder getParent() { - return parent; - } - - @Override - public WebDavFolder withCloud(Cloud cloud) { - return new WebDavFolder(parent.withCloud(cloud), name, path); - } - - @NotNull - @Override - public String toString() { - return format("WebDavFolder(%s)", path); - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavFolder.kt b/data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavFolder.kt new file mode 100644 index 00000000..b23b0259 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavFolder.kt @@ -0,0 +1,14 @@ +package org.cryptomator.data.cloud.webdav; + +import org.cryptomator.domain.Cloud +import org.cryptomator.domain.CloudFolder + +open class WebDavFolder(override val parent: WebDavFolder?, override val name: String, override val path: String) : CloudFolder, WebDavNode { + + override val cloud: Cloud? + get() = parent?.cloud + + override fun withCloud(cloud: Cloud?): WebDavFolder? { + return WebDavFolder(parent?.withCloud(cloud), name, path) + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavImpl.java b/data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavImpl.java deleted file mode 100644 index 9a6d49a7..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavImpl.java +++ /dev/null @@ -1,254 +0,0 @@ -package org.cryptomator.data.cloud.webdav; - -import android.content.Context; - -import org.cryptomator.data.cloud.webdav.network.ConnectionHandlerHandlerImpl; -import org.cryptomator.data.util.CopyStream; -import org.cryptomator.data.util.TransferredBytesAwareInputStream; -import org.cryptomator.data.util.TransferredBytesAwareOutputStream; -import org.cryptomator.domain.CloudFile; -import org.cryptomator.domain.CloudFolder; -import org.cryptomator.domain.CloudNode; -import org.cryptomator.domain.WebDavCloud; -import org.cryptomator.domain.exception.BackendException; -import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException; -import org.cryptomator.domain.exception.FatalBackendException; -import org.cryptomator.domain.exception.NotFoundException; -import org.cryptomator.domain.exception.ParentFolderDoesNotExistException; -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 java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.List; - -import okhttp3.HttpUrl; - -import static org.cryptomator.domain.usecases.cloud.Progress.progress; - -class WebDavImpl { - - private final WebDavCloud cloud; - private final HttpUrl baseUrl; - private final RootWebDavFolder root; - private final ConnectionHandlerHandlerImpl connectionHandler; - - WebDavImpl(WebDavCloud cloud, ConnectionHandlerHandlerImpl connectionHandler) { - this.cloud = cloud; - this.baseUrl = HttpUrl.parse(cloud.url()); - this.root = new RootWebDavFolder(cloud); - this.connectionHandler = connectionHandler; - } - - public WebDavFolder root() { - return root; - } - - public WebDavFolder resolve(String path) { - if (path.startsWith("/")) { - path = path.substring(1); - } - String[] names = path.split("/"); - WebDavFolder folder = root; - for (String name : names) { - folder = folder(folder, name); - } - return folder; - } - - public WebDavFile file(CloudFolder parent, String name) { - return file(parent, name, Optional.empty()); - } - - public WebDavFile file(CloudFolder parent, String name, Optional size) { - return new WebDavFile((WebDavFolder) parent, name, parent.getPath() + '/' + name, size, Optional.empty()); - } - - public WebDavFolder folder(CloudFolder parent, String name) { - return new WebDavFolder((WebDavFolder) parent, name, parent.getPath() + '/' + name); - } - - public boolean exists(CloudNode node) throws BackendException { - try { - return connectionHandler // - .get(absoluteUriFrom(node.getPath()), // - node.getParent()) != null; - } catch (NotFoundException e) { - return false; - } - } - - public List list(WebDavFolder folder) throws BackendException { - return connectionHandler // - .dirList(absoluteUriFrom(folder.getPath()), // - folder); - } - - public WebDavFolder create(WebDavFolder folder) throws BackendException { - try { - return createExcludingParents(folder); - } catch (NotFoundException | ParentFolderDoesNotExistException e) { - create(folder.getParent()); - return createExcludingParents(folder); - } - } - - private WebDavFolder createExcludingParents(WebDavFolder folder) throws BackendException { - if (folder.getParent() == null) { - return folder; - } else { - return connectionHandler.createFolder( // - absoluteUriFrom(folder.getPath()), // - folder); - } - } - - public WebDavFolder move(CloudFolder source, CloudFolder target) throws BackendException { - moveFileOrFolder(source, target); - return new WebDavFolder( // - (WebDavFolder) target.getParent() // - , target.getName() // - , target.getPath()); - } - - public WebDavFile move(CloudFile source, CloudFile target) throws BackendException { - moveFileOrFolder(source, target); - return new WebDavFile( // - (WebDavFolder) target.getParent() // - , target.getName() // - , target.getPath() // - , source.getSize() // - , source.getModified()); - } - - private void moveFileOrFolder(CloudNode source, CloudNode target) throws BackendException { - if (exists(target)) { - throw new CloudNodeAlreadyExistsException(target.getName()); - } - - connectionHandler // - .move(absoluteUriFrom(source.getPath()), // - absoluteUriFrom(target.getPath())); - } - - public WebDavFile write(final WebDavFile uploadFile, DataSource data, final ProgressAware progressAware, boolean replace, final long size) // - throws BackendException, IOException { - if (!replace && exists(uploadFile)) { - throw new CloudNodeAlreadyExistsException("CloudNode already exists and replace is false"); - } - - progressAware.onProgress(Progress.started(UploadState.upload(uploadFile))); - - try (TransferredBytesAwareDataSource out = new TransferredBytesAwareDataSource(data) { - @Override - public void bytesTrasferred(long transferred) { - progressAware.onProgress( // - progress(UploadState.upload(uploadFile)) // - .between(0) // - .and(size) // - .withValue(transferred)); - } - }) { - connectionHandler // - .writeFile( // - absoluteUriFrom(uploadFile.getPath()), out); - } - - WebDavFile cloudFile = (WebDavFile) connectionHandler // - .get(absoluteUriFrom(uploadFile.getPath()), // - uploadFile.getParent()); - - if (cloudFile == null) { - throw new FatalBackendException("Unable to get CloudFile after upload."); - } - - return cloudFile; - } - - public void checkAuthenticationAndServerCompatibility(String url) throws BackendException { - connectionHandler.checkAuthenticationAndServerCompatibility(url); - } - - public void read(final CloudFile file, OutputStream data, final ProgressAware progressAware) throws BackendException, IOException { - progressAware.onProgress(Progress.started(DownloadState.download(file))); - - try (InputStream in = connectionHandler.readFile(absoluteUriFrom(file.getPath())); // - TransferredBytesAwareOutputStream out = new TransferredBytesAwareOutputStream(data) { - @Override - public void bytesTransferred(long transferred) { - progressAware.onProgress( // - progress(DownloadState.download(file)) // - .between(0) // - .and(file.getSize().orElse(Long.MAX_VALUE)) // - .withValue(transferred)); - } - }) { - CopyStream.copyStreamToStream(in, out); - } - - progressAware.onProgress(Progress.completed(DownloadState.download(file))); - } - - public void delete(CloudNode node) throws BackendException { - connectionHandler.delete(absoluteUriFrom(node.getPath())); - } - - private String absoluteUriFrom(String path) { - path = removeLeadingSlash(path); - - return baseUrl.newBuilder() // - .addPathSegments(path) // - .build() // - .toString(); - } - - private String removeLeadingSlash(String path) { - return path.length() > 0 && path.charAt(0) == '/' ? path.substring(1) : path; - } - - public String currentAccount() throws BackendException { - checkAuthenticationAndServerCompatibility(cloud.url()); - return cloud.url(); - } - - private static abstract class TransferredBytesAwareDataSource implements DataSource { - - private final DataSource data; - - TransferredBytesAwareDataSource(DataSource data) { - this.data = data; - } - - @Override - public Optional size(Context context) { - return data.size(context); - } - - @Override - public InputStream open(Context context) throws IOException { - return new TransferredBytesAwareInputStream(data.open(context)) { - @Override - public void bytesTransferred(long transferred) { - TransferredBytesAwareDataSource.this.bytesTrasferred(transferred); - } - }; - } - - @Override - public void close() throws IOException { - data.close(); - } - - public abstract void bytesTrasferred(long transferred); - - @Override - public DataSource decorate(DataSource delegate) { - return delegate; - } - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavImpl.kt b/data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavImpl.kt new file mode 100644 index 00000000..f8b66e75 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavImpl.kt @@ -0,0 +1,185 @@ +package org.cryptomator.data.cloud.webdav + +import android.content.Context +import org.cryptomator.data.cloud.webdav.network.ConnectionHandlerHandlerImpl +import org.cryptomator.data.util.CopyStream +import org.cryptomator.data.util.TransferredBytesAwareInputStream +import org.cryptomator.data.util.TransferredBytesAwareOutputStream +import org.cryptomator.domain.CloudFile +import org.cryptomator.domain.CloudNode +import org.cryptomator.domain.WebDavCloud +import org.cryptomator.domain.exception.BackendException +import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException +import org.cryptomator.domain.exception.FatalBackendException +import org.cryptomator.domain.exception.NotFoundException +import org.cryptomator.domain.exception.ParentFolderDoesNotExistException +import org.cryptomator.domain.exception.ParentFolderIsNullException +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 java.io.IOException +import java.io.OutputStream +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull + +internal class WebDavImpl(private val cloud: WebDavCloud, private val connectionHandler: ConnectionHandlerHandlerImpl, private val context: Context) { + + private val baseUrl: HttpUrl = cloud.url().toHttpUrlOrNull() ?: throw FatalBackendException("Cloud url shouldn't be null") + + private val root: RootWebDavFolder = RootWebDavFolder(cloud) + + fun root(): WebDavFolder { + return root + } + + fun resolve(path: String): WebDavFolder { + val names = path.removePrefix("/").split("/").toTypedArray() + var folder: WebDavFolder = root + for (name in names) { + folder = folder(folder, name) + } + return folder + } + + fun file(parent: WebDavFolder, name: String, size: Long?): WebDavFile { + return WebDavFile(parent, name, parent.path + '/' + name, size, null) + } + + fun folder(parent: WebDavFolder, name: String): WebDavFolder { + return WebDavFolder(parent, name, parent.path + '/' + name) + } + + @Throws(BackendException::class) + fun exists(node: WebDavNode): Boolean { + node.parent?.let { + return try { + connectionHandler.get(absoluteUriFrom(node.path), it) != null + } catch (e: NotFoundException) { + false + } + } ?: throw ParentFolderIsNullException(node.name) + } + + @Throws(BackendException::class) + fun list(folder: WebDavFolder): List { + return connectionHandler.dirList(absoluteUriFrom(folder.path), folder) + } + + @Throws(BackendException::class) + fun create(folder: WebDavFolder): WebDavFolder { + return try { + createExcludingParents(folder) + } catch (e: NotFoundException) { + folder.parent?.let { + create(it) + createExcludingParents(folder) + } ?: throw ParentFolderIsNullException(folder.name) + } catch (e: ParentFolderDoesNotExistException) { + folder.parent?.let { + create(it) + createExcludingParents(folder) + } ?: throw ParentFolderIsNullException(folder.name) + } + } + + @Throws(BackendException::class) + private fun createExcludingParents(folder: WebDavFolder): WebDavFolder { + return if (folder.parent == null) { + folder + } else { + connectionHandler.createFolder(absoluteUriFrom(folder.path), folder) + } + } + + @Throws(BackendException::class) + fun move(source: WebDavFolder, target: WebDavFolder): WebDavFolder { + moveFileOrFolder(source, target) + return WebDavFolder( + target.parent, target.name, target.path + ) + } + + @Throws(BackendException::class) + fun move(source: WebDavFile, target: WebDavFile): WebDavFile { + moveFileOrFolder(source, target) + return WebDavFile(target.parent, target.name, target.path, source.size, source.modified) + } + + @Throws(BackendException::class) + private fun moveFileOrFolder(source: WebDavNode, target: WebDavNode) { + if (exists(target)) { + throw CloudNodeAlreadyExistsException(target.name) + } + connectionHandler.move(absoluteUriFrom(source.path), absoluteUriFrom(target.path)) + } + + @Throws(BackendException::class, IOException::class) + fun write(uploadFile: WebDavFile, data: DataSource, progressAware: ProgressAware, replace: Boolean, size: Long): WebDavFile { + if (!replace && exists(uploadFile)) { + throw CloudNodeAlreadyExistsException("CloudNode already exists and replace is false") + } + + progressAware.onProgress(Progress.started(UploadState.upload(uploadFile))) + data.open(context)?.use { inputStream -> + object : TransferredBytesAwareInputStream(inputStream) { + override fun bytesTransferred(transferred: Long) { + progressAware.onProgress( // + Progress.progress(UploadState.upload(uploadFile)) // + .between(0) // + .and(size) // + .withValue(transferred) + ) + } + }.use { + connectionHandler.writeFile(absoluteUriFrom(uploadFile.path), it) + } + } ?: throw FatalBackendException("InputStream shouldn't bee null") + + return connectionHandler.get(absoluteUriFrom(uploadFile.path), uploadFile.parent) as WebDavFile? ?: throw FatalBackendException("Unable to get CloudFile after upload.") + } + + @Throws(BackendException::class) + fun checkAuthenticationAndServerCompatibility(url: String) { + connectionHandler.checkAuthenticationAndServerCompatibility(url) + } + + @Throws(BackendException::class, IOException::class) + fun read(file: CloudFile, data: OutputStream, progressAware: ProgressAware) { + progressAware.onProgress(Progress.started(DownloadState.download(file))) + connectionHandler.readFile(absoluteUriFrom(file.path)).use { inputStream -> + object : TransferredBytesAwareOutputStream(data) { + override fun bytesTransferred(transferred: Long) { + progressAware.onProgress( // + Progress.progress(DownloadState.download(file)) // + .between(0) // + .and(file.size ?: Long.MAX_VALUE) // + .withValue(transferred) + ) + } + }.use { out -> CopyStream.copyStreamToStream(inputStream, out) } + } + progressAware.onProgress(Progress.completed(DownloadState.download(file))) + } + + @Throws(BackendException::class) + fun delete(node: CloudNode) { + connectionHandler.delete(absoluteUriFrom(node.path)) + } + + private fun absoluteUriFrom(path: String): String { + return baseUrl // + .newBuilder() // + .addPathSegments(path.removePrefix("/")) // + .build() // + .toString() + } + + @Throws(BackendException::class) + fun currentAccount(): String { + checkAuthenticationAndServerCompatibility(cloud.url()) + return cloud.url() + } + +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavNode.java b/data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavNode.java deleted file mode 100644 index db3cf006..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavNode.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.cryptomator.data.cloud.webdav; - -import org.cryptomator.domain.CloudNode; - -public interface WebDavNode extends CloudNode { - - @Override - WebDavFolder getParent(); -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavNode.kt b/data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavNode.kt new file mode 100644 index 00000000..10aff33b --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavNode.kt @@ -0,0 +1,9 @@ +package org.cryptomator.data.cloud.webdav + +import org.cryptomator.domain.CloudNode + +interface WebDavNode : CloudNode { + + override val parent: WebDavFolder? + +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/webdav/network/ConnectionHandlerFactory.java b/data/src/main/java/org/cryptomator/data/cloud/webdav/network/ConnectionHandlerFactory.java deleted file mode 100644 index a4ed60c2..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/webdav/network/ConnectionHandlerFactory.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.cryptomator.data.cloud.webdav.network; - -import android.content.Context; - -import org.cryptomator.domain.WebDavCloud; - -import javax.inject.Inject; -import javax.inject.Singleton; - -@Singleton -public class ConnectionHandlerFactory { - - private final Context context; - - @Inject - public ConnectionHandlerFactory(Context context) { - this.context = context; - } - - public ConnectionHandlerHandlerImpl createConnectionHandler(WebDavCloud cloud) { - return new ConnectionHandlerHandlerImpl(new WebDavCompatibleHttpClient(cloud, context), context); - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/webdav/network/ConnectionHandlerFactory.kt b/data/src/main/java/org/cryptomator/data/cloud/webdav/network/ConnectionHandlerFactory.kt new file mode 100644 index 00000000..fcf758ca --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/webdav/network/ConnectionHandlerFactory.kt @@ -0,0 +1,14 @@ +package org.cryptomator.data.cloud.webdav.network + +import android.content.Context +import org.cryptomator.domain.WebDavCloud +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ConnectionHandlerFactory @Inject constructor(private val context: Context) { + + fun createConnectionHandler(cloud: WebDavCloud): ConnectionHandlerHandlerImpl { + return ConnectionHandlerHandlerImpl(WebDavCompatibleHttpClient(cloud, context), context) + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/webdav/network/ConnectionHandlerHandlerImpl.java b/data/src/main/java/org/cryptomator/data/cloud/webdav/network/ConnectionHandlerHandlerImpl.java deleted file mode 100644 index a7ce466f..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/webdav/network/ConnectionHandlerHandlerImpl.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.cryptomator.data.cloud.webdav.network; - -import android.content.Context; - -import org.cryptomator.data.cloud.webdav.WebDavFolder; -import org.cryptomator.data.cloud.webdav.WebDavNode; -import org.cryptomator.domain.CloudFolder; -import org.cryptomator.domain.CloudNode; -import org.cryptomator.domain.exception.BackendException; -import org.cryptomator.domain.usecases.cloud.DataSource; - -import java.io.InputStream; -import java.util.List; - -import javax.inject.Inject; - -public class ConnectionHandlerHandlerImpl { - - private final WebDavClient webDavClient; - - @Inject - ConnectionHandlerHandlerImpl(WebDavCompatibleHttpClient httpClient, Context context) { - this.webDavClient = new WebDavClient(context, httpClient); - } - - public List dirList(String url, WebDavFolder listedFolder) throws BackendException { - return webDavClient.dirList(url, listedFolder); - } - - public void move(String from, String to) throws BackendException { - webDavClient.move(from, to); - } - - public WebDavNode get(String url, CloudFolder parent) throws BackendException { - return webDavClient.get(url, parent); - } - - public void writeFile(String url, DataSource data) throws BackendException { - webDavClient.writeFile(url, data); - } - - public void delete(String url) throws BackendException { - webDavClient.delete(url); - } - - public WebDavFolder createFolder(String path, WebDavFolder folder) throws BackendException { - return webDavClient.createFolder(path, folder); - } - - public InputStream readFile(String url) throws BackendException { - return webDavClient.readFile(url); - } - - public void checkAuthenticationAndServerCompatibility(String url) throws BackendException { - webDavClient.checkAuthenticationAndServerCompatibility(url); - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/webdav/network/ConnectionHandlerHandlerImpl.kt b/data/src/main/java/org/cryptomator/data/cloud/webdav/network/ConnectionHandlerHandlerImpl.kt new file mode 100644 index 00000000..b99392f3 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/webdav/network/ConnectionHandlerHandlerImpl.kt @@ -0,0 +1,55 @@ +package org.cryptomator.data.cloud.webdav.network + +import android.content.Context +import org.cryptomator.data.cloud.webdav.WebDavFolder +import org.cryptomator.data.cloud.webdav.WebDavNode +import org.cryptomator.domain.CloudFolder +import org.cryptomator.domain.exception.BackendException +import java.io.InputStream +import javax.inject.Inject + +class ConnectionHandlerHandlerImpl @Inject internal constructor(httpClient: WebDavCompatibleHttpClient, context: Context) { + + private val webDavClient: WebDavClient = WebDavClient(context, httpClient) + + @Throws(BackendException::class) + fun dirList(url: String, listedFolder: WebDavFolder): List { + return webDavClient.dirList(url, listedFolder) + } + + @Throws(BackendException::class) + fun move(from: String, to: String) { + webDavClient.move(from, to) + } + + @Throws(BackendException::class) + fun get(url: String, parent: CloudFolder): WebDavNode? { + return webDavClient[url, parent] + } + + @Throws(BackendException::class) + fun writeFile(url: String, inputStream: InputStream) { + webDavClient.writeFile(url, inputStream) + } + + @Throws(BackendException::class) + fun delete(url: String) { + webDavClient.delete(url) + } + + @Throws(BackendException::class) + fun createFolder(path: String, folder: WebDavFolder): WebDavFolder { + return webDavClient.createFolder(path, folder) + } + + @Throws(BackendException::class) + fun readFile(url: String): InputStream { + return webDavClient.readFile(url) + } + + @Throws(BackendException::class) + fun checkAuthenticationAndServerCompatibility(url: String) { + webDavClient.checkAuthenticationAndServerCompatibility(url) + } + +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/webdav/network/DataSourceBasedRequestBody.java b/data/src/main/java/org/cryptomator/data/cloud/webdav/network/DataSourceBasedRequestBody.java deleted file mode 100644 index 0b7040d9..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/webdav/network/DataSourceBasedRequestBody.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.cryptomator.data.cloud.webdav.network; - -import android.content.Context; - -import org.cryptomator.domain.usecases.cloud.DataSource; - -import java.io.IOException; - -import okhttp3.MediaType; -import okhttp3.RequestBody; -import okio.BufferedSink; -import okio.Okio; - -class DataSourceBasedRequestBody extends RequestBody { - - private final Context context; - private final DataSource data; - - private DataSourceBasedRequestBody(Context context, DataSource data) { - this.context = context; - this.data = data; - } - - public static RequestBody from(Context context, DataSource data) { - return new DataSourceBasedRequestBody(context, data); - } - - @Override - public long contentLength() { - return data.size(context).get(); - } - - @Override - public MediaType contentType() { - return MediaType.parse("application/octet-stream"); - } - - @Override - public void writeTo(BufferedSink sink) throws IOException { - sink.writeAll(Okio.buffer(Okio.source(data.open(context)))); - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/webdav/network/DefaultTrustManager.java b/data/src/main/java/org/cryptomator/data/cloud/webdav/network/DefaultTrustManager.java deleted file mode 100644 index ad49b02d..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/webdav/network/DefaultTrustManager.java +++ /dev/null @@ -1,66 +0,0 @@ -package org.cryptomator.data.cloud.webdav.network; - -import org.cryptomator.domain.exception.NotTrustableCertificateException; - -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; - -import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; -import javax.net.ssl.X509TrustManager; - -import static org.cryptomator.data.util.X509CertificateHelper.convertToPem; - -class DefaultTrustManager implements X509TrustManager { - - private final X509TrustManager delegate; - - public DefaultTrustManager() { - this.delegate = findDefaultTrustManager(); - } - - private static X509TrustManager findDefaultTrustManager() { - try { - return tryToFindDefaultTrustManager(); - } catch (KeyStoreException | NoSuchAlgorithmException e) { - throw new IllegalStateException("Failed to obtain default trust manager", e); - } - } - - private static X509TrustManager tryToFindDefaultTrustManager() throws NoSuchAlgorithmException, KeyStoreException { - TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - trustManagerFactory.init((KeyStore) null); - for (TrustManager trustManager : trustManagerFactory.getTrustManagers()) { - if (trustManager instanceof X509TrustManager) { - return (X509TrustManager) trustManager; - } - } - throw new IllegalStateException("Failed to obtain default trust manager: No X509TrustManager available."); - } - - @Override - public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { - try { - delegate.checkClientTrusted(chain, authType); - } catch (CertificateException e) { - throw new NotTrustableCertificateException(convertToPem(chain[0]), e); - } - } - - @Override - public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { - try { - delegate.checkServerTrusted(chain, authType); - } catch (CertificateException e) { - throw new NotTrustableCertificateException(convertToPem(chain[0]), e); - } - } - - @Override - public X509Certificate[] getAcceptedIssuers() { - return delegate.getAcceptedIssuers(); - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/webdav/network/DefaultTrustManager.kt b/data/src/main/java/org/cryptomator/data/cloud/webdav/network/DefaultTrustManager.kt new file mode 100644 index 00000000..ca3d6f76 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/webdav/network/DefaultTrustManager.kt @@ -0,0 +1,67 @@ +package org.cryptomator.data.cloud.webdav.network + +import org.cryptomator.data.util.X509CertificateHelper.convertToPem +import org.cryptomator.domain.exception.NotTrustableCertificateException +import java.security.KeyStore +import java.security.KeyStoreException +import java.security.NoSuchAlgorithmException +import java.security.cert.CertificateException +import java.security.cert.X509Certificate +import javax.net.ssl.TrustManagerFactory +import javax.net.ssl.X509TrustManager + +internal class DefaultTrustManager : X509TrustManager { + + private val delegate: X509TrustManager = findDefaultTrustManager() + + @Throws(CertificateException::class) + override fun checkClientTrusted(chain: Array, authType: String) { + try { + delegate.checkClientTrusted(chain, authType) + } catch (e: CertificateException) { + throw NotTrustableCertificateException(convertToPem(chain[0]), e) + } + } + + @Throws(CertificateException::class) + override fun checkServerTrusted(chain: Array, authType: String) { + try { + delegate.checkServerTrusted(chain, authType) + } catch (e: CertificateException) { + throw NotTrustableCertificateException(convertToPem(chain[0]), e) + } + } + + override fun getAcceptedIssuers(): Array { + return delegate.acceptedIssuers + } + + companion object { + + private fun findDefaultTrustManager(): X509TrustManager { + return try { + tryToFindDefaultTrustManager() + } catch (e: KeyStoreException) { + throw IllegalStateException("Failed to obtain default trust manager", e) + } catch (e: NoSuchAlgorithmException) { + throw IllegalStateException("Failed to obtain default trust manager", e) + } + } + + @Throws(NoSuchAlgorithmException::class, KeyStoreException::class) + private fun tryToFindDefaultTrustManager(): X509TrustManager { + val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) + + trustManagerFactory.init(null as KeyStore?) + + trustManagerFactory.trustManagers.forEach { trustManager -> + if (trustManager is X509TrustManager) { + return trustManager + } + } + + throw IllegalStateException("Failed to obtain default trust manager: No X509TrustManager available.") + } + } + +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/webdav/network/InputStreamSourceBasedRequestBody.kt b/data/src/main/java/org/cryptomator/data/cloud/webdav/network/InputStreamSourceBasedRequestBody.kt new file mode 100644 index 00000000..51db86cf --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/webdav/network/InputStreamSourceBasedRequestBody.kt @@ -0,0 +1,37 @@ +package org.cryptomator.data.cloud.webdav.network + +import java.io.IOException +import java.io.InputStream +import okhttp3.MediaType +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.RequestBody +import okio.BufferedSink +import okio.source + + +internal class InputStreamSourceBasedRequestBody private constructor(private val inputStream: InputStream) : RequestBody() { + + @Throws(IOException::class) + override fun contentLength(): Long { + return inputStream.available().toLong() + } + + override fun contentType(): MediaType? { + return "application/octet-stream".toMediaTypeOrNull() + } + + @Throws(IOException::class) + override fun writeTo(sink: BufferedSink) { + inputStream.source().use { + sink.writeAll(it) + } + } + + companion object { + + fun from(data: InputStream): RequestBody { + return InputStreamSourceBasedRequestBody(data) + } + + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/webdav/network/PinningTrustManager.java b/data/src/main/java/org/cryptomator/data/cloud/webdav/network/PinningTrustManager.java deleted file mode 100644 index 232baa84..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/webdav/network/PinningTrustManager.java +++ /dev/null @@ -1,91 +0,0 @@ -package org.cryptomator.data.cloud.webdav.network; - -import org.cryptomator.data.util.X509CertificateHelper; -import org.cryptomator.domain.exception.FatalBackendException; -import org.cryptomator.domain.exception.NotTrustableCertificateException; -import org.cryptomator.util.Optional; - -import java.security.cert.Certificate; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; - -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.SSLPeerUnverifiedException; -import javax.net.ssl.SSLSession; -import javax.net.ssl.X509TrustManager; - -import okhttp3.CertificatePinner; - -/** - * An {@link X509TrustManager} which always trusts one specific certificate but denies all others. - */ -class PinningTrustManager implements X509TrustManager { - - private final String expectedPin; - - /** - * Creates a {@code PinningTrustManager} which trusts the provided certificate. - * - * @param trustedCertPemEncoded the {@link X509Certificate} to trust in PEM encoded form - */ - public PinningTrustManager(String trustedCertPemEncoded) { - try { - X509Certificate trustedCert = X509CertificateHelper.convertFromPem(trustedCertPemEncoded); - expectedPin = CertificatePinner.pin(trustedCert); - } catch (CertificateException e) { - throw new FatalBackendException(e); - } - } - - @Override - public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { - if (!isPinnedCertificate(chain[0])) { - throw new NotTrustableCertificateException(X509CertificateHelper.convertToPem(chain[0])); - } - } - - @Override - public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { - if (!isPinnedCertificate(chain[0])) { - throw new NotTrustableCertificateException(X509CertificateHelper.convertToPem(chain[0])); - } - } - - private boolean isPinnedCertificate(X509Certificate certificate) { - return expectedPin.equals(CertificatePinner.pin(certificate)); - } - - @Override - public X509Certificate[] getAcceptedIssuers() { - return new X509Certificate[0]; - } - - /** - * @return a HostnameVerifier accepting any host when the pinned certificate is used and denying all other - */ - public HostnameVerifier hostnameVerifier() { - return new HostnameVerifier() { - @Override - public boolean verify(String hostname, SSLSession session) { - Optional peerX509Cert = peerX509Cert(session); - if (peerX509Cert.isPresent()) { - return isPinnedCertificate(peerX509Cert.get()); - } else { - return false; - } - } - - private Optional peerX509Cert(SSLSession session) { - try { - Certificate[] certificates = session.getPeerCertificates(); - if (certificates != null && certificates.length > 0 && certificates[0] instanceof X509Certificate) { - return Optional.of((X509Certificate) certificates[0]); - } - } catch (SSLPeerUnverifiedException e) { - // leads to return of Optional.empty(), intended! - } - return Optional.empty(); - } - }; - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/webdav/network/PinningTrustManager.kt b/data/src/main/java/org/cryptomator/data/cloud/webdav/network/PinningTrustManager.kt new file mode 100644 index 00000000..088354ac --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/webdav/network/PinningTrustManager.kt @@ -0,0 +1,81 @@ +package org.cryptomator.data.cloud.webdav.network + +import org.cryptomator.data.util.X509CertificateHelper +import org.cryptomator.domain.exception.FatalBackendException +import org.cryptomator.domain.exception.NotTrustableCertificateException +import java.security.cert.CertificateException +import java.security.cert.X509Certificate +import javax.net.ssl.HostnameVerifier +import javax.net.ssl.SSLPeerUnverifiedException +import javax.net.ssl.SSLSession +import javax.net.ssl.X509TrustManager +import okhttp3.CertificatePinner + +/** + * An [X509TrustManager] which always trusts one specific certificate but denies all others. + */ +internal class PinningTrustManager(trustedCertPemEncoded: String) : X509TrustManager { + + private var expectedPin: String? = null + + @Throws(CertificateException::class) + override fun checkClientTrusted(chain: Array, authType: String) { + if (!isPinnedCertificate(chain[0])) { + throw NotTrustableCertificateException(X509CertificateHelper.convertToPem(chain[0])) + } + } + + @Throws(CertificateException::class) + override fun checkServerTrusted(chain: Array, authType: String) { + if (!isPinnedCertificate(chain[0])) { + throw NotTrustableCertificateException(X509CertificateHelper.convertToPem(chain[0])) + } + } + + private fun isPinnedCertificate(certificate: X509Certificate): Boolean { + return expectedPin == CertificatePinner.pin(certificate) + } + + override fun getAcceptedIssuers(): Array { + return arrayOfNulls(0) + } + + /** + * @return a HostnameVerifier accepting any host when the pinned certificate is used and denying all other + */ + fun hostnameVerifier(): HostnameVerifier { + + return object : HostnameVerifier { + + override fun verify(hostname: String, session: SSLSession): Boolean { + return peerX509Cert(session)?.let { isPinnedCertificate(it) } ?: false + } + + private fun peerX509Cert(session: SSLSession): X509Certificate? { + try { + val certificates = session.peerCertificates + if (certificates != null && certificates.isNotEmpty() && certificates[0] is X509Certificate) { + return certificates[0] as X509Certificate + } + } catch (e: SSLPeerUnverifiedException) { + // leads to return of null, intended! + } + return null + } + } + } + + /** + * Creates a `PinningTrustManager` which trusts the provided certificate. + * + * @param trustedCertPemEncoded the [X509Certificate] to trust in PEM encoded form + */ + init { + expectedPin = try { + val trustedCert = X509CertificateHelper.convertFromPem(trustedCertPemEncoded) + CertificatePinner.pin(trustedCert) + } catch (e: CertificateException) { + throw FatalBackendException(e) + } + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/webdav/network/PropfindEntryData.java b/data/src/main/java/org/cryptomator/data/cloud/webdav/network/PropfindEntryData.java deleted file mode 100644 index 5c5335af..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/webdav/network/PropfindEntryData.java +++ /dev/null @@ -1,89 +0,0 @@ -package org.cryptomator.data.cloud.webdav.network; - -import org.cryptomator.data.cloud.webdav.WebDavFile; -import org.cryptomator.data.cloud.webdav.WebDavFolder; -import org.cryptomator.data.cloud.webdav.WebDavNode; -import org.cryptomator.util.Optional; - -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; -import java.util.Date; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -class PropfindEntryData { - - private static final Pattern URI_PATTERN = Pattern.compile("^[a-z]+://[^/]+/(.*)$"); - - private String path; - private String[] pathSegments; - - private boolean file = true; - private Optional lastModified = Optional.empty(); - private Optional size = Optional.empty(); - - private String extractPath(String pathOrUri) { - Matcher matcher = URI_PATTERN.matcher(pathOrUri); - if (matcher.matches()) { - return urlDecode(matcher.group(1)); - } else if (!pathOrUri.startsWith("/")) { - return urlDecode("/" + pathOrUri); - } else { - return urlDecode(pathOrUri); - } - } - - void setLastModified(Optional lastModified) { - this.lastModified = lastModified; - } - - public String getPath() { - return path; - } - - public void setPath(String pathOrUri) { - this.path = extractPath(pathOrUri); - this.pathSegments = path.split("/"); - } - - public Optional getSize() { - return size; - } - - public void setSize(Optional size) { - this.size = size; - } - - private boolean isFile() { - return file; - } - - public void setFile(boolean file) { - this.file = file; - } - - public WebDavNode toCloudNode(WebDavFolder parent) { - if (isFile()) { - return new WebDavFile(parent, getName(), size, lastModified); - } else { - return new WebDavFolder(parent, getName()); - } - } - - private String urlDecode(String value) { - try { - return URLDecoder.decode(value, "UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new IllegalStateException("UTF-8 must be supported by every JVM", e); - } - } - - int getDepth() { - return pathSegments.length; - } - - private String getName() { - return pathSegments[pathSegments.length - 1]; - } - -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/webdav/network/PropfindEntryData.kt b/data/src/main/java/org/cryptomator/data/cloud/webdav/network/PropfindEntryData.kt new file mode 100644 index 00000000..51448cd1 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/webdav/network/PropfindEntryData.kt @@ -0,0 +1,81 @@ +package org.cryptomator.data.cloud.webdav.network + +import org.cryptomator.data.cloud.webdav.WebDavFile +import org.cryptomator.data.cloud.webdav.WebDavFolder +import org.cryptomator.data.cloud.webdav.WebDavNode +import java.io.UnsupportedEncodingException +import java.net.URLDecoder +import java.util.Date +import java.util.regex.Pattern + +internal class PropfindEntryData { + + var path: String? = null + private set + + lateinit var pathSegments: Array + + private var isFile = true + + private var lastModified: Date? = null + + var size: Long? = null + + private fun extractPath(pathOrUri: String): String { + val matcher = URI_PATTERN.matcher(pathOrUri) + return if (matcher.matches()) { + urlDecode(matcher.group(1)) + } else if (!pathOrUri.startsWith("/")) { + urlDecode("/$pathOrUri") + } else { + urlDecode(pathOrUri) + } + } + + fun setLastModified(lastModified: Date?) { + this.lastModified = lastModified + } + + fun setPath(pathOrUri: String) { + path = extractPath(pathOrUri).also { + var pathSegs = it.split("/") + if(pathSegs.last() == "") { + pathSegs = pathSegs.subList(0, pathSegs.size - 1) + } + pathSegments = pathSegs.toTypedArray() + } + } + + fun toCloudNode(parent: WebDavFolder): WebDavNode { + return if (isFile) { + WebDavFile(parent, getName(), size, lastModified) + } else { + WebDavFolder(parent, getName(), parent.path + '/' + getName()) + } + } + + private fun urlDecode(value: String): String { + return try { + URLDecoder.decode(value, "UTF-8") + } catch (e: UnsupportedEncodingException) { + throw IllegalStateException("UTF-8 must be supported by every JVM", e) + } + } + + fun getDepth(): Int { + return pathSegments.size + } + + private fun getName(): String { + return pathSegments[pathSegments.size - 1] + } + + fun setFile(boolean: Boolean) { + isFile = boolean + } + + companion object { + + private val URI_PATTERN = Pattern.compile("^[a-z]+://[^/]+/(.*)$") + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/webdav/network/PropfindResponseParser.java b/data/src/main/java/org/cryptomator/data/cloud/webdav/network/PropfindResponseParser.java deleted file mode 100644 index 55d2de3f..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/webdav/network/PropfindResponseParser.java +++ /dev/null @@ -1,188 +0,0 @@ -package org.cryptomator.data.cloud.webdav.network; - -import org.cryptomator.data.cloud.webdav.WebDavFolder; -import org.cryptomator.domain.exception.FatalBackendException; -import org.cryptomator.util.Optional; -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; -import org.xmlpull.v1.XmlPullParserFactory; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -import timber.log.Timber; - -class PropfindResponseParser { - - private static final String TAG_RESPONSE = "response"; - private static final String TAG_HREF = "href"; - private static final String TAG_COLLECTION = "collection"; - private static final String TAG_LAST_MODIFIED = "getlastmodified"; - private static final String TAG_CONTENT_LENGTH = "getcontentlength"; - private static final String TAG_PROPSTAT = "propstat"; - private static final String TAG_STATUS = "status"; - private static final String STATUS_OK = "200"; - - private final WebDavFolder requestedFolder; - private final XmlPullParser xmlPullParser; - - PropfindResponseParser(WebDavFolder requestedFolder) { - this.requestedFolder = requestedFolder; - try { - this.xmlPullParser = XmlPullParserFactory.newInstance().newPullParser(); - } catch (XmlPullParserException e) { - throw new FatalBackendException(e); - } - } - - List parse(InputStream responseBody) throws XmlPullParserException, IOException { - List entryData = new ArrayList<>(); - xmlPullParser.setInput(responseBody, "UTF-8"); - - while (skipToStartOf(TAG_RESPONSE)) { - PropfindEntryData entry = parseResponse(); - if (entry != null) { - entryData.add(entry); - } - } - - return entryData; - } - - private boolean skipToStartOf(String tag) throws XmlPullParserException, IOException { - do { - xmlPullParser.next(); - } while (!endOfDocument() && !startOf(tag)); - return startOf(tag); - } - - private PropfindEntryData parseResponse() throws XmlPullParserException, IOException { - PropfindEntryData entry = null; - String path = null; - - while (nextTagUntilEndOf(TAG_RESPONSE)) { - if (tagIs(TAG_PROPSTAT)) { - entry = defaultIfNull(parsePropstatWith200Status(), entry); - } else if (tagIs(TAG_HREF)) { - path = textInCurrentTag().trim(); - } - } - - if (entry == null) { - Timber.tag("WebDAV").w("No propstat element with 200 status in response element. Entry ignored."); - Timber.tag("WebDAV").v("No propstat element with 200 status in response element. Entry ignored. Dir: %s, Path: %s", requestedFolder.getPath(), path); - return null; - } - if (path == null) { - Timber.tag("WebDAV").w("Missing href in response element. Entry ignored."); - Timber.tag("WebDAV").v("Missing href in response element. Entry ignored. Dir: %s", requestedFolder.getPath()); - return null; - } - - entry.setPath(path); - return entry; - } - - private PropfindEntryData parsePropstatWith200Status() throws IOException, XmlPullParserException { - PropfindEntryData result = new PropfindEntryData(); - boolean statusOk = false; - while (nextTagUntilEndOf(TAG_PROPSTAT)) { - if (tagIs(TAG_STATUS)) { - String text = textInCurrentTag().trim(); - String[] statusSegments = text.split(" "); - String code = statusSegments.length > 0 ? statusSegments[1] : ""; - statusOk = STATUS_OK.equals(code); - } else if (tagIs(TAG_COLLECTION)) { - result.setFile(false); - } else if (tagIs(TAG_LAST_MODIFIED)) { - result.setLastModified(parseDate(textInCurrentTag())); - } else if (tagIs(TAG_CONTENT_LENGTH)) { - result.setSize(parseLong(textInCurrentTag())); - } - } - if (statusOk) { - return result; - } else { - return null; - } - } - - private boolean nextTagUntilEndOf(String tag) throws XmlPullParserException, IOException { - do { - xmlPullParser.next(); - } while (!endOfDocument() && !startOfATag() && !endOf(tag)); - return startOfATag(); - } - - private boolean startOf(String tag) throws XmlPullParserException { - return startOfATag() && tagIs(tag); - } - - private boolean tagIs(String tag) { - return tag.equalsIgnoreCase(localName()); - } - - private boolean startOfATag() throws XmlPullParserException { - return xmlPullParser.getEventType() == XmlPullParser.START_TAG; - } - - private boolean endOf(String tag) throws XmlPullParserException { - return xmlPullParser.getEventType() == XmlPullParser.END_TAG && tag.equalsIgnoreCase(localName()); - } - - private String localName() { - String rawName = xmlPullParser.getName(); - String[] namespaceAndLocalName = rawName.split(":", 2); - return namespaceAndLocalName[namespaceAndLocalName.length - 1]; - } - - private boolean endOfDocument() throws XmlPullParserException { - return xmlPullParser.getEventType() == XmlPullParser.END_DOCUMENT; - } - - private String textInCurrentTag() throws IOException, XmlPullParserException { - if (!startOfATag()) { - throw new IllegalStateException("textInCurrentTag may only be called at start of a tag"); - } - StringBuilder result = new StringBuilder(); - int ident = 0; - do { - switch (xmlPullParser.next()) { - case XmlPullParser.TEXT: - result.append(xmlPullParser.getText()); - break; - case XmlPullParser.START_TAG: - ident++; - break; - case XmlPullParser.END_TAG: - ident--; - break; - } - } while (!endOfDocument() && ident >= 0); - return result.toString(); - } - - private PropfindEntryData defaultIfNull(PropfindEntryData value, PropfindEntryData defaultValue) { - return value == null ? defaultValue : value; - } - - private Optional parseDate(String text) { - try { - return Optional.of(new Date(text)); - } catch (IllegalArgumentException e) { - return Optional.empty(); - } - } - - private Optional parseLong(String text) { - try { - return Optional.of(Long.parseLong(text)); - } catch (NumberFormatException e) { - return Optional.empty(); - } - } - -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/webdav/network/PropfindResponseParser.kt b/data/src/main/java/org/cryptomator/data/cloud/webdav/network/PropfindResponseParser.kt new file mode 100644 index 00000000..1e9856c6 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/webdav/network/PropfindResponseParser.kt @@ -0,0 +1,186 @@ +package org.cryptomator.data.cloud.webdav.network + +import org.cryptomator.data.cloud.webdav.WebDavFolder +import org.cryptomator.domain.exception.FatalBackendException +import org.xmlpull.v1.XmlPullParser +import org.xmlpull.v1.XmlPullParserException +import org.xmlpull.v1.XmlPullParserFactory +import java.io.IOException +import java.io.InputStream +import java.util.ArrayList +import java.util.Date +import timber.log.Timber + +internal class PropfindResponseParser(private val requestedFolder: WebDavFolder) { + + private var xmlPullParser: XmlPullParser + + @Throws(XmlPullParserException::class, IOException::class) + fun parse(responseBody: InputStream?): List { + val entryData: MutableList = ArrayList() + xmlPullParser.setInput(responseBody, "UTF-8") + while (skipToStartOf(TAG_RESPONSE)) { + val entry = parseResponse() + if (entry != null) { + entryData.add(entry) + } + } + return entryData + } + + @Throws(XmlPullParserException::class, IOException::class) + private fun skipToStartOf(tag: String): Boolean { + do { + xmlPullParser.next() + } while (!endOfDocument() && !startOf(tag)) + return startOf(tag) + } + + @Throws(XmlPullParserException::class, IOException::class) + private fun parseResponse(): PropfindEntryData? { + var entry: PropfindEntryData? = null + var path: String? = null + while (nextTagUntilEndOf(TAG_RESPONSE)) { + if (tagIs(TAG_PROPSTAT)) { + entry = defaultIfNull(parsePropstatWith200Status(), entry) + } else if (tagIs(TAG_HREF)) { + path = textInCurrentTag().trim { it <= ' ' } + } + } + if (entry == null) { + Timber.tag("WebDAV").w("No propstat element with 200 status in response element. Entry ignored.") + Timber.tag("WebDAV").v("No propstat element with 200 status in response element. Entry ignored. Dir: %s, Path: %s", requestedFolder.path, path) + return null + } + if (path == null) { + Timber.tag("WebDAV").w("Missing href in response element. Entry ignored.") + Timber.tag("WebDAV").v("Missing href in response element. Entry ignored. Dir: %s", requestedFolder.path) + return null + } + entry.setPath(path) + return entry + } + + @Throws(IOException::class, XmlPullParserException::class) + private fun parsePropstatWith200Status(): PropfindEntryData? { + val result = PropfindEntryData() + var statusOk = false + while (nextTagUntilEndOf(TAG_PROPSTAT)) { + when { + tagIs(TAG_STATUS) -> { + val text = textInCurrentTag().trim { it <= ' ' } + val statusSegments = text.split(" ".toRegex()).toTypedArray() + val code = if (statusSegments.isNotEmpty()) statusSegments[1] else "" + statusOk = STATUS_OK == code + } + tagIs(TAG_COLLECTION) -> { + result.setFile(false) + } + tagIs(TAG_LAST_MODIFIED) -> { + result.setLastModified(parseDate(textInCurrentTag())) + } + tagIs(TAG_CONTENT_LENGTH) -> { + result.size = parseLong(textInCurrentTag()) + } + } + } + return if (statusOk) { + result + } else { + null + } + } + + @Throws(XmlPullParserException::class, IOException::class) + private fun nextTagUntilEndOf(tag: String): Boolean { + do { + xmlPullParser.next() + } while (!endOfDocument() && !startOfATag() && !endOf(tag)) + return startOfATag() + } + + @Throws(XmlPullParserException::class) + private fun startOf(tag: String): Boolean { + return startOfATag() && tagIs(tag) + } + + private fun tagIs(tag: String): Boolean { + return tag.equals(localName(), ignoreCase = true) + } + + @Throws(XmlPullParserException::class) + private fun startOfATag(): Boolean { + return xmlPullParser.eventType == XmlPullParser.START_TAG + } + + @Throws(XmlPullParserException::class) + private fun endOf(tag: String): Boolean { + return xmlPullParser.eventType == XmlPullParser.END_TAG && tag.equals(localName(), ignoreCase = true) + } + + private fun localName(): String { + val rawName = xmlPullParser.name + val namespaceAndLocalName = rawName.split(":".toRegex(), 2).toTypedArray() + return namespaceAndLocalName[namespaceAndLocalName.size - 1] + } + + @Throws(XmlPullParserException::class) + private fun endOfDocument(): Boolean { + return xmlPullParser.eventType == XmlPullParser.END_DOCUMENT + } + + @Throws(IOException::class, XmlPullParserException::class) + private fun textInCurrentTag(): String { + check(startOfATag()) { "textInCurrentTag may only be called at start of a tag" } + val result = StringBuilder() + var ident = 0 + do { + when (xmlPullParser.next()) { + XmlPullParser.TEXT -> result.append(xmlPullParser.text) + XmlPullParser.START_TAG -> ident++ + XmlPullParser.END_TAG -> ident-- + } + } while (!endOfDocument() && ident >= 0) + return result.toString() + } + + private fun defaultIfNull(value: PropfindEntryData?, defaultValue: PropfindEntryData?): PropfindEntryData? { + return value ?: defaultValue + } + + private fun parseDate(text: String): Date? { + return try { + Date(text) + } catch (e: IllegalArgumentException) { + null + } + } + + private fun parseLong(text: String): Long? { + return try { + text.toLong() + } catch (e: NumberFormatException) { + null + } + } + + companion object { + + private const val TAG_RESPONSE = "response" + private const val TAG_HREF = "href" + private const val TAG_COLLECTION = "collection" + private const val TAG_LAST_MODIFIED = "getlastmodified" + private const val TAG_CONTENT_LENGTH = "getcontentlength" + private const val TAG_PROPSTAT = "propstat" + private const val TAG_STATUS = "status" + private const val STATUS_OK = "200" + } + + init { + try { + xmlPullParser = XmlPullParserFactory.newInstance().newPullParser() + } catch (e: XmlPullParserException) { + throw FatalBackendException(e) + } + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/webdav/network/SSLSocketFactories.java b/data/src/main/java/org/cryptomator/data/cloud/webdav/network/SSLSocketFactories.java deleted file mode 100644 index 9f66a72f..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/webdav/network/SSLSocketFactories.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.cryptomator.data.cloud.webdav.network; - -import org.cryptomator.domain.exception.FatalBackendException; - -import java.security.GeneralSecurityException; - -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.TrustManager; -import javax.net.ssl.X509TrustManager; - -class SSLSocketFactories { - - public static SSLSocketFactory from(X509TrustManager trustManager) { - try { - SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(null, new TrustManager[] {trustManager}, null); - return sslContext.getSocketFactory(); - } catch (GeneralSecurityException e) { - throw new FatalBackendException(e); - } - } - -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/webdav/network/SSLSocketFactories.kt b/data/src/main/java/org/cryptomator/data/cloud/webdav/network/SSLSocketFactories.kt new file mode 100644 index 00000000..b2aba76c --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/webdav/network/SSLSocketFactories.kt @@ -0,0 +1,21 @@ +package org.cryptomator.data.cloud.webdav.network + +import org.cryptomator.domain.exception.FatalBackendException +import java.security.GeneralSecurityException +import javax.net.ssl.SSLContext +import javax.net.ssl.SSLSocketFactory +import javax.net.ssl.TrustManager +import javax.net.ssl.X509TrustManager + +internal object SSLSocketFactories { + + fun from(trustManager: X509TrustManager): SSLSocketFactory { + return try { + val sslContext = SSLContext.getInstance("TLS") + sslContext.init(null, arrayOf(trustManager), null) + sslContext.socketFactory + } catch (e: GeneralSecurityException) { + throw FatalBackendException(e) + } + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/webdav/network/WebDavClient.java b/data/src/main/java/org/cryptomator/data/cloud/webdav/network/WebDavClient.java deleted file mode 100644 index f913e13e..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/webdav/network/WebDavClient.java +++ /dev/null @@ -1,317 +0,0 @@ -package org.cryptomator.data.cloud.webdav.network; - -import android.content.Context; - -import org.cryptomator.data.cloud.webdav.WebDavFolder; -import org.cryptomator.data.cloud.webdav.WebDavNode; -import org.cryptomator.domain.CloudFolder; -import org.cryptomator.domain.CloudNode; -import org.cryptomator.domain.exception.AlreadyExistException; -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.NotFoundException; -import org.cryptomator.domain.exception.ParentFolderDoesNotExistException; -import org.cryptomator.domain.exception.ServerNotWebdavCompatibleException; -import org.cryptomator.domain.exception.TypeMismatchException; -import org.cryptomator.domain.exception.UnauthorizedException; -import org.cryptomator.domain.usecases.cloud.DataSource; -import org.xmlpull.v1.XmlPullParserException; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; - -import okhttp3.MediaType; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; -import okhttp3.ResponseBody; - -import static java.util.Collections.sort; - -class WebDavClient { - - private final Context context; - private final WebDavCompatibleHttpClient httpClient; - private final Comparator ASCENDING_BY_DEPTH = (o1, o2) -> o1.getDepth() - o2.getDepth(); - - WebDavClient(Context context, WebDavCompatibleHttpClient httpClient) { - this.context = context; - this.httpClient = httpClient; - } - - List dirList(String url, WebDavFolder listedFolder) throws BackendException { - try (Response response = executePropfindRequest(url, PropfindDepth.ONE)) { - checkPropfindExecutionSucceeded(response.code()); - - List nodes = getEntriesFromResponse(listedFolder, response); - - return processDirList(nodes, listedFolder); - } catch (IOException | XmlPullParserException e) { - throw new FatalBackendException(e); - } - } - - public WebDavNode get(String url, CloudFolder parent) throws BackendException { - try (Response response = executePropfindRequest(url, PropfindDepth.ZERO)) { - checkPropfindExecutionSucceeded(response.code()); - - List nodes = getEntriesFromResponse((WebDavFolder) parent, response); - - return processGet(nodes, (WebDavFolder) parent); - } catch (IOException | XmlPullParserException e) { - throw new FatalBackendException(e); - } - } - - private Response executePropfindRequest(String url, PropfindDepth depth) throws IOException { - String body = "\n" // - + "\n" // - + "\n" // - + "\n" // - + "\n" // - + "\n" // - + "\n" // - + ""; - - Request.Builder builder = new Request.Builder() // - .method("PROPFIND", RequestBody.create(MediaType.parse(body), body)) // - .url(url) // - .header("DEPTH", depth.value) // - .header("Content-Type", "text/xml"); - - return httpClient.execute(builder); - } - - private void checkPropfindExecutionSucceeded(int responseCode) throws BackendException { - switch (responseCode) { - case HttpURLConnection.HTTP_UNAUTHORIZED: - throw new UnauthorizedException(); - case HttpURLConnection.HTTP_FORBIDDEN: - throw new ForbiddenException(); - case HttpURLConnection.HTTP_NOT_FOUND: - throw new NotFoundException(); - } - - if (responseCode < 199 || responseCode > 300) { - throw new FatalBackendException("Response code isn't between 200 and 300: " + responseCode); - } - } - - private List getEntriesFromResponse(WebDavFolder listedFolder, Response response) throws IOException, XmlPullParserException { - try (final ResponseBody responseBody = response.body()) { - return new PropfindResponseParser(listedFolder).parse(responseBody.byteStream()); - } - } - - public void move(String from, String to) throws BackendException { - Request.Builder builder = new Request.Builder() // - .method("MOVE", null) // - .url(from) // - .header("Content-Type", "text/xml") // - .header("Destination", to) // - .header("Depth", "infinity") // - .header("Overwrite", "F"); - - try (Response response = httpClient.execute(builder)) { - if (!response.isSuccessful()) { - switch (response.code()) { - case HttpURLConnection.HTTP_UNAUTHORIZED: - throw new UnauthorizedException(); - case HttpURLConnection.HTTP_FORBIDDEN: - throw new ForbiddenException(); - case HttpURLConnection.HTTP_NOT_FOUND: - throw new NotFoundException(); - case HttpURLConnection.HTTP_CONFLICT: - throw new ParentFolderDoesNotExistException(); - case HttpURLConnection.HTTP_PRECON_FAILED: - throw new CloudNodeAlreadyExistsException(to); - default: - throw new FatalBackendException("Response code isn't between 200 and 300: " + response.code()); - } - } - } catch (IOException e) { - throw new FatalBackendException(e); - } - } - - InputStream readFile(String url) throws BackendException { - Request.Builder builder = new Request.Builder() // - .get() // - .url(url); - - Response response = null; - boolean success = false; - - try { - response = httpClient.execute(builder); - - if (response.isSuccessful()) { - success = true; - return response.body().byteStream(); - } else { - switch (response.code()) { - case HttpURLConnection.HTTP_UNAUTHORIZED: - throw new UnauthorizedException(); - case HttpURLConnection.HTTP_FORBIDDEN: - throw new ForbiddenException(); - case HttpURLConnection.HTTP_NOT_FOUND: - throw new NotFoundException(); - case 416: // UNSATISFIABLE_RANGE - return new ByteArrayInputStream(new byte[0]); - default: - throw new FatalBackendException("Response code isn't between 200 and 300: " + response.code()); - } - } - } catch (IOException e) { - throw new FatalBackendException(e); - } finally { - if (response != null && !success) { - response.close(); - } - } - } - - void writeFile(String url, DataSource data) throws BackendException { - Request.Builder builder = new Request.Builder() // - .put(DataSourceBasedRequestBody.from(context, data)) // - .url(url); - - try (Response response = httpClient.execute(builder)) { - if (!response.isSuccessful()) { - switch (response.code()) { - case HttpURLConnection.HTTP_UNAUTHORIZED: - throw new UnauthorizedException(); - case HttpURLConnection.HTTP_FORBIDDEN: - throw new ForbiddenException(); - case HttpURLConnection.HTTP_BAD_METHOD: - throw new TypeMismatchException(); - case HttpURLConnection.HTTP_CONFLICT: // fall through - case HttpURLConnection.HTTP_NOT_FOUND: // necessary due to a bug in Nextcloud, see https://github.com/nextcloud/server/issues/23519 - throw new ParentFolderDoesNotExistException(); - default: - throw new FatalBackendException("Response code isn't between 200 and 300: " + response.code()); - } - } - } catch (IOException e) { - throw new FatalBackendException(e); - } - } - - WebDavFolder createFolder(String path, WebDavFolder folder) throws BackendException { - Request.Builder builder = new Request.Builder() // - .method("MKCOL", null) // - .url(path); - - try (Response response = httpClient.execute(builder)) { - if (response.isSuccessful()) { - return folder; - } else { - switch (response.code()) { - case HttpURLConnection.HTTP_UNAUTHORIZED: - throw new UnauthorizedException(); - case HttpURLConnection.HTTP_FORBIDDEN: - throw new ForbiddenException(); - case HttpURLConnection.HTTP_BAD_METHOD: - throw new AlreadyExistException(); - case HttpURLConnection.HTTP_CONFLICT: - throw new ParentFolderDoesNotExistException(); - default: - throw new FatalBackendException("Response code isn't between 200 and 300: " + response.code()); - } - } - } catch (IOException e) { - throw new FatalBackendException(e); - } - } - - public void delete(String url) throws BackendException { - Request.Builder builder = new Request.Builder() // - .delete() // - .url(url); - - try (Response response = httpClient.execute(builder)) { - if (!response.isSuccessful()) { - switch (response.code()) { - case HttpURLConnection.HTTP_UNAUTHORIZED: - throw new UnauthorizedException(); - case HttpURLConnection.HTTP_FORBIDDEN: - throw new ForbiddenException(); - case HttpURLConnection.HTTP_NOT_FOUND: - throw new NotFoundException(String.format("Node %s doesn't exists", url)); - default: - throw new FatalBackendException("Response code isn't between 200 and 300: " + response.code()); - } - } - } catch (IOException e) { - throw new FatalBackendException(e); - } - } - - void checkAuthenticationAndServerCompatibility(String url) throws BackendException { - final Request.Builder optionsRequest = new Request.Builder() // - .method("OPTIONS", null) // - .url(url); - - try (Response response = httpClient.execute(optionsRequest)) { - if (response.isSuccessful()) { - final boolean containsDavHeader = response.headers().names().contains("DAV"); - if (!containsDavHeader) { - throw new ServerNotWebdavCompatibleException(); - } - } else { - switch (response.code()) { - case HttpURLConnection.HTTP_UNAUTHORIZED: - throw new UnauthorizedException(); - case HttpURLConnection.HTTP_FORBIDDEN: - throw new ForbiddenException(); - default: - throw new FatalBackendException("Response code isn't between 200 and 300: " + response.code()); - } - } - } catch (IOException e) { - throw new FatalBackendException(e); - } - - try (Response response = executePropfindRequest(url, PropfindDepth.ZERO)) { - checkPropfindExecutionSucceeded(response.code()); - } catch (IOException e) { - throw new FatalBackendException(e); - } - } - - private List processDirList(List entryData, WebDavFolder requestedFolder) { - List result = new ArrayList<>(); - sort(entryData, ASCENDING_BY_DEPTH); - // after sorting the first entry is the parent - // because it's depth is 1 smaller than the depth - // ot the other entries, thus we skip the first entry - for (PropfindEntryData childEntry : entryData.subList(1, entryData.size())) { - result.add(childEntry.toCloudNode(requestedFolder)); - } - return result; - } - - private WebDavNode processGet(List entryData, WebDavFolder requestedFolder) { - sort(entryData, ASCENDING_BY_DEPTH); - return entryData.size() >= 1 ? entryData.get(0).toCloudNode(requestedFolder) : null; - } - - private enum PropfindDepth { - ZERO("0"), // - ONE("1"), // - INFINITY("infinity"); - - private final String value; - - PropfindDepth(final String value) { - this.value = value; - } - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/webdav/network/WebDavClient.kt b/data/src/main/java/org/cryptomator/data/cloud/webdav/network/WebDavClient.kt new file mode 100644 index 00000000..2f55d0e0 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/webdav/network/WebDavClient.kt @@ -0,0 +1,270 @@ +package org.cryptomator.data.cloud.webdav.network + +import android.content.Context +import org.cryptomator.data.cloud.webdav.WebDavFolder +import org.cryptomator.data.cloud.webdav.WebDavNode +import org.cryptomator.domain.CloudFolder +import org.cryptomator.domain.exception.AlreadyExistException +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.NotFoundException +import org.cryptomator.domain.exception.ParentFolderDoesNotExistException +import org.cryptomator.domain.exception.ServerNotWebdavCompatibleException +import org.cryptomator.domain.exception.TypeMismatchException +import org.cryptomator.domain.exception.UnauthorizedException +import org.xmlpull.v1.XmlPullParserException +import java.io.ByteArrayInputStream +import java.io.IOException +import java.io.InputStream +import java.net.HttpURLConnection +import java.util.ArrayList +import java.util.Collections +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response + +internal class WebDavClient(private val context: Context, private val httpClient: WebDavCompatibleHttpClient) { + + private val ASCENDING_BY_DEPTH = Comparator { o1: PropfindEntryData, o2: PropfindEntryData -> o1.getDepth() - o2.getDepth() } + + @Throws(BackendException::class) + fun dirList(url: String, listedFolder: WebDavFolder): List { + try { + executePropfindRequest(url, PropfindDepth.ONE).use { response -> + checkPropfindExecutionSucceeded(response.code) + val nodes = getEntriesFromResponse(listedFolder, response) + return processDirList(nodes, listedFolder) + } + } catch (e: IOException) { + throw FatalBackendException(e) + } catch (e: XmlPullParserException) { + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + operator fun get(url: String, parent: CloudFolder): WebDavNode? { + try { + executePropfindRequest(url, PropfindDepth.ZERO).use { response -> + checkPropfindExecutionSucceeded(response.code) + val nodes = getEntriesFromResponse(parent as WebDavFolder, response) + return processGet(nodes, parent) + } + } catch (e: IOException) { + throw FatalBackendException(e) + } catch (e: XmlPullParserException) { + throw FatalBackendException(e) + } + } + + @Throws(IOException::class) + private fun executePropfindRequest(url: String, depth: PropfindDepth): Response { + val body = """ + + + + + + + """ + val builder = Request.Builder() // + .method("PROPFIND", body.toRequestBody(body.toMediaTypeOrNull())) // + .url(url) // + .header("DEPTH", depth.value) // + .header("Content-Type", "text/xml") + return httpClient.execute(builder) + } + + @Throws(BackendException::class) + private fun checkPropfindExecutionSucceeded(responseCode: Int) { + when (responseCode) { + HttpURLConnection.HTTP_UNAUTHORIZED -> throw UnauthorizedException() + HttpURLConnection.HTTP_FORBIDDEN -> throw ForbiddenException() + HttpURLConnection.HTTP_NOT_FOUND -> throw NotFoundException() + } + if (responseCode < 199 || responseCode > 300) { + throw FatalBackendException("Response code isn't between 200 and 300: $responseCode") + } + } + + @Throws(IOException::class, XmlPullParserException::class) + private fun getEntriesFromResponse(listedFolder: WebDavFolder, response: Response): List { + return response.body?.use { responseBody -> return PropfindResponseParser(listedFolder).parse(responseBody.byteStream()) } ?: emptyList() + } + + @Throws(BackendException::class) + fun move(from: String, to: String) { + val builder = Request.Builder() // + .method("MOVE", null) // + .url(from) // + .header("Content-Type", "text/xml") // + .header("Destination", to) // + .header("Depth", "infinity") // + .header("Overwrite", "F") + try { + httpClient.execute(builder).use { response -> + if (!response.isSuccessful) { + when (response.code) { + HttpURLConnection.HTTP_UNAUTHORIZED -> throw UnauthorizedException() + HttpURLConnection.HTTP_FORBIDDEN -> throw ForbiddenException() + HttpURLConnection.HTTP_NOT_FOUND -> throw NotFoundException() + HttpURLConnection.HTTP_CONFLICT -> throw ParentFolderDoesNotExistException() + HttpURLConnection.HTTP_PRECON_FAILED -> throw CloudNodeAlreadyExistsException(to) + else -> throw FatalBackendException("Response code isn't between 200 and 300: " + response.code) + } + } + } + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + fun readFile(url: String): InputStream { + val builder = Request.Builder() // + .get() // + .url(url) + var response: Response? = null + var success = false + return try { + response = httpClient.execute(builder) + if (response.isSuccessful) { + success = true + response.body?.byteStream() ?: throw FatalBackendException("Response body is null") + } else { + when (response.code) { + HttpURLConnection.HTTP_UNAUTHORIZED -> throw UnauthorizedException() + HttpURLConnection.HTTP_FORBIDDEN -> throw ForbiddenException() + HttpURLConnection.HTTP_NOT_FOUND -> throw NotFoundException() + 416 -> ByteArrayInputStream(ByteArray(0)) + else -> throw FatalBackendException("Response code isn't between 200 and 300: " + response.code) + } + } + } catch (e: IOException) { + throw FatalBackendException(e) + } finally { + if (response != null && !success) { + response.close() + } + } + } + + @Throws(BackendException::class) + fun writeFile(url: String, inputStream: InputStream) { + val builder = Request.Builder() // + .put(InputStreamSourceBasedRequestBody.from(inputStream)) // + .url(url) + try { + httpClient.execute(builder).use { response -> + if (!response.isSuccessful) { + when (response.code) { + HttpURLConnection.HTTP_UNAUTHORIZED -> throw UnauthorizedException() + HttpURLConnection.HTTP_FORBIDDEN -> throw ForbiddenException() + HttpURLConnection.HTTP_BAD_METHOD -> throw TypeMismatchException() + HttpURLConnection.HTTP_CONFLICT, HttpURLConnection.HTTP_NOT_FOUND -> throw ParentFolderDoesNotExistException() + else -> throw FatalBackendException("Response code isn't between 200 and 300: " + response.code) + } + } + } + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + fun createFolder(path: String, folder: WebDavFolder): WebDavFolder { + val builder = Request.Builder() // + .method("MKCOL", null) // + .url(path) + try { + httpClient.execute(builder).use { response -> + return if (response.isSuccessful) { + folder + } else { + when (response.code) { + HttpURLConnection.HTTP_UNAUTHORIZED -> throw UnauthorizedException() + HttpURLConnection.HTTP_FORBIDDEN -> throw ForbiddenException() + HttpURLConnection.HTTP_BAD_METHOD -> throw AlreadyExistException() + HttpURLConnection.HTTP_CONFLICT -> throw ParentFolderDoesNotExistException() + else -> throw FatalBackendException("Response code isn't between 200 and 300: " + response.code) + } + } + } + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + fun delete(url: String) { + val builder = Request.Builder() // + .delete() // + .url(url) + try { + httpClient.execute(builder).use { response -> + if (!response.isSuccessful) { + when (response.code) { + HttpURLConnection.HTTP_UNAUTHORIZED -> throw UnauthorizedException() + HttpURLConnection.HTTP_FORBIDDEN -> throw ForbiddenException() + HttpURLConnection.HTTP_NOT_FOUND -> throw NotFoundException(String.format("Node %s doesn't exists", url)) + else -> throw FatalBackendException("Response code isn't between 200 and 300: " + response.code) + } + } + } + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + fun checkAuthenticationAndServerCompatibility(url: String) { + val optionsRequest = Request.Builder() // + .method("OPTIONS", null) // + .url(url) + try { + httpClient.execute(optionsRequest).use { response -> + if (response.isSuccessful) { + val containsDavHeader = response.headers.names().contains("DAV") + if (!containsDavHeader) { + throw ServerNotWebdavCompatibleException() + } + } else { + when (response.code) { + HttpURLConnection.HTTP_UNAUTHORIZED -> throw UnauthorizedException() + HttpURLConnection.HTTP_FORBIDDEN -> throw ForbiddenException() + else -> throw FatalBackendException("Response code isn't between 200 and 300: " + response.code) + } + } + } + } catch (e: IOException) { + throw FatalBackendException(e) + } + try { + executePropfindRequest(url, PropfindDepth.ZERO).use { response -> checkPropfindExecutionSucceeded(response.code) } + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + + private fun processDirList(entryData: List, requestedFolder: WebDavFolder): List { + Collections.sort(entryData, ASCENDING_BY_DEPTH) + // after sorting the first entry is the parent + // because it's depth is 1 smaller than the depth + // ot the other entries, thus we skip the first entry + return entryData.subList(1, entryData.size).mapTo(ArrayList()) { it.toCloudNode(requestedFolder) } + } + + private fun processGet(entryData: List, requestedFolder: WebDavFolder): WebDavNode? { + Collections.sort(entryData, ASCENDING_BY_DEPTH) + return if (entryData.isNotEmpty()) entryData[0].toCloudNode(requestedFolder) else null + } + + private enum class PropfindDepth(val value: String) { + ZERO("0"), // + ONE("1"), // + INFINITY("infinity"); + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/webdav/network/WebDavCompatibleHttpClient.java b/data/src/main/java/org/cryptomator/data/cloud/webdav/network/WebDavCompatibleHttpClient.java deleted file mode 100644 index 8ec386fb..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/webdav/network/WebDavCompatibleHttpClient.java +++ /dev/null @@ -1,165 +0,0 @@ -package org.cryptomator.data.cloud.webdav.network; - -import android.content.Context; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; - -import com.burgstaller.okhttp.AuthenticationCacheInterceptor; -import com.burgstaller.okhttp.CachingAuthenticatorDecorator; -import com.burgstaller.okhttp.DispatchingAuthenticator; -import com.burgstaller.okhttp.basic.BasicAuthenticator; -import com.burgstaller.okhttp.digest.CachingAuthenticator; -import com.burgstaller.okhttp.digest.Credentials; -import com.burgstaller.okhttp.digest.DigestAuthenticator; - -import org.cryptomator.data.cloud.okhttplogging.HttpLoggingInterceptor; -import org.cryptomator.domain.WebDavCloud; -import org.cryptomator.domain.exception.UnableToDecryptWebdavPasswordException; -import org.cryptomator.util.SharedPreferencesHandler; -import org.cryptomator.util.crypto.CredentialCryptor; -import org.cryptomator.util.file.LruFileCacheUtil; - -import java.io.IOException; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.TimeUnit; - -import javax.net.ssl.X509TrustManager; - -import okhttp3.Authenticator; -import okhttp3.Cache; -import okhttp3.CacheControl; -import okhttp3.Interceptor; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; -import timber.log.Timber; - -import static com.google.common.net.HttpHeaders.CACHE_CONTROL; -import static org.cryptomator.data.util.NetworkTimeout.CONNECTION; -import static org.cryptomator.data.util.NetworkTimeout.READ; -import static org.cryptomator.data.util.NetworkTimeout.WRITE; -import static org.cryptomator.util.file.LruFileCacheUtil.Cache.WEBDAV; - -class WebDavCompatibleHttpClient { - - private final WebDavRedirectHandler webDavRedirectHandler; - - WebDavCompatibleHttpClient(WebDavCloud cloud, Context context) { - final SharedPreferencesHandler sharedPreferencesHandler = new SharedPreferencesHandler(context); - this.webDavRedirectHandler = new WebDavRedirectHandler(httpClientFor(cloud, context, sharedPreferencesHandler.useLruCache(), sharedPreferencesHandler.lruCacheSize())); - } - - private static OkHttpClient httpClientFor(WebDavCloud webDavCloud, Context context, boolean useLruCache, int lruCacheSize) { - final Map authCache = new ConcurrentHashMap<>(); - - OkHttpClient.Builder builder = new OkHttpClient() // - .newBuilder() // - .connectTimeout(CONNECTION.getTimeout(), CONNECTION.getUnit()) // - .readTimeout(READ.getTimeout(), READ.getUnit()) // - .writeTimeout(WRITE.getTimeout(), WRITE.getUnit()) // - .followRedirects(false) // - .addInterceptor(httpLoggingInterceptor(context)) // - .authenticator(httpAuthenticator(context, webDavCloud, authCache)) // - .addInterceptor(new AuthenticationCacheInterceptor(authCache)); - - if (useLruCache) { - final Cache cache = new Cache(new LruFileCacheUtil(context).resolve(WEBDAV), lruCacheSize); - builder.cache(cache) // - .addNetworkInterceptor(provideCacheInterceptor()) // - .addInterceptor(provideOfflineCacheInterceptor(context)); - } - - X509TrustManager trustManager; - if (usingWebDavWithSelfSignedCertificate(webDavCloud)) { - PinningTrustManager pinningTrustManager = new PinningTrustManager(webDavCloud.certificate()); - trustManager = pinningTrustManager; - builder.hostnameVerifier(pinningTrustManager.hostnameVerifier()); - } else { - trustManager = new DefaultTrustManager(); - } - builder.sslSocketFactory(SSLSocketFactories.from(trustManager), trustManager); - - return builder.build(); - } - - private static Interceptor provideOfflineCacheInterceptor(final Context context) { - return chain -> { - Request request = chain.request(); - - if (isNetworkAvailable(context)) { - final CacheControl cacheControl = new CacheControl.Builder() // - .maxAge(0, TimeUnit.DAYS) // - .build(); - - request = request.newBuilder() // - .cacheControl(cacheControl) // - .build(); - } - - return chain.proceed(request); - }; - } - - private static Interceptor provideCacheInterceptor() { - return chain -> { - final Response response = chain.proceed(chain.request()); - final CacheControl cacheControl = new CacheControl.Builder() // - .maxAge(0, TimeUnit.DAYS) // - .build(); - - return response.newBuilder() // - .removeHeader("Pragma") // - .removeHeader("Cache-Control") // - .header(CACHE_CONTROL, cacheControl.toString()) // - .build(); - }; - } - - private static Authenticator httpAuthenticator(Context context, WebDavCloud webDavCloud, Map authCache) { - Credentials credentials = new Credentials(webDavCloud.username(), decryptPassword(context, webDavCloud.password())); - final BasicAuthenticator basicAuthenticator = new BasicAuthenticator(credentials); - final DigestAuthenticator digestAuthenticator = new DigestAuthenticator(credentials); - - Authenticator result = new DispatchingAuthenticator // - .Builder() // - .with("digest", digestAuthenticator) // - .with("basic", basicAuthenticator) // - .build(); - result = new CachingAuthenticatorDecorator(result, authCache); - - return result; - } - - private static String decryptPassword(Context context, String password) throws UnableToDecryptWebdavPasswordException { - try { - return CredentialCryptor // - .getInstance(context) // - .decrypt(password); - } catch (RuntimeException e) { - throw new UnableToDecryptWebdavPasswordException(e); - } - } - - private static Interceptor httpLoggingInterceptor(Context context) { - return new HttpLoggingInterceptor(message -> Timber.tag("OkHttp").d(message), context); - } - - private static boolean usingWebDavWithSelfSignedCertificate(WebDavCloud webDavCloud) { - return webDavCloud.certificate() != null; - } - - private static boolean isNetworkAvailable(final Context context) { - ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo(); - return activeNetworkInfo != null && activeNetworkInfo.isConnected(); - } - - Response execute(Request.Builder requestBuilder) throws IOException { - return execute(requestBuilder.build()); - } - - private Response execute(Request request) throws IOException { - return webDavRedirectHandler.executeFollowingRedirects(request); - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/webdav/network/WebDavCompatibleHttpClient.kt b/data/src/main/java/org/cryptomator/data/cloud/webdav/network/WebDavCompatibleHttpClient.kt new file mode 100644 index 00000000..c0dea8cf --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/webdav/network/WebDavCompatibleHttpClient.kt @@ -0,0 +1,172 @@ +package org.cryptomator.data.cloud.webdav.network + +import android.content.Context +import android.net.ConnectivityManager +import com.burgstaller.okhttp.AuthenticationCacheInterceptor +import com.burgstaller.okhttp.CachingAuthenticatorDecorator +import com.burgstaller.okhttp.DispatchingAuthenticator +import com.burgstaller.okhttp.basic.BasicAuthenticator +import com.burgstaller.okhttp.digest.CachingAuthenticator +import com.burgstaller.okhttp.digest.Credentials +import com.burgstaller.okhttp.digest.DigestAuthenticator +import com.google.common.net.HttpHeaders +import org.cryptomator.data.cloud.okhttplogging.HttpLoggingInterceptor +import org.cryptomator.data.util.NetworkTimeout +import org.cryptomator.domain.WebDavCloud +import org.cryptomator.domain.exception.UnableToDecryptWebdavPasswordException +import org.cryptomator.util.SharedPreferencesHandler +import org.cryptomator.util.crypto.CredentialCryptor +import org.cryptomator.util.file.LruFileCacheUtil +import java.io.IOException +import java.nio.charset.StandardCharsets +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.TimeUnit +import javax.net.ssl.X509TrustManager +import okhttp3.Authenticator +import okhttp3.Cache +import okhttp3.CacheControl +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import timber.log.Timber + +internal class WebDavCompatibleHttpClient(cloud: WebDavCloud, context: Context) { + + private val webDavRedirectHandler: WebDavRedirectHandler + + @Throws(IOException::class) + fun execute(requestBuilder: Request.Builder): Response { + return execute(requestBuilder.build()) + } + + @Throws(IOException::class) + private fun execute(request: Request): Response { + return webDavRedirectHandler.executeFollowingRedirects(request) + } + + companion object { + + private fun httpClientFor(webDavCloud: WebDavCloud, context: Context, useLruCache: Boolean, lruCacheSize: Int): OkHttpClient { + val authCache: Map = ConcurrentHashMap() + + val builder = OkHttpClient() // + .newBuilder() // + .connectTimeout(NetworkTimeout.CONNECTION.timeout, NetworkTimeout.CONNECTION.unit) // + .readTimeout(NetworkTimeout.READ.timeout, NetworkTimeout.READ.unit) // + .writeTimeout(NetworkTimeout.WRITE.timeout, NetworkTimeout.WRITE.unit) // + .followRedirects(false) // + .addInterceptor(httpLoggingInterceptor(context)) // + .authenticator(httpAuthenticator(context, webDavCloud, authCache)) // + .addInterceptor(AuthenticationCacheInterceptor(authCache)) + .addInterceptor(UserAgentInterceptor()) + + if (useLruCache) { + val cache = Cache(LruFileCacheUtil(context).resolve(LruFileCacheUtil.Cache.WEBDAV), lruCacheSize.toLong()) + builder.cache(cache) // + .addNetworkInterceptor(provideCacheInterceptor()) // + .addInterceptor(provideOfflineCacheInterceptor(context)) + } + + val trustManager: X509TrustManager + if (usingWebDavWithSelfSignedCertificate(webDavCloud)) { + val pinningTrustManager = PinningTrustManager(webDavCloud.certificate()) + trustManager = pinningTrustManager + builder.hostnameVerifier(pinningTrustManager.hostnameVerifier()) + } else { + trustManager = DefaultTrustManager() + } + + builder.sslSocketFactory(SSLSocketFactories.from(trustManager), trustManager) + return builder.build() + } + + private fun provideOfflineCacheInterceptor(context: Context): Interceptor { + return Interceptor { chain: Interceptor.Chain -> + var request = chain.request() + if (isNetworkAvailable(context)) { + val cacheControl = CacheControl.Builder() // + .maxAge(0, TimeUnit.DAYS) // + .build() + request = request.newBuilder() // + .cacheControl(cacheControl) // + .build() + } + chain.proceed(request) + } + } + + private fun provideCacheInterceptor(): Interceptor { + return Interceptor { chain: Interceptor.Chain -> + val response = chain.proceed(chain.request()) + val cacheControl = CacheControl.Builder() // + .maxAge(0, TimeUnit.DAYS) // + .build() + response.newBuilder() // + .removeHeader("Pragma") // + .removeHeader("Cache-Control") // + .header(HttpHeaders.CACHE_CONTROL, cacheControl.toString()) // + .build() + } + } + + private fun httpAuthenticator(context: Context, webDavCloud: WebDavCloud, authCache: Map): Authenticator { + val credentials = Credentials(webDavCloud.username(), decryptPassword(context, webDavCloud.password())) + val basicAuthenticator = BasicAuthenticator(credentials, StandardCharsets.UTF_8) + val digestAuthenticator = DigestAuthenticator(credentials) + var result: Authenticator = DispatchingAuthenticator.Builder() // + .with("digest", digestAuthenticator) // + .with("basic", basicAuthenticator) // + .build() + result = CachingAuthenticatorDecorator(result, authCache) + return result + } + + @Throws(UnableToDecryptWebdavPasswordException::class) + private fun decryptPassword(context: Context, password: String): String { + return try { + CredentialCryptor // + .getInstance(context) // + .decrypt(password) + } catch (e: RuntimeException) { + throw UnableToDecryptWebdavPasswordException(e) + } + } + + private fun httpLoggingInterceptor(context: Context): Interceptor { + val logger = object : HttpLoggingInterceptor.Logger { + override fun log(message: String) { + Timber.tag("OkHttp").d(message) + } + } + + return HttpLoggingInterceptor(logger, context) + } + + private fun usingWebDavWithSelfSignedCertificate(webDavCloud: WebDavCloud): Boolean { + return webDavCloud.certificate() != null + } + + private fun isNetworkAvailable(context: Context): Boolean { + val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val activeNetworkInfo = connectivityManager.activeNetworkInfo + return activeNetworkInfo != null && activeNetworkInfo.isConnected + } + } + + class UserAgentInterceptor : Interceptor { + + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + val originalRequest: Request = chain.request() + val userAgent = "davfs2/1.5.2" + val requestWithUserAgent = originalRequest.newBuilder().header("User-Agent", userAgent).build() + return chain.proceed(requestWithUserAgent) + } + } + + init { + val sharedPreferencesHandler = SharedPreferencesHandler(context) + webDavRedirectHandler = WebDavRedirectHandler(httpClientFor(cloud, context, sharedPreferencesHandler.useLruCache(), sharedPreferencesHandler.lruCacheSize())) + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/webdav/network/WebDavRedirectHandler.java b/data/src/main/java/org/cryptomator/data/cloud/webdav/network/WebDavRedirectHandler.java deleted file mode 100644 index 2c895ff8..00000000 --- a/data/src/main/java/org/cryptomator/data/cloud/webdav/network/WebDavRedirectHandler.java +++ /dev/null @@ -1,96 +0,0 @@ -package org.cryptomator.data.cloud.webdav.network; - -import java.io.IOException; -import java.net.ProtocolException; - -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; - -class WebDavRedirectHandler { - - private static final int MAX_REDIRECT_COUNT = 20; - private static final Request NO_REDIRECTED_REQUEST = null; - private static final HttpUrl NO_REDIRECT_URL = null; - - private final OkHttpClient httpClient; - - WebDavRedirectHandler(OkHttpClient httpClient) { - this.httpClient = httpClient; - } - - public Response executeFollowingRedirects(Request request) throws IOException { - Response response; - int redirectCount = 0; - do { - if (redirectCount > MAX_REDIRECT_COUNT) { - throw new ProtocolException("Too many redirects: " + redirectCount); - } - response = httpClient.newCall(request).execute(); - request = redirectedRequestFor(response); - redirectCount++; - } while (request != NO_REDIRECTED_REQUEST); - return response; - } - - private Request redirectedRequestFor(Response response) { - switch (response.code()) { - case 300: // fall through - case 301: // fall through - case 302: // fall through - case 303: // fall through - case 307: // fall through - case 308: - return createRedirectedRequest(response); - default: - return NO_REDIRECTED_REQUEST; - } - } - - private Request createRedirectedRequest(Response response) { - HttpUrl url = redirectUrl(response); - if (url == NO_REDIRECT_URL) { - return NO_REDIRECTED_REQUEST; - } - return createRedirectedRequest(response, url); - } - - private Request createRedirectedRequest(Response response, HttpUrl url) { - Request.Builder requestBuilder = response.request().newBuilder().url(url); - if (methodShouldBeChangedToGet(response)) { - changeMethodToGet(requestBuilder); - } - if (!connectionMatches(response.request().url(), url)) { - requestBuilder.removeHeader("Authorization"); - } - return requestBuilder.build(); - } - - private boolean methodShouldBeChangedToGet(Response response) { - return response.code() == 300 // - || response.code() == 303; - } - - private void changeMethodToGet(Request.Builder requestBuilder) { - requestBuilder.method("GET", null) // - .removeHeader("Transfer-Encoding") // - .removeHeader("Content-Length") // - .removeHeader("Content-Type"); - } - - private boolean connectionMatches(HttpUrl url1, HttpUrl url2) { - return url1.scheme().equals(url2.scheme()) // - && url1.host().equals(url2.host()) // - && url1.port() == url2.port(); - } - - private HttpUrl redirectUrl(Response response) { - String location = response.header("Location"); - if (location == null) { - return NO_REDIRECT_URL; - } - return response.request().url().resolve(location); - } - -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/webdav/network/WebDavRedirectHandler.kt b/data/src/main/java/org/cryptomator/data/cloud/webdav/network/WebDavRedirectHandler.kt new file mode 100644 index 00000000..cf2a53c0 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/webdav/network/WebDavRedirectHandler.kt @@ -0,0 +1,80 @@ +package org.cryptomator.data.cloud.webdav.network + +import java.io.IOException +import java.net.ProtocolException +import okhttp3.HttpUrl +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response + +internal class WebDavRedirectHandler(private val httpClient: OkHttpClient) { + + @Throws(IOException::class) + fun executeFollowingRedirects(request: Request?): Response { + var request = request + var response: Response + var redirectCount = 0 + do { + if (redirectCount > MAX_REDIRECT_COUNT) { + throw ProtocolException("Too many redirects: $redirectCount") + } + response = httpClient.newCall(request!!).execute() + request = redirectedRequestFor(response) + redirectCount++ + } while (request != NO_REDIRECTED_REQUEST) + return response + } + + private fun redirectedRequestFor(response: Response): Request? { + return when (response.code) { + 300, 301, 302, 303, 307, 308 -> createRedirectedRequest(response) + else -> NO_REDIRECTED_REQUEST + } + } + + private fun createRedirectedRequest(response: Response): Request? { + val url = redirectUrl(response) + return if (url === NO_REDIRECT_URL) { + NO_REDIRECTED_REQUEST + } else createRedirectedRequest(response, url!!) + } + + private fun createRedirectedRequest(response: Response, url: HttpUrl): Request { + val requestBuilder = response.request.newBuilder().url(url) + if (methodShouldBeChangedToGet(response)) { + changeMethodToGet(requestBuilder) + } + if (!connectionMatches(response.request.url, url)) { + requestBuilder.removeHeader("Authorization") + } + return requestBuilder.build() + } + + private fun methodShouldBeChangedToGet(response: Response): Boolean { + return (response.code == 300 // + || response.code == 303) + } + + private fun changeMethodToGet(requestBuilder: Request.Builder) { + requestBuilder.method("GET", null) // + .removeHeader("Transfer-Encoding") // + .removeHeader("Content-Length") // + .removeHeader("Content-Type") + } + + private fun connectionMatches(url1: HttpUrl, url2: HttpUrl): Boolean { + return url1.scheme == url2.scheme && url1.host == url2.host && url1.port == url2.port + } + + private fun redirectUrl(response: Response): HttpUrl? { + val location = response.header("Location") ?: return NO_REDIRECT_URL + return response.request.url.resolve(location) + } + + companion object { + + private const val MAX_REDIRECT_COUNT = 20 + private val NO_REDIRECTED_REQUEST: Request? = null + private val NO_REDIRECT_URL: HttpUrl? = null + } +} diff --git a/data/src/main/java/org/cryptomator/data/db/Upgrade0To1.java b/data/src/main/java/org/cryptomator/data/db/Upgrade0To1.java deleted file mode 100644 index 9ec8560e..00000000 --- a/data/src/main/java/org/cryptomator/data/db/Upgrade0To1.java +++ /dev/null @@ -1,105 +0,0 @@ -package org.cryptomator.data.db; - -import org.cryptomator.domain.CloudType; -import org.greenrobot.greendao.database.Database; - -import javax.inject.Inject; -import javax.inject.Singleton; - -import static org.cryptomator.data.db.Sql.SqlCreateTableBuilder.ForeignKeyBehaviour.ON_DELETE_SET_NULL; -import static org.cryptomator.data.db.Sql.createTable; -import static org.cryptomator.data.db.Sql.createUniqueIndex; -import static org.cryptomator.data.db.Sql.insertInto; - -@Singleton -class Upgrade0To1 extends DatabaseUpgrade { - - @Inject - public Upgrade0To1() { - super(0, 1); - } - - @Override - protected void internalApplyTo(Database db, int origin) { - createCloudEntityTable(db); - createVaultEntityTable(db); - - createDropboxCloud(db); - createGoogleDriveCloud(db); - createLocalStorageCloud(db); - createOnedriveCloud(db); - } - - private void createCloudEntityTable(Database db) { - createTable("CLOUD_ENTITY") // - .id() // - .requiredText("TYPE") // - .optionalText("ACCESS_TOKEN") // - .optionalText("WEBDAV_URL") // - .optionalText("USERNAME") // - .optionalText("WEBDAV_CERTIFICATE") // - .executeOn(db); - } - - private void createVaultEntityTable(Database db) { - createTable("VAULT_ENTITY") // - .id() // - .optionalInt("FOLDER_CLOUD_ID") // - .optionalText("FOLDER_PATH") // - .optionalText("FOLDER_NAME") // - .requiredText("CLOUD_TYPE") // - .optionalText("PASSWORD") // - .foreignKey("FOLDER_CLOUD_ID", "CLOUD_ENTITY", ON_DELETE_SET_NULL) // - .executeOn(db); - - createUniqueIndex("IDX_VAULT_ENTITY_FOLDER_PATH_FOLDER_CLOUD_ID") // - .on("VAULT_ENTITY") // - .asc("FOLDER_PATH") // - .asc("FOLDER_CLOUD_ID") // - .executeOn(db); - } - - private void createDropboxCloud(Database db) { - insertInto("CLOUD_ENTITY") // - .integer("_id", 1) // - .text("TYPE", CloudType.DROPBOX.name()) // - .text("ACCESS_TOKEN", null) // - .text("WEBDAV_URL", null) // - .text("USERNAME", null) // - .text("WEBDAV_CERTIFICATE", null) // - .executeOn(db); - } - - private void createGoogleDriveCloud(Database db) { - insertInto("CLOUD_ENTITY") // - .integer("_id", 2) // - .text("TYPE", CloudType.GOOGLE_DRIVE.name()) // - .text("ACCESS_TOKEN", null) // - .text("WEBDAV_URL", null) // - .text("USERNAME", null) // - .text("WEBDAV_CERTIFICATE", null) // - .executeOn(db); - } - - private void createOnedriveCloud(Database db) { - insertInto("CLOUD_ENTITY") // - .integer("_id", 3) // - .text("TYPE", CloudType.ONEDRIVE.name()) // - .text("ACCESS_TOKEN", null) // - .text("WEBDAV_URL", null) // - .text("USERNAME", null) // - .text("WEBDAV_CERTIFICATE", null) // - .executeOn(db); - } - - private void createLocalStorageCloud(Database db) { - insertInto("CLOUD_ENTITY") // - .integer("_id", 4) // - .text("TYPE", CloudType.LOCAL.name()) // - .text("ACCESS_TOKEN", null) // - .text("WEBDAV_URL", null) // - .text("USERNAME", null) // - .text("WEBDAV_CERTIFICATE", null) // - .executeOn(db); - } -} diff --git a/data/src/main/java/org/cryptomator/data/db/Upgrade0To1.kt b/data/src/main/java/org/cryptomator/data/db/Upgrade0To1.kt new file mode 100644 index 00000000..a8f7849f --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/db/Upgrade0To1.kt @@ -0,0 +1,92 @@ +package org.cryptomator.data.db + +import org.cryptomator.data.db.Sql.SqlCreateTableBuilder.ForeignKeyBehaviour +import org.cryptomator.domain.CloudType +import org.greenrobot.greendao.database.Database +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class Upgrade0To1 @Inject constructor() : DatabaseUpgrade(0, 1) { + + override fun internalApplyTo(db: Database, origin: Int) { + createCloudEntityTable(db) + createVaultEntityTable(db) + createDropboxCloud(db) + createGoogleDriveCloud(db) + createLocalStorageCloud(db) + createOnedriveCloud(db) + } + + private fun createCloudEntityTable(db: Database) { + Sql.createTable("CLOUD_ENTITY") // + .id() // + .requiredText("TYPE") // + .optionalText("ACCESS_TOKEN") // + .optionalText("WEBDAV_URL") // + .optionalText("USERNAME") // + .optionalText("WEBDAV_CERTIFICATE") // + .executeOn(db) + } + + private fun createVaultEntityTable(db: Database) { + Sql.createTable("VAULT_ENTITY") // + .id() // + .optionalInt("FOLDER_CLOUD_ID") // + .optionalText("FOLDER_PATH") // + .optionalText("FOLDER_NAME") // + .requiredText("CLOUD_TYPE") // + .optionalText("PASSWORD") // + .foreignKey("FOLDER_CLOUD_ID", "CLOUD_ENTITY", ForeignKeyBehaviour.ON_DELETE_SET_NULL) // + .executeOn(db) + Sql.createUniqueIndex("IDX_VAULT_ENTITY_FOLDER_PATH_FOLDER_CLOUD_ID") // + .on("VAULT_ENTITY") // + .asc("FOLDER_PATH") // + .asc("FOLDER_CLOUD_ID") // + .executeOn(db) + } + + private fun createDropboxCloud(db: Database) { + Sql.insertInto("CLOUD_ENTITY") // + .integer("_id", 1) // + .text("TYPE", CloudType.DROPBOX.name) // + .text("ACCESS_TOKEN", null) // + .text("WEBDAV_URL", null) // + .text("USERNAME", null) // + .text("WEBDAV_CERTIFICATE", null) // + .executeOn(db) + } + + private fun createGoogleDriveCloud(db: Database) { + Sql.insertInto("CLOUD_ENTITY") // + .integer("_id", 2) // + .text("TYPE", CloudType.GOOGLE_DRIVE.name) // + .text("ACCESS_TOKEN", null) // + .text("WEBDAV_URL", null) // + .text("USERNAME", null) // + .text("WEBDAV_CERTIFICATE", null) // + .executeOn(db) + } + + private fun createOnedriveCloud(db: Database) { + Sql.insertInto("CLOUD_ENTITY") // + .integer("_id", 3) // + .text("TYPE", CloudType.ONEDRIVE.name) // + .text("ACCESS_TOKEN", null) // + .text("WEBDAV_URL", null) // + .text("USERNAME", null) // + .text("WEBDAV_CERTIFICATE", null) // + .executeOn(db) + } + + private fun createLocalStorageCloud(db: Database) { + Sql.insertInto("CLOUD_ENTITY") // + .integer("_id", 4) // + .text("TYPE", CloudType.LOCAL.name) // + .text("ACCESS_TOKEN", null) // + .text("WEBDAV_URL", null) // + .text("USERNAME", null) // + .text("WEBDAV_CERTIFICATE", null) // + .executeOn(db) + } +} diff --git a/data/src/main/java/org/cryptomator/data/db/Upgrade1To2.java b/data/src/main/java/org/cryptomator/data/db/Upgrade1To2.java deleted file mode 100644 index 61ae9979..00000000 --- a/data/src/main/java/org/cryptomator/data/db/Upgrade1To2.java +++ /dev/null @@ -1,52 +0,0 @@ -package org.cryptomator.data.db; - -import org.greenrobot.greendao.database.Database; - -import javax.inject.Inject; -import javax.inject.Singleton; - -import static org.cryptomator.data.db.Sql.createTable; -import static org.cryptomator.data.db.Sql.insertInto; - -@Singleton -class Upgrade1To2 extends DatabaseUpgrade { - - @Inject - Upgrade1To2() { - super(1, 2); - } - - @Override - protected void internalApplyTo(Database db, int origin) { - createUpdateCheckTable(db); - createInitialUpdateStatus(db); - } - - private void createUpdateCheckTable(Database db) { - db.beginTransaction(); - try { - createTable("UPDATE_CHECK_ENTITY") // - .id() // - .optionalText("LICENSE_TOKEN") // - .optionalText("RELEASE_NOTE") // - .optionalText("VERSION") // - .optionalText("URL_TO_APK") // - .optionalText("URL_TO_RELEASE_NOTE") // - .executeOn(db); - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - } - - private void createInitialUpdateStatus(Database db) { - insertInto("UPDATE_CHECK_ENTITY") // - .integer("_id", 1) // - .bool("LICENSE_TOKEN", null) // - .text("RELEASE_NOTE", null) // - .text("VERSION", null) // - .text("URL_TO_APK", null) // - .text("URL_TO_RELEASE_NOTE", null) // - .executeOn(db); - } -} diff --git a/data/src/main/java/org/cryptomator/data/db/Upgrade1To2.kt b/data/src/main/java/org/cryptomator/data/db/Upgrade1To2.kt new file mode 100644 index 00000000..38815b52 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/db/Upgrade1To2.kt @@ -0,0 +1,42 @@ +package org.cryptomator.data.db + +import org.greenrobot.greendao.database.Database +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class Upgrade1To2 @Inject constructor() : DatabaseUpgrade(1, 2) { + + override fun internalApplyTo(db: Database, origin: Int) { + createUpdateCheckTable(db) + createInitialUpdateStatus(db) + } + + private fun createUpdateCheckTable(db: Database) { + db.beginTransaction() + try { + Sql.createTable("UPDATE_CHECK_ENTITY") // + .id() // + .optionalText("LICENSE_TOKEN") // + .optionalText("RELEASE_NOTE") // + .optionalText("VERSION") // + .optionalText("URL_TO_APK") // + .optionalText("URL_TO_RELEASE_NOTE") // + .executeOn(db) + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + } + + private fun createInitialUpdateStatus(db: Database) { + Sql.insertInto("UPDATE_CHECK_ENTITY") // + .integer("_id", 1) // + .bool("LICENSE_TOKEN", null) // + .text("RELEASE_NOTE", null) // + .text("VERSION", null) // + .text("URL_TO_APK", null) // + .text("URL_TO_RELEASE_NOTE", null) // + .executeOn(db) + } +} 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/repository/CloudRepositoryImpl.java b/data/src/main/java/org/cryptomator/data/repository/CloudRepositoryImpl.java index 5ddafba6..bdb422c3 100644 --- a/data/src/main/java/org/cryptomator/data/repository/CloudRepositoryImpl.java +++ b/data/src/main/java/org/cryptomator/data/repository/CloudRepositoryImpl.java @@ -1,5 +1,7 @@ package org.cryptomator.data.repository; +import com.google.common.base.Optional; + import org.cryptomator.data.cloud.crypto.CryptoCloudFactory; import org.cryptomator.data.db.Database; import org.cryptomator.data.db.entities.CloudEntity; @@ -13,7 +15,6 @@ import org.cryptomator.domain.exception.BackendException; import org.cryptomator.domain.repository.CloudRepository; import org.cryptomator.domain.usecases.cloud.Flag; import org.cryptomator.domain.usecases.vault.UnlockToken; -import org.cryptomator.util.Optional; import java.util.ArrayList; import java.util.List; diff --git a/data/src/main/java/org/cryptomator/data/repository/DispatchingCloudContentRepository.java b/data/src/main/java/org/cryptomator/data/repository/DispatchingCloudContentRepository.java deleted file mode 100644 index 8d679703..00000000 --- a/data/src/main/java/org/cryptomator/data/repository/DispatchingCloudContentRepository.java +++ /dev/null @@ -1,251 +0,0 @@ -package org.cryptomator.data.repository; - -import org.cryptomator.data.cloud.CloudContentRepositoryFactories; -import org.cryptomator.data.cloud.crypto.CryptoCloud; -import org.cryptomator.data.cloud.crypto.CryptoCloudContentRepositoryFactory; -import org.cryptomator.data.util.NetworkConnectionCheck; -import org.cryptomator.domain.Cloud; -import org.cryptomator.domain.CloudFile; -import org.cryptomator.domain.CloudFolder; -import org.cryptomator.domain.CloudNode; -import org.cryptomator.domain.exception.BackendException; -import org.cryptomator.domain.exception.authentication.AuthenticationException; -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.OutputStream; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.WeakHashMap; - -import javax.inject.Inject; -import javax.inject.Singleton; - -@Singleton -public class DispatchingCloudContentRepository implements CloudContentRepository { - - private final Map delegates = new WeakHashMap<>(); - private final CloudContentRepositoryFactories cloudContentRepositoryFactories; - private final NetworkConnectionCheck networkConnectionCheck; - private final CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory; - - @Inject - public DispatchingCloudContentRepository(CloudContentRepositoryFactories cloudContentRepositoryFactories, NetworkConnectionCheck networkConnectionCheck, CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory) { - this.cloudContentRepositoryFactories = cloudContentRepositoryFactories; - this.networkConnectionCheck = networkConnectionCheck; - this.cryptoCloudContentRepositoryFactory = cryptoCloudContentRepositoryFactory; - } - - @Override - public CloudFolder root(Cloud cloud) throws BackendException { - try { - networkConnectionCheck.assertConnectionIsPresent(cloud); - return delegateFor(cloud).root(cloud); - } catch (AuthenticationException e) { - delegates.remove(cloud); - throw e; - } - } - - @Override - public CloudFolder resolve(Cloud cloud, String path) throws BackendException { - try { - // do not check for network connection - return delegateFor(cloud).resolve(cloud, path); - } catch (AuthenticationException e) { - delegates.remove(cloud); - throw e; - } - } - - @Override - public CloudFile file(CloudFolder parent, String name) throws BackendException { - try { - networkConnectionCheck.assertConnectionIsPresent(parent.getCloud()); - return delegateFor(parent).file(parent, name); - } catch (AuthenticationException e) { - delegates.remove(parent.getCloud()); - throw e; - } - } - - @Override - public CloudFile file(CloudFolder parent, String name, Optional size) throws BackendException { - try { - networkConnectionCheck.assertConnectionIsPresent(parent.getCloud()); - return delegateFor(parent).file(parent, name, size); - } catch (AuthenticationException e) { - delegates.remove(parent.getCloud()); - throw e; - } - } - - @Override - public CloudFolder folder(CloudFolder parent, String name) throws BackendException { - try { - networkConnectionCheck.assertConnectionIsPresent(parent.getCloud()); - return delegateFor(parent).folder(parent, name); - } catch (AuthenticationException e) { - delegates.remove(parent.getCloud()); - throw e; - } - } - - @Override - public boolean exists(CloudNode node) throws BackendException { - try { - networkConnectionCheck.assertConnectionIsPresent(node.getCloud()); - return delegateFor(node).exists(node); - } catch (AuthenticationException e) { - delegates.remove(node.getCloud()); - throw e; - } - } - - @Override - public List list(CloudFolder folder) throws BackendException { - try { - networkConnectionCheck.assertConnectionIsPresent(folder.getCloud()); - return delegateFor(folder).list(folder); - } catch (AuthenticationException e) { - delegates.remove(folder.getCloud()); - throw e; - } - } - - @Override - public CloudFolder create(CloudFolder folder) throws BackendException { - try { - networkConnectionCheck.assertConnectionIsPresent(folder.getCloud()); - return delegateFor(folder).create(folder); - } catch (AuthenticationException e) { - delegates.remove(folder.getCloud()); - throw e; - } - } - - @Override - public CloudFolder move(CloudFolder source, CloudFolder target) throws BackendException { - try { - networkConnectionCheck.assertConnectionIsPresent(source.getCloud()); - if (!source.getCloud().equals(target.getCloud())) { - throw new IllegalArgumentException("Cloud of parameters must match"); - } - return delegateFor(source).move(source, target); - } catch (AuthenticationException e) { - delegates.remove(source.getCloud()); - throw e; - } - } - - @Override - public CloudFile move(CloudFile source, CloudFile target) throws BackendException { - try { - networkConnectionCheck.assertConnectionIsPresent(source.getCloud()); - if (!source.getCloud().equals(target.getCloud())) { - throw new IllegalArgumentException("Cloud of parameters must match"); - } - return delegateFor(source).move(source, target); - } catch (AuthenticationException e) { - delegates.remove(source.getCloud()); - throw e; - } - } - - @Override - public CloudFile write(CloudFile source, DataSource data, ProgressAware progressAware, boolean replace, long size) throws BackendException { - try { - networkConnectionCheck.assertConnectionIsPresent(source.getCloud()); - return delegateFor(source).write(source, data, progressAware, replace, size); - } catch (AuthenticationException e) { - delegates.remove(source.getCloud()); - throw e; - } - } - - @Override - public void read(CloudFile file, Optional tempEncryptedFile, OutputStream data, ProgressAware progressAware) throws BackendException { - try { - networkConnectionCheck.assertConnectionIsPresent(file.getCloud()); - delegateFor(file).read(file, tempEncryptedFile, data, progressAware); - } catch (AuthenticationException e) { - delegates.remove(file.getCloud()); - throw e; - } - } - - @Override - public void delete(CloudNode node) throws BackendException { - try { - networkConnectionCheck.assertConnectionIsPresent(node.getCloud()); - delegateFor(node).delete(node); - } catch (AuthenticationException e) { - delegates.remove(node.getCloud()); - throw e; - } - } - - @Override - public String checkAuthenticationAndRetrieveCurrentAccount(Cloud cloud) throws BackendException { - try { - networkConnectionCheck.assertConnectionIsPresent(cloud); - return delegateFor(cloud).checkAuthenticationAndRetrieveCurrentAccount(cloud); - } catch (AuthenticationException e) { - delegates.remove(cloud); - throw e; - } - } - - @Override - public void logout(Cloud cloud) throws BackendException { - delegateFor(cloud).logout(cloud); - removeCloudContentRepositoryFor(cloud); - } - - public void removeCloudContentRepositoryFor(Cloud cloud) { - Iterator clouds = delegates.keySet().iterator(); - while (clouds.hasNext()) { - Cloud current = clouds.next(); - if (cloud.equals(current)) { - clouds.remove(); - } else if (cloudIsDelegateOfCryptoCloud(current, cloud)) { - cryptoCloudContentRepositoryFactory.deregisterCryptor(((CryptoCloud) current).getVault(), false); - } - } - } - - private boolean cloudIsDelegateOfCryptoCloud(Cloud potentialCryptoCloud, Cloud cloud) { - if (potentialCryptoCloud instanceof CryptoCloud) { - CryptoCloud cryptoCloud = (CryptoCloud) potentialCryptoCloud; - Cloud delegate = cryptoCloud.getVault().getCloud(); - return cloud.equals(delegate); - } - return false; - } - - private CloudContentRepository delegateFor(CloudNode cloudNode) { - return delegateFor(cloudNode.getCloud()); - } - - private CloudContentRepository delegateFor(Cloud cloud) { - if (!delegates.containsKey(cloud)) { - delegates.put(cloud, createCloudContentRepositoryFor(cloud)); - } - return delegates.get(cloud); - } - - private CloudContentRepository createCloudContentRepositoryFor(Cloud cloud) { - for (CloudContentRepositoryFactory cloudContentRepositoryFactory : cloudContentRepositoryFactories) { - if (cloudContentRepositoryFactory.supports(cloud)) { - return cloudContentRepositoryFactory.cloudContentRepositoryFor(cloud); - } - } - throw new IllegalStateException("Unsupported cloud " + cloud); - } -} diff --git a/data/src/main/java/org/cryptomator/data/repository/DispatchingCloudContentRepository.kt b/data/src/main/java/org/cryptomator/data/repository/DispatchingCloudContentRepository.kt new file mode 100644 index 00000000..602c0cd0 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/repository/DispatchingCloudContentRepository.kt @@ -0,0 +1,235 @@ +package org.cryptomator.data.repository + +import org.cryptomator.data.cloud.CloudContentRepositoryFactories +import org.cryptomator.data.cloud.crypto.CryptoCloud +import org.cryptomator.data.cloud.crypto.CryptoCloudContentRepositoryFactory +import org.cryptomator.data.util.NetworkConnectionCheck +import org.cryptomator.domain.Cloud +import org.cryptomator.domain.CloudFile +import org.cryptomator.domain.CloudFolder +import org.cryptomator.domain.CloudNode +import org.cryptomator.domain.exception.BackendException +import org.cryptomator.domain.exception.authentication.AuthenticationException +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 java.io.File +import java.io.OutputStream +import java.util.WeakHashMap +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class DispatchingCloudContentRepository @Inject constructor( + private val cloudContentRepositoryFactories: CloudContentRepositoryFactories, + private val networkConnectionCheck: NetworkConnectionCheck, + private val cryptoCloudContentRepositoryFactory: CryptoCloudContentRepositoryFactory +) : CloudContentRepository { + + private val delegates: MutableMap> = WeakHashMap() + + @Throws(BackendException::class) + override fun root(cloud: Cloud): CloudFolder { + return try { + networkConnectionCheck.assertConnectionIsPresent(cloud) + delegateFor(cloud).root(cloud) + } catch (e: AuthenticationException) { + delegates.remove(cloud) + throw e + } + } + + @Throws(BackendException::class) + override fun resolve(cloud: Cloud, path: String): CloudFolder { + return try { + // do not check for network connection + delegateFor(cloud).resolve(cloud, path) + } catch (e: AuthenticationException) { + delegates.remove(cloud) + throw e + } + } + + @Throws(BackendException::class) + override fun file(parent: CloudFolder, name: String): CloudFile { + return try { + parent.cloud?.let { networkConnectionCheck.assertConnectionIsPresent(it) } ?: throw IllegalStateException("Parent's cloud shouldn't be null") + delegateFor(parent).file(parent, name) + } catch (e: AuthenticationException) { + delegates.remove(parent.cloud) + throw e + } + } + + @Throws(BackendException::class) + override fun file(parent: CloudFolder, name: String, size: Long?): CloudFile { + return try { + parent.cloud?.let { networkConnectionCheck.assertConnectionIsPresent(it) } ?: throw IllegalStateException("Parent's cloud shouldn't be null") + delegateFor(parent).file(parent, name, size) + } catch (e: AuthenticationException) { + delegates.remove(parent.cloud) + throw e + } + } + + @Throws(BackendException::class) + override fun folder(parent: CloudFolder, name: String): CloudFolder { + return try { + parent.cloud?.let { networkConnectionCheck.assertConnectionIsPresent(it) } ?: throw IllegalStateException("Parent's cloud shouldn't be null") + delegateFor(parent).folder(parent, name) + } catch (e: AuthenticationException) { + delegates.remove(parent.cloud) + throw e + } + } + + @Throws(BackendException::class) + override fun exists(node: CloudNode): Boolean { + return try { + node.cloud?.let { networkConnectionCheck.assertConnectionIsPresent(it) } ?: throw IllegalStateException("Parent's cloud shouldn't be null") + delegateFor(node).exists(node) + } catch (e: AuthenticationException) { + delegates.remove(node.cloud) + throw e + } + } + + @Throws(BackendException::class) + override fun list(folder: CloudFolder): List { + return try { + folder.cloud?.let { networkConnectionCheck.assertConnectionIsPresent(it) } ?: throw IllegalStateException("Parent's cloud shouldn't be null") + delegateFor(folder).list(folder) + } catch (e: AuthenticationException) { + delegates.remove(folder.cloud) + throw e + } + } + + @Throws(BackendException::class) + override fun create(folder: CloudFolder): CloudFolder { + return try { + folder.cloud?.let { networkConnectionCheck.assertConnectionIsPresent(it) } ?: throw IllegalStateException("Parent's cloud shouldn't be null") + delegateFor(folder).create(folder) + } catch (e: AuthenticationException) { + delegates.remove(folder.cloud) + throw e + } + } + + @Throws(BackendException::class) + override fun move(source: CloudFolder, target: CloudFolder): CloudFolder { + return try { + source.cloud?.let { networkConnectionCheck.assertConnectionIsPresent(it) } ?: throw IllegalStateException("Parent's cloud shouldn't be null") + require(source.cloud == target.cloud) { "Cloud of parameters must match" } + delegateFor(source).move(source, target) + } catch (e: AuthenticationException) { + delegates.remove(source.cloud) + throw e + } + } + + @Throws(BackendException::class) + override fun move(source: CloudFile, target: CloudFile): CloudFile { + return try { + source.cloud?.let { networkConnectionCheck.assertConnectionIsPresent(it) } ?: throw IllegalStateException("Parent's cloud shouldn't be null") + require(source.cloud == target.cloud) { "Cloud of parameters must match" } + delegateFor(source).move(source, target) + } catch (e: AuthenticationException) { + delegates.remove(source.cloud) + throw e + } + } + + @Throws(BackendException::class) + override fun write(file: CloudFile, data: DataSource, progressAware: ProgressAware, replace: Boolean, size: Long): CloudFile { + return try { + file.cloud?.let { networkConnectionCheck.assertConnectionIsPresent(it) } ?: throw IllegalStateException("Parent's cloud shouldn't be null") + delegateFor(file).write(file, data, progressAware, replace, size) + } catch (e: AuthenticationException) { + delegates.remove(file.cloud) + throw e + } + } + + @Throws(BackendException::class) + override fun read(file: CloudFile, encryptedTmpFile: File?, data: OutputStream, progressAware: ProgressAware) { + try { + file.cloud?.let { networkConnectionCheck.assertConnectionIsPresent(it) } ?: throw IllegalStateException("Parent's cloud shouldn't be null") + delegateFor(file).read(file, encryptedTmpFile, data, progressAware) + } catch (e: AuthenticationException) { + delegates.remove(file.cloud) + throw e + } + } + + @Throws(BackendException::class) + override fun delete(node: CloudNode) { + try { + node.cloud?.let { networkConnectionCheck.assertConnectionIsPresent(it) } ?: throw IllegalStateException("Parent's cloud shouldn't be null") + delegateFor(node).delete(node) + } catch (e: AuthenticationException) { + delegates.remove(node.cloud) + throw e + } + } + + @Throws(BackendException::class) + override fun checkAuthenticationAndRetrieveCurrentAccount(cloud: Cloud): String { + return try { + networkConnectionCheck.assertConnectionIsPresent(cloud) + delegateFor(cloud).checkAuthenticationAndRetrieveCurrentAccount(cloud) + } catch (e: AuthenticationException) { + delegates.remove(cloud) + throw e + } + } + + @Throws(BackendException::class) + override fun logout(cloud: Cloud) { + delegateFor(cloud).logout(cloud) + removeCloudContentRepositoryFor(cloud) + } + + fun removeCloudContentRepositoryFor(cloud: Cloud) { + val clouds = delegates.keys.iterator() + while (clouds.hasNext()) { + val current = clouds.next() + if (cloud == current) { + clouds.remove() + } else if (cloudIsDelegateOfCryptoCloud(current, cloud)) { + cryptoCloudContentRepositoryFactory.deregisterCryptor((current as CryptoCloud).vault, false) + } + } + } + + private fun cloudIsDelegateOfCryptoCloud(potentialCryptoCloud: Cloud, cloud: Cloud): Boolean { + if (potentialCryptoCloud is CryptoCloud) { + val delegate = potentialCryptoCloud.vault.cloud + return cloud == delegate + } + return false + } + + private fun delegateFor(cloudNode: CloudNode): CloudContentRepository { + return cloudNode.cloud?.let { + delegateFor(it) + } ?: throw IllegalStateException("CloudNode's cloud shouldn't be null") + } + + private fun delegateFor(cloud: Cloud): CloudContentRepository { + return delegates.getOrPut(cloud, { + createCloudContentRepositoryFor(cloud) + }) + } + + private fun createCloudContentRepositoryFor(cloud: Cloud): CloudContentRepository { + for (cloudContentRepositoryFactory in cloudContentRepositoryFactories) { + if (cloudContentRepositoryFactory.supports(cloud)) { + return cloudContentRepositoryFactory.cloudContentRepositoryFor(cloud) + } + } + throw IllegalStateException("Unsupported cloud $cloud") + } +} diff --git a/data/src/main/java/org/cryptomator/data/repository/UpdateCheckRepositoryImpl.java b/data/src/main/java/org/cryptomator/data/repository/UpdateCheckRepositoryImpl.java index 3f7411d2..962ad968 100644 --- a/data/src/main/java/org/cryptomator/data/repository/UpdateCheckRepositoryImpl.java +++ b/data/src/main/java/org/cryptomator/data/repository/UpdateCheckRepositoryImpl.java @@ -3,6 +3,7 @@ package org.cryptomator.data.repository; import android.content.Context; import android.net.Uri; +import com.google.common.base.Optional; import com.google.common.io.BaseEncoding; import org.apache.commons.codec.binary.Hex; @@ -15,7 +16,6 @@ import org.cryptomator.domain.exception.update.GeneralUpdateErrorException; import org.cryptomator.domain.exception.update.HashMismatchUpdateCheckException; import org.cryptomator.domain.repository.UpdateCheckRepository; import org.cryptomator.domain.usecases.UpdateCheck; -import org.cryptomator.util.Optional; import java.io.File; import java.io.IOException; @@ -67,7 +67,7 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository { LatestVersion latestVersion = loadLatestVersion(); if (appVersion.equals(latestVersion.version)) { - return Optional.empty(); + return Optional.absent(); } final UpdateCheckEntity entity = database.load(UpdateCheckEntity.class, 1L); diff --git a/data/src/main/java/org/cryptomator/data/util/CopyStream.java b/data/src/main/java/org/cryptomator/data/util/CopyStream.java deleted file mode 100644 index b645d665..00000000 --- a/data/src/main/java/org/cryptomator/data/util/CopyStream.java +++ /dev/null @@ -1,66 +0,0 @@ -package org.cryptomator.data.util; - -import org.cryptomator.domain.exception.FatalBackendException; - -import java.io.ByteArrayOutputStream; -import java.io.Closeable; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -public class CopyStream { - - private static final int DEFAULT_COPY_BUFFER_SIZE = 16 << 10; // 16 KiB - - public static void copyStreamToStream(InputStream in, OutputStream out) { - copyStreamToStream(in, out, new byte[DEFAULT_COPY_BUFFER_SIZE]); - } - - private static void copyStreamToStream(InputStream in, OutputStream out, byte[] copyBuffer) { - while (true) { - int count; - try { - count = in.read(copyBuffer); - } catch (IOException ex) { - throw new FatalBackendException(ex); - } - - if (count == -1) { - break; - } - - try { - out.write(copyBuffer, 0, count); - } catch (IOException ex) { - throw new FatalBackendException(ex); - } - } - } - - public static void closeQuietly(Closeable closeable) { - if (closeable != null) { - try { - closeable.close(); - } catch (RuntimeException rethrown) { - throw rethrown; - } catch (IOException e) { - // ignore - } - } - } - - public static byte[] toByteArray(InputStream inputStream) { - ByteArrayOutputStream buffer = new ByteArrayOutputStream(); - int read; - byte[] data = new byte[1024]; - try { - while ((read = inputStream.read(data, 0, data.length)) != -1) { - buffer.write(data, 0, read); - } - buffer.flush(); - } catch (IOException e) { - throw new FatalBackendException(e); - } - return buffer.toByteArray(); - } -} diff --git a/data/src/main/java/org/cryptomator/data/util/CopyStream.kt b/data/src/main/java/org/cryptomator/data/util/CopyStream.kt new file mode 100644 index 00000000..23106088 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/util/CopyStream.kt @@ -0,0 +1,62 @@ +package org.cryptomator.data.util + +import org.cryptomator.domain.exception.FatalBackendException +import java.io.ByteArrayOutputStream +import java.io.Closeable +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream + +object CopyStream { + + private const val DEFAULT_COPY_BUFFER_SIZE = 16 shl 10 // 16 KiB + + fun copyStreamToStream(inputStream: InputStream, out: OutputStream) { + copyStreamToStream(inputStream, out, ByteArray(DEFAULT_COPY_BUFFER_SIZE)) + } + + private fun copyStreamToStream(inputStream: InputStream, out: OutputStream, copyBuffer: ByteArray) { + while (true) { + val count: Int = try { + inputStream.read(copyBuffer) + } catch (ex: IOException) { + throw FatalBackendException(ex) + } + if (count == -1) { + break + } + try { + out.write(copyBuffer, 0, count) + } catch (ex: IOException) { + throw FatalBackendException(ex) + } + } + } + + fun closeQuietly(closeable: Closeable?) { + if (closeable != null) { + try { + closeable.close() + } catch (rethrown: RuntimeException) { + throw rethrown + } catch (e: IOException) { + // ignore + } + } + } + + fun toByteArray(inputStream: InputStream): ByteArray { + val buffer = ByteArrayOutputStream() + var read: Int + val data = ByteArray(1024) + try { + while (inputStream.read(data, 0, data.size).also { read = it } != -1) { + buffer.write(data, 0, read) + } + buffer.flush() + } catch (e: IOException) { + throw FatalBackendException(e) + } + return buffer.toByteArray() + } +} diff --git a/data/src/main/java/org/cryptomator/data/util/NetworkConnectionCheck.java b/data/src/main/java/org/cryptomator/data/util/NetworkConnectionCheck.java deleted file mode 100644 index d140a3e0..00000000 --- a/data/src/main/java/org/cryptomator/data/util/NetworkConnectionCheck.java +++ /dev/null @@ -1,52 +0,0 @@ -package org.cryptomator.data.util; - -import android.content.Context; -import android.net.ConnectivityManager; -import android.net.Network; -import android.net.NetworkCapabilities; -import android.net.NetworkInfo; -import android.net.wifi.WifiManager; - -import org.cryptomator.domain.Cloud; -import org.cryptomator.domain.exception.NetworkConnectionException; - -import javax.inject.Inject; -import javax.inject.Singleton; - -@Singleton -public class NetworkConnectionCheck { - - private final Context context; - - @Inject - NetworkConnectionCheck(Context context) { - this.context = context; - } - - public void assertConnectionIsPresent(Cloud cloud) throws NetworkConnectionException { - if (cloud.requiresNetwork() && !isPresent()) { - throw new NetworkConnectionException(); - } - } - - public boolean isPresent() { - ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); - return networkInfo != null // - && networkInfo.isConnectedOrConnecting(); - } - - public boolean checkWifiOnAndConnected() { - ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { - Network activeNetwork = connectivityManager.getActiveNetwork(); - return connectivityManager.getNetworkCapabilities(activeNetwork).hasTransport(NetworkCapabilities.TRANSPORT_WIFI); - } else { - final WifiManager wifiManager = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE); - if (wifiManager.isWifiEnabled()) { - return wifiManager.getConnectionInfo().getNetworkId() != -1; // fails on devices post 8.x - } - return false; - } - } -} diff --git a/data/src/main/java/org/cryptomator/data/util/NetworkConnectionCheck.kt b/data/src/main/java/org/cryptomator/data/util/NetworkConnectionCheck.kt new file mode 100644 index 00000000..14332c8d --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/util/NetworkConnectionCheck.kt @@ -0,0 +1,33 @@ +package org.cryptomator.data.util + +import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import org.cryptomator.domain.Cloud +import org.cryptomator.domain.exception.NetworkConnectionException +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class NetworkConnectionCheck @Inject internal constructor(private val context: Context) { + + @Throws(NetworkConnectionException::class) + fun assertConnectionIsPresent(cloud: Cloud) { + if (cloud.requiresNetwork() && !isPresent) { + throw NetworkConnectionException() + } + } + + val isPresent: Boolean + get() { + val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val networkInfo = connectivityManager.activeNetworkInfo + return (networkInfo != null && networkInfo.isConnectedOrConnecting) + } + + fun checkWifiOnAndConnected(): Boolean { + val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val activeNetwork = connectivityManager.activeNetwork + return connectivityManager.getNetworkCapabilities(activeNetwork).hasTransport(NetworkCapabilities.TRANSPORT_WIFI) + } +} diff --git a/data/src/main/java/org/cryptomator/data/util/NetworkTimeout.java b/data/src/main/java/org/cryptomator/data/util/NetworkTimeout.java deleted file mode 100644 index 72cc734d..00000000 --- a/data/src/main/java/org/cryptomator/data/util/NetworkTimeout.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.cryptomator.data.util; - -import java.util.concurrent.TimeUnit; - -import static java.util.concurrent.TimeUnit.MINUTES; - -public enum NetworkTimeout { - - CONNECTION(2L, MINUTES), // - READ(2L, MINUTES), // - WRITE(2L, MINUTES); - - private final long timeout; - private final TimeUnit unit; - - NetworkTimeout(long timeout, TimeUnit unit) { - this.timeout = timeout; - this.unit = unit; - } - - public long getTimeout() { - return timeout; - } - - public TimeUnit getUnit() { - return unit; - } - - public long asMilliseconds() { - return unit.toMillis(timeout); - } -} diff --git a/data/src/main/java/org/cryptomator/data/util/NetworkTimeout.kt b/data/src/main/java/org/cryptomator/data/util/NetworkTimeout.kt new file mode 100644 index 00000000..808600b3 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/util/NetworkTimeout.kt @@ -0,0 +1,13 @@ +package org.cryptomator.data.util + +import java.util.concurrent.TimeUnit + +enum class NetworkTimeout(val timeout: Long, val unit: TimeUnit) { + CONNECTION(2L, TimeUnit.MINUTES), // + READ(2L, TimeUnit.MINUTES), // + WRITE(2L, TimeUnit.MINUTES); + + fun asMilliseconds(): Long { + return unit.toMillis(timeout) + } +} diff --git a/data/src/main/java/org/cryptomator/data/util/TransferredBytesAwareInputStream.java b/data/src/main/java/org/cryptomator/data/util/TransferredBytesAwareInputStream.java deleted file mode 100644 index af92e92b..00000000 --- a/data/src/main/java/org/cryptomator/data/util/TransferredBytesAwareInputStream.java +++ /dev/null @@ -1,68 +0,0 @@ -package org.cryptomator.data.util; - -import org.jetbrains.annotations.NotNull; - -import java.io.IOException; -import java.io.InputStream; - -public abstract class TransferredBytesAwareInputStream extends InputStream { - - private static final int EOF = -1; - - private final InputStream in; - - private long transferred; - - public TransferredBytesAwareInputStream(InputStream in) { - this.in = in; - } - - @Override - public int read() throws IOException { - int result = in.read(); - if (result != EOF) { - bytesTransferred(++transferred); - } - return result; - } - - @Override - public int read(byte @NotNull [] b) throws IOException { - int result = in.read(b); - if (result != EOF) { - transferred += result; - bytesTransferred(transferred); - } - return result; - } - - @Override - public int read(byte @NotNull [] b, int off, int len) throws IOException { - int result = in.read(b, off, len); - if (result != EOF) { - transferred += result; - bytesTransferred(transferred); - } - return result; - } - - @Override - public void close() throws IOException { - in.close(); - } - - @Override - public int available() throws IOException { - return in.available(); - } - - @Override - public long skip(long n) throws IOException { - long result = in.skip(n); - transferred += result; - bytesTransferred(transferred); - return result; - } - - public abstract void bytesTransferred(long transferred); -} diff --git a/data/src/main/java/org/cryptomator/data/util/TransferredBytesAwareInputStream.kt b/data/src/main/java/org/cryptomator/data/util/TransferredBytesAwareInputStream.kt new file mode 100644 index 00000000..3d3a136e --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/util/TransferredBytesAwareInputStream.kt @@ -0,0 +1,63 @@ +package org.cryptomator.data.util + +import java.io.IOException +import java.io.InputStream + +abstract class TransferredBytesAwareInputStream(private val inputStream: InputStream) : InputStream() { + + private var transferred: Long = 0 + + @Throws(IOException::class) + override fun read(): Int { + val result = inputStream.read() + if (result != EOF) { + bytesTransferred(++transferred) + } + return result + } + + @Throws(IOException::class) + override fun read(b: ByteArray): Int { + val result = inputStream.read(b) + if (result != EOF) { + transferred += result.toLong() + bytesTransferred(transferred) + } + return result + } + + @Throws(IOException::class) + override fun read(b: ByteArray, off: Int, len: Int): Int { + val result = inputStream.read(b, off, len) + if (result != EOF) { + transferred += result.toLong() + bytesTransferred(transferred) + } + return result + } + + @Throws(IOException::class) + override fun close() { + inputStream.close() + } + + @Throws(IOException::class) + override fun available(): Int { + return inputStream.available() + } + + @Throws(IOException::class) + override fun skip(n: Long): Long { + val result = inputStream.skip(n) + transferred += result + bytesTransferred(transferred) + return result + } + + abstract fun bytesTransferred(transferred: Long) + + companion object { + + private const val EOF = -1 + } +} diff --git a/data/src/main/java/org/cryptomator/data/util/TransferredBytesAwareOutputStream.java b/data/src/main/java/org/cryptomator/data/util/TransferredBytesAwareOutputStream.java deleted file mode 100644 index 3f1f27bc..00000000 --- a/data/src/main/java/org/cryptomator/data/util/TransferredBytesAwareOutputStream.java +++ /dev/null @@ -1,50 +0,0 @@ -package org.cryptomator.data.util; - -import org.jetbrains.annotations.NotNull; - -import java.io.IOException; -import java.io.OutputStream; - -public abstract class TransferredBytesAwareOutputStream extends OutputStream { - - private final OutputStream out; - - private long transferred; - - public TransferredBytesAwareOutputStream(OutputStream out) { - this.out = out; - } - - @Override - public void write(byte @NotNull [] b) throws IOException { - out.write(b); - transferred += b.length; - bytesTransferred(transferred); - } - - @Override - public void write(byte @NotNull [] b, int off, int len) throws IOException { - out.write(b, off, len); - transferred += len; - bytesTransferred(transferred); - } - - @Override - public void write(int i) throws IOException { - out.write(i); - bytesTransferred(++transferred); - } - - @Override - public void close() throws IOException { - out.close(); - } - - @Override - public void flush() throws IOException { - out.flush(); - } - - public abstract void bytesTransferred(long transferred); - -} diff --git a/data/src/main/java/org/cryptomator/data/util/TransferredBytesAwareOutputStream.kt b/data/src/main/java/org/cryptomator/data/util/TransferredBytesAwareOutputStream.kt new file mode 100644 index 00000000..aba8a47b --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/util/TransferredBytesAwareOutputStream.kt @@ -0,0 +1,41 @@ +package org.cryptomator.data.util + +import java.io.IOException +import java.io.OutputStream + +abstract class TransferredBytesAwareOutputStream(private val outputStream: OutputStream) : OutputStream() { + + private var transferred: Long = 0 + + @Throws(IOException::class) + override fun write(b: ByteArray) { + outputStream.write(b) + transferred += b.size.toLong() + bytesTransferred(transferred) + } + + @Throws(IOException::class) + override fun write(b: ByteArray, off: Int, len: Int) { + outputStream.write(b, off, len) + transferred += len.toLong() + bytesTransferred(transferred) + } + + @Throws(IOException::class) + override fun write(i: Int) { + outputStream.write(i) + bytesTransferred(++transferred) + } + + @Throws(IOException::class) + override fun close() { + outputStream.close() + } + + @Throws(IOException::class) + override fun flush() { + outputStream.flush() + } + + abstract fun bytesTransferred(transferred: Long) +} diff --git a/data/src/main/java/org/cryptomator/data/util/UserAgentInterceptor.java b/data/src/main/java/org/cryptomator/data/util/UserAgentInterceptor.java deleted file mode 100644 index f6c9d442..00000000 --- a/data/src/main/java/org/cryptomator/data/util/UserAgentInterceptor.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.cryptomator.data.util; - -import org.cryptomator.data.BuildConfig; - -import java.io.IOException; - -import okhttp3.Interceptor; -import okhttp3.Request; -import okhttp3.Response; - -public class UserAgentInterceptor implements Interceptor { - - @Override - public Response intercept(Chain chain) throws IOException { - Request originalRequest = chain.request(); - String userAgent = "Cryptomator-Android/" + BuildConfig.VERSION_NAME + " " + System.getProperty("http.agent"); - Request requestWithUserAgent = originalRequest.newBuilder().header("User-Agent", userAgent).build(); - return chain.proceed(requestWithUserAgent); - } - -} diff --git a/data/src/main/java/org/cryptomator/data/util/UserAgentInterceptor.kt b/data/src/main/java/org/cryptomator/data/util/UserAgentInterceptor.kt new file mode 100644 index 00000000..095cc015 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/util/UserAgentInterceptor.kt @@ -0,0 +1,18 @@ +package org.cryptomator.data.util + +import org.cryptomator.data.BuildConfig +import java.io.IOException +import okhttp3.Interceptor +import okhttp3.Request +import okhttp3.Response + +class UserAgentInterceptor : Interceptor { + + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + val originalRequest: Request = chain.request() + val userAgent = "Cryptomator-Android/" + BuildConfig.VERSION_NAME + " " + System.getProperty("http.agent") + val requestWithUserAgent = originalRequest.newBuilder().header("User-Agent", userAgent).build() + return chain.proceed(requestWithUserAgent) + } +} diff --git a/data/src/main/java/org/cryptomator/data/util/X509CertificateHelper.java b/data/src/main/java/org/cryptomator/data/util/X509CertificateHelper.java deleted file mode 100644 index d0293972..00000000 --- a/data/src/main/java/org/cryptomator/data/util/X509CertificateHelper.java +++ /dev/null @@ -1,41 +0,0 @@ -package org.cryptomator.data.util; - -import android.util.Base64; - -import org.apache.commons.codec.binary.Hex; -import org.apache.commons.codec.digest.DigestUtils; - -import java.io.ByteArrayInputStream; -import java.security.cert.CertificateEncodingException; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; - -public class X509CertificateHelper { - - private static final String CERT_BEGIN = "-----BEGIN CERTIFICATE-----\n"; - private static final String CERT_END = "-----END CERTIFICATE-----"; - - public static String convertToPem(X509Certificate cert) throws CertificateEncodingException { - String pemCertPre = Base64.encodeToString(cert.getEncoded(), Base64.DEFAULT); - return CERT_BEGIN + pemCertPre + CERT_END; - } - - public static X509Certificate convertFromPem(String pem) throws CertificateException { - byte[] decoded = Base64 // - .decode(pem.replaceAll(CERT_BEGIN, "").replaceAll(CERT_END, ""), Base64.DEFAULT); - - return (X509Certificate) CertificateFactory // - .getInstance("X.509") // - .generateCertificate(new ByteArrayInputStream(decoded)); - } - - public static String getFingerprintFormatted(X509Certificate certificate) throws CertificateEncodingException { - String hash = new String(Hex.encodeHex(DigestUtils.sha1(certificate.getEncoded()))) // - .toUpperCase() // - .replaceAll("(.{2})", "$1:"); - hash = hash.substring(0, hash.length() - 1); - return "SHA-256 " + hash; - } - -} diff --git a/data/src/main/java/org/cryptomator/data/util/X509CertificateHelper.kt b/data/src/main/java/org/cryptomator/data/util/X509CertificateHelper.kt new file mode 100644 index 00000000..163ba70a --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/util/X509CertificateHelper.kt @@ -0,0 +1,41 @@ +package org.cryptomator.data.util + +import android.util.Base64 +import org.apache.commons.codec.binary.Hex +import org.apache.commons.codec.digest.DigestUtils +import java.io.ByteArrayInputStream +import java.security.cert.CertificateEncodingException +import java.security.cert.CertificateException +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate + +object X509CertificateHelper { + + private const val CERT_BEGIN = "-----BEGIN CERTIFICATE-----\n" + private const val CERT_END = "-----END CERTIFICATE-----" + + @JvmStatic + @Throws(CertificateEncodingException::class) + fun convertToPem(cert: X509Certificate): String { + val pemCertPre = Base64.encodeToString(cert.encoded, Base64.DEFAULT) + return CERT_BEGIN + pemCertPre + CERT_END + } + + @Throws(CertificateException::class) + fun convertFromPem(pem: String): X509Certificate { + val decoded = Base64 // + .decode(pem.replace(CERT_BEGIN.toRegex(), "").replace(CERT_END.toRegex(), ""), Base64.DEFAULT) + return CertificateFactory // + .getInstance("X.509") // + .generateCertificate(ByteArrayInputStream(decoded)) as X509Certificate + } + + @Throws(CertificateEncodingException::class) + fun getFingerprintFormatted(certificate: X509Certificate): String { + var hash = String(Hex.encodeHex(DigestUtils.sha1(certificate.encoded))) // + .uppercase() // + .replace("(.{2})".toRegex(), "$1:") + hash = hash.substring(0, hash.length - 1) + return "SHA-256 $hash" + } +} diff --git a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/FixedGoogleAccountCredential.java b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/FixedGoogleAccountCredential.java index b8a2b4e8..129f0112 100644 --- a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/FixedGoogleAccountCredential.java +++ b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/FixedGoogleAccountCredential.java @@ -28,7 +28,7 @@ import java.util.Collection; import static com.google.android.gms.auth.GoogleAuthUtil.GOOGLE_ACCOUNT_TYPE; -class FixedGoogleAccountCredential extends GoogleAccountCredential { +public class FixedGoogleAccountCredential extends GoogleAccountCredential { private String accountName; diff --git a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveClientFactory.java b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveClientFactory.java deleted file mode 100644 index c4a41c62..00000000 --- a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveClientFactory.java +++ /dev/null @@ -1,65 +0,0 @@ -package org.cryptomator.data.cloud.googledrive; - -import android.content.Context; - -import com.google.api.client.http.javanet.NetHttpTransport; -import com.google.api.client.json.jackson2.JacksonFactory; -import com.google.api.services.drive.Drive; -import com.google.api.services.drive.DriveScopes; - -import org.cryptomator.data.BuildConfig; -import org.cryptomator.domain.exception.FatalBackendException; -import org.cryptomator.util.SharedPreferencesHandler; - -import java.util.Collections; -import java.util.logging.Handler; -import java.util.logging.Level; -import java.util.logging.LogRecord; -import java.util.logging.Logger; - -import timber.log.Timber; - -class GoogleDriveClientFactory { - - private final Context context; - private final SharedPreferencesHandler sharedPreferencesHandler; - - GoogleDriveClientFactory(Context context, SharedPreferencesHandler sharedPreferencesHandler) { - this.context = context; - this.sharedPreferencesHandler = sharedPreferencesHandler; - } - - Drive getClient(String accountName) throws FatalBackendException { - if (sharedPreferencesHandler.debugMode()) { - Logger.getLogger("com.google.api.client").setLevel(Level.CONFIG); - Logger.getLogger("com.google.api.client").addHandler(new Handler() { - @Override - public void publish(LogRecord record) { - if (record.getMessage().startsWith("-------------- RESPONSE --------------") // - || record.getMessage().startsWith("-------------- REQUEST --------------") // - || record.getMessage().startsWith("{\n \"files\": [\n")) { - Timber.tag("GoogleDriveClient").d(record.getMessage()); - } - } - - @Override - public void flush() { - } - - @Override - public void close() throws SecurityException { - } - }); - } - - try { - FixedGoogleAccountCredential credential = FixedGoogleAccountCredential.usingOAuth2(context, Collections.singleton(DriveScopes.DRIVE)); - credential.setAccountName(accountName); - return new Drive.Builder(new NetHttpTransport(), JacksonFactory.getDefaultInstance(), credential) // - .setApplicationName("Cryptomator-Android/" + BuildConfig.VERSION_NAME) // - .build(); - } catch (Exception e) { - throw new FatalBackendException(e); - } - } -} diff --git a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveClientFactory.kt b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveClientFactory.kt new file mode 100644 index 00000000..fe8c13ec --- /dev/null +++ b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveClientFactory.kt @@ -0,0 +1,59 @@ +package org.cryptomator.data.cloud.googledrive + +import android.content.Context +import com.google.api.client.http.javanet.NetHttpTransport +import com.google.api.client.json.jackson2.JacksonFactory +import com.google.api.services.drive.Drive +import com.google.api.services.drive.DriveScopes +import org.cryptomator.data.BuildConfig +import org.cryptomator.domain.exception.FatalBackendException +import org.cryptomator.util.SharedPreferencesHandler +import java.util.logging.Handler +import java.util.logging.Level +import java.util.logging.LogRecord +import java.util.logging.Logger +import timber.log.Timber + +class GoogleDriveClientFactory internal constructor() { + + companion object { + + @Volatile + private var instance: Drive? = null + + @Synchronized + fun getInstance(accountName: String, context: Context): Drive = instance ?: createClient(accountName, context).also { instance = it } + + @Throws(FatalBackendException::class) + fun createClient(accountName: String, context: Context): Drive { + if ( SharedPreferencesHandler(context).debugMode()) { + Logger.getLogger("com.google.api.client").level = Level.CONFIG + Logger.getLogger("com.google.api.client").addHandler(object : Handler() { + override fun publish(record: LogRecord) { + if (record.message.startsWith("-------------- RESPONSE --------------") // + || record.message.startsWith("-------------- REQUEST --------------") // + || record.message.startsWith("{\n \"files\": [\n") + ) { + Timber.tag("GoogleDriveClient").d(record.message) + } + } + + override fun flush() {} + + @Throws(SecurityException::class) + override fun close() { + } + }) + } + return try { + val credential: FixedGoogleAccountCredential = FixedGoogleAccountCredential.usingOAuth2(context, setOf(DriveScopes.DRIVE)) + credential.setAccountName(accountName) + Drive.Builder(NetHttpTransport(), JacksonFactory.getDefaultInstance(), credential) // + .setApplicationName("Cryptomator-Android/" + BuildConfig.VERSION_NAME) // + .build() + } catch (e: Exception) { + throw FatalBackendException(e) + } + } + } +} diff --git a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveCloudContentRepository.java b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveCloudContentRepository.java deleted file mode 100644 index a7e4b86c..00000000 --- a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveCloudContentRepository.java +++ /dev/null @@ -1,213 +0,0 @@ -package org.cryptomator.data.cloud.googledrive; - -import android.content.Context; - -import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException; -import com.google.api.client.googleapis.json.GoogleJsonResponseException; -import com.google.api.client.http.HttpStatusCodes; - -import org.cryptomator.data.cloud.InterceptingCloudContentRepository; -import org.cryptomator.domain.CloudNode; -import org.cryptomator.domain.GoogleDriveCloud; -import org.cryptomator.domain.exception.BackendException; -import org.cryptomator.domain.exception.FatalBackendException; -import org.cryptomator.domain.exception.NetworkConnectionException; -import org.cryptomator.domain.exception.NoSuchCloudFileException; -import org.cryptomator.domain.exception.authentication.UserRecoverableAuthenticationException; -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.ExceptionUtil; -import org.cryptomator.util.Optional; - -import java.io.File; -import java.io.IOException; -import java.io.OutputStream; -import java.net.SocketTimeoutException; -import java.util.List; - -import static org.cryptomator.util.ExceptionUtil.contains; -import static org.cryptomator.util.ExceptionUtil.extract; - -class GoogleDriveCloudContentRepository extends InterceptingCloudContentRepository { - - private final GoogleDriveCloud cloud; - - GoogleDriveCloudContentRepository(Context context, GoogleDriveCloud cloud, GoogleDriveIdCache idCache) { - super(new Intercepted(context, cloud, idCache)); - this.cloud = cloud; - } - - @Override - protected void throwWrappedIfRequired(Exception e) throws BackendException { - throwConnectionErrorIfRequired(e); - throwUserRecoverableAuthenticationExceptionIfRequired(e); - throwNoSuchCloudFileExceptionIfRequired(e); - } - - private void throwUserRecoverableAuthenticationExceptionIfRequired(Exception e) { - Optional userRecoverableAuthIOException = extract(e, UserRecoverableAuthIOException.class); - if (userRecoverableAuthIOException.isPresent()) { - throw new UserRecoverableAuthenticationException(cloud, userRecoverableAuthIOException.get().getIntent()); - } - } - - private void throwConnectionErrorIfRequired(Exception e) throws NetworkConnectionException { - if (contains(e, SocketTimeoutException.class) || contains(e, IOException.class, ExceptionUtil.thatHasMessage("NetworkError"))) { - throw new NetworkConnectionException(e); - } - } - - private void throwNoSuchCloudFileExceptionIfRequired(Exception e) throws NoSuchCloudFileException { - if (contains(e, GoogleJsonResponseException.class)) { - if (extract(e, GoogleJsonResponseException.class).get().getStatusCode() == HttpStatusCodes.STATUS_CODE_NOT_FOUND) { - throw new NoSuchCloudFileException(); - } - } - } - - private static class Intercepted implements CloudContentRepository { - - private final GoogleDriveImpl impl; - - public Intercepted(Context context, GoogleDriveCloud cloud, GoogleDriveIdCache idCache) { - this.impl = new GoogleDriveImpl(context, cloud, idCache); - } - - @Override - public GoogleDriveFolder root(GoogleDriveCloud cloud) throws BackendException { - return impl.root(); - } - - @Override - public GoogleDriveFolder resolve(GoogleDriveCloud cloud, String path) throws BackendException { - try { - return impl.resolve(path); - } catch (IOException e) { - throw new FatalBackendException(e); - } - } - - @Override - public GoogleDriveFile file(GoogleDriveFolder parent, String name) throws BackendException { - try { - return impl.file(parent, name); - } catch (IOException e) { - throw new FatalBackendException(e); - } - } - - @Override - public GoogleDriveFile file(GoogleDriveFolder parent, String name, Optional size) throws BackendException { - try { - return impl.file(parent, name, size); - } catch (IOException e) { - throw new FatalBackendException(e); - } - } - - @Override - public GoogleDriveFolder folder(GoogleDriveFolder parent, String name) throws BackendException { - try { - return impl.folder(parent, name); - } catch (IOException e) { - throw new FatalBackendException(e); - } - } - - @Override - public boolean exists(GoogleDriveNode node) throws BackendException { - try { - return impl.exists(node); - } catch (IOException e) { - throw new FatalBackendException(e); - } - } - - @Override - public List list(GoogleDriveFolder folder) throws BackendException { - try { - return impl.list(folder); - } catch (IOException e) { - throw new FatalBackendException(e); - } - } - - @Override - public GoogleDriveFolder create(GoogleDriveFolder folder) throws BackendException { - try { - return impl.create(folder); - } catch (IOException e) { - throw new FatalBackendException(e); - } - } - - @Override - public GoogleDriveFolder move(GoogleDriveFolder source, GoogleDriveFolder target) throws BackendException { - try { - if (source.getDriveId() == null) { - throw new NoSuchCloudFileException(source.getName()); - } - return (GoogleDriveFolder) impl.move(source, target); - } catch (IOException e) { - throw new FatalBackendException(e); - } - } - - @Override - public GoogleDriveFile move(GoogleDriveFile source, GoogleDriveFile target) throws BackendException { - try { - return (GoogleDriveFile) impl.move(source, target); - } catch (IOException e) { - throw new FatalBackendException(e); - } - } - - @Override - public GoogleDriveFile write(GoogleDriveFile file, DataSource data, ProgressAware progressAware, boolean replace, long size) throws BackendException { - try { - return impl.write(file, data, progressAware, replace, size); - } catch (IOException e) { - throw new FatalBackendException(e); - } - } - - @Override - public void read(GoogleDriveFile file, Optional encryptedTmpFile, OutputStream data, ProgressAware progressAware) throws BackendException { - try { - if (file.getDriveId() == null) { - throw new NoSuchCloudFileException(file.getName()); - } - impl.read(file, encryptedTmpFile, data, progressAware); - } catch (IOException e) { - throw new FatalBackendException(e); - } - } - - @Override - public void delete(GoogleDriveNode node) throws BackendException { - try { - impl.delete(node); - } catch (IOException e) { - throw new FatalBackendException(e); - } - } - - @Override - public String checkAuthenticationAndRetrieveCurrentAccount(GoogleDriveCloud cloud) throws BackendException { - try { - return impl.currentAccount(); - } catch (IOException e) { - throw new FatalBackendException(e); - } - } - - @Override - public void logout(GoogleDriveCloud cloud) throws BackendException { - // empty - } - } - -} diff --git a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveCloudContentRepository.kt b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveCloudContentRepository.kt new file mode 100644 index 00000000..85f160d7 --- /dev/null +++ b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveCloudContentRepository.kt @@ -0,0 +1,195 @@ +package org.cryptomator.data.cloud.googledrive + +import android.content.Context +import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException +import com.google.api.client.googleapis.json.GoogleJsonResponseException +import com.google.api.client.http.HttpStatusCodes +import org.cryptomator.data.cloud.InterceptingCloudContentRepository +import org.cryptomator.domain.GoogleDriveCloud +import org.cryptomator.domain.exception.BackendException +import org.cryptomator.domain.exception.FatalBackendException +import org.cryptomator.domain.exception.NetworkConnectionException +import org.cryptomator.domain.exception.NoSuchCloudFileException +import org.cryptomator.domain.exception.authentication.UserRecoverableAuthenticationException +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.ExceptionUtil +import java.io.File +import java.io.IOException +import java.io.OutputStream +import java.net.SocketTimeoutException + +internal class GoogleDriveCloudContentRepository(context: Context, private val cloud: GoogleDriveCloud, idCache: GoogleDriveIdCache) : + InterceptingCloudContentRepository(Intercepted(context, cloud, idCache)) { + + @Throws(BackendException::class) + override fun throwWrappedIfRequired(e: Exception) { + throwConnectionErrorIfRequired(e) + throwUserRecoverableAuthenticationExceptionIfRequired(e) + throwNoSuchCloudFileExceptionIfRequired(e) + } + + private fun throwUserRecoverableAuthenticationExceptionIfRequired(e: Exception) { + val userRecoverableAuthIOException = ExceptionUtil.extract(e, UserRecoverableAuthIOException::class.java) + if (userRecoverableAuthIOException.isPresent) { + throw UserRecoverableAuthenticationException(cloud, userRecoverableAuthIOException.get().intent) + } + } + + @Throws(NetworkConnectionException::class) + private fun throwConnectionErrorIfRequired(e: Exception) { + if (ExceptionUtil.contains(e, SocketTimeoutException::class.java) || ExceptionUtil.contains(e, IOException::class.java, ExceptionUtil.thatHasMessage("NetworkError"))) { + throw NetworkConnectionException(e) + } + } + + @Throws(NoSuchCloudFileException::class) + private fun throwNoSuchCloudFileExceptionIfRequired(e: Exception) { + if (ExceptionUtil.contains(e, GoogleJsonResponseException::class.java)) { + if (ExceptionUtil.extract(e, GoogleJsonResponseException::class.java).get().statusCode == HttpStatusCodes.STATUS_CODE_NOT_FOUND) { + throw NoSuchCloudFileException() + } + } + } + + private class Intercepted(context: Context, cloud: GoogleDriveCloud, idCache: GoogleDriveIdCache) : CloudContentRepository { + + private val impl: GoogleDriveImpl = GoogleDriveImpl(context, cloud, idCache) + + @Throws(BackendException::class) + override fun root(cloud: GoogleDriveCloud): GoogleDriveFolder { + return impl.root() + } + + override fun resolve(cloud: GoogleDriveCloud, path: String): GoogleDriveFolder { + return try { + impl.resolve(path) + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun file(parent: GoogleDriveFolder, name: String): GoogleDriveFile { + return try { + impl.file(parent, name, null) + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun file(parent: GoogleDriveFolder, name: String, size: Long?): GoogleDriveFile { + return try { + impl.file(parent, name, size) + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun folder(parent: GoogleDriveFolder, name: String): GoogleDriveFolder { + return try { + impl.folder(parent, name) + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun exists(node: GoogleDriveNode): Boolean { + return try { + impl.exists(node) + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun list(folder: GoogleDriveFolder): List { + return try { + impl.list(folder) + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun create(folder: GoogleDriveFolder): GoogleDriveFolder { + return try { + impl.create(folder) + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun move(source: GoogleDriveFolder, target: GoogleDriveFolder): GoogleDriveFolder { + return try { + if (source.driveId == null) { + throw NoSuchCloudFileException(source.name) + } + impl.move(source, target) as GoogleDriveFolder + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun move(source: GoogleDriveFile, target: GoogleDriveFile): GoogleDriveFile { + return try { + impl.move(source, target) as GoogleDriveFile + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun write(file: GoogleDriveFile, data: DataSource, progressAware: ProgressAware, replace: Boolean, size: Long): GoogleDriveFile { + return try { + impl.write(file, data, progressAware, replace, size) + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun read(file: GoogleDriveFile, encryptedTmpFile: File?, data: OutputStream, progressAware: ProgressAware) { + try { + if (file.driveId == null) { + throw NoSuchCloudFileException(file.name) + } + impl.read(file, encryptedTmpFile, data, progressAware) + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun delete(node: GoogleDriveNode) { + try { + impl.delete(node) + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun checkAuthenticationAndRetrieveCurrentAccount(cloud: GoogleDriveCloud): String { + return try { + impl.currentAccount() + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + + @Throws(BackendException::class) + override fun logout(cloud: GoogleDriveCloud) { + // empty + } + + } +} diff --git a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveCloudContentRepositoryFactory.java b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveCloudContentRepositoryFactory.java index b48527e7..ff831344 100644 --- a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveCloudContentRepositoryFactory.java +++ b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveCloudContentRepositoryFactory.java @@ -29,7 +29,7 @@ public class GoogleDriveCloudContentRepositoryFactory implements CloudContentRep } @Override - public CloudContentRepository cloudContentRepositoryFor(Cloud cloud) { + public CloudContentRepository cloudContentRepositoryFor(Cloud cloud) { return new GoogleDriveCloudContentRepository(context, (GoogleDriveCloud) cloud, idCache); } } diff --git a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveCloudNodeFactory.java b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveCloudNodeFactory.java deleted file mode 100644 index a3f35e33..00000000 --- a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveCloudNodeFactory.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.cryptomator.data.cloud.googledrive; - -import com.google.api.services.drive.model.File; - -import org.cryptomator.util.Optional; - -import java.util.Date; - -class GoogleDriveCloudNodeFactory { - - public static GoogleDriveFile file(GoogleDriveFolder parent, File file) { - return new GoogleDriveFile(parent, file.getName(), getNodePath(parent, file.getName()), file.getId(), getFileSize(file), getModified(file)); - } - - public static GoogleDriveFile file(GoogleDriveFolder parent, String name, Optional size) { - return new GoogleDriveFile(parent, name, getNodePath(parent, name), null, size, Optional.empty()); - } - - private static Optional getModified(File file) { - return file.getModifiedTime() != null ? Optional.of(new Date(file.getModifiedTime().getValue())) : Optional.empty(); - } - - private static Optional getFileSize(File file) { - return file.getSize() != null ? Optional.of(file.getSize()) : Optional.empty(); - } - - public static GoogleDriveFile file(GoogleDriveFolder parent, String name, Optional size, String path, String driveId) { - return new GoogleDriveFile(parent, name, path, driveId, size, Optional.empty()); - } - - public static GoogleDriveFolder folder(GoogleDriveFolder parent, File file) { - return new GoogleDriveFolder(parent, file.getName(), getNodePath(parent, file.getName()), file.getId()); - } - - public static GoogleDriveFolder folder(GoogleDriveFolder parent, String name) { - return new GoogleDriveFolder(parent, name, getNodePath(parent, name), null); - } - - public static GoogleDriveFolder folder(GoogleDriveFolder parent, String name, String path, String driveId) { - return new GoogleDriveFolder(parent, name, path, driveId); - } - - public static GoogleDriveNode from(GoogleDriveFolder parent, File file) { - if (isFolder(file)) { - return folder(parent, file); - } else { - return file(parent, file); - } - } - - public static boolean isFolder(File file) { - return file.getMimeType().equals("application/vnd.google-apps.folder"); - } - - public static String getNodePath(GoogleDriveFolder parent, String name) { - return parent.getPath() + "/" + name; - } - -} diff --git a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveCloudNodeFactory.kt b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveCloudNodeFactory.kt new file mode 100644 index 00000000..a78d7751 --- /dev/null +++ b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveCloudNodeFactory.kt @@ -0,0 +1,55 @@ +package org.cryptomator.data.cloud.googledrive + +import com.google.api.services.drive.model.File +import java.util.Date + +internal object GoogleDriveCloudNodeFactory { + + fun file(parent: GoogleDriveFolder, file: File): GoogleDriveFile { + return GoogleDriveFile(parent, file.name, getNodePath(parent, file.name), file.id, getFileSize(file), getModified(file)) + } + + fun file(parent: GoogleDriveFolder, name: String, size: Long?): GoogleDriveFile { + return GoogleDriveFile(parent, name, getNodePath(parent, name), null, size, null) + } + + private fun getModified(file: File): Date? { + return if (file.modifiedTime != null) Date(file.modifiedTime.value) else null + } + + private fun getFileSize(file: File): Long? { + return if (file.getSize() != null) file.getSize() else null + } + + fun file(parent: GoogleDriveFolder, name: String, size: Long?, path: String, driveId: String): GoogleDriveFile { + return GoogleDriveFile(parent, name, path, driveId, size, null) + } + + fun folder(parent: GoogleDriveFolder, file: File): GoogleDriveFolder { + return GoogleDriveFolder(parent, file.name, getNodePath(parent, file.name), file.id) + } + + fun folder(parent: GoogleDriveFolder, name: String): GoogleDriveFolder { + return GoogleDriveFolder(parent, name, getNodePath(parent, name), null) + } + + fun folder(parent: GoogleDriveFolder?, name: String, path: String, driveId: String): GoogleDriveFolder { + return GoogleDriveFolder(parent, name, path, driveId) + } + + fun from(parent: GoogleDriveFolder, file: File): GoogleDriveNode { + return if (isFolder(file)) { + folder(parent, file) + } else { + file(parent, file) + } + } + + fun isFolder(file: File): Boolean { + return file.mimeType == "application/vnd.google-apps.folder" + } + + fun getNodePath(parent: GoogleDriveFolder, name: String): String { + return parent.path + "/" + name + } +} diff --git a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveFile.java b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveFile.java deleted file mode 100644 index b0716a8a..00000000 --- a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveFile.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.cryptomator.data.cloud.googledrive; - -import org.cryptomator.domain.Cloud; -import org.cryptomator.domain.CloudFile; -import org.cryptomator.util.Optional; - -import java.util.Date; - -class GoogleDriveFile implements CloudFile, GoogleDriveNode { - - private final GoogleDriveFolder parent; - private final String name; - private final String path; - private final String driveId; - private final Optional size; - private final Optional modified; - - public GoogleDriveFile(GoogleDriveFolder parent, String name, String path, String driveId, Optional size, Optional modified) { - this.parent = parent; - this.name = name; - this.path = path; - this.driveId = driveId; - 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 String getDriveId() { - return driveId; - } - - @Override - public GoogleDriveFolder getParent() { - return parent; - } - - @Override - public Optional getSize() { - return size; - } - - @Override - public Optional getModified() { - return modified; - } -} diff --git a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveFile.kt b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveFile.kt new file mode 100644 index 00000000..99475804 --- /dev/null +++ b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveFile.kt @@ -0,0 +1,19 @@ +package org.cryptomator.data.cloud.googledrive + +import org.cryptomator.domain.Cloud +import org.cryptomator.domain.CloudFile +import java.util.Date + +internal class GoogleDriveFile( + override val parent: GoogleDriveFolder, + override val name: String, + override val path: String, + override val driveId: String?, + override val size: Long?, + override val modified: Date? +) : CloudFile, GoogleDriveNode { + + override val cloud: Cloud? + get() = parent.cloud + +} diff --git a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveFolder.java b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveFolder.java deleted file mode 100644 index 60d2bf93..00000000 --- a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveFolder.java +++ /dev/null @@ -1,49 +0,0 @@ -package org.cryptomator.data.cloud.googledrive; - -import org.cryptomator.domain.Cloud; -import org.cryptomator.domain.CloudFolder; - -class GoogleDriveFolder implements CloudFolder, GoogleDriveNode { - - private final GoogleDriveFolder parent; - private final String name; - private final String path; - private final String driveId; - - public GoogleDriveFolder(GoogleDriveFolder parent, String name, String path, String driveId) { - this.parent = parent; - this.name = name; - this.path = path; - this.driveId = driveId; - } - - @Override - public Cloud getCloud() { - return parent.getCloud(); - } - - @Override - public String getName() { - return name; - } - - @Override - public String getPath() { - return path; - } - - @Override - public String getDriveId() { - return driveId; - } - - @Override - public GoogleDriveFolder getParent() { - return parent; - } - - @Override - public GoogleDriveFolder withCloud(Cloud cloud) { - return new GoogleDriveFolder(parent.withCloud(cloud), name, path, driveId); - } -} diff --git a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveFolder.kt b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveFolder.kt new file mode 100644 index 00000000..7a674dc5 --- /dev/null +++ b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveFolder.kt @@ -0,0 +1,14 @@ +package org.cryptomator.data.cloud.googledrive + +import org.cryptomator.domain.Cloud +import org.cryptomator.domain.CloudFolder + +open class GoogleDriveFolder(override val parent: GoogleDriveFolder?, override val name: String, override val path: String, override val driveId: String?) : CloudFolder, GoogleDriveNode { + + override val cloud: Cloud? + get() = parent?.cloud + + override fun withCloud(cloud: Cloud?): GoogleDriveFolder? { + return GoogleDriveFolder(parent?.withCloud(cloud), name, path, driveId) + } +} diff --git a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveIdCache.java b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveIdCache.java deleted file mode 100644 index b75ce333..00000000 --- a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveIdCache.java +++ /dev/null @@ -1,77 +0,0 @@ -package org.cryptomator.data.cloud.googledrive; - -import android.util.LruCache; - -import org.cryptomator.domain.CloudFolder; - -import javax.inject.Inject; - -class GoogleDriveIdCache { - - private final LruCache cache; - - @Inject - GoogleDriveIdCache() { - cache = new LruCache<>(1000); - } - - public NodeInfo get(String path) { - return cache.get(path); - } - - T cache(T value) { - add(value); - return value; - } - - public void add(GoogleDriveIdCloudNode node) { - add(node.getPath(), new NodeInfo(node)); - } - - private void add(String path, NodeInfo info) { - cache.put(path, info); - } - - public void remove(GoogleDriveIdCloudNode node) { - remove(node.getPath()); - } - - private void remove(String path) { - removeChildren(path); - cache.remove(path); - } - - private void removeChildren(String path) { - String prefix = path + '/'; - for (String key : cache.snapshot().keySet()) { - if (key.startsWith(prefix)) { - cache.remove(key); - } - } - } - - static class NodeInfo { - - private final String id; - private final boolean isFolder; - - private NodeInfo(GoogleDriveIdCloudNode node) { - this(node.getDriveId(), node instanceof CloudFolder); - } - - NodeInfo(String id, boolean isFolder) { - this.id = id; - this.isFolder = isFolder; - } - - public String getId() { - return id; - } - - public boolean isFolder() { - return isFolder; - } - - } - -} diff --git a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveIdCache.kt b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveIdCache.kt new file mode 100644 index 00000000..467b9950 --- /dev/null +++ b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveIdCache.kt @@ -0,0 +1,51 @@ +package org.cryptomator.data.cloud.googledrive + +import android.util.LruCache +import org.cryptomator.domain.CloudFolder +import javax.inject.Inject + +internal class GoogleDriveIdCache @Inject constructor() { + + private val cache: LruCache = LruCache(1000) + + operator fun get(path: String): NodeInfo? { + return cache[path] + } + + fun cache(value: T): T { + add(value) + return value + } + + fun add(node: GoogleDriveIdCloudNode) { + add(node.path, NodeInfo(node)) + } + + private fun add(path: String, info: NodeInfo) { + cache.put(path, info) + } + + fun remove(node: GoogleDriveIdCloudNode) { + remove(node.path) + } + + private fun remove(path: String) { + removeChildren(path) + cache.remove(path) + } + + private fun removeChildren(path: String) { + val prefix = "$path/" + for (key in cache.snapshot().keys) { + if (key.startsWith(prefix)) { + cache.remove(key) + } + } + } + + internal class NodeInfo(val id: String?, val isFolder: Boolean) { + + constructor(node: GoogleDriveIdCloudNode) : this(node.driveId, node is CloudFolder) + } + +} diff --git a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveIdCloudNode.java b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveIdCloudNode.java deleted file mode 100644 index 83f70c74..00000000 --- a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveIdCloudNode.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.cryptomator.data.cloud.googledrive; - -import org.cryptomator.domain.CloudNode; - -interface GoogleDriveIdCloudNode extends CloudNode { - - String getDriveId(); - -} diff --git a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveIdCloudNode.kt b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveIdCloudNode.kt new file mode 100644 index 00000000..4277ae84 --- /dev/null +++ b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveIdCloudNode.kt @@ -0,0 +1,9 @@ +package org.cryptomator.data.cloud.googledrive + +import org.cryptomator.domain.CloudNode + +internal interface GoogleDriveIdCloudNode : CloudNode { + + val driveId: String? + +} diff --git a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveImpl.java b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveImpl.java deleted file mode 100644 index a13487f8..00000000 --- a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveImpl.java +++ /dev/null @@ -1,446 +0,0 @@ -package org.cryptomator.data.cloud.googledrive; - -import android.content.Context; - -import com.google.api.client.googleapis.json.GoogleJsonResponseException; -import com.google.api.client.http.HttpResponseException; -import com.google.api.services.drive.Drive; -import com.google.api.services.drive.model.About; -import com.google.api.services.drive.model.File; -import com.google.api.services.drive.model.FileList; -import com.google.api.services.drive.model.Revision; -import com.google.api.services.drive.model.RevisionList; -import com.tomclaw.cache.DiskLruCache; - -import org.cryptomator.data.util.TransferredBytesAwareOutputStream; -import org.cryptomator.domain.CloudNode; -import org.cryptomator.domain.GoogleDriveCloud; -import org.cryptomator.domain.exception.BackendException; -import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException; -import org.cryptomator.domain.exception.NoSuchCloudFileException; -import org.cryptomator.domain.exception.authentication.NoAuthenticationProvidedException; -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.IOException; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import timber.log.Timber; - -import static org.cryptomator.data.cloud.googledrive.GoogleDriveCloudNodeFactory.from; -import static org.cryptomator.data.cloud.googledrive.GoogleDriveCloudNodeFactory.isFolder; -import static org.cryptomator.domain.usecases.cloud.Progress.progress; -import static org.cryptomator.util.file.LruFileCacheUtil.Cache.GOOGLE_DRIVE; -import static org.cryptomator.util.file.LruFileCacheUtil.retrieveFromLruCache; -import static org.cryptomator.util.file.LruFileCacheUtil.storeToLruCache; - -class GoogleDriveImpl { - - private static final int STATUS_REQUEST_RANGE_NOT_SATISFIABLE = 416; - - private final GoogleDriveIdCache idCache; - - private final Context context; - private final GoogleDriveCloud googleDriveCloud; - private final SharedPreferencesHandler sharedPreferencesHandler; - private final RootGoogleDriveFolder root; - - private DiskLruCache diskLruCache; - - GoogleDriveImpl(Context context, GoogleDriveCloud googleDriveCloud, GoogleDriveIdCache idCache) { - if (googleDriveCloud.accessToken() == null) { - throw new NoAuthenticationProvidedException(googleDriveCloud); - } - this.context = context; - this.googleDriveCloud = googleDriveCloud; - this.idCache = idCache; - this.root = new RootGoogleDriveFolder(googleDriveCloud); - - sharedPreferencesHandler = new SharedPreferencesHandler(context); - } - - private Drive client() { - return new GoogleDriveClientFactory(context, sharedPreferencesHandler) // - .getClient(googleDriveCloud.accessToken()); - } - - public GoogleDriveFolder root() { - return root; - } - - public GoogleDriveFolder resolve(String path) throws IOException { - if (path.startsWith("/")) { - path = path.substring(1); - } - String[] names = path.split("/"); - GoogleDriveFolder folder = root; - for (String name : names) { - folder = folder(folder, name); - } - return folder; - } - - private Optional findFile(String parentDriveId, String name) throws IOException { - Drive.Files.List fileListQuery = client().files().list() // - .setFields("files(id,mimeType,name,size)") // - .setSupportsAllDrives(true); - - if (parentDriveId != null && parentDriveId.equals("root")) { - fileListQuery.setQ("name contains '" + name + "' and '" + parentDriveId + "' in parents and trashed = false or sharedWithMe"); - } else { - fileListQuery.setQ("name contains '" + name + "' and '" + parentDriveId + "' in parents and trashed = false"); - } - - FileList files = fileListQuery.execute(); - - for (File file : files.getFiles()) { - if (name.equals(file.getName())) { - return Optional.of(file); - } - } - return Optional.empty(); - } - - public GoogleDriveFile file(GoogleDriveFolder parent, String name) throws IOException { - return file(parent, name, Optional.empty()); - } - - public GoogleDriveFile file(GoogleDriveFolder parent, String name, Optional size) throws IOException { - if (parent.getDriveId() == null) { - return GoogleDriveCloudNodeFactory.file(parent, name, size); - } - String path = GoogleDriveCloudNodeFactory.getNodePath(parent, name); - GoogleDriveIdCache.NodeInfo nodeInfo = idCache.get(path); - if (nodeInfo != null && !nodeInfo.isFolder()) { - return GoogleDriveCloudNodeFactory.file( // - parent, // - name, // - size, // - path, // - nodeInfo.getId()); - } - - Optional file = findFile(parent.getDriveId(), name); - if (file.isPresent()) { - if (!isFolder(file.get())) { - return idCache.cache(GoogleDriveCloudNodeFactory.file(parent, file.get())); - } - } - - return GoogleDriveCloudNodeFactory.file(parent, name, size); - } - - public GoogleDriveFolder folder(GoogleDriveFolder parent, String name) throws IOException { - if (parent.getDriveId() == null) { - return GoogleDriveCloudNodeFactory.folder(parent, name); - } - String path = GoogleDriveCloudNodeFactory.getNodePath(parent, name); - GoogleDriveIdCache.NodeInfo nodeInfo = idCache.get(path); - if (nodeInfo != null && nodeInfo.isFolder()) { - return GoogleDriveCloudNodeFactory.folder( // - parent, // - name, // - path, // - nodeInfo.getId()); - } - Optional folder = findFile(parent.getDriveId(), name); - if (folder.isPresent()) { - if (isFolder(folder.get())) { - return idCache.cache( // - GoogleDriveCloudNodeFactory.folder(parent, folder.get())); - } - } - - return GoogleDriveCloudNodeFactory.folder(parent, name); - } - - public boolean exists(GoogleDriveNode node) throws IOException { - try { - Optional file = findFile( // - node.getParent().getDriveId(), // - node.getName()); - boolean fileExists = file.isPresent(); - if (fileExists) { - idCache.add(from( // - node.getParent(), // - file.get())); - } - return fileExists; - } catch (GoogleJsonResponseException e) { - return false; - } - } - - public List list(GoogleDriveFolder folder) throws IOException { - List result = new ArrayList<>(); - String pageToken = null; - do { - Drive.Files.List fileListQuery = client() // - .files() // - .list() // - .setFields("nextPageToken,files(id,mimeType,modifiedTime,name,size)") // - .setPageSize(1000) // - .setSupportsAllDrives(true).setIncludeItemsFromAllDrives(true).setPageToken(pageToken); - - if (folder.getDriveId().equals("root")) { - fileListQuery.setQ("'" + folder.getDriveId() + "' in parents and trashed = false or sharedWithMe"); - } else { - fileListQuery.setQ("'" + folder.getDriveId() + "' in parents and trashed = false"); - } - - FileList fileList = fileListQuery.execute(); - - for (File file : fileList.getFiles()) { - result.add(idCache.cache(from(folder, file))); - } - pageToken = fileList.getNextPageToken(); - } while (pageToken != null); - return result; - } - - public GoogleDriveFolder create(GoogleDriveFolder folder) throws IOException { - if (folder.getParent().getDriveId() == null) { - folder = new GoogleDriveFolder( // - create(folder.getParent()), // - folder.getName(), // - folder.getPath(), // - folder.getDriveId()); - } - File metadata = new File(); - metadata.setName(folder.getName()); - metadata.setMimeType("application/vnd.google-apps.folder"); - metadata.setParents( // - Collections.singletonList(folder.getParent().getDriveId())); - - File createdFolder = client() // - .files() // - .create(metadata) // - .setFields("id,name") // - .setSupportsAllDrives(true).execute(); - - return idCache.cache( // - GoogleDriveCloudNodeFactory.folder( // - folder.getParent(), // - createdFolder)); - } - - public GoogleDriveNode move(GoogleDriveNode source, GoogleDriveNode target) throws IOException, CloudNodeAlreadyExistsException { - if (exists(target)) { - throw new CloudNodeAlreadyExistsException(target.getName()); - } - - File metadata = new File(); - metadata.setName(target.getName()); - - File movedFile = client() // - .files() // - .update(source.getDriveId(), metadata) // - .setFields("id,mimeType,modifiedTime,name,size") // - .setAddParents(target.getParent().getDriveId()) // - .setRemoveParents(source.getParent().getDriveId()) // - .setSupportsAllDrives(true).execute(); - - idCache.remove(source); - return idCache.cache(from(target.getParent(), movedFile)); - } - - public GoogleDriveFile write(final GoogleDriveFile file, DataSource data, final ProgressAware progressAware, boolean replace, final long size) // - throws IOException, BackendException { - if (!replace && exists(file)) { - throw new CloudNodeAlreadyExistsException("CloudNode already exists and replace is false"); - } - - if (file.getParent().getDriveId() == null) { - throw new NoSuchCloudFileException(String.format("The parent folder of %s doesn't have a driveId. The file would remain in root folder", file.getPath())); - } - - File metadata = new File(); - metadata.setName(file.getName()); - - progressAware.onProgress(Progress.started(UploadState.upload(file))); - File uploadedFile; - if (file.getDriveId() != null && replace) { - try (TransferredBytesAwareGoogleContentInputStream in = new TransferredBytesAwareGoogleContentInputStream(null, data.open(context), size) { - @Override - public void bytesTransferred(long transferred) { - progressAware.onProgress( // - progress(UploadState.upload(file)) // - .between(0) // - .and(size) // - .withValue(transferred)); - } - }) { - uploadedFile = client() // - .files() // - .update( // - file.getDriveId(), // - metadata, // - in).setFields("id,modifiedTime,name,size") // - .setSupportsAllDrives(true) // - .execute(); - } - } else { - metadata.setParents( // - Collections.singletonList(file.getParent().getDriveId())); - - try (TransferredBytesAwareGoogleContentInputStream in = new TransferredBytesAwareGoogleContentInputStream(null, data.open(context), size) { - @Override - public void bytesTransferred(long transferred) { - progressAware.onProgress( // - progress(UploadState.upload(file)) // - .between(0) // - .and(size) // - .withValue(transferred)); - } - }) { - uploadedFile = client() // - .files() // - .create(metadata, in).setFields("id,modifiedTime,name,size") // - .setSupportsAllDrives(true) // - .execute(); - } - } - progressAware.onProgress(Progress.completed(UploadState.upload(file))); - return idCache.cache( // - GoogleDriveCloudNodeFactory.file(file.getParent(), uploadedFile)); - } - - public void read(final GoogleDriveFile file, Optional encryptedTmpFile, OutputStream data, final ProgressAware progressAware) throws IOException { - progressAware.onProgress(Progress.started(DownloadState.download(file))); - - Optional cacheKey = Optional.empty(); - Optional cacheFile = Optional.empty(); - - if (sharedPreferencesHandler.useLruCache() && createLruCache(sharedPreferencesHandler.lruCacheSize())) { - List revisions = new ArrayList<>(); - String pageToken = null; - do { - final RevisionList revisionList = client() // - .revisions() // - .list(file.getDriveId()) // - .setPageToken(pageToken).execute(); // - - revisions.addAll(revisionList.getRevisions()); - - pageToken = revisionList.getNextPageToken(); - } while (pageToken != null); - - Collections.sort(revisions, (revision1, revision2) -> { - Long modified1 = revision1.getModifiedTime().getValue(); - Long modified2 = revision2.getModifiedTime().getValue(); - return Integer.compare(modified1.compareTo(modified2), 0); - }); - - int revisionIndex = revisions.size() > 0 ? revisions.size() - 1 : 0; - cacheKey = Optional.of(file.getDriveId() + revisions.get(revisionIndex).getId()); - java.io.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("GoogleDriveImpl").w(e, "Error while retrieving content from Cache, get from web request"); - writeToDate(file, data, encryptedTmpFile, cacheKey, progressAware); - } - } else { - writeToDate(file, data, encryptedTmpFile, cacheKey, progressAware); - } - - progressAware.onProgress(Progress.completed(DownloadState.download(file))); - } - - private void writeToDate(final GoogleDriveFile file, // - final OutputStream data, // - final Optional encryptedTmpFile, // - final Optional cacheKey, // - final ProgressAware progressAware) throws IOException { - try (TransferredBytesAwareOutputStream out = new TransferredBytesAwareOutputStream(data) { - @Override - public void bytesTransferred(long transferred) { - progressAware.onProgress( // - progress(DownloadState.download(file)) // - .between(0) // - .and(file.getSize().orElse(Long.MAX_VALUE)) // - .withValue(transferred)); - } - }) { - client() // - .files() // - .get(file.getDriveId()) // - .setAlt("media") // - .setSupportsAllDrives(true) // - .executeMediaAndDownloadTo(out); - } catch (HttpResponseException e) { - ignoreEmptyFileErrorAndRethrowOthers(e, file); - } - - if (sharedPreferencesHandler.useLruCache() && encryptedTmpFile.isPresent() && cacheKey.isPresent()) { - try { - storeToLruCache(diskLruCache, cacheKey.get(), encryptedTmpFile.get()); - } catch (IOException e) { - Timber.tag("GoogleDriveImpl").e(e, "Failed to write downloaded file in LRU cache"); - } - } - } - - private boolean createLruCache(int cacheSize) { - if (diskLruCache == null) { - try { - diskLruCache = DiskLruCache.create(new LruFileCacheUtil(context).resolve(GOOGLE_DRIVE), cacheSize); - } catch (IOException e) { - Timber.tag("GoogleDriveImpl").e(e, "Failed to setup LRU cache"); - return false; - } - } - - return true; - } - - /* - * Workaround a bug in gdrive which does not allow to download empty files. - * - * In this case an HttpResponseException with status code 416 is thrown. The filesize is checked. - * If zero, the exception is ignored - nothing has been read, so the OutputStream is in the correct - * state. - */ - private void ignoreEmptyFileErrorAndRethrowOthers(HttpResponseException e, GoogleDriveFile file) throws IOException { - if (e.getStatusCode() == STATUS_REQUEST_RANGE_NOT_SATISFIABLE) { - Optional foundFile = findFile( // - file.getParent().getDriveId(), // - file.getName()); - if (sizeOfFile(foundFile) == 0) { - return; - } - } - throw e; - } - - private long sizeOfFile(Optional foundFile) { - if (foundFile.isAbsent() || isFolder(foundFile.get())) { - return -1; - } - return foundFile.get().getSize(); - } - - public void delete(GoogleDriveNode node) throws IOException { - client().files().delete(node.getDriveId()).setSupportsAllDrives(true).execute(); - idCache.remove(node); - } - - public String currentAccount() throws IOException { - About about = client().about().get().execute(); - return about.getUser().getDisplayName(); - } - -} diff --git a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveImpl.kt b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveImpl.kt new file mode 100644 index 00000000..c01f1541 --- /dev/null +++ b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveImpl.kt @@ -0,0 +1,416 @@ +package org.cryptomator.data.cloud.googledrive + +import android.content.Context +import com.google.api.client.googleapis.json.GoogleJsonResponseException +import com.google.api.client.http.HttpResponseException +import com.google.api.services.drive.Drive +import com.google.api.services.drive.model.File +import com.google.api.services.drive.model.Revision +import com.tomclaw.cache.DiskLruCache +import org.cryptomator.data.util.TransferredBytesAwareOutputStream +import org.cryptomator.domain.GoogleDriveCloud +import org.cryptomator.domain.exception.BackendException +import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException +import org.cryptomator.domain.exception.FatalBackendException +import org.cryptomator.domain.exception.NoSuchCloudFileException +import org.cryptomator.domain.exception.ParentFolderIsNullException +import org.cryptomator.domain.exception.authentication.NoAuthenticationProvidedException +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.SharedPreferencesHandler +import org.cryptomator.util.file.LruFileCacheUtil +import org.cryptomator.util.file.LruFileCacheUtil.Companion.retrieveFromLruCache +import java.io.IOException +import java.io.OutputStream +import java.util.ArrayList +import timber.log.Timber + +internal class GoogleDriveImpl(context: Context, googleDriveCloud: GoogleDriveCloud, idCache: GoogleDriveIdCache) { + + private val idCache: GoogleDriveIdCache + private val context: Context + private val googleDriveCloud: GoogleDriveCloud + private val sharedPreferencesHandler: SharedPreferencesHandler + private val root: RootGoogleDriveFolder + private var diskLruCache: DiskLruCache? = null + + private fun client(): Drive { + return GoogleDriveClientFactory.getInstance(googleDriveCloud.accessToken(), context) + } + + fun root(): GoogleDriveFolder { + return root + } + + @Throws(IOException::class) + fun resolve(path: String): GoogleDriveFolder { + val names = path.removePrefix("/").split("/").toTypedArray() + var folder: GoogleDriveFolder = root + for (name in names) { + folder = folder(folder, name) + } + return folder + } + + @Throws(IOException::class) + private fun findFile(parentDriveId: String?, name: String): File? { + val fileListQuery = client().files().list() // + .setFields("files(id,mimeType,name,size)") // + .setSupportsAllDrives(true) + if (parentDriveId != null && parentDriveId == "root") { + fileListQuery.q = "name contains '$name' and '$parentDriveId' in parents and trashed = false or sharedWithMe" + } else { + fileListQuery.q = "name contains '$name' and '$parentDriveId' in parents and trashed = false" + } + return fileListQuery.execute().files.firstOrNull { it.name == name } + } + + @Throws(IOException::class) + fun file(parent: GoogleDriveFolder, name: String, size: Long?): GoogleDriveFile { + if (parent.driveId == null) { + return GoogleDriveCloudNodeFactory.file(parent, name, size) + } + val path = GoogleDriveCloudNodeFactory.getNodePath(parent, name) + val nodeInfo = idCache[path] + if (nodeInfo != null && !nodeInfo.isFolder) { + requireNotNull(nodeInfo.id) + return GoogleDriveCloudNodeFactory.file(parent, name, size, path, nodeInfo.id) + } + + findFile(parent.driveId, name)?.let { + if (!GoogleDriveCloudNodeFactory.isFolder(it)) { + return idCache.cache(GoogleDriveCloudNodeFactory.file(parent, it)) + } + } + + return GoogleDriveCloudNodeFactory.file(parent, name, size) + } + + @Throws(IOException::class) + fun folder(parent: GoogleDriveFolder, name: String): GoogleDriveFolder { + if (parent.driveId == null) { + return GoogleDriveCloudNodeFactory.folder(parent, name) + } + val path = GoogleDriveCloudNodeFactory.getNodePath(parent, name) + val nodeInfo = idCache[path] + if (nodeInfo != null && nodeInfo.isFolder) { + requireNotNull(nodeInfo.id) + return GoogleDriveCloudNodeFactory.folder(parent, name, path, nodeInfo.id) + } + val folder = findFile(parent.driveId, name) + + folder?.let { + if (GoogleDriveCloudNodeFactory.isFolder(it)) { + return idCache.cache(GoogleDriveCloudNodeFactory.folder(parent, it)) + } + } + + return GoogleDriveCloudNodeFactory.folder(parent, name) + } + + @Throws(IOException::class) + fun exists(node: GoogleDriveNode): Boolean { + return try { + requireNotNull(node.parent) + val file = findFile(node.parent!!.driveId, node.name) + file?.let { idCache.add(GoogleDriveCloudNodeFactory.from(node.parent!!, it)) } + file != null + } catch (e: GoogleJsonResponseException) { + if(e.statusCode == 404) { + return false + } + throw e + } + } + + @Throws(IOException::class) + fun list(folder: GoogleDriveFolder): List { + val result: MutableList = ArrayList() + var pageToken: String? = null + do { + val fileListQuery = client() // + .files() // + .list() // + .setFields("nextPageToken,files(id,mimeType,modifiedTime,name,size)") // + .setPageSize(1000) // + .setSupportsAllDrives(true) // + .setIncludeItemsFromAllDrives(true) // + .setPageToken(pageToken) + if (folder.driveId == "root") { + fileListQuery.q = "'" + folder.driveId + "' in parents and trashed = false or sharedWithMe" + } else { + fileListQuery.q = "'" + folder.driveId + "' in parents and trashed = false" + } + val fileList = fileListQuery.execute() + for (file in fileList.files) { + result.add(idCache.cache(GoogleDriveCloudNodeFactory.from(folder, file))) + } + pageToken = fileList.nextPageToken + } while (pageToken != null) + return result + } + + @Throws(IOException::class) + fun create(folder: GoogleDriveFolder): GoogleDriveFolder { + var folder = folder + + if (folder.parent?.driveId == null) { + folder.parent?.let { + folder = GoogleDriveFolder(create(it), folder.name, folder.path, folder.driveId) + } ?: throw ParentFolderIsNullException(folder.name) + } + + folder.parent?.let { parentFolder -> + val metadata = File() + metadata.name = folder.name + metadata.mimeType = "application/vnd.google-apps.folder" + metadata.parents = listOf(parentFolder.driveId) + val createdFolder = client() // + .files() // + .create(metadata) // + .setFields("id,name") // + .setSupportsAllDrives(true) // + .execute() + return idCache.cache(GoogleDriveCloudNodeFactory.folder(parentFolder, createdFolder)) + } ?: throw ParentFolderIsNullException(folder.name) + } + + @Throws(IOException::class, CloudNodeAlreadyExistsException::class) + fun move(source: GoogleDriveNode, target: GoogleDriveNode): GoogleDriveNode { + if (exists(target)) { + throw CloudNodeAlreadyExistsException(target.name) + } + + target.parent?.let { targetsParent -> + val metadata = File() + metadata.name = target.name + val movedFile = client() // + .files() // + .update(source.driveId, metadata) // + .setFields("id,mimeType,modifiedTime,name,size") // + .setAddParents(targetsParent.driveId) // + .setRemoveParents(targetsParent.driveId) // + .setSupportsAllDrives(true) // + .execute() + idCache.remove(source) + return idCache.cache(GoogleDriveCloudNodeFactory.from(targetsParent, movedFile)) + } ?: throw ParentFolderIsNullException(target.name) + } + + @Throws(IOException::class, BackendException::class) + fun write(file: GoogleDriveFile, data: DataSource, progressAware: ProgressAware, replace: Boolean, size: Long): GoogleDriveFile { + if (!replace && exists(file)) { + throw CloudNodeAlreadyExistsException("CloudNode already exists and replace is false") + } + if (file.parent.driveId == null) { + throw NoSuchCloudFileException(String.format("The parent folder of %s doesn't have a driveId. The file would remain in root folder", file.path)) + } + val metadata = File() + metadata.name = file.name + progressAware.onProgress(Progress.started(UploadState.upload(file))) + val uploadedFile = if (file.driveId != null && replace) { + updateFile(file, data, progressAware, size, metadata) + } else { + createNewFile(file, data, progressAware, size, metadata) + } ?: throw FatalBackendException("InputStream shouldn't be null") + progressAware.onProgress(Progress.completed(UploadState.upload(file))) + return idCache.cache(GoogleDriveCloudNodeFactory.file(file.parent, uploadedFile)) + } + + private fun updateFile(file: GoogleDriveFile, data: DataSource, progressAware: ProgressAware, size: Long, metadata: File): File? { + return data.open(context)?.use { inputStream -> + object : TransferredBytesAwareGoogleContentInputStream(null, inputStream, size) { + override fun bytesTransferred(transferred: Long) { + progressAware.onProgress( // + Progress.progress(UploadState.upload(file)) // + .between(0) // + .and(size) // + .withValue(transferred) + ) + } + }.use { + client() // + .files() // + .update(file.driveId, metadata, it) // + .setFields("id,modifiedTime,name,size") // + .setSupportsAllDrives(true) // + .execute() + } + } + } + + private fun createNewFile(file: GoogleDriveFile, data: DataSource, progressAware: ProgressAware, size: Long, metadata: File): File? { + return data.open(context)?.use { inputStream -> + metadata.parents = listOf(file.parent.driveId) + object : TransferredBytesAwareGoogleContentInputStream(null, inputStream, size) { + override fun bytesTransferred(transferred: Long) { + progressAware.onProgress( // + Progress.progress(UploadState.upload(file)) // + .between(0) // + .and(size) // + .withValue(transferred) + ) + } + }.use { + client() // + .files() // + .create(metadata, it) // + .setFields("id,modifiedTime,name,size") // + .setSupportsAllDrives(true) // + .execute() + } + } + } + + @Throws(IOException::class) + fun read(file: GoogleDriveFile, encryptedTmpFile: java.io.File?, data: OutputStream, progressAware: ProgressAware) { + progressAware.onProgress(Progress.started(DownloadState.download(file))) + var cacheKey: String? = null + var cacheFile: java.io.File? = null + if (sharedPreferencesHandler.useLruCache() && createLruCache(sharedPreferencesHandler.lruCacheSize())) { + cacheKey = file.driveId + getRevisionIdFor(file) + cacheFile = diskLruCache?.let { it[cacheKey] } + } + if (sharedPreferencesHandler.useLruCache() && cacheFile != null) { + try { + retrieveFromLruCache(cacheFile, data) + } catch (e: IOException) { + Timber.tag("GoogleDriveImpl").w(e, "Error while retrieving content from Cache, get from web request") + writeToDate(file, data, encryptedTmpFile, cacheKey, progressAware) + } + } else { + writeToDate(file, data, encryptedTmpFile, cacheKey, progressAware) + } + progressAware.onProgress(Progress.completed(DownloadState.download(file))) + } + + private fun getRevisionIdFor(file: GoogleDriveFile): String? { + val revisions: MutableList = ArrayList() + var pageToken: String? = null + do { + val revisionList = client() // + .revisions() // + .list(file.driveId) // + .setPageToken(pageToken) // + .execute() + revisions.addAll(revisionList.revisions) + pageToken = revisionList.nextPageToken + } while (pageToken != null) + revisions.sortWith { revision1: Revision, revision2: Revision -> + val modified1 = revision1.modifiedTime.value + val modified2 = revision2.modifiedTime.value + modified1.compareTo(modified2).compareTo(0) + } + val revisionIndex = if (revisions.size > 0) revisions.size - 1 else 0 + return revisions[revisionIndex].id + } + + @Throws(IOException::class) + private fun writeToDate( + file: GoogleDriveFile, // + data: OutputStream, // + encryptedTmpFile: java.io.File?, // + cacheKey: String?, // + progressAware: ProgressAware + ) { + try { + object : TransferredBytesAwareOutputStream(data) { + override fun bytesTransferred(transferred: Long) { + progressAware.onProgress( // + Progress.progress(DownloadState.download(file)) // + .between(0) // + .and(file.size ?: Long.MAX_VALUE) // + .withValue(transferred) + ) + } + }.use { + client() // + .files()[file.driveId] // + .setAlt("media") // + .setSupportsAllDrives(true) // + .executeMediaAndDownloadTo(it) + } + } catch (e: HttpResponseException) { + ignoreEmptyFileErrorAndRethrowOthers(e, file) + } + if (sharedPreferencesHandler.useLruCache() && encryptedTmpFile != null && cacheKey != null) { + try { + diskLruCache?.let { + LruFileCacheUtil.storeToLruCache(it, cacheKey, encryptedTmpFile) + } ?: Timber.tag("GoogleDriveImpl").e("Failed to store item in LRU cache") + } catch (e: IOException) { + Timber.tag("GoogleDriveImpl").e(e, "Failed to write downloaded file in LRU cache") + } + } + } + + private fun createLruCache(cacheSize: Int): Boolean { + if (diskLruCache == null) { + diskLruCache = try { + DiskLruCache.create(LruFileCacheUtil(context).resolve(LruFileCacheUtil.Cache.GOOGLE_DRIVE), cacheSize.toLong()) + } catch (e: IOException) { + Timber.tag("GoogleDriveImpl").e(e, "Failed to setup LRU cache") + return false + } + } + return true + } + + /* + * Workaround a bug in gdrive which does not allow to download empty files. + * + * In this case an HttpResponseException with status code 416 is thrown. The filesize is checked. + * If zero, the exception is ignored - nothing has been read, so the OutputStream is in the correct + * state. + */ + @Throws(IOException::class) + private fun ignoreEmptyFileErrorAndRethrowOthers(e: HttpResponseException, file: GoogleDriveFile) { + if (e.statusCode == STATUS_REQUEST_RANGE_NOT_SATISFIABLE) { + val foundFile = findFile( // + file.parent.driveId, // + file.name + ) + if (sizeOfFile(foundFile) == 0L) { + return + } + } + throw e + } + + private fun sizeOfFile(foundFile: File?): Long { + return if (foundFile == null || GoogleDriveCloudNodeFactory.isFolder(foundFile)) { + -1 + } else foundFile.getSize() + } + + @Throws(IOException::class) + fun delete(node: GoogleDriveNode) { + client().files().delete(node.driveId).setSupportsAllDrives(true).execute() + idCache.remove(node) + } + + @Throws(IOException::class) + fun currentAccount(): String { + val about = client().about().get().execute() + return about.user.displayName + } + + companion object { + + private const val STATUS_REQUEST_RANGE_NOT_SATISFIABLE = 416 + } + + init { + if (googleDriveCloud.accessToken() == null) { + throw NoAuthenticationProvidedException(googleDriveCloud) + } + this.context = context + this.googleDriveCloud = googleDriveCloud + this.idCache = idCache + this.root = RootGoogleDriveFolder(googleDriveCloud) + sharedPreferencesHandler = SharedPreferencesHandler(context) + } +} diff --git a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveNode.java b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveNode.java deleted file mode 100644 index 478a32af..00000000 --- a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveNode.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.cryptomator.data.cloud.googledrive; - -interface GoogleDriveNode extends GoogleDriveIdCloudNode { - - @Override - String getDriveId(); - - @Override - GoogleDriveFolder getParent(); -} diff --git a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveNode.kt b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveNode.kt new file mode 100644 index 00000000..0110fbfd --- /dev/null +++ b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveNode.kt @@ -0,0 +1,7 @@ +package org.cryptomator.data.cloud.googledrive + +internal interface GoogleDriveNode : GoogleDriveIdCloudNode { + + override val parent: GoogleDriveFolder? + +} diff --git a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/RootGoogleDriveFolder.java b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/RootGoogleDriveFolder.java deleted file mode 100644 index 474fc530..00000000 --- a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/RootGoogleDriveFolder.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.cryptomator.data.cloud.googledrive; - -import org.cryptomator.domain.Cloud; -import org.cryptomator.domain.GoogleDriveCloud; - -public class RootGoogleDriveFolder extends GoogleDriveFolder { - - private final GoogleDriveCloud cloud; - - public RootGoogleDriveFolder(GoogleDriveCloud cloud) { - super(null, "", "", "root"); - this.cloud = cloud; - } - - @Override - public GoogleDriveCloud getCloud() { - return cloud; - } - - @Override - public GoogleDriveFolder withCloud(Cloud cloud) { - return new RootGoogleDriveFolder((GoogleDriveCloud) cloud); - } -} diff --git a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/RootGoogleDriveFolder.kt b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/RootGoogleDriveFolder.kt new file mode 100644 index 00000000..751ff68f --- /dev/null +++ b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/RootGoogleDriveFolder.kt @@ -0,0 +1,11 @@ +package org.cryptomator.data.cloud.googledrive + +import org.cryptomator.domain.Cloud +import org.cryptomator.domain.GoogleDriveCloud + +class RootGoogleDriveFolder(override val cloud: GoogleDriveCloud) : GoogleDriveFolder(null, "", "", "root") { + + override fun withCloud(cloud: Cloud?): GoogleDriveFolder { + return RootGoogleDriveFolder(cloud as GoogleDriveCloud) + } +} diff --git a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/TransferredBytesAwareGoogleContentInputStream.java b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/TransferredBytesAwareGoogleContentInputStream.java deleted file mode 100644 index 2648efcd..00000000 --- a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/TransferredBytesAwareGoogleContentInputStream.java +++ /dev/null @@ -1,51 +0,0 @@ -package org.cryptomator.data.cloud.googledrive; - -import com.google.api.client.http.AbstractInputStreamContent; - -import org.cryptomator.data.util.TransferredBytesAwareInputStream; - -import java.io.Closeable; -import java.io.IOException; -import java.io.InputStream; - -public abstract class TransferredBytesAwareGoogleContentInputStream extends AbstractInputStreamContent implements Closeable { - - private final InputStream data; - private final long size; - - /** - * @param size the size of the data to upload or less than zero if not known - */ - public TransferredBytesAwareGoogleContentInputStream(String type, InputStream data, long size) { - super(type); - this.data = new TransferredBytesAwareInputStream(data) { - @Override - public void bytesTransferred(long transferred) { - TransferredBytesAwareGoogleContentInputStream.this.bytesTransferred(transferred); - } - }; - this.size = size; - } - - @Override - public InputStream getInputStream() throws IOException { - return data; - } - - @Override - public long getLength() throws IOException { - return size; - } - - @Override - public boolean retrySupported() { - return false; - } - - @Override - public void close() throws IOException { - data.close(); - } - - public abstract void bytesTransferred(long transferred); -} diff --git a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/TransferredBytesAwareGoogleContentInputStream.kt b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/TransferredBytesAwareGoogleContentInputStream.kt new file mode 100644 index 00000000..65148e71 --- /dev/null +++ b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/TransferredBytesAwareGoogleContentInputStream.kt @@ -0,0 +1,46 @@ +package org.cryptomator.data.cloud.googledrive + +import com.google.api.client.http.AbstractInputStreamContent +import org.cryptomator.data.util.TransferredBytesAwareInputStream +import java.io.Closeable +import java.io.IOException +import java.io.InputStream + +abstract class TransferredBytesAwareGoogleContentInputStream(type: String?, data: InputStream, size: Long) : AbstractInputStreamContent(type), Closeable { + + private val data: InputStream + private val size: Long + + @Throws(IOException::class) + override fun getInputStream(): InputStream { + return data + } + + @Throws(IOException::class) + override fun getLength(): Long { + return size + } + + override fun retrySupported(): Boolean { + return false + } + + @Throws(IOException::class) + override fun close() { + data.close() + } + + abstract fun bytesTransferred(transferred: Long) + + /** + * @param size the size of the data to upload or less than zero if not known + */ + init { + this.data = object : TransferredBytesAwareInputStream(data) { + override fun bytesTransferred(transferred: Long) { + this@TransferredBytesAwareGoogleContentInputStream.bytesTransferred(transferred) + } + } + this.size = size + } +} diff --git a/data/src/test/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7Test.java b/data/src/test/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7Test.java deleted file mode 100644 index 2a4e8c44..00000000 --- a/data/src/test/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7Test.java +++ /dev/null @@ -1,1087 +0,0 @@ -package org.cryptomator.data.cloud.crypto; - -import android.content.Context; - -import com.google.common.base.Strings; -import com.google.common.io.BaseEncoding; - -import org.cryptomator.cryptolib.api.Cryptor; -import org.cryptomator.cryptolib.api.FileContentCryptor; -import org.cryptomator.cryptolib.api.FileHeader; -import org.cryptomator.cryptolib.api.FileHeaderCryptor; -import org.cryptomator.cryptolib.api.FileNameCryptor; -import org.cryptomator.cryptolib.common.MessageDigestSupplier; -import org.cryptomator.data.util.CopyStream; -import org.cryptomator.domain.Cloud; -import org.cryptomator.domain.CloudFile; -import org.cryptomator.domain.CloudFolder; -import org.cryptomator.domain.CloudNode; -import org.cryptomator.domain.exception.BackendException; -import org.cryptomator.domain.repository.CloudContentRepository; -import org.cryptomator.domain.usecases.ProgressAware; -import org.cryptomator.domain.usecases.cloud.ByteArrayDataSource; -import org.cryptomator.domain.usecases.cloud.DataSource; -import org.cryptomator.util.Encodings; -import org.cryptomator.util.Optional; -import org.hamcrest.Matchers; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.mockito.AdditionalMatchers; -import org.mockito.Answers; -import org.mockito.Mockito; - -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; - -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; - -/** - * - * path/to/vault/d/00 - * ├─ Directory 1 - * │ ├─ Directory 2 - * │ ├─ Directory 3x250 - * │ │ ├─ Directory 4x250 - * │ │ └─ File 5x250 - * │ └─ File 3 - * ├─ File 1 - * ├─ File 2 - * ├─ File 4 - * - */ -public class CryptoImplVaultFormat7Test { - - private final String dirIdRoot = ""; - private final String dirId1 = "dir1-id"; - private final String dirId2 = "dir2-id"; - - private Cloud cloud; - private CryptoCloud cryptoCloud; - private Context context; - private Cryptor cryptor; - private CloudContentRepository cloudContentRepository; - private DirIdCache dirIdCache; - private FileNameCryptor fileNameCryptor; - private FileContentCryptor fileContentCryptor; - private FileHeaderCryptor fileHeaderCryptor; - - private CryptoImplVaultFormat7 inTest; - - private TestFolder rootFolder = new RootTestFolder(cloud); - private TestFolder d = new TestFolder(rootFolder, "d", "/d"); - private TestFolder lvl2Dir = new TestFolder(d, "00", "/d/00"); - private TestFolder aaFolder = new TestFolder(lvl2Dir, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); - - private RootCryptoFolder root; - private CryptoFile cryptoFile1; - private CryptoFile cryptoFile2; - private CryptoFile cryptoFile4; - private CryptoFolder cryptoFolder1; - - @BeforeEach - public void setup() throws BackendException { - cloud = Mockito.mock(Cloud.class); - cryptoCloud = Mockito.mock(CryptoCloud.class); - context = Mockito.mock(Context.class); - cryptor = Mockito.mock(Cryptor.class); - cloudContentRepository = Mockito.mock(CloudContentRepository.class, Answers.RETURNS_DEEP_STUBS); - dirIdCache = Mockito.mock(DirIdCache.class); - fileNameCryptor = Mockito.mock(FileNameCryptor.class); - fileContentCryptor = Mockito.mock(FileContentCryptor.class); - fileHeaderCryptor = Mockito.mock(FileHeaderCryptor.class); - - Mockito.when(cryptor.fileNameCryptor()).thenReturn(fileNameCryptor); - Mockito.when(cryptor.fileNameCryptor()).thenReturn(fileNameCryptor); - Mockito.when(cryptor.fileContentCryptor()).thenReturn(fileContentCryptor); - Mockito.when(cryptor.fileHeaderCryptor()).thenReturn(fileHeaderCryptor); - - root = new RootCryptoFolder(cryptoCloud); - inTest = new CryptoImplVaultFormat7(context, () -> cryptor, cloudContentRepository, rootFolder, dirIdCache); - - Mockito.when(fileNameCryptor.hashDirectoryId(dirIdRoot)).thenReturn("00AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); - Mockito.when(fileNameCryptor.hashDirectoryId(dirId1)).thenReturn("11BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"); - Mockito.when(fileNameCryptor.hashDirectoryId(dirId2)).thenReturn("22CCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"); - Mockito.when(fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), "dir1", dirIdRoot.getBytes())).thenReturn("Directory 1"); - Mockito.when(fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), "file1", dirIdRoot.getBytes())).thenReturn("File 1"); - Mockito.when(fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), "file2", dirIdRoot.getBytes())).thenReturn("File 2"); - Mockito.when(fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), "dir2", dirId1.getBytes())).thenReturn("Directory 2"); - Mockito.when(fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), "file3", dirId1.getBytes())).thenReturn("File 3"); - Mockito.when(fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), "file4", dirIdRoot.getBytes())).thenReturn("File 4"); - - TestFile testFile1 = new TestFile(aaFolder, "file1.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/file1.c9r", Optional.empty(), Optional.empty()); - TestFile testFile2 = new TestFile(aaFolder, "file2.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/file2.c9r", Optional.empty(), Optional.empty()); - TestFile testFile4 = new TestFile(aaFolder, "file4.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/file4.c9r", Optional.empty(), Optional.empty()); - TestFolder testDir1 = new TestFolder(aaFolder, "dir1.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/dir1.c9r"); - TestFile testDir1DirFile = new TestFile(testDir1, "dir.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/dir1.c9r/dir.c9r", Optional.empty(), Optional.empty()); - - ArrayList rootItems = new ArrayList() { - { - add(testFile1); - add(testFile2); - add(testFile4); - add(testDir1); - } - }; - - cryptoFile1 = new CryptoFile(root, "File 1", "/File 1", Optional.of(15l), testFile1); - cryptoFile2 = new CryptoFile(root, "File 2", "/File 2", Optional.empty(), testFile2); - cryptoFile4 = new CryptoFile(root, "File 4", "/File 4", Optional.empty(), testFile4); - cryptoFolder1 = new CryptoFolder(root, "Directory 1", "/Directory 1", testDir1DirFile); - - Mockito.when(cloudContentRepository.folder(rootFolder, "d")).thenReturn(d); - Mockito.when(cloudContentRepository.folder(d, "00")).thenReturn(lvl2Dir); - Mockito.when(cloudContentRepository.folder(lvl2Dir, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")).thenReturn(aaFolder); - Mockito.when(cloudContentRepository.file(testDir1, "dir.c9r")).thenReturn(testDir1DirFile); - Mockito.when(cloudContentRepository.exists(testDir1DirFile)).thenReturn(true); - Mockito.doAnswer(invocation -> { - OutputStream out = invocation.getArgument(2); - CopyStream.copyStreamToStream(new ByteArrayInputStream(dirId1.getBytes()), out); - return null; - }).when(cloudContentRepository).read(Mockito.eq(cryptoFolder1.getDirFile()), Mockito.any(), Mockito.any(), Mockito.any()); - Mockito.when(cloudContentRepository.list(aaFolder)).thenReturn(rootItems); - Mockito.when(dirIdCache.put(Mockito.eq(root), Mockito.any())).thenReturn(new DirIdCache.DirIdInfo("", aaFolder)); - } - - @Test - @DisplayName("list(\"/\")") - public void testListRoot() throws BackendException { - List rootDirContent = inTest.list(root); - - Matchers.contains(rootDirContent, cryptoFile1); - Matchers.contains(rootDirContent, cryptoFile2); - Matchers.contains(rootDirContent, cryptoFile4); - Matchers.contains(rootDirContent, cryptoFolder1); - } - - @Test - @DisplayName("list(\"/Directory 1/Directory 3x250\")") - public void testListDirectory3x250() throws BackendException { - String dir3Name = "Directory " + Strings.repeat("3", 250); - String dir3Cipher = "dir" + Strings.repeat("3", 250); - - byte[] longFilenameBytes = (dir3Cipher + ".c9r").getBytes(Encodings.UTF_8); - byte[] hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes); - String shortenedFileName = BaseEncoding.base64Url().encode(hash) + ".c9s"; - - TestFolder bbLvl2Dir = new TestFolder(d, "11", "/d/11"); - TestFolder bbFolder = new TestFolder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"); - TestFolder ddLvl2Dir = new TestFolder(d, "33", "/d/33"); - TestFolder ddFolder = new TestFolder(ddLvl2Dir, "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD", "/d/33/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD"); - TestFolder testDir3 = new TestFolder(bbFolder, shortenedFileName, "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/" + shortenedFileName); - TestFile testDir3DirFile = new TestFile(testDir3, "dir.c9r", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/" + shortenedFileName + "/dir.c9r", Optional.empty(), Optional.empty()); - TestFile testDir3NameFile = new TestFile(testDir3, "name.c9s", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/" + shortenedFileName + "/name.c9s", Optional.empty(), Optional.empty()); - - Mockito.when(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), dir3Name, dirId1.getBytes())).thenReturn(dir3Cipher); - Mockito.when(fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), dir3Cipher, dirId1.getBytes())).thenReturn(dir3Name); - Mockito.when(fileNameCryptor.hashDirectoryId(AdditionalMatchers.not(Mockito.eq("")))).thenReturn("33DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD"); - - Mockito.when(cloudContentRepository.folder(d, "33")).thenReturn(ddLvl2Dir); - Mockito.when(cloudContentRepository.folder(ddLvl2Dir, "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD")).thenReturn(ddFolder); - Mockito.when(cloudContentRepository.file(testDir3, "dir.c9r")).thenReturn(testDir3DirFile); - Mockito.when(cloudContentRepository.file(testDir3, "name.c9s")).thenReturn(testDir3NameFile); - - Mockito.when(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), "Directory 1", dirIdRoot.getBytes())).thenReturn("dir1"); - Mockito.when(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), "Directory 2", dirId1.getBytes())).thenReturn("dir2"); - - CryptoFolder cryptoFolder3 = new CryptoFolder(cryptoFolder1, dir3Name, "/Directory 1/" + dir3Name, testDir3DirFile); - Mockito.doAnswer(invocation -> { - OutputStream out = invocation.getArgument(2); - CopyStream.copyStreamToStream(new ByteArrayInputStream("dir3-id".getBytes()), out); - return null; - }).when(cloudContentRepository).read(Mockito.eq(cryptoFolder3.getDirFile()), Mockito.any(), Mockito.any(), Mockito.any()); - - /* - * │ ├─ Directory 3x250 - * │ │ ├─ Directory 4x250 - * │ │ └─ File 5x250 - */ - - String dir4Name = "Directory " + Strings.repeat("4", 250); - String dir4Cipher = "dir" + Strings.repeat("4", 250); - - byte[] longFilenameBytes4 = (dir4Cipher + ".c9r").getBytes(Encodings.UTF_8); - byte[] hash4 = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes4); - String shortenedFileName4 = BaseEncoding.base64Url().encode(hash4) + ".c9s"; - - TestFolder directory4x250 = new TestFolder(ddFolder, shortenedFileName4, "/d/33/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD" + shortenedFileName4); - TestFile testDir4DirFile = new TestFile(directory4x250, "dir.c9r", "/d/33/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD/" + shortenedFileName4 + "/dir.c9r", Optional.empty(), Optional.empty()); - TestFile testDir4NameFile = new TestFile(directory4x250, "name.c9s", "/d/33/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD/" + shortenedFileName4 + "/name.c9s", Optional.empty(), Optional.empty()); - - Mockito.when(cloudContentRepository.file(directory4x250, "dir.c9r")).thenReturn(testDir4DirFile); - Mockito.when(cloudContentRepository.file(directory4x250, "name.c9s")).thenReturn(testDir4NameFile); - Mockito.when(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), dir4Name, "dir3-id".getBytes())).thenReturn(dir4Cipher); - Mockito.when(fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), dir4Cipher, "dir3-id".getBytes())).thenReturn(dir4Name); - Mockito.doAnswer(invocation -> { - OutputStream out = invocation.getArgument(2); - CopyStream.copyStreamToStream(new ByteArrayInputStream(dir4Cipher.getBytes("UTF-8")), out); - return null; - }).when(cloudContentRepository).read(Mockito.eq(testDir4NameFile), Mockito.any(), Mockito.any(), Mockito.any()); - - ArrayList dir4Files = new ArrayList() { - { - add(testDir4DirFile); - add(testDir4NameFile); - } - }; - - String file5Name = "File " + Strings.repeat("5", 250); - String file5Cipher = "file" + Strings.repeat("5", 250); - - byte[] longFilenameBytes5 = (file5Cipher + ".c9r").getBytes(Encodings.UTF_8); - byte[] hash5 = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes5); - String shortenedFileName5 = BaseEncoding.base64Url().encode(hash5) + ".c9s"; - - TestFolder directory5x250 = new TestFolder(ddFolder, shortenedFileName5, "/d/33/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD" + shortenedFileName5); - TestFile testFile5ContentFile = new TestFile(directory5x250, "contents.c9r", "/d/33/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD/" + shortenedFileName5 + "/contents.c9r", Optional.empty(), Optional.empty()); - TestFile testFile5NameFile = new TestFile(directory5x250, "name.c9s", "/d/33/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD/" + shortenedFileName5 + "/name.c9s", Optional.empty(), Optional.empty()); - - Mockito.when(cloudContentRepository.file(directory5x250, "contents.c9r")).thenReturn(testFile5ContentFile); - Mockito.when(cloudContentRepository.file(directory5x250, "name.c9s")).thenReturn(testFile5NameFile); - Mockito.when(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), file5Name, "dir3-id".getBytes())).thenReturn(file5Cipher); - Mockito.when(fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), file5Cipher, "dir3-id".getBytes())).thenReturn(file5Name); - Mockito.doAnswer(invocation -> { - OutputStream out = invocation.getArgument(2); - CopyStream.copyStreamToStream(new ByteArrayInputStream(file5Cipher.getBytes("UTF-8")), out); - return null; - }).when(cloudContentRepository).read(Mockito.eq(testFile5NameFile), Mockito.any(), Mockito.any(), Mockito.any()); - - ArrayList dir5Files = new ArrayList() { - { - add(testFile5ContentFile); - add(testFile5NameFile); - } - }; - - ArrayList dir3Items = new ArrayList() { - { - add(directory4x250); - add(directory5x250); - } - }; - - Mockito.when(cloudContentRepository.exists(testDir3DirFile)).thenReturn(true); - Mockito.when(cloudContentRepository.list(ddFolder)).thenReturn(dir3Items); - Mockito.when(cloudContentRepository.list(directory4x250)).thenReturn(dir4Files); - Mockito.when(cloudContentRepository.list(directory5x250)).thenReturn(dir5Files); - Mockito.when(dirIdCache.put(Mockito.eq(cryptoFolder3), Mockito.any())).thenReturn(new DirIdCache.DirIdInfo("dir3-id", ddFolder)); - - List folder3Content = inTest.list(cryptoFolder3); - - Matchers.contains(folder3Content, new CryptoFolder(cryptoFolder3, dir4Name, "/Directory 1/" + dir3Name + "/" + dir4Name, testDir4DirFile)); - Matchers.contains(folder3Content, new CryptoFile(cryptoFolder3, file5Name, "/Directory 1/" + dir3Name + "/" + file5Name, Optional.empty(), testFile5ContentFile)); - } - - @Test - @DisplayName("read(\"/File 1\", NO_PROGRESS_AWARE)") - public void testReadFromShortFile() throws BackendException { - byte[] file1Content = "hhhhhTOPSECRET!TOPSECRET!TOPSECRET!TOPSECRET!".getBytes(); - FileHeader header = Mockito.mock(FileHeader.class); - - Mockito.when(fileContentCryptor.cleartextChunkSize()).thenReturn(8); - Mockito.when(fileContentCryptor.ciphertextChunkSize()).thenReturn(10); - Mockito.when(fileHeaderCryptor.headerSize()).thenReturn(5); - - Mockito.when(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), "File 1", dirIdRoot.getBytes())).thenReturn("file1"); - Mockito.when(fileHeaderCryptor.decryptHeader(UTF_8.encode("hhhhh"))).thenReturn(header); - Mockito.when(fileContentCryptor.decryptChunk(Mockito.eq(UTF_8.encode("TOPSECRET!")), Mockito.anyLong(), Mockito.eq(header), Mockito.anyBoolean())).then(invocation -> UTF_8.encode("geheim!!")); - - Mockito.doAnswer(invocation -> { - OutputStream out = invocation.getArgument(2); - CopyStream.copyStreamToStream(new ByteArrayInputStream(file1Content), out); - return null; - }).when(cloudContentRepository).read(Mockito.eq(cryptoFile1.getCloudFile()), Mockito.any(), Mockito.any(), Mockito.any()); - - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(1000); - - inTest.read(cryptoFile1, outputStream, ProgressAware.NO_OP_PROGRESS_AWARE); - - assertThat(outputStream.toString(), is("geheim!!geheim!!geheim!!geheim!!")); - } - - @Test - @DisplayName("read(\"/File 15x250\", NO_PROGRESS_AWARE)") - public void testReadFromLongFile() throws BackendException { - String file3Name = "File " + Strings.repeat("15", 250); - - byte[] longFilenameBytes = file3Name.getBytes(Encodings.UTF_8); - byte[] hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes); - String shortenedFileName = BaseEncoding.base64Url().encode(hash) + ".c9s"; - - TestFolder testFile3Folder = new TestFolder(aaFolder, shortenedFileName, "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/" + shortenedFileName); - TestFile testFile3ContentFile = new TestFile(testFile3Folder, "content.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/" + shortenedFileName + "/content.c9r", Optional.empty(), Optional.empty()); - - byte[] file1Content = "hhhhhTOPSECRET!TOPSECRET!TOPSECRET!TOPSECRET!".getBytes(); - FileHeader header = Mockito.mock(FileHeader.class); - - Mockito.when(fileContentCryptor.cleartextChunkSize()).thenReturn(8); - Mockito.when(fileContentCryptor.ciphertextChunkSize()).thenReturn(10); - Mockito.when(fileHeaderCryptor.headerSize()).thenReturn(5); - - Mockito.when(fileHeaderCryptor.decryptHeader(UTF_8.encode("hhhhh"))).thenReturn(header); - Mockito.when(fileContentCryptor.decryptChunk(Mockito.eq(UTF_8.encode("TOPSECRET!")), Mockito.anyLong(), Mockito.eq(header), Mockito.anyBoolean())).then(invocation -> UTF_8.encode("geheim!!")); - - CryptoFile cryptoFile15 = new CryptoFile(root, file3Name, "/" + file3Name, Optional.empty(), testFile3ContentFile); - - Mockito.doAnswer(invocation -> { - OutputStream out = invocation.getArgument(2); - CopyStream.copyStreamToStream(new ByteArrayInputStream(file1Content), out); - return null; - }).when(cloudContentRepository).read(Mockito.eq(cryptoFile15.getCloudFile()), Mockito.any(), Mockito.any(), Mockito.any()); - - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(1000); - - inTest.read(cryptoFile15, outputStream, ProgressAware.NO_OP_PROGRESS_AWARE); - - assertThat(outputStream.toString(), is("geheim!!geheim!!geheim!!geheim!!")); - - } - - @Test - @DisplayName("write(\"/File 1\", text, NO_PROGRESS_AWARE, replace=false, 10bytes)") - public void testWriteToShortFile() throws BackendException { - Mockito.when(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), "File 1", dirIdRoot.getBytes())).thenReturn("file1"); - - FileHeader header = Mockito.mock(FileHeader.class); - Mockito.when(fileHeaderCryptor.create()).thenReturn(header); - Mockito.when(fileHeaderCryptor.encryptHeader(header)).thenReturn(ByteBuffer.wrap("hhhhh".getBytes())); - Mockito.when(fileHeaderCryptor.headerSize()).thenReturn(5); - Mockito.when(fileContentCryptor.cleartextChunkSize()).thenReturn(10); - Mockito.when(fileContentCryptor.ciphertextChunkSize()).thenReturn(10); - Mockito.when(fileContentCryptor.encryptChunk(Mockito.any(ByteBuffer.class), Mockito.anyLong(), Mockito.any(FileHeader.class))).thenAnswer(invocation -> { - ByteBuffer input = invocation.getArgument(0); - String inStr = UTF_8.decode(input).toString(); - return ByteBuffer.wrap(inStr.toLowerCase().getBytes(UTF_8)); - }); - - Mockito.when(cloudContentRepository.write(Mockito.eq(cryptoFile1.getCloudFile()), Mockito.any(DataSource.class), Mockito.any(), Mockito.eq(false), Mockito.anyLong())).thenAnswer(invocationOnMock -> { - DataSource in = invocationOnMock.getArgument(1); - String encrypted = new BufferedReader(new InputStreamReader(in.open(context), StandardCharsets.UTF_8)).readLine(); - assertThat(encrypted, is("hhhhhtopsecret!")); - return invocationOnMock.getArgument(0); - }); - - CryptoFile cryptoFile = inTest.write(cryptoFile1, ByteArrayDataSource.from("TOPSECRET!".getBytes(UTF_8)), ProgressAware.NO_OP_PROGRESS_AWARE, false, 10l); - assertThat(cryptoFile, is(cryptoFile1)); - } - - @Test - @DisplayName("write(\"/File 15x250\", text, NO_PROGRESS_AWARE, replace=false, 10bytes)") - public void testWriteToLongFile() throws BackendException { - String file15Name = "File " + Strings.repeat("15", 250); - String file15Cipher = "file" + Strings.repeat("15", 250); - - byte[] longFilenameBytes = (file15Cipher + ".c9r").getBytes(Encodings.UTF_8); - byte[] hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes); - String shortenedFileName = BaseEncoding.base64Url().encode(hash) + ".c9s"; - - TestFolder testFile3Folder = new TestFolder(aaFolder, shortenedFileName, "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/" + shortenedFileName); - TestFile testFile3WhatTheHellCLoudFile = new TestFile(aaFolder, shortenedFileName, "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/" + shortenedFileName, Optional.empty(), Optional.empty()); - TestFile testFile15ContentFile = new TestFile(testFile3Folder, "contents.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/" + shortenedFileName + "/contents.c9r", Optional.of(10l), Optional.empty()); - - Mockito.when(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), file15Name, dirIdRoot.getBytes())).thenReturn(file15Cipher); - - CryptoFile cryptoFile15 = new CryptoFile(root, file15Name, "/" + file15Name, Optional.of(15l), testFile3WhatTheHellCLoudFile); - - Mockito.when(cloudContentRepository.folder(aaFolder, shortenedFileName)).thenReturn(testFile3Folder); - Mockito.when(cloudContentRepository.file(testFile3Folder, "contents.c9r", Optional.of(10l))).thenReturn(testFile15ContentFile); - - Mockito.when(cloudContentRepository.exists(testFile3Folder)).thenReturn(true); - - FileHeader header = Mockito.mock(FileHeader.class); - Mockito.when(fileHeaderCryptor.create()).thenReturn(header); - Mockito.when(fileHeaderCryptor.encryptHeader(header)).thenReturn(ByteBuffer.wrap("hhhhh".getBytes())); - Mockito.when(fileHeaderCryptor.headerSize()).thenReturn(5); - Mockito.when(fileContentCryptor.cleartextChunkSize()).thenReturn(10); - Mockito.when(fileContentCryptor.ciphertextChunkSize()).thenReturn(10); - Mockito.when(fileContentCryptor.encryptChunk(Mockito.any(ByteBuffer.class), Mockito.anyLong(), Mockito.any(FileHeader.class))).thenAnswer(invocation -> { - ByteBuffer input = invocation.getArgument(0); - String inStr = UTF_8.decode(input).toString(); - return ByteBuffer.wrap(inStr.toLowerCase().getBytes(UTF_8)); - }); - - Mockito.when(cloudContentRepository.write(Mockito.eq(testFile15ContentFile), Mockito.any(DataSource.class), Mockito.any(), Mockito.eq(false), Mockito.anyLong())).thenAnswer(invocationOnMock -> { - DataSource in = invocationOnMock.getArgument(1); - String encrypted = new BufferedReader(new InputStreamReader(in.open(context), StandardCharsets.UTF_8)).readLine(); - assertThat(encrypted, is("hhhhhtopsecret!")); - return invocationOnMock.getArgument(0); - }); - - CryptoFile cryptoFile = inTest.write(cryptoFile15, ByteArrayDataSource.from("TOPSECRET!".getBytes(UTF_8)), ProgressAware.NO_OP_PROGRESS_AWARE, false, 10l); - assertThat(cryptoFile, is(cryptoFile15)); - - Mockito.verify(cloudContentRepository).write(Mockito.eq(testFile15ContentFile), Mockito.any(DataSource.class), Mockito.any(), Mockito.eq(false), Mockito.anyLong()); - } - - @Test - @DisplayName("write(\"/File 15x250\", text, NO_PROGRESS_AWARE, replace=false, 10bytes)") - public void testWriteToLongFileUsingAutoRename() throws BackendException { - String file15Name = "File " + Strings.repeat("15", 250); - String file15Cipher = "file" + Strings.repeat("15", 250); - - byte[] longFilenameBytes = (file15Cipher + ".c9r").getBytes(Encodings.UTF_8); - byte[] hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes); - String shortenedFileName = BaseEncoding.base64Url().encode(hash) + ".c9s"; - - TestFolder testFile15Folder = new TestFolder(aaFolder, shortenedFileName, "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/" + shortenedFileName); - TestFile testFile15WhatTheHellCLoudFile = new TestFile(aaFolder, shortenedFileName, "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/" + shortenedFileName, Optional.empty(), Optional.empty()); - TestFile testFile15ContentFile = new TestFile(testFile15Folder, "contents.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/" + shortenedFileName + "/contents.c9r", Optional.of(10l), Optional.empty()); - - Mockito.when(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), file15Name, dirIdRoot.getBytes())).thenReturn(file15Cipher); - - String file15CipherRename = file15Cipher + "(1)"; - byte[] hashRename = MessageDigestSupplier.SHA1.get().digest((file15CipherRename + ".c9r").getBytes(Encodings.UTF_8)); - String shortenedFileNameRename = BaseEncoding.base64Url().encode(hashRename) + ".c9s"; - - TestFolder testFile15FolderRename = new TestFolder(aaFolder, shortenedFileNameRename, "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/" + shortenedFileNameRename); - TestFile testFile15WhatTheHellCloudFileRename = new TestFile(aaFolder, shortenedFileNameRename, "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/" + shortenedFileNameRename, Optional.of(20l), Optional.empty()); - - TestFile testFile15ContentFileRename = new TestFile(testFile15FolderRename, "contents.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/" + shortenedFileNameRename + "/contents.c9r", Optional.of(10l), Optional.empty()); - TestFile testFile15NameFileRename = new TestFile(testFile15FolderRename, "name.c9s", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/" + shortenedFileNameRename + "/name.c9s", Optional.of(511l), Optional.empty()); - - Mockito.when(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), file15Name + " (1)", dirIdRoot.getBytes())).thenReturn(file15Cipher + "(1)"); - - CryptoFile cryptoFile15 = new CryptoFile(root, file15Name, "/" + file15Name, Optional.of(15l), testFile15WhatTheHellCLoudFile); - - Mockito.when(cloudContentRepository.file(testFile15Folder, "contents.c9r", Optional.of(10l))).thenReturn(testFile15ContentFile); - Mockito.when(cloudContentRepository.exists(testFile15ContentFile)).thenReturn(true); - Mockito.when(cloudContentRepository.folder(aaFolder, shortenedFileName)).thenReturn(testFile15Folder); - Mockito.when(cloudContentRepository.exists(testFile15Folder)).thenReturn(true); - Mockito.when(cloudContentRepository.file(testFile15Folder, "contents.c9r", Optional.of(10l))).thenReturn(testFile15ContentFile); - Mockito.when(cloudContentRepository.file(testFile15FolderRename, "contents.c9r", Optional.of(15l))).thenReturn(testFile15ContentFileRename); - - Mockito.when(cloudContentRepository.folder(aaFolder, shortenedFileNameRename)).thenReturn(testFile15FolderRename); - Mockito.when(cloudContentRepository.exists(testFile15FolderRename)).thenReturn(false); - Mockito.when(cloudContentRepository.create(testFile15FolderRename)).thenReturn(testFile15FolderRename); - Mockito.when(cloudContentRepository.file(testFile15FolderRename, "name.c9s", Optional.of(511l))).thenReturn(testFile15NameFileRename); - Mockito.when(cloudContentRepository.write(Mockito.eq(testFile15NameFileRename), Mockito.any(DataSource.class), Mockito.any(), Mockito.eq(true), Mockito.anyLong())).thenAnswer(invocationOnMock -> { - DataSource in = invocationOnMock.getArgument(1); - String encrypted = new BufferedReader(new InputStreamReader(in.open(context), StandardCharsets.UTF_8)).readLine(); - assertThat(encrypted, is(file15CipherRename + ".c9r")); - return invocationOnMock.getArgument(0); - }); - Mockito.when(cloudContentRepository.file(aaFolder, shortenedFileNameRename, Optional.of(20l))).thenReturn(testFile15WhatTheHellCloudFileRename); - - FileHeader header = Mockito.mock(FileHeader.class); - Mockito.when(fileHeaderCryptor.create()).thenReturn(header); - Mockito.when(fileHeaderCryptor.encryptHeader(header)).thenReturn(ByteBuffer.wrap("hhhhh".getBytes())); - Mockito.when(fileHeaderCryptor.headerSize()).thenReturn(5); - Mockito.when(fileContentCryptor.cleartextChunkSize()).thenReturn(10); - Mockito.when(fileContentCryptor.ciphertextChunkSize()).thenReturn(10); - Mockito.when(fileContentCryptor.encryptChunk(Mockito.any(ByteBuffer.class), Mockito.anyLong(), Mockito.any(FileHeader.class))).thenAnswer(invocation -> { - ByteBuffer input = invocation.getArgument(0); - String inStr = UTF_8.decode(input).toString(); - return ByteBuffer.wrap(inStr.toLowerCase().getBytes(UTF_8)); - }); - - Mockito.when(cloudContentRepository.write(Mockito.eq(testFile15ContentFileRename), Mockito.any(DataSource.class), Mockito.any(), Mockito.eq(false), Mockito.anyLong())).thenAnswer(invocationOnMock -> { - DataSource in = invocationOnMock.getArgument(1); - String encrypted = new BufferedReader(new InputStreamReader(in.open(context), StandardCharsets.UTF_8)).readLine(); - assertThat(encrypted, is("hhhhhtopsecret!")); - return invocationOnMock.getArgument(0); - }); - - CryptoFile cryptoFile = inTest.write(cryptoFile15, ByteArrayDataSource.from("TOPSECRET!".getBytes(UTF_8)), ProgressAware.NO_OP_PROGRESS_AWARE, false, 10l); - assertThat(cryptoFile, is(cryptoFile15)); - - Mockito.verify(cloudContentRepository).create(testFile15FolderRename); - Mockito.verify(cloudContentRepository).write(Mockito.eq(testFile15NameFileRename), Mockito.any(DataSource.class), Mockito.any(), Mockito.eq(true), Mockito.anyLong()); - Mockito.verify(cloudContentRepository).write(Mockito.eq(testFile15ContentFileRename), Mockito.any(DataSource.class), Mockito.any(), Mockito.eq(false), Mockito.anyLong()); - } - - @Test - @DisplayName("create(\"/Directory 3/\")") - public void testCreateShortFolder() throws BackendException { - /* - * - * path/to/vault/d - * ├─ Directory 1 - * │ ├─ ... - * ├─ Directory 3 - * ├─ ... - * - */ - - lvl2Dir = new TestFolder(d, "33", "/d/33"); - TestFolder ddFolder = new TestFolder(lvl2Dir, "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD", "/d/33/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD"); - TestFolder testDir3 = new TestFolder(aaFolder, "dir3.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/dir3.c9r"); - TestFile testDir3DirFile = new TestFile(testDir3, "dir.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/dir3.c9r/dir.c9r", Optional.empty(), Optional.empty()); - CryptoFolder cryptoFolder3 = new CryptoFolder(root, "Directory 3", "/Directory 3", testDir3DirFile); - - Mockito.when(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), "Directory 3", dirIdRoot.getBytes())).thenReturn("dir3"); - Mockito.when(fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), "dir3", dirIdRoot.getBytes())).thenReturn("Directory 3"); - Mockito.when(fileNameCryptor.hashDirectoryId(AdditionalMatchers.not(Mockito.eq("")))).thenReturn("33DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD"); - - Mockito.when(cloudContentRepository.folder(d, "33")).thenReturn(lvl2Dir); - Mockito.when(cloudContentRepository.folder(lvl2Dir, "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD")).thenReturn(ddFolder); - Mockito.when(cloudContentRepository.file(testDir3, "dir.c9r")).thenReturn(testDir3DirFile); - Mockito.when(dirIdCache.put(Mockito.eq(cryptoFolder3), Mockito.any())).thenReturn(new DirIdCache.DirIdInfo("dir3-id", ddFolder)); - - Mockito.when(cloudContentRepository.create(lvl2Dir)).thenReturn(lvl2Dir); - Mockito.when(cloudContentRepository.create(ddFolder)).thenReturn(ddFolder); - Mockito.when(cloudContentRepository.create(testDir3)).thenReturn(testDir3); - Mockito.when(cloudContentRepository.write(Mockito.eq(testDir3DirFile), Mockito.any(), Mockito.any(), Mockito.eq(false), Mockito.anyLong())).thenReturn(testDir3DirFile); - - Mockito.when(cloudContentRepository.file(aaFolder, "dir3.c9r")).thenReturn(null); - - CloudFolder cloudFolder = inTest.create(cryptoFolder3); - assertThat(cloudFolder, is(cryptoFolder3)); - - Mockito.verify(cloudContentRepository).create(ddFolder); - Mockito.verify(cloudContentRepository).create(testDir3); - Mockito.verify(cloudContentRepository).write(Mockito.eq(testDir3DirFile), Mockito.any(), Mockito.any(), Mockito.eq(false), Mockito.anyLong()); - } - - @Test - @DisplayName("create(\"/Directory 3x250/\")") - public void testCreateLongFolder() throws BackendException { - /* - * - * path/to/vault/d - * ├─ Directory 1 - * │ ├─ ... - * ├─ Directory 3x250 - * ├─ ... - * - */ - String dir3Name = "Directory " + Strings.repeat("3", 250); - String dir3Cipher = "dir" + Strings.repeat("3", 250); - byte[] longFilenameBytes = (dir3Cipher + ".c9r").getBytes(Encodings.UTF_8); - byte[] hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes); - String shortenedFileName = BaseEncoding.base64Url().encode(hash) + ".c9s"; - - TestFolder ddLvl2Dir = new TestFolder(d, "33", "/d/33"); - TestFolder ddFolder = new TestFolder(ddLvl2Dir, "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD", "/d/33/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD"); - - TestFolder testDir3 = new TestFolder(aaFolder, shortenedFileName, "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/" + shortenedFileName); - TestFile testDir3DirFile = new TestFile(testDir3, "dir.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/" + shortenedFileName + "/dir.c9r", Optional.empty(), Optional.empty()); - TestFile testDir3NameFile = new TestFile(testDir3, "name.c9s", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/" + shortenedFileName + "/name.c9s", Optional.of(257L), Optional.empty()); - - Mockito.when(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), dir3Name, dirIdRoot.getBytes())).thenReturn(dir3Cipher); - Mockito.when(fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), dir3Cipher, dirIdRoot.getBytes())).thenReturn(dir3Name); - Mockito.when(fileNameCryptor.hashDirectoryId(AdditionalMatchers.not(Mockito.eq("")))).thenReturn("33DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD"); - - Mockito.when(cloudContentRepository.folder(d, "33")).thenReturn(ddLvl2Dir); - Mockito.when(cloudContentRepository.folder(ddLvl2Dir, "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD")).thenReturn(ddFolder); - Mockito.when(cloudContentRepository.folder(aaFolder, shortenedFileName)).thenReturn(testDir3); - Mockito.when(cloudContentRepository.exists(testDir3)).thenReturn(false); - Mockito.when(cloudContentRepository.file(testDir3, "dir.c9r")).thenReturn(testDir3DirFile); - Mockito.when(cloudContentRepository.file(testDir3, "name.c9s", Optional.of(257L))).thenReturn(testDir3NameFile); - - CryptoFolder cryptoFolder3 = new CryptoFolder(root, dir3Name, "/" + dir3Name, testDir3DirFile); - - Mockito.when(fileNameCryptor.hashDirectoryId(AdditionalMatchers.not(Mockito.eq("")))).thenReturn("33DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD"); - - Mockito.when(cloudContentRepository.folder(d, "33")).thenReturn(ddLvl2Dir); - Mockito.when(cloudContentRepository.folder(lvl2Dir, "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD")).thenReturn(ddFolder); - Mockito.when(cloudContentRepository.file(testDir3, "dir.c9r")).thenReturn(testDir3DirFile); - Mockito.when(dirIdCache.put(Mockito.eq(cryptoFolder3), Mockito.any())).thenReturn(new DirIdCache.DirIdInfo("dir3-id", ddFolder)); - - Mockito.when(cloudContentRepository.create(ddLvl2Dir)).thenReturn(ddLvl2Dir); - Mockito.when(cloudContentRepository.create(ddFolder)).thenReturn(ddFolder); - Mockito.when(cloudContentRepository.create(testDir3)).thenReturn(testDir3); - Mockito.when(cloudContentRepository.write(Mockito.eq(testDir3DirFile), Mockito.any(), Mockito.any(), Mockito.eq(false), Mockito.anyLong())).thenAnswer(invocationOnMock -> { - DataSource in = invocationOnMock.getArgument(1); - String dirContent = new BufferedReader(new InputStreamReader(in.open(context), StandardCharsets.UTF_8)).readLine(); - assertThat(dirContent, is("dir3-id")); - return testDir3DirFile; - }); - Mockito.when(cloudContentRepository.write(Mockito.eq(testDir3NameFile), Mockito.any(), Mockito.any(), Mockito.eq(true), Mockito.anyLong())).thenAnswer(invocationOnMock -> { - DataSource in = invocationOnMock.getArgument(1); - String nameContent = new BufferedReader(new InputStreamReader(in.open(context), StandardCharsets.UTF_8)).readLine(); - assertThat(nameContent, is(dir3Cipher + ".c9r")); - return testDir3NameFile; - }); - - Mockito.when(cloudContentRepository.file(aaFolder, "dir3.c9r")).thenReturn(null); - - CloudFolder cloudFolder = inTest.folder(root, dir3Name); - cloudFolder = inTest.create(cryptoFolder3); - assertThat(cloudFolder, is(cryptoFolder3)); - - Mockito.verify(cloudContentRepository).create(ddFolder); - Mockito.verify(cloudContentRepository).create(testDir3); - Mockito.verify(cloudContentRepository).write(Mockito.eq(testDir3DirFile), Mockito.any(), Mockito.any(), Mockito.eq(false), Mockito.anyLong()); - Mockito.verify(cloudContentRepository).write(Mockito.eq(testDir3NameFile), Mockito.any(), Mockito.any(), Mockito.eq(true), Mockito.anyLong()); - } - - @Test - @DisplayName("delete(\"/File 4\")") - public void testDeleteShortFile() throws BackendException { - inTest.delete(cryptoFile4); - Mockito.verify(cloudContentRepository).delete(cryptoFile4.getCloudFile()); - } - - @Test - @DisplayName("delete(\"/File 15x250\")") - public void testDeleteLongFile() throws BackendException { - String file15Name = "File " + Strings.repeat("15", 250); - String file15Cipher = "file" + Strings.repeat("15", 250); - - byte[] longFilenameBytes = (file15Cipher + ".c9r").getBytes(Encodings.UTF_8); - byte[] hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes); - String shortenedFileName = BaseEncoding.base64Url().encode(hash) + ".c9s"; - - TestFolder testFile3Folder = new TestFolder(aaFolder, shortenedFileName, "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/" + shortenedFileName); - TestFile testFile3ContentFile = new TestFile(testFile3Folder, "content.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/" + shortenedFileName + "/content.c9r", Optional.empty(), Optional.empty()); - - Mockito.when(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), file15Name, dirIdRoot.getBytes())).thenReturn(file15Cipher); - - CryptoFile cryptoFile15 = new CryptoFile(root, file15Name, "/" + file15Name, Optional.of(15l), testFile3ContentFile); - - Mockito.when(cloudContentRepository.folder(aaFolder, shortenedFileName)).thenReturn(testFile3Folder); - - inTest.delete(cryptoFile15); - - Mockito.verify(cloudContentRepository).delete(testFile3Folder); - } - - @Test - @DisplayName("delete(\"/Directory 1/Directory 2/\")") - public void testDeleteSingleShortFolder() throws BackendException { - TestFolder bbLvl2Dir = new TestFolder(d, "11", "/d/11"); - TestFolder bbFolder = new TestFolder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"); - TestFolder ccLvl2Dir = new TestFolder(d, "22", "/d/22"); - TestFolder ccFolder = new TestFolder(ccLvl2Dir, "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCC", "/d/22/CCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"); - - Mockito.when(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), "Directory 1", dirIdRoot.getBytes())).thenReturn("dir1"); - Mockito.when(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), "Directory 2", dirId1.getBytes())).thenReturn("dir2"); - - TestFolder testDir2 = new TestFolder(bbFolder, "dir2.c9r", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/dir2.c9r"); - TestFile testDir2DirFile = new TestFile(testDir2, "dir.c9r", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/dir2.c9r/dir.c9r", Optional.empty(), Optional.empty()); - - CryptoFolder cryptoFolder2 = new CryptoFolder(cryptoFolder1, "Directory 2", "/Directory 1/Directory 2", testDir2DirFile); - Mockito.doAnswer(invocation -> { - OutputStream out = invocation.getArgument(2); - CopyStream.copyStreamToStream(new ByteArrayInputStream(dirId2.getBytes()), out); - return null; - }).when(cloudContentRepository).read(Mockito.eq(cryptoFolder2.getDirFile()), Mockito.any(), Mockito.any(), Mockito.any()); - - ArrayList dir1Items = new ArrayList() { - { - add(testDir2); - } - }; - - Mockito.when(cloudContentRepository.folder(rootFolder, "d")).thenReturn(d); - Mockito.when(cloudContentRepository.folder(d, "22")).thenReturn(ccLvl2Dir); - Mockito.when(cloudContentRepository.folder(ccLvl2Dir, "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCC")).thenReturn(ccFolder); - Mockito.when(cloudContentRepository.file(testDir2, "dir.c9r")).thenReturn(testDir2DirFile); - Mockito.when(cloudContentRepository.list(bbFolder)).thenReturn(dir1Items); - Mockito.when(dirIdCache.put(Mockito.eq(cryptoFolder2), Mockito.any())).thenReturn(new DirIdCache.DirIdInfo(dirId2, ccFolder)); - Mockito.when(cloudContentRepository.exists(testDir2DirFile)).thenReturn(true); - Mockito.when(cloudContentRepository.list(ccFolder)).thenReturn(new ArrayList()); - - inTest.delete(cryptoFolder2); - - Mockito.verify(cloudContentRepository).delete(ccFolder); - Mockito.verify(cloudContentRepository).delete(testDir2); - Mockito.verify(dirIdCache).evict(cryptoFolder2); - } - - @Test - @DisplayName("delete(\"/Directory 3x250\")") - public void testDeleteSingleLongFolder() throws BackendException { - String dir3Name = "Directory " + Strings.repeat("3", 250); - String dir3Cipher = "dir" + Strings.repeat("3", 250); - byte[] longFilenameBytes = (dir3Cipher + ".c9r").getBytes(Encodings.UTF_8); - byte[] hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes); - String shortenedFileName = BaseEncoding.base64Url().encode(hash) + ".c9s"; - - TestFolder ddLvl2Dir = new TestFolder(d, "33", "/d/33"); - TestFolder ddFolder = new TestFolder(ddLvl2Dir, "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD", "/d/33/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD"); - - TestFolder testDir3 = new TestFolder(aaFolder, shortenedFileName, "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/" + shortenedFileName); - TestFile testDir3DirFile = new TestFile(testDir3, "dir.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/" + shortenedFileName + "/dir.c9r", Optional.empty(), Optional.empty()); - TestFile testDir3NameFile = new TestFile(testDir3, "name.c9s", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/" + shortenedFileName + "/name.c9s", Optional.of(257L), Optional.empty()); - - CryptoFolder cryptoFolder3 = new CryptoFolder(root, dir3Name, "/" + dir3Name, testDir3DirFile); - - Mockito.when(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), dir3Name, dirIdRoot.getBytes())).thenReturn(dir3Cipher); - Mockito.when(fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), dir3Cipher, dirIdRoot.getBytes())).thenReturn(dir3Name); - Mockito.when(fileNameCryptor.hashDirectoryId(AdditionalMatchers.not(Mockito.eq("")))).thenReturn("33DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD"); - - Mockito.when(cloudContentRepository.folder(d, "33")).thenReturn(ddLvl2Dir); - Mockito.when(cloudContentRepository.folder(ddLvl2Dir, "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD")).thenReturn(ddFolder); - Mockito.when(cloudContentRepository.folder(aaFolder, shortenedFileName)).thenReturn(testDir3); - Mockito.when(cloudContentRepository.exists(testDir3)).thenReturn(false); - Mockito.when(dirIdCache.put(Mockito.eq(cryptoFolder3), Mockito.any())).thenReturn(new DirIdCache.DirIdInfo("dir3-id", ddFolder)); - Mockito.when(cloudContentRepository.file(testDir3, "dir.c9r")).thenReturn(testDir3DirFile); - Mockito.when(cloudContentRepository.file(testDir3, "name.c9s", Optional.of(257L))).thenReturn(testDir3NameFile); - Mockito.when(cloudContentRepository.list(ddFolder)).thenReturn(new ArrayList()); - - Mockito.when(fileNameCryptor.hashDirectoryId(AdditionalMatchers.not(Mockito.eq("")))).thenReturn("33DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD"); - - inTest.delete(cryptoFolder3); - - Mockito.verify(cloudContentRepository).delete(ddFolder); - Mockito.verify(cloudContentRepository).delete(testDir3); - Mockito.verify(dirIdCache).evict(cryptoFolder3); - } - - @Test - @DisplayName("move(\"/File 4\", \"/Directory 1/File 4\")") - public void testMoveShortFileToNewShortFile() throws BackendException { - TestFolder bbLvl2Dir = new TestFolder(d, "11", "/d/11"); - TestFolder bbFolder = new TestFolder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"); - - TestFile testFile4 = new TestFile(aaFolder, "file4.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/file4.c9r", Optional.empty(), Optional.empty()); - TestFile testMovedFile4 = new TestFile(bbFolder, "file4.c9r", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/file4.c9r", Optional.empty(), Optional.empty()); - CryptoFile cryptoFile4 = new CryptoFile(root, "File 4", "/File 4", Optional.empty(), testFile4); - CryptoFile cryptoMovedFile4 = new CryptoFile(cryptoFolder1, "File 4", "/Directory 1/File 4", Optional.empty(), testMovedFile4); - - Mockito.when(cloudContentRepository.file(aaFolder, "file4.c9r")).thenReturn(testFile4); - Mockito.when(cloudContentRepository.file(bbFolder, "file4.c9r")).thenReturn(testMovedFile4); - Mockito.when(cloudContentRepository.move(testFile4, testMovedFile4)).thenReturn(testMovedFile4); - Mockito.when(cloudContentRepository.folder(rootFolder, "d")).thenReturn(d); - Mockito.when(cloudContentRepository.folder(d, "11")).thenReturn(bbLvl2Dir); - Mockito.when(cloudContentRepository.folder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB")).thenReturn(bbFolder); - Mockito.when(cloudContentRepository.folder(bbFolder, "file4.c9r")).thenReturn(null); - Mockito.when(dirIdCache.put(Mockito.eq(cryptoFolder1), Mockito.any())).thenReturn(new DirIdCache.DirIdInfo(dirId1, bbFolder)); - - Mockito.when(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), "File 4", dirId1.getBytes())).thenReturn("file4"); - Mockito.when(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), "File 4", dirIdRoot.getBytes())).thenReturn("file4"); - - CryptoFile result = inTest.move(cryptoFile4, cryptoMovedFile4); - - Assertions.assertEquals("File 4", result.getName()); - - Mockito.verify(cloudContentRepository).move(testFile4, testMovedFile4); - } - - @Test - @DisplayName("move(\"/File 4\", \"/Directory 1/File 4x250\")") - public void testMoveShortFileToNewLongFile() throws BackendException { - String file4Name = "File " + Strings.repeat("4", 250); - String file4Cipher = "file" + Strings.repeat("4", 250); - byte[] longFilenameBytes = (file4Cipher + ".c9r").getBytes(Encodings.UTF_8); - byte[] hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes); - String shortenedFileName = BaseEncoding.base64Url().encode(hash) + ".c9s"; - - TestFolder bbLvl2Dir = new TestFolder(d, "11", "/d/11"); - TestFolder bbFolder = new TestFolder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"); - - TestFile testFile4 = new TestFile(aaFolder, "file4.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/file4.c9r", Optional.empty(), Optional.empty()); - CryptoFile cryptoFile4 = new CryptoFile(root, "File 4", "/File 4", Optional.empty(), testFile4); - - TestFolder testDir4 = new TestFolder(bbFolder, shortenedFileName, "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/" + shortenedFileName); - TestFile testFile4ContentFile = new TestFile(testDir4, "contents.c9r", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/" + shortenedFileName + "/contents.c9r", Optional.empty(), Optional.empty()); - TestFile testFile4NameFile = new TestFile(testDir4, "name.c9s", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/" + shortenedFileName + "/name.c9s", Optional.of(258L), Optional.empty()); - - TestFile testFile4WhatTheHellCLoudFile = new TestFile(bbFolder, shortenedFileName, "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/" + shortenedFileName, Optional.empty(), Optional.empty()); // ugly hack - CryptoFile cryptoMovedFile4 = new CryptoFile(cryptoFolder1, file4Name, "/Directory 1/" + file4Name, Optional.empty(), testFile4WhatTheHellCLoudFile); - - Mockito.when(cloudContentRepository.file(aaFolder, "file4.c9r")).thenReturn(testFile4); - Mockito.when(cloudContentRepository.file(testDir4, "contents.c9r")).thenReturn(testFile4ContentFile); - Mockito.when(cloudContentRepository.file(testDir4, "name.c9s")).thenReturn(testFile4NameFile); - Mockito.when(cloudContentRepository.file(testDir4, "name.c9s", Optional.of(258L))).thenReturn(testFile4NameFile); - Mockito.when(cloudContentRepository.file(bbFolder, shortenedFileName, Optional.ofNullable(null))).thenReturn(testFile4WhatTheHellCLoudFile); - Mockito.when(cloudContentRepository.move(testFile4, testFile4ContentFile)).thenReturn(testFile4ContentFile); - Mockito.when(cloudContentRepository.folder(rootFolder, "d")).thenReturn(d); - Mockito.when(cloudContentRepository.folder(d, "11")).thenReturn(bbLvl2Dir); - Mockito.when(cloudContentRepository.folder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB")).thenReturn(bbFolder); - Mockito.when(cloudContentRepository.folder(bbFolder, "file4.c9r")).thenReturn(null); - Mockito.when(cloudContentRepository.folder(bbFolder, shortenedFileName)).thenReturn(testDir4); - Mockito.when(cloudContentRepository.create(testDir4)).thenReturn(testDir4); - Mockito.when(cloudContentRepository.write(Mockito.eq(testFile4NameFile), Mockito.any(), Mockito.any(), Mockito.eq(true), Mockito.anyLong())).thenAnswer(invocationOnMock -> { - DataSource in = invocationOnMock.getArgument(1); - String dirContent = new BufferedReader(new InputStreamReader(in.open(context), StandardCharsets.UTF_8)).readLine(); - assertThat(dirContent, is(file4Cipher + ".c9r")); - return testFile4NameFile; - }); - - Mockito.when(dirIdCache.put(Mockito.eq(cryptoFolder1), Mockito.any())).thenReturn(new DirIdCache.DirIdInfo(dirId1, bbFolder)); - - Mockito.when(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), "File 4", dirIdRoot.getBytes())).thenReturn("file4"); - Mockito.when(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), file4Name, dirId1.getBytes())).thenReturn(file4Cipher); - - CloudFile targetFile = inTest.file(cryptoFolder1, file4Name); // needed due to ugly side effect - CryptoFile result = inTest.move(cryptoFile4, cryptoMovedFile4); - - Assertions.assertEquals(file4Name, result.getName()); - - Mockito.verify(cloudContentRepository).create(testDir4); - Mockito.verify(cloudContentRepository).move(testFile4, testFile4ContentFile); - Mockito.verify(cloudContentRepository).write(Mockito.eq(testFile4NameFile), Mockito.any(), Mockito.any(), Mockito.eq(true), Mockito.anyLong()); - } - - @Test - @DisplayName("move(\"/File 4x250\", \"/Directory 1/File 4x250\")") - public void testMoveLongFileToNewLongFile() throws BackendException { - String file4Name = "File " + Strings.repeat("4", 250); - String file4Cipher = "file" + Strings.repeat("4", 250); - byte[] longFilenameBytes = (file4Cipher + ".c9r").getBytes(Encodings.UTF_8); - byte[] hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes); - String shortenedFileName = BaseEncoding.base64Url().encode(hash) + ".c9s"; - - TestFolder bbLvl2Dir = new TestFolder(d, "11", "/d/11"); - TestFolder bbFolder = new TestFolder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"); - - TestFolder testDir4Old = new TestFolder(aaFolder, shortenedFileName, "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/" + shortenedFileName); - TestFile testFile4ContentFileOld = new TestFile(testDir4Old, "contents.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/" + shortenedFileName + "/contents.c9r", Optional.empty(), Optional.empty()); - - CryptoFile cryptoFile4Old = new CryptoFile(root, file4Name, "/" + file4Name, Optional.empty(), testFile4ContentFileOld); - - TestFolder testDir4 = new TestFolder(bbFolder, shortenedFileName, "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/" + shortenedFileName); - TestFile testFile4ContentFile = new TestFile(testDir4, "contents.c9r", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/" + shortenedFileName + "/contents.c9r", Optional.empty(), Optional.empty()); - TestFile testFile4NameFile = new TestFile(testDir4, "name.c9s", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/" + shortenedFileName + "/name.c9s", Optional.of(258L), Optional.empty()); - - TestFile testFile4WhatTheHellCLoudFile = new TestFile(bbFolder, shortenedFileName, "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/" + shortenedFileName, Optional.empty(), Optional.empty()); // ugly hack - CryptoFile cryptoMovedFile4 = new CryptoFile(cryptoFolder1, file4Name, "/Directory 1/" + file4Name, Optional.empty(), testFile4WhatTheHellCLoudFile); - - Mockito.when(cloudContentRepository.file(testDir4, "contents.c9r")).thenReturn(testFile4ContentFile); - Mockito.when(cloudContentRepository.file(testDir4, "name.c9s")).thenReturn(testFile4NameFile); - Mockito.when(cloudContentRepository.file(testDir4, "name.c9s", Optional.of(258L))).thenReturn(testFile4NameFile); - Mockito.when(cloudContentRepository.file(bbFolder, shortenedFileName, Optional.ofNullable(null))).thenReturn(testFile4WhatTheHellCLoudFile); - Mockito.when(cloudContentRepository.file(testDir4Old, "contents.c9r")).thenReturn(testFile4ContentFileOld); - Mockito.when(cloudContentRepository.folder(aaFolder, shortenedFileName)).thenReturn(testDir4Old); - Mockito.when(cloudContentRepository.move(testFile4ContentFileOld, testFile4ContentFile)).thenReturn(testFile4ContentFile); - Mockito.when(cloudContentRepository.folder(rootFolder, "d")).thenReturn(d); - Mockito.when(cloudContentRepository.folder(d, "11")).thenReturn(bbLvl2Dir); - Mockito.when(cloudContentRepository.folder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB")).thenReturn(bbFolder); - Mockito.when(cloudContentRepository.folder(bbFolder, "file4.c9r")).thenReturn(null); - Mockito.when(cloudContentRepository.folder(bbFolder, shortenedFileName)).thenReturn(testDir4); - Mockito.when(cloudContentRepository.create(testDir4)).thenReturn(testDir4); - Mockito.when(cloudContentRepository.write(Mockito.eq(testFile4NameFile), Mockito.any(), Mockito.any(), Mockito.eq(true), Mockito.anyLong())).thenAnswer(invocationOnMock -> { - DataSource in = invocationOnMock.getArgument(1); - String dirContent = new BufferedReader(new InputStreamReader(in.open(context), StandardCharsets.UTF_8)).readLine(); - assertThat(dirContent, is(file4Cipher + ".c9r")); - return testFile4NameFile; - }); - - Mockito.when(dirIdCache.put(Mockito.eq(cryptoFolder1), Mockito.any())).thenReturn(new DirIdCache.DirIdInfo(dirId1, bbFolder)); - - Mockito.when(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), file4Name, dirIdRoot.getBytes())).thenReturn(file4Cipher); - Mockito.when(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), file4Name, dirId1.getBytes())).thenReturn(file4Cipher); - - CloudFile targetFile = inTest.file(cryptoFolder1, file4Name); // needed due to ugly side effect - CryptoFile result = inTest.move(cryptoFile4Old, cryptoMovedFile4); - - Assertions.assertEquals(file4Name, result.getName()); - - Mockito.verify(cloudContentRepository).create(testDir4); - Mockito.verify(cloudContentRepository).write(Mockito.eq(testFile4NameFile), Mockito.any(), Mockito.any(), Mockito.eq(true), Mockito.anyLong()); - Mockito.verify(cloudContentRepository).move(testFile4ContentFileOld, testFile4ContentFile); - Mockito.verify(cloudContentRepository).delete(testDir4Old); - } - - @Test - @DisplayName("move(\"/Directory 1/File 4x250\", \"/File 4\")") - public void testMoveLongFileToNewShortFile() throws BackendException { - String file4Name = "File " + Strings.repeat("4", 250); - String file4Cipher = "file" + Strings.repeat("4", 250); - byte[] longFilenameBytes = (file4Cipher + ".c9r").getBytes(Encodings.UTF_8); - byte[] hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes); - String shortenedFileName = BaseEncoding.base64Url().encode(hash) + ".c9s"; - - TestFolder bbLvl2Dir = new TestFolder(d, "11", "/d/11"); - TestFolder bbFolder = new TestFolder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"); - - TestFile testFile4 = new TestFile(aaFolder, "file4.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/file4.c9r", Optional.empty(), Optional.empty()); - CryptoFile cryptoFile4 = new CryptoFile(root, "File 4", "/File 4", Optional.empty(), testFile4); - - TestFolder testDir4 = new TestFolder(bbFolder, shortenedFileName, "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/" + shortenedFileName); - TestFile testFile4ContentFile = new TestFile(testDir4, "contents.c9r", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/" + shortenedFileName + "/contents.c9r", Optional.empty(), Optional.empty()); - TestFile testFile4NameFile = new TestFile(testDir4, "name.c9s", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/" + shortenedFileName + "/name.c9s", Optional.of(258L), Optional.empty()); - - TestFile testFile4WhatTheHellCLoudFile = new TestFile(bbFolder, shortenedFileName, "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/" + shortenedFileName, Optional.empty(), Optional.empty()); // ugly hack - CryptoFile cryptoMovedFile4 = new CryptoFile(cryptoFolder1, file4Name, "/Directory 1/" + file4Name, Optional.empty(), testFile4ContentFile); - - Mockito.when(cloudContentRepository.file(aaFolder, "file4.c9r")).thenReturn(testFile4); - Mockito.when(cloudContentRepository.file(testDir4, "contents.c9r")).thenReturn(testFile4ContentFile); - Mockito.when(cloudContentRepository.file(testDir4, "name.c9s")).thenReturn(testFile4NameFile); - Mockito.when(cloudContentRepository.file(testDir4, "name.c9s", Optional.of(258L))).thenReturn(testFile4NameFile); - Mockito.when(cloudContentRepository.file(bbFolder, shortenedFileName, Optional.ofNullable(null))).thenReturn(testFile4WhatTheHellCLoudFile); // bad - Mockito.when(cloudContentRepository.move(testFile4, testFile4ContentFile)).thenReturn(testFile4ContentFile); - Mockito.when(cloudContentRepository.folder(rootFolder, "d")).thenReturn(d); - Mockito.when(cloudContentRepository.folder(d, "11")).thenReturn(bbLvl2Dir); - Mockito.when(cloudContentRepository.folder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB")).thenReturn(bbFolder); - Mockito.when(cloudContentRepository.folder(bbFolder, "file4.c9r")).thenReturn(null); - Mockito.when(cloudContentRepository.folder(bbFolder, shortenedFileName)).thenReturn(testDir4); - Mockito.when(cloudContentRepository.create(testDir4)).thenReturn(testDir4); - Mockito.when(cloudContentRepository.write(Mockito.eq(testFile4NameFile), Mockito.any(), Mockito.any(), Mockito.eq(true), Mockito.anyLong())).thenAnswer(invocationOnMock -> { - DataSource in = invocationOnMock.getArgument(1); - String dirContent = new BufferedReader(new InputStreamReader(in.open(context), StandardCharsets.UTF_8)).readLine(); - assertThat(dirContent, is(file4Cipher + ".c9r")); - return testFile4NameFile; - }); - - Mockito.when(dirIdCache.put(Mockito.eq(cryptoFolder1), Mockito.any())).thenReturn(new DirIdCache.DirIdInfo(dirId1, bbFolder)); - - Mockito.when(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), "File 4", dirIdRoot.getBytes())).thenReturn("file4"); - Mockito.when(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), file4Name, dirId1.getBytes())).thenReturn(file4Cipher); - - CryptoFile result = inTest.move(cryptoMovedFile4, cryptoFile4); - - Mockito.verify(cloudContentRepository).delete(testDir4); - Mockito.verify(cloudContentRepository).move(testFile4ContentFile, testFile4); - } - - @Test - @DisplayName("move(\"/Directory 1\", \"/Directory 15\")") - public void testMoveShortFolderToNewShortFolder() throws BackendException { - TestFolder bbLvl2Dir = new TestFolder(d, "11", "/d/11"); - TestFolder bbFolder = new TestFolder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"); - - TestFolder testDir15 = new TestFolder(aaFolder, "dir15.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/dir15.c9r"); - TestFile testDir15DirFile = new TestFile(testDir15, "dir.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/dir15.c9r/dir.c9r", Optional.empty(), Optional.empty()); - - CryptoFolder cryptoFolder15 = new CryptoFolder(root, "Directory 15", "/Directory 15/", testDir15DirFile); - - Mockito.when(cloudContentRepository.file(aaFolder, "dir15.c9r", Optional.ofNullable(null))).thenReturn(new TestFile(aaFolder, "dir15.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/dir15.c9r", Optional.empty(), Optional.empty())); - Mockito.when(cloudContentRepository.folder(rootFolder, "d")).thenReturn(d); - Mockito.when(cloudContentRepository.folder(d, "11")).thenReturn(bbLvl2Dir); - Mockito.when(cloudContentRepository.folder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB")).thenReturn(bbFolder); - Mockito.when(dirIdCache.put(Mockito.eq(cryptoFolder1), Mockito.any())).thenReturn(new DirIdCache.DirIdInfo(dirId1, bbFolder)); - Mockito.when(dirIdCache.put(Mockito.eq(cryptoFolder15), Mockito.any())).thenReturn(new DirIdCache.DirIdInfo(dirId1, bbFolder)); - - Mockito.when(cloudContentRepository.create(testDir15)).thenReturn(testDir15); - Mockito.when(cloudContentRepository.file(testDir15, "dir.c9r")).thenReturn(testDir15DirFile); - Mockito.when(cloudContentRepository.move(cryptoFolder1.getDirFile(), testDir15DirFile)).thenReturn(testDir15DirFile); - - Mockito.when(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), "Directory 15", dirIdRoot.getBytes())).thenReturn(dirId1); - - CryptoFolder result = inTest.move(cryptoFolder1, cryptoFolder15); - - Mockito.verify(cloudContentRepository).create(testDir15); - Mockito.verify(cloudContentRepository).move(cryptoFolder1.getDirFile(), testDir15DirFile); - Mockito.verify(cloudContentRepository).delete(cryptoFolder1.getDirFile().getParent()); - } - - @Test - @DisplayName("move(\"/Directory 1\", \"/Directory 15x200\")") - public void testMoveShortFolderToNewLongFolder() throws BackendException { - String dir15Name = "Dir " + Strings.repeat("15", 250); - String dir15Cipher = "dir" + Strings.repeat("15", 250); - byte[] longFilenameBytes = (dir15Cipher + ".c9r").getBytes(Encodings.UTF_8); - byte[] hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes); - String shortenedFileName = BaseEncoding.base64Url().encode(hash) + ".c9s"; - - TestFolder bbLvl2Dir = new TestFolder(d, "11", "/d/11"); - TestFolder bbFolder = new TestFolder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"); - - TestFolder testDir15 = new TestFolder(aaFolder, shortenedFileName, "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/" + shortenedFileName); - TestFile testDir15DirFile = new TestFile(testDir15, "dir.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/" + shortenedFileName + "/dir.c9r", Optional.empty(), Optional.empty()); - TestFile testDir15NameFile = new TestFile(testDir15, "name.c9s", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/" + shortenedFileName + "/name.c9s", Optional.of(507L), Optional.empty()); - - CryptoFolder cryptoFolder15 = new CryptoFolder(root, dir15Name, "/" + dir15Name, testDir15DirFile); - - Mockito.when(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), dir15Name, dirId1.getBytes())).thenReturn(dir15Cipher); - - Mockito.when(cloudContentRepository.file(aaFolder, "dir15.c9r", Optional.ofNullable(null))).thenReturn(new TestFile(aaFolder, "dir15.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/dir15.c9r", Optional.empty(), Optional.empty())); - Mockito.when(cloudContentRepository.folder(aaFolder, shortenedFileName)).thenReturn(testDir15); - Mockito.when(cloudContentRepository.folder(rootFolder, "d")).thenReturn(d); - Mockito.when(cloudContentRepository.folder(d, "11")).thenReturn(bbLvl2Dir); - Mockito.when(cloudContentRepository.folder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB")).thenReturn(bbFolder); - Mockito.when(dirIdCache.put(Mockito.eq(cryptoFolder1), Mockito.any())).thenReturn(new DirIdCache.DirIdInfo(dirId1, bbFolder)); - Mockito.when(dirIdCache.put(Mockito.eq(cryptoFolder15), Mockito.any())).thenReturn(new DirIdCache.DirIdInfo(dirId1, bbFolder)); - - Mockito.when(cloudContentRepository.create(testDir15)).thenReturn(testDir15); - Mockito.when(cloudContentRepository.file(testDir15, "dir.c9r")).thenReturn(testDir15DirFile); - Mockito.when(cloudContentRepository.file(testDir15, "name.c9s", Optional.of(507L))).thenReturn(testDir15NameFile); - Mockito.when(cloudContentRepository.move(cryptoFolder1.getDirFile(), testDir15DirFile)).thenReturn(testDir15DirFile); - Mockito.when(cloudContentRepository.create(testDir15)).thenReturn(testDir15); - - Mockito.when(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), dir15Name, dirIdRoot.getBytes())).thenReturn(dir15Cipher); - - Mockito.when(cloudContentRepository.write(Mockito.eq(testDir15NameFile), Mockito.any(), Mockito.any(), Mockito.eq(true), Mockito.anyLong())).thenAnswer(invocationOnMock -> { - DataSource in = invocationOnMock.getArgument(1); - String dirContent = new BufferedReader(new InputStreamReader(in.open(context), StandardCharsets.UTF_8)).readLine(); - assertThat(dirContent, is(dir15Cipher + ".c9r")); - return testDir15NameFile; - }); - - CryptoFolder targetFile = inTest.folder(root, dir15Name); // needed due to ugly side effect - CryptoFolder result = inTest.move(cryptoFolder1, cryptoFolder15); - - Mockito.verify(cloudContentRepository).create(testDir15); - Mockito.verify(cloudContentRepository).move(cryptoFolder1.getDirFile(), testDir15DirFile); - Mockito.verify(cloudContentRepository).write(Mockito.eq(testDir15NameFile), Mockito.any(), Mockito.any(), Mockito.eq(true), Mockito.anyLong()); - Mockito.verify(cloudContentRepository).delete(cryptoFolder1.getDirFile().getParent()); - } - - @Test - @DisplayName("move(\"/Directory 15x200\", \"/Directory 3000\")") - public void testMoveLongFolderToNewShortFolder() throws BackendException { - String dir15Name = "Dir " + Strings.repeat("15", 250); - String dir15Cipher = "dir" + Strings.repeat("15", 250); - byte[] longFilenameBytes = (dir15Cipher + ".c9r").getBytes(Encodings.UTF_8); - byte[] hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes); - String shortenedFileName = BaseEncoding.base64Url().encode(hash) + ".c9s"; - - TestFolder bbLvl2Dir = new TestFolder(d, "11", "/d/11"); - TestFolder bbFolder = new TestFolder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"); - - TestFolder testDir15 = new TestFolder(aaFolder, shortenedFileName, "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/" + shortenedFileName); - TestFile testDir15DirFile = new TestFile(testDir15, "dir.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/" + shortenedFileName + "/dir.c9r", Optional.empty(), Optional.empty()); - TestFile testDir15NameFile = new TestFile(testDir15, "name.c9s", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/" + shortenedFileName + "/name.c9s", Optional.of(507L), Optional.empty()); - - CryptoFolder cryptoFolder15 = new CryptoFolder(root, dir15Name, "/" + dir15Name, testDir15DirFile); - - Mockito.when(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), dir15Name, dirId1.getBytes())).thenReturn(dir15Cipher); - - Mockito.when(cloudContentRepository.file(aaFolder, "dir15.c9r", Optional.ofNullable(null))).thenReturn(new TestFile(aaFolder, "dir15.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/dir15.c9r", Optional.empty(), Optional.empty())); - Mockito.when(cloudContentRepository.folder(aaFolder, shortenedFileName)).thenReturn(testDir15); - Mockito.when(cloudContentRepository.folder(rootFolder, "d")).thenReturn(d); - Mockito.when(cloudContentRepository.folder(d, "11")).thenReturn(bbLvl2Dir); - Mockito.when(cloudContentRepository.folder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB")).thenReturn(bbFolder); - Mockito.when(dirIdCache.put(Mockito.eq(cryptoFolder1), Mockito.any())).thenReturn(new DirIdCache.DirIdInfo(dirId1, bbFolder)); - Mockito.when(dirIdCache.put(Mockito.eq(cryptoFolder15), Mockito.any())).thenReturn(new DirIdCache.DirIdInfo(dirId1, bbFolder)); - - Mockito.when(cloudContentRepository.create(testDir15)).thenReturn(testDir15); - Mockito.when(cloudContentRepository.file(testDir15, "dir.c9r")).thenReturn(testDir15DirFile); - Mockito.when(cloudContentRepository.file(testDir15, "name.c9s", Optional.of(507L))).thenReturn(testDir15NameFile); - Mockito.when(cloudContentRepository.move(cryptoFolder1.getDirFile(), testDir15DirFile)).thenReturn(testDir15DirFile); - Mockito.when(cloudContentRepository.create(testDir15)).thenReturn(testDir15); - - Mockito.when(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), dir15Name, dirIdRoot.getBytes())).thenReturn(dir15Cipher); - - Mockito.when(cloudContentRepository.write(Mockito.eq(testDir15NameFile), Mockito.any(), Mockito.any(), Mockito.eq(true), Mockito.anyLong())).thenAnswer(invocationOnMock -> { - DataSource in = invocationOnMock.getArgument(1); - String dirContent = new BufferedReader(new InputStreamReader(in.open(context), StandardCharsets.UTF_8)).readLine(); - assertThat(dirContent, is(dir15Cipher + ".c9r")); - return testDir15NameFile; - }); - - lvl2Dir = new TestFolder(d, "33", "/d/33"); - TestFolder ddFolder = new TestFolder(lvl2Dir, "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD", "/d/33/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD"); - TestFolder testDir3 = new TestFolder(aaFolder, "dir3.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/dir3.c9r"); - TestFile testDir3DirFile = new TestFile(testDir3, "dir.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/dir3.c9r/dir.c9r", Optional.empty(), Optional.empty()); - CryptoFolder cryptoFolder3 = new CryptoFolder(root, "Directory 3", "/Directory 3", testDir3DirFile); - - Mockito.when(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), "Directory 3", dirIdRoot.getBytes())).thenReturn("dir3"); - Mockito.when(fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), "dir3", dirIdRoot.getBytes())).thenReturn("Directory 3"); - Mockito.when(fileNameCryptor.hashDirectoryId(AdditionalMatchers.not(Mockito.eq("")))).thenReturn("33DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD"); - - Mockito.when(cloudContentRepository.folder(d, "33")).thenReturn(lvl2Dir); - Mockito.when(cloudContentRepository.folder(aaFolder, "dir3.c9r")).thenReturn(lvl2Dir); - Mockito.when(cloudContentRepository.folder(lvl2Dir, "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD")).thenReturn(ddFolder); - Mockito.when(cloudContentRepository.file(testDir3, "dir.c9r")).thenReturn(testDir3DirFile); - Mockito.when(dirIdCache.put(Mockito.eq(cryptoFolder3), Mockito.any())).thenReturn(new DirIdCache.DirIdInfo("dir3-id", ddFolder)); - - Mockito.when(cloudContentRepository.create(lvl2Dir)).thenReturn(lvl2Dir); - Mockito.when(cloudContentRepository.create(ddFolder)).thenReturn(ddFolder); - Mockito.when(cloudContentRepository.create(testDir3)).thenReturn(testDir3); - Mockito.when(cloudContentRepository.write(Mockito.eq(testDir3DirFile), Mockito.any(), Mockito.any(), Mockito.eq(false), Mockito.anyLong())).thenReturn(testDir3DirFile); - - Mockito.when(cloudContentRepository.file(aaFolder, "dir3.c9r")).thenReturn(null); - - CryptoFolder targetFile = inTest.folder(root, cryptoFolder3.getName()); // needed due to ugly side effect - CryptoFolder result = inTest.move(cryptoFolder15, cryptoFolder3); - - Mockito.verify(cloudContentRepository).create(testDir3); - Mockito.verify(cloudContentRepository).move(testDir15DirFile, cryptoFolder3.getDirFile()); - Mockito.verify(cloudContentRepository).delete(testDir15); - } - -} diff --git a/data/src/test/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7Test.kt b/data/src/test/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7Test.kt new file mode 100644 index 00000000..b210694b --- /dev/null +++ b/data/src/test/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7Test.kt @@ -0,0 +1,1088 @@ +package org.cryptomator.data.cloud.crypto + +import android.content.Context +import com.google.common.base.Strings +import com.google.common.io.BaseEncoding +import org.cryptomator.cryptolib.api.Cryptor +import org.cryptomator.cryptolib.api.FileContentCryptor +import org.cryptomator.cryptolib.api.FileHeader +import org.cryptomator.cryptolib.api.FileHeaderCryptor +import org.cryptomator.cryptolib.api.FileNameCryptor +import org.cryptomator.cryptolib.common.MessageDigestSupplier +import org.cryptomator.data.cloud.crypto.DirIdCache.DirIdInfo +import org.cryptomator.data.util.CopyStream.copyStreamToStream +import org.cryptomator.domain.Cloud +import org.cryptomator.domain.CloudFile +import org.cryptomator.domain.CloudFolder +import org.cryptomator.domain.CloudNode +import org.cryptomator.domain.exception.BackendException +import org.cryptomator.domain.repository.CloudContentRepository +import org.cryptomator.domain.usecases.ProgressAware +import org.cryptomator.domain.usecases.cloud.ByteArrayDataSource.Companion.from +import org.cryptomator.domain.usecases.cloud.DataSource +import org.hamcrest.CoreMatchers +import org.hamcrest.MatcherAssert +import org.hamcrest.Matchers +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.mockito.AdditionalMatchers +import org.mockito.Mockito +import org.mockito.invocation.InvocationOnMock +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import java.io.BufferedReader +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.InputStreamReader +import java.io.OutputStream +import java.nio.ByteBuffer +import java.nio.charset.StandardCharsets +import java.util.ArrayList +import kotlin.io.path.createTempDirectory +import kotlin.io.path.deleteExisting + + +/** + * ` + * path/to/vault/d/00 + * ├─ Directory 1 + * │ ├─ Directory 2 + * │ ├─ Directory 3x250 + * │ │ ├─ Directory 4x250 + * │ │ └─ File 5x250 + * │ └─ File 3 + * ├─ File 1 + * ├─ File 2 + * ├─ File 4 +` * + */ +class CryptoImplVaultFormat7Test { + + private val dirIdRoot = "" + private val dirId1 = "dir1-id" + private val dirId2 = "dir2-id" + + private var cloud: Cloud = mock() + private var cryptoCloud: CryptoCloud = mock() + private var context: Context = mock() + private var cryptor: Cryptor = mock() + private var cloudContentRepository: CloudContentRepository = mock() + private var dirIdCache: DirIdCache = mock() + private var fileNameCryptor: FileNameCryptor = mock() + private var fileContentCryptor: FileContentCryptor = mock() + private var fileHeaderCryptor: FileHeaderCryptor = mock() + private var tmpDir = createTempDirectory() + + private lateinit var rootFolder: TestFolder + private lateinit var d: TestFolder + private lateinit var lvl2Dir: TestFolder + private lateinit var aaFolder: TestFolder + private lateinit var root: RootCryptoFolder + private lateinit var cryptoFile1: CryptoFile + private lateinit var cryptoFile2: CryptoFile + private lateinit var cryptoFile4: CryptoFile + private lateinit var cryptoFolder1: CryptoFolder + + private lateinit var inTest: CryptoImplVaultFormat7 + + private fun any(type: Class): T = Mockito.any(type) + + @BeforeEach + @Throws(BackendException::class) + fun setup() { + whenever(context.cacheDir).thenReturn(tmpDir.toFile()) + + rootFolder = RootTestFolder(cloud) + d = TestFolder(rootFolder, "d", "/d") + lvl2Dir = TestFolder(d, "00", "/d/00") + aaFolder = TestFolder(lvl2Dir, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") + + whenever(cryptor.fileNameCryptor()).thenReturn(fileNameCryptor) + whenever(cryptor.fileContentCryptor()).thenReturn(fileContentCryptor) + whenever(cryptor.fileHeaderCryptor()).thenReturn(fileHeaderCryptor) + + root = RootCryptoFolder(cryptoCloud) + inTest = CryptoImplVaultFormat7(context, { cryptor }, cloudContentRepository, rootFolder, dirIdCache) + + whenever(fileNameCryptor.hashDirectoryId(dirIdRoot)).thenReturn("00AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") + whenever(fileNameCryptor.hashDirectoryId(dirId1)).thenReturn("11BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB") + whenever(fileNameCryptor.hashDirectoryId(dirId2)).thenReturn("22CCCCCCCCCCCCCCCCCCCCCCCCCCCCCC") + whenever(fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), "dir1", dirIdRoot.toByteArray())).thenReturn("Directory 1") + whenever(fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), "file1", dirIdRoot.toByteArray())).thenReturn("File 1") + whenever(fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), "file2", dirIdRoot.toByteArray())).thenReturn("File 2") + whenever(fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), "dir2", dirId1.toByteArray())).thenReturn("Directory 2") + whenever(fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), "file3", dirId1.toByteArray())).thenReturn("File 3") + whenever(fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), "file4", dirIdRoot.toByteArray())).thenReturn("File 4") + + val testFile1 = TestFile(aaFolder, "file1.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/file1.c9r", null, null) + val testFile2 = TestFile(aaFolder, "file2.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/file2.c9r", null, null) + val testFile4 = TestFile(aaFolder, "file4.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/file4.c9r", null, null) + val testDir1 = TestFolder(aaFolder, "dir1.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/dir1.c9r") + val testDir1DirFile = TestFile(testDir1, "dir.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/dir1.c9r/dir.c9r", null, null) + val rootItems: ArrayList = object : ArrayList() { + init { + add(testFile1) + add(testFile2) + add(testFile4) + add(testDir1) + } + } + + cryptoFile1 = CryptoFile(root, "File 1", "/File 1", 15L, testFile1) + cryptoFile2 = CryptoFile(root, "File 2", "/File 2", null, testFile2) + cryptoFile4 = CryptoFile(root, "File 4", "/File 4", null, testFile4) + cryptoFolder1 = CryptoFolder(root, "Directory 1", "/Directory 1", testDir1DirFile) + + whenever(cloudContentRepository.folder(rootFolder, "d")).thenReturn(d) + whenever(cloudContentRepository.folder(d, "00")).thenReturn(lvl2Dir) + whenever(cloudContentRepository.folder(lvl2Dir, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")).thenReturn(aaFolder) + whenever(cloudContentRepository.file(testDir1, "dir.c9r")).thenReturn(testDir1DirFile) + whenever(cloudContentRepository.exists(testDir1DirFile)).thenReturn(true) + Mockito.doAnswer { invocation: InvocationOnMock -> + val out = invocation.getArgument(2) + copyStreamToStream(ByteArrayInputStream(dirId1.toByteArray()), out) + null + }.whenever(cloudContentRepository).read(eq(cryptoFolder1.dirFile!!), any(), any(), any()) + whenever>(cloudContentRepository.list(aaFolder)).thenReturn(rootItems) + whenever(dirIdCache.put(eq(root), any())).thenReturn(DirIdInfo("", aaFolder)) + } + + @AfterEach + fun tearDown() { + tmpDir.deleteExisting() + } + + @Test + @DisplayName("list(\"/\")") + @Throws(BackendException::class) + fun testListRoot() { + val rootDirContent = inTest.list(root) + + Matchers.contains(rootDirContent, cryptoFile1) + Matchers.contains(rootDirContent, cryptoFile2) + Matchers.contains(rootDirContent, cryptoFile4) + Matchers.contains(rootDirContent, cryptoFolder1) + } + + @Test + @DisplayName("list(\"/Directory 1/Directory 3x250\")") + @Throws(BackendException::class) + fun testListDirectory3x250() { + val dir3Name = "Directory " + Strings.repeat("3", 250) + val dir3Cipher = "dir" + Strings.repeat("3", 250) + val longFilenameBytes = "$dir3Cipher.c9r".toByteArray(StandardCharsets.UTF_8) + val hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes) + val shortenedFileName = BaseEncoding.base64Url().encode(hash) + ".c9s" + val bbLvl2Dir = TestFolder(d, "11", "/d/11") + val bbFolder = TestFolder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB") + val ddLvl2Dir = TestFolder(d, "33", "/d/33") + val ddFolder = TestFolder(ddLvl2Dir, "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD", "/d/33/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD") + val testDir3 = TestFolder(bbFolder, shortenedFileName, "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/$shortenedFileName") + val testDir3DirFile = TestFile(testDir3, "dir.c9r", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/$shortenedFileName/dir.c9r", null, null) + val testDir3NameFile = TestFile(testDir3, "name.c9s", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/$shortenedFileName/name.c9s", null, null) + + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), dir3Name, dirId1.toByteArray())).thenReturn(dir3Cipher) + whenever(fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), dir3Cipher, dirId1.toByteArray())).thenReturn(dir3Name) + whenever(fileNameCryptor.hashDirectoryId(AdditionalMatchers.not(eq("")))).thenReturn("33DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD") + whenever(cloudContentRepository.folder(d, "33")).thenReturn(ddLvl2Dir) + whenever(cloudContentRepository.folder(ddLvl2Dir, "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD")).thenReturn(ddFolder) + whenever(cloudContentRepository.file(testDir3, "dir.c9r")).thenReturn(testDir3DirFile) + whenever(cloudContentRepository.file(testDir3, "name.c9s")).thenReturn(testDir3NameFile) + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), "Directory 1", dirIdRoot.toByteArray())).thenReturn("dir1") + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), "Directory 2", dirId1.toByteArray())).thenReturn("dir2") + + val cryptoFolder3 = CryptoFolder(cryptoFolder1, dir3Name, "/Directory 1/$dir3Name", testDir3DirFile) + + Mockito.doAnswer { invocation: InvocationOnMock -> + val out = invocation.getArgument(2) + copyStreamToStream(ByteArrayInputStream("dir3-id".toByteArray()), out) + null + }.whenever(cloudContentRepository).read(eq(cryptoFolder3.dirFile!!), any(), any(), any()) + + /* + * │ ├─ Directory 3x250 + * │ │ ├─ Directory 4x250 + * │ │ └─ File 5x250 + */ + val dir4Name = "Directory " + Strings.repeat("4", 250) + val dir4Cipher = "dir" + Strings.repeat("4", 250) + val longFilenameBytes4 = "$dir4Cipher.c9r".toByteArray(StandardCharsets.UTF_8) + val hash4 = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes4) + val shortenedFileName4 = BaseEncoding.base64Url().encode(hash4) + ".c9s" + val directory4x250 = TestFolder(ddFolder, shortenedFileName4, "/d/33/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD$shortenedFileName4") + val testDir4DirFile = TestFile(directory4x250, "dir.c9r", "/d/33/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD/$shortenedFileName4/dir.c9r", null, null) + val testDir4NameFile = TestFile(directory4x250, "name.c9s", "/d/33/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD/$shortenedFileName4/name.c9s", null, null) + + whenever(cloudContentRepository.file(directory4x250, "dir.c9r")).thenReturn(testDir4DirFile) + whenever(cloudContentRepository.file(directory4x250, "name.c9s")).thenReturn(testDir4NameFile) + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), dir4Name, "dir3-id".toByteArray())).thenReturn(dir4Cipher) + whenever(fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), dir4Cipher, "dir3-id".toByteArray())).thenReturn(dir4Name) + Mockito.doAnswer { invocation: InvocationOnMock -> + val out = invocation.getArgument(2) + copyStreamToStream(ByteArrayInputStream(dir4Cipher.toByteArray(charset("UTF-8"))), out) + null + }.whenever(cloudContentRepository).read(eq(testDir4NameFile), any(), any(), any()) + + val dir4Files: ArrayList = object : ArrayList() { + init { + add(testDir4DirFile) + add(testDir4NameFile) + } + } + val file5Name = "File " + Strings.repeat("5", 250) + val file5Cipher = "file" + Strings.repeat("5", 250) + val longFilenameBytes5 = "$file5Cipher.c9r".toByteArray(StandardCharsets.UTF_8) + val hash5 = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes5) + val shortenedFileName5 = BaseEncoding.base64Url().encode(hash5) + ".c9s" + val directory5x250 = TestFolder(ddFolder, shortenedFileName5, "/d/33/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD$shortenedFileName5") + val testFile5ContentFile = TestFile(directory5x250, "contents.c9r", "/d/33/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD/$shortenedFileName5/contents.c9r", null, null) + val testFile5NameFile = TestFile(directory5x250, "name.c9s", "/d/33/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD/$shortenedFileName5/name.c9s", null, null) + + whenever(cloudContentRepository.file(directory5x250, "contents.c9r")).thenReturn(testFile5ContentFile) + whenever(cloudContentRepository.file(directory5x250, "name.c9s")).thenReturn(testFile5NameFile) + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), file5Name, "dir3-id".toByteArray())).thenReturn(file5Cipher) + whenever(fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), file5Cipher, "dir3-id".toByteArray())).thenReturn(file5Name) + Mockito.doAnswer { invocation: InvocationOnMock -> + val out = invocation.getArgument(2) + copyStreamToStream(ByteArrayInputStream(file5Cipher.toByteArray(charset("UTF-8"))), out) + null + }.whenever(cloudContentRepository).read(eq(testFile5NameFile), any(), any(), any()) + + val dir5Files: ArrayList = object : ArrayList() { + init { + add(testFile5ContentFile) + add(testFile5NameFile) + } + } + val dir3Items: ArrayList = object : ArrayList() { + init { + add(directory4x250) + add(directory5x250) + } + } + + whenever(cloudContentRepository.exists(testDir3DirFile)).thenReturn(true) + whenever>(cloudContentRepository.list(ddFolder)).thenReturn(dir3Items) + whenever>(cloudContentRepository.list(directory4x250)).thenReturn(dir4Files) + whenever>(cloudContentRepository.list(directory5x250)).thenReturn(dir5Files) + whenever(dirIdCache.put(eq(cryptoFolder3), any())).thenReturn(DirIdInfo("dir3-id", ddFolder)) + whenever(dirIdCache[cryptoFolder3]).thenReturn(DirIdInfo("dir3-id", ddFolder)) + + val folder3Content = inTest.list(cryptoFolder3) + + Matchers.contains(folder3Content, CryptoFolder(cryptoFolder3, dir4Name, "/Directory 1/$dir3Name/$dir4Name", testDir4DirFile)) + Matchers.contains(folder3Content, CryptoFile(cryptoFolder3, file5Name, "/Directory 1/$dir3Name/$file5Name", null, testFile5ContentFile)) + } + + @Test + @DisplayName("read(\"/File 1\", NO_PROGRESS_AWARE)") + @Throws(BackendException::class) + fun testReadFromShortFile() { + val file1Content = "hhhhhTOPSECRET!TOPSECRET!TOPSECRET!TOPSECRET!".toByteArray() + val header = Mockito.mock(FileHeader::class.java) + + whenever(fileContentCryptor.cleartextChunkSize()).thenReturn(8) + whenever(fileContentCryptor.ciphertextChunkSize()).thenReturn(10) + whenever(fileHeaderCryptor.headerSize()).thenReturn(5) + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), "File 1", dirIdRoot.toByteArray())).thenReturn("file1") + whenever(fileHeaderCryptor.decryptHeader(StandardCharsets.UTF_8.encode("hhhhh"))).thenReturn(header) + whenever(fileContentCryptor.decryptChunk(eq(StandardCharsets.UTF_8.encode("TOPSECRET!")), any(), eq(header), any())) + .then { StandardCharsets.UTF_8.encode("geheim!!") } + Mockito.doAnswer { invocation: InvocationOnMock -> + val out = invocation.getArgument(2) + copyStreamToStream(ByteArrayInputStream(file1Content), out) + null + }.whenever(cloudContentRepository).read(eq(cryptoFile1.cloudFile), any(), any(), any()) + + val outputStream = ByteArrayOutputStream(1000) + inTest.read(cryptoFile1, outputStream, ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD) + + MatcherAssert.assertThat(outputStream.toString(), CoreMatchers.`is`("geheim!!geheim!!geheim!!geheim!!")) + } + + @Test + @DisplayName("read(\"/File 15x250\", NO_PROGRESS_AWARE)") + @Throws(BackendException::class) + fun testReadFromLongFile() { + val file3Name = "File " + Strings.repeat("15", 250) + val longFilenameBytes = file3Name.toByteArray(StandardCharsets.UTF_8) + val hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes) + val shortenedFileName = BaseEncoding.base64Url().encode(hash) + ".c9s" + val testFile3Folder = TestFolder(aaFolder, shortenedFileName, "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/$shortenedFileName") + val testFile3ContentFile = TestFile(testFile3Folder, "content.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/$shortenedFileName/content.c9r", null, null) + val file1Content = "hhhhhTOPSECRET!TOPSECRET!TOPSECRET!TOPSECRET!".toByteArray() + val header = Mockito.mock(FileHeader::class.java) + + whenever(fileContentCryptor.cleartextChunkSize()).thenReturn(8) + whenever(fileContentCryptor.ciphertextChunkSize()).thenReturn(10) + whenever(fileHeaderCryptor.headerSize()).thenReturn(5) + whenever(fileHeaderCryptor.decryptHeader(StandardCharsets.UTF_8.encode("hhhhh"))).thenReturn(header) + whenever(fileContentCryptor.decryptChunk(eq(StandardCharsets.UTF_8.encode("TOPSECRET!")), any(), eq(header), any())) + .then { StandardCharsets.UTF_8.encode("geheim!!") } + + val cryptoFile15 = CryptoFile(root, file3Name, "/$file3Name", null, testFile3ContentFile) + + Mockito.doAnswer { invocation: InvocationOnMock -> + val out = invocation.getArgument(2) + copyStreamToStream(ByteArrayInputStream(file1Content), out) + null + }.whenever(cloudContentRepository).read(eq(cryptoFile15.cloudFile), any(), any(), any()) + + val outputStream = ByteArrayOutputStream(1000) + inTest.read(cryptoFile15, outputStream, ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD) + + MatcherAssert.assertThat(outputStream.toString(), CoreMatchers.`is`("geheim!!geheim!!geheim!!geheim!!")) + } + + @Test + @DisplayName("write(\"/File 1\", text, NO_PROGRESS_AWARE, replace=false, 10bytes)") + @Throws(BackendException::class) + fun testWriteToShortFile() { + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), "File 1", dirIdRoot.toByteArray())).thenReturn("file1") + + val header = Mockito.mock(FileHeader::class.java) + + whenever(fileHeaderCryptor.create()).thenReturn(header) + whenever(fileHeaderCryptor.encryptHeader(header)).thenReturn(ByteBuffer.wrap("hhhhh".toByteArray())) + whenever(fileHeaderCryptor.headerSize()).thenReturn(5) + whenever(fileContentCryptor.cleartextChunkSize()).thenReturn(10) + whenever(fileContentCryptor.ciphertextChunkSize()).thenReturn(10) + whenever(fileContentCryptor.encryptChunk(any(ByteBuffer::class.java), any(), any(FileHeader::class.java))) + .thenAnswer { invocation: InvocationOnMock -> + val input = invocation.getArgument(0) + val inStr = StandardCharsets.UTF_8.decode(input).toString() + ByteBuffer.wrap(inStr.lowercase().toByteArray(StandardCharsets.UTF_8)) + } + whenever(cloudContentRepository.write(eq(cryptoFile1.cloudFile), any(DataSource::class.java), any(), eq(false), any())) + .thenAnswer { invocationOnMock: InvocationOnMock -> + val inputStream = invocationOnMock.getArgument(1) + val encrypted = BufferedReader(InputStreamReader(inputStream.open(context)!!, StandardCharsets.UTF_8)).readLine() + MatcherAssert.assertThat(encrypted, CoreMatchers.`is`("hhhhhtopsecret!")) + invocationOnMock.getArgument(0) + } + + // just for the exits check + val tmpFileExistFolder = TestFolder(aaFolder, cryptoFile1.cloudFile.name, cryptoFile1.cloudFile.path) + whenever(cloudContentRepository.folder(aaFolder, cryptoFile1.cloudFile.name)) + .thenReturn(tmpFileExistFolder) + whenever(cloudContentRepository.file(tmpFileExistFolder, "dir.c9r")) + .thenReturn(TestFile(tmpFileExistFolder, "dir.c9r", tmpFileExistFolder.path + "/dir.c9r", null, null)) + + val cryptoFile = inTest.write(cryptoFile1, from("TOPSECRET!".toByteArray(StandardCharsets.UTF_8)), ProgressAware.NO_OP_PROGRESS_AWARE_UPLOAD, false, 10L) + + MatcherAssert.assertThat(cryptoFile, CoreMatchers.`is`(cryptoFile1)) + } + + @Test + @DisplayName("write(\"/File 15x250\", text, NO_PROGRESS_AWARE, replace=false, 10bytes)") + @Throws(BackendException::class) + fun testWriteToLongFile() { + val file15Name = "File " + Strings.repeat("15", 250) + val file15Cipher = "file" + Strings.repeat("15", 250) + val longFilenameBytes = "$file15Cipher.c9r".toByteArray(StandardCharsets.UTF_8) + val hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes) + val shortenedFileName = BaseEncoding.base64Url().encode(hash) + ".c9s" + val testFile3Folder = TestFolder(aaFolder, shortenedFileName, "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/$shortenedFileName") + val testFile3WhatTheHellCLoudFile = TestFile(aaFolder, shortenedFileName, "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/$shortenedFileName", null, null) + val testFile15ContentFile = TestFile(testFile3Folder, "contents.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/$shortenedFileName/contents.c9r", 10L, null) + + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), file15Name, dirIdRoot.toByteArray())).thenReturn(file15Cipher) + + val cryptoFile15 = CryptoFile(root, file15Name, "/$file15Name", 15L, testFile3WhatTheHellCLoudFile) + + whenever(cloudContentRepository.folder(aaFolder, shortenedFileName)).thenReturn(testFile3Folder) + whenever(cloudContentRepository.file(testFile3Folder, "contents.c9r", 10L)).thenReturn(testFile15ContentFile) + whenever(cloudContentRepository.exists(testFile3Folder)).thenReturn(true) + + val header = Mockito.mock(FileHeader::class.java) + + whenever(fileHeaderCryptor.create()).thenReturn(header) + whenever(fileHeaderCryptor.encryptHeader(header)).thenReturn(ByteBuffer.wrap("hhhhh".toByteArray())) + whenever(fileHeaderCryptor.headerSize()).thenReturn(5) + whenever(fileContentCryptor.cleartextChunkSize()).thenReturn(10) + whenever(fileContentCryptor.ciphertextChunkSize()).thenReturn(10) + whenever(fileContentCryptor.encryptChunk(any(ByteBuffer::class.java), any(), any(FileHeader::class.java))) + .thenAnswer { invocation: InvocationOnMock -> + val input = invocation.getArgument(0) + val inStr = StandardCharsets.UTF_8.decode(input).toString() + ByteBuffer.wrap(inStr.lowercase().toByteArray(StandardCharsets.UTF_8)) + } + whenever(cloudContentRepository.write(eq(testFile15ContentFile), any(DataSource::class.java), any(), eq(false), any())) + .thenAnswer { invocationOnMock: InvocationOnMock -> + val inputStream = invocationOnMock.getArgument(1) + val encrypted = BufferedReader(InputStreamReader(inputStream.open(context)!!, StandardCharsets.UTF_8)).readLine() + MatcherAssert.assertThat(encrypted, CoreMatchers.`is`("hhhhhtopsecret!")) + invocationOnMock.getArgument(0) + } + + val cryptoFile = inTest.write(cryptoFile15, from("TOPSECRET!".toByteArray(StandardCharsets.UTF_8)), ProgressAware.NO_OP_PROGRESS_AWARE_UPLOAD, false, 10L) + + MatcherAssert.assertThat(cryptoFile, CoreMatchers.`is`(cryptoFile15)) + + Mockito.verify(cloudContentRepository).write( + eq(testFile15ContentFile), any( + DataSource::class.java + ), any(), eq(false), any() + ) + } + + /*@Test FIXME + @DisplayName("write(\"/File 15x250\", text, NO_PROGRESS_AWARE, replace=false, 10bytes)") + @Throws(BackendException::class) + fun testWriteToLongFileUsingAutoRename() { + val file15Name = "File " + Strings.repeat("15", 250) + val file15Cipher = "file" + Strings.repeat("15", 250) + val longFilenameBytes = "$file15Cipher.c9r".toByteArray(StandardCharsets.UTF_8) + val hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes) + val shortenedFileName = BaseEncoding.base64Url().encode(hash) + ".c9s" + val testFile15Folder = TestFolder(aaFolder, shortenedFileName, "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/$shortenedFileName") + val testFile15WhatTheHellCLoudFile = TestFile(aaFolder, shortenedFileName, "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/$shortenedFileName", null, null) + val testFile15ContentFile = TestFile(testFile15Folder, "contents.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/$shortenedFileName/contents.c9r", 10L, null) + + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), file15Name, dirIdRoot.toByteArray())).thenReturn(file15Cipher) + + val file15CipherRename = "$file15Cipher(1)" + val hashRename = MessageDigestSupplier.SHA1.get().digest("$file15CipherRename.c9r".toByteArray(StandardCharsets.UTF_8)) + val shortenedFileNameRename = BaseEncoding.base64Url().encode(hashRename) + ".c9s" + val testFile15FolderRename = TestFolder(aaFolder, shortenedFileNameRename, "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/$shortenedFileNameRename") + val testFile15WhatTheHellCloudFileRename = TestFile(aaFolder, shortenedFileNameRename, "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/$shortenedFileNameRename", 20L, null) + val testFile15ContentFileRename = TestFile(testFile15FolderRename, "contents.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/$shortenedFileNameRename/contents.c9r", 10L, null) + val testFile15NameFileRename = TestFile(testFile15FolderRename, "name.c9s", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/$shortenedFileNameRename/name.c9s", 511L, null) + + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), "$file15Name (1)", dirIdRoot.toByteArray())).thenReturn("$file15Cipher(1)") + + val cryptoFile15 = CryptoFile(root, file15Name, "/$file15Name", 15L, testFile15WhatTheHellCLoudFile) + + whenever(cloudContentRepository.file(testFile15Folder, "contents.c9r", 10L)).thenReturn(testFile15ContentFile) + whenever(cloudContentRepository.exists(testFile15ContentFile)).thenReturn(true) + whenever(cloudContentRepository.folder(aaFolder, shortenedFileName)).thenReturn(testFile15Folder) + whenever(cloudContentRepository.exists(testFile15Folder)).thenReturn(true) + whenever(cloudContentRepository.file(testFile15Folder, "contents.c9r", 10L)).thenReturn(testFile15ContentFile) + whenever(cloudContentRepository.file(testFile15FolderRename, "contents.c9r", 15L)).thenReturn(testFile15ContentFileRename) + whenever(cloudContentRepository.folder(aaFolder, shortenedFileNameRename)).thenReturn(testFile15FolderRename) + whenever(cloudContentRepository.exists(testFile15FolderRename)).thenReturn(false) + whenever(cloudContentRepository.create(testFile15FolderRename)).thenReturn(testFile15FolderRename) + whenever(cloudContentRepository.file(testFile15FolderRename, "name.c9s", 511L)).thenReturn(testFile15NameFileRename) + whenever(cloudContentRepository.write(eq(testFile15NameFileRename), any(DataSource::class.java), any(), eq(true), any())) + .thenAnswer { invocationOnMock: InvocationOnMock -> + val inputStream = invocationOnMock.getArgument(1) + val encrypted = BufferedReader(InputStreamReader(inputStream.open(context)!!, StandardCharsets.UTF_8)).readLine() + MatcherAssert.assertThat(encrypted, CoreMatchers.`is`("$file15CipherRename.c9r")) + invocationOnMock.getArgument(0) + } + whenever(cloudContentRepository.file(aaFolder, shortenedFileNameRename, 20L)).thenReturn(testFile15WhatTheHellCloudFileRename) + + val header = Mockito.mock(FileHeader::class.java) + + whenever(fileHeaderCryptor.create()).thenReturn(header) + whenever(fileHeaderCryptor.encryptHeader(header)).thenReturn(ByteBuffer.wrap("hhhhh".toByteArray())) + whenever(fileHeaderCryptor.headerSize()).thenReturn(5) + whenever(fileContentCryptor.cleartextChunkSize()).thenReturn(10) + whenever(fileContentCryptor.ciphertextChunkSize()).thenReturn(10) + whenever(fileContentCryptor.encryptChunk(any(ByteBuffer::class.java), any(), any(FileHeader::class.java))) + .thenAnswer { invocation: InvocationOnMock -> + val input = invocation.getArgument(0) + val inStr = StandardCharsets.UTF_8.decode(input).toString() + ByteBuffer.wrap(inStr.lowercase().toByteArray(StandardCharsets.UTF_8)) + } + whenever(cloudContentRepository.write(eq(testFile15ContentFileRename), any(DataSource::class.java), any(), eq(false), any())) + .thenAnswer { invocationOnMock: InvocationOnMock -> + val inputStream = invocationOnMock.getArgument(1) + val encrypted = BufferedReader(InputStreamReader(inputStream.open(context)!!, StandardCharsets.UTF_8)).readLine() + MatcherAssert.assertThat(encrypted, CoreMatchers.`is`("hhhhhtopsecret!")) + invocationOnMock.getArgument(0) + } + + val cryptoFile = inTest.write(cryptoFile15, from("TOPSECRET!".toByteArray(StandardCharsets.UTF_8)), ProgressAware.NO_OP_PROGRESS_AWARE_UPLOAD, false, 10L) + + MatcherAssert.assertThat(cryptoFile, CoreMatchers.`is`(cryptoFile15)) + Mockito.verify(cloudContentRepository).create(testFile15FolderRename) + Mockito.verify(cloudContentRepository).write( + eq(testFile15NameFileRename), any( + DataSource::class.java + ), any(), eq(true), any() + ) + Mockito.verify(cloudContentRepository).write( + eq(testFile15ContentFileRename), any( + DataSource::class.java + ), any(), eq(false), any() + ) + }*/ + + @Test + @DisplayName("create(\"/Directory 3/\")") + @Throws(BackendException::class) + fun testCreateShortFolder() { + /* + * + * path/to/vault/d + * ├─ Directory 1 + * │ ├─ ... + * ├─ Directory 3 + * ├─ ... + * + */ + lvl2Dir = TestFolder(d, "33", "/d/33") + val ddFolder = TestFolder(lvl2Dir, "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD", "/d/33/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD") + val testDir3 = TestFolder(aaFolder, "dir3.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/dir3.c9r") + val testDir3DirFile = TestFile(testDir3, "dir.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/dir3.c9r/dir.c9r", null, null) + val cryptoFolder3 = CryptoFolder(root, "Directory 3", "/Directory 3", testDir3DirFile) + + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), "Directory 3", dirIdRoot.toByteArray())).thenReturn("dir3") + whenever(fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), "dir3", dirIdRoot.toByteArray())).thenReturn("Directory 3") + whenever(fileNameCryptor.hashDirectoryId(AdditionalMatchers.not(eq("")))).thenReturn("33DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD") + whenever(cloudContentRepository.folder(d, "33")).thenReturn(lvl2Dir) + whenever(cloudContentRepository.folder(lvl2Dir, "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD")).thenReturn(ddFolder) + whenever(cloudContentRepository.file(testDir3, "dir.c9r")).thenReturn(testDir3DirFile) + whenever(dirIdCache.put(eq(cryptoFolder3), any())).thenReturn(DirIdInfo("dir3-id", ddFolder)) + whenever(cloudContentRepository.create(lvl2Dir)).thenReturn(lvl2Dir) + whenever(cloudContentRepository.create(ddFolder)).thenReturn(ddFolder) + whenever(cloudContentRepository.create(testDir3)).thenReturn(testDir3) + whenever(cloudContentRepository.write(eq(testDir3DirFile), any(), any(), eq(false), any())).thenReturn(testDir3DirFile) + + // just for the exits check + whenever(cloudContentRepository.file(aaFolder, "dir3.c9r", null)) + .thenReturn(TestFile(aaFolder, "dir3.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/dir3.c9r", null, null)) + + val cloudFolder: CloudFolder = inTest.create(cryptoFolder3) + + MatcherAssert.assertThat(cloudFolder, CoreMatchers.`is`(cryptoFolder3)) + + Mockito.verify(cloudContentRepository).create(ddFolder) + Mockito.verify(cloudContentRepository).create(testDir3) + Mockito.verify(cloudContentRepository).write(eq(testDir3DirFile), any(), any(), eq(false), any()) + } + + @Test + @DisplayName("create(\"/Directory 3x250/\")") + @Throws(BackendException::class) + fun testCreateLongFolder() { + /* + * + * path/to/vault/d + * ├─ Directory 1 + * │ ├─ ... + * ├─ Directory 3x250 + * ├─ ... + * + */ + val dir3Name = "Directory " + Strings.repeat("3", 250) + val dir3Cipher = "dir" + Strings.repeat("3", 250) + val longFilenameBytes = "$dir3Cipher.c9r".toByteArray(StandardCharsets.UTF_8) + val hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes) + val shortenedFileName = BaseEncoding.base64Url().encode(hash) + ".c9s" + val ddLvl2Dir = TestFolder(d, "33", "/d/33") + val ddFolder = TestFolder(ddLvl2Dir, "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD", "/d/33/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD") + val testDir3 = TestFolder(aaFolder, shortenedFileName, "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/$shortenedFileName") + val testDir3DirFile = TestFile(testDir3, "dir.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/$shortenedFileName/dir.c9r", null, null) + val testDir3NameFile = TestFile(testDir3, "name.c9s", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/$shortenedFileName/name.c9s", 257L, null) + + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), dir3Name, dirIdRoot.toByteArray())).thenReturn(dir3Cipher) + whenever(fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), dir3Cipher, dirIdRoot.toByteArray())).thenReturn(dir3Name) + whenever(fileNameCryptor.hashDirectoryId(AdditionalMatchers.not(eq("")))).thenReturn("33DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD") + whenever(cloudContentRepository.folder(d, "33")).thenReturn(ddLvl2Dir) + whenever(cloudContentRepository.folder(ddLvl2Dir, "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD")).thenReturn(ddFolder) + whenever(cloudContentRepository.folder(aaFolder, shortenedFileName)).thenReturn(testDir3) + whenever(cloudContentRepository.exists(testDir3)).thenReturn(false) + whenever(cloudContentRepository.file(testDir3, "dir.c9r")).thenReturn(testDir3DirFile) + whenever(cloudContentRepository.file(testDir3, "name.c9s", 257L)).thenReturn(testDir3NameFile) + + val cryptoFolder3 = CryptoFolder(root, dir3Name, "/$dir3Name", testDir3DirFile) + + whenever(fileNameCryptor.hashDirectoryId(AdditionalMatchers.not(eq("")))).thenReturn("33DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD") + whenever(cloudContentRepository.folder(d, "33")).thenReturn(ddLvl2Dir) + whenever(cloudContentRepository.folder(lvl2Dir, "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD")).thenReturn(ddFolder) + whenever(cloudContentRepository.file(testDir3, "dir.c9r")).thenReturn(testDir3DirFile) + whenever(dirIdCache.put(eq(cryptoFolder3), any())).thenReturn(DirIdInfo("dir3-id", ddFolder)) + whenever(dirIdCache[cryptoFolder3]).thenReturn(DirIdInfo("dir3-id", ddFolder)) + whenever(cloudContentRepository.create(ddLvl2Dir)).thenReturn(ddLvl2Dir) + whenever(cloudContentRepository.create(ddFolder)).thenReturn(ddFolder) + whenever(cloudContentRepository.create(testDir3)).thenReturn(testDir3) + whenever(cloudContentRepository.write(eq(testDir3DirFile), any(), any(), eq(false), any())).thenAnswer { invocationOnMock: InvocationOnMock -> + val inputStream = invocationOnMock.getArgument(1) + val dirContent = BufferedReader(InputStreamReader(inputStream.open(context)!!, StandardCharsets.UTF_8)).readLine() + MatcherAssert.assertThat(dirContent, CoreMatchers.`is`("dir3-id")) + testDir3DirFile + } + whenever(cloudContentRepository.write(eq(testDir3NameFile), any(), any(), eq(true), any())).thenAnswer { invocationOnMock: InvocationOnMock -> + val inputStream = invocationOnMock.getArgument(1) + val nameContent = BufferedReader(InputStreamReader(inputStream.open(context)!!, StandardCharsets.UTF_8)).readLine() + MatcherAssert.assertThat(nameContent, CoreMatchers.`is`("$dir3Cipher.c9r")) + testDir3NameFile + } + // just for the exits check + whenever(cloudContentRepository.file(aaFolder, "dir3.c9r", null)) + .thenReturn(TestFile(aaFolder, "dir3.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/dir3.c9r", null, null)) + + var cloudFolder: CloudFolder = inTest.folder(root, dir3Name) + + cloudFolder = inTest.create(cryptoFolder3) + + MatcherAssert.assertThat(cloudFolder, CoreMatchers.`is`(cryptoFolder3)) + Mockito.verify(cloudContentRepository).create(ddFolder) + Mockito.verify(cloudContentRepository).create(testDir3) + Mockito.verify(cloudContentRepository).write(eq(testDir3DirFile), any(), any(), eq(false), any()) + Mockito.verify(cloudContentRepository).write(eq(testDir3NameFile), any(), any(), eq(true), any()) + } + + @Test + @DisplayName("delete(\"/File 4\")") + @Throws(BackendException::class) + fun testDeleteShortFile() { + inTest.delete(cryptoFile4) + Mockito.verify(cloudContentRepository).delete(cryptoFile4.cloudFile) + } + + @Test + @DisplayName("delete(\"/File 15x250\")") + @Throws(BackendException::class) + fun testDeleteLongFile() { + val file15Name = "File " + Strings.repeat("15", 250) + val file15Cipher = "file" + Strings.repeat("15", 250) + val longFilenameBytes = "$file15Cipher.c9r".toByteArray(StandardCharsets.UTF_8) + val hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes) + val shortenedFileName = BaseEncoding.base64Url().encode(hash) + ".c9s" + val testFile3Folder = TestFolder(aaFolder, shortenedFileName, "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/$shortenedFileName") + val testFile3ContentFile = TestFile(testFile3Folder, "content.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/$shortenedFileName/content.c9r", null, null) + + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), file15Name, dirIdRoot.toByteArray())).thenReturn(file15Cipher) + + val cryptoFile15 = CryptoFile(root, file15Name, "/$file15Name", 15L, testFile3ContentFile) + + whenever(cloudContentRepository.folder(aaFolder, shortenedFileName)).thenReturn(testFile3Folder) + + inTest.delete(cryptoFile15) + + Mockito.verify(cloudContentRepository).delete(testFile3Folder) + } + + @Test + @DisplayName("delete(\"/Directory 1/Directory 2/\")") + @Throws(BackendException::class) + fun testDeleteSingleShortFolder() { + val bbLvl2Dir = TestFolder(d, "11", "/d/11") + val bbFolder = TestFolder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB") + val ccLvl2Dir = TestFolder(d, "22", "/d/22") + val ccFolder = TestFolder(ccLvl2Dir, "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCC", "/d/22/CCCCCCCCCCCCCCCCCCCCCCCCCCCCCC") + + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), "Directory 1", dirIdRoot.toByteArray())).thenReturn("dir1") + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), "Directory 2", dirId1.toByteArray())).thenReturn("dir2") + + val testDir2 = TestFolder(bbFolder, "dir2.c9r", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/dir2.c9r") + val testDir2DirFile = TestFile(testDir2, "dir.c9r", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/dir2.c9r/dir.c9r", null, null) + val cryptoFolder2 = CryptoFolder(cryptoFolder1, "Directory 2", "/Directory 1/Directory 2", testDir2DirFile) + + Mockito.doAnswer { invocation: InvocationOnMock -> + val out = invocation.getArgument(2) + copyStreamToStream(ByteArrayInputStream(dirId2.toByteArray()), out) + null + }.whenever(cloudContentRepository).read(eq(cryptoFolder2.dirFile!!), any(), any(), any()) + + val dir1Items: ArrayList = object : ArrayList() { + init { + add(testDir2) + } + } + + whenever(cloudContentRepository.folder(rootFolder, "d")).thenReturn(d) + whenever(cloudContentRepository.folder(d, "22")).thenReturn(ccLvl2Dir) + whenever(cloudContentRepository.folder(ccLvl2Dir, "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCC")).thenReturn(ccFolder) + whenever(cloudContentRepository.file(testDir2, "dir.c9r")).thenReturn(testDir2DirFile) + whenever>(cloudContentRepository.list(bbFolder)).thenReturn(dir1Items) + whenever(dirIdCache.put(eq(cryptoFolder2), any())).thenReturn(DirIdInfo(dirId2, ccFolder)) + whenever(dirIdCache[cryptoFolder2]).thenReturn(DirIdInfo(dirId2, ccFolder)) + whenever(cloudContentRepository.exists(testDir2DirFile)).thenReturn(true) + whenever>(cloudContentRepository.list(ccFolder)).thenReturn(ArrayList()) + + inTest.delete(cryptoFolder2) + + Mockito.verify(cloudContentRepository).delete(ccFolder) + Mockito.verify(cloudContentRepository).delete(testDir2) + Mockito.verify(dirIdCache).evict(cryptoFolder2) + } + + @Test + @DisplayName("delete(\"/Directory 3x250\")") + @Throws(BackendException::class) + fun testDeleteSingleLongFolder() { + val dir3Name = "Directory " + Strings.repeat("3", 250) + val dir3Cipher = "dir" + Strings.repeat("3", 250) + val longFilenameBytes = "$dir3Cipher.c9r".toByteArray(StandardCharsets.UTF_8) + val hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes) + val shortenedFileName = BaseEncoding.base64Url().encode(hash) + ".c9s" + val ddLvl2Dir = TestFolder(d, "33", "/d/33") + val ddFolder = TestFolder(ddLvl2Dir, "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD", "/d/33/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD") + val testDir3 = TestFolder(aaFolder, shortenedFileName, "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/$shortenedFileName") + val testDir3DirFile = TestFile(testDir3, "dir.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/$shortenedFileName/dir.c9r", null, null) + val testDir3NameFile = TestFile(testDir3, "name.c9s", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/$shortenedFileName/name.c9s", 257L, null) + val cryptoFolder3 = CryptoFolder(root, dir3Name, "/$dir3Name", testDir3DirFile) + + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), dir3Name, dirIdRoot.toByteArray())).thenReturn(dir3Cipher) + whenever(fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), dir3Cipher, dirIdRoot.toByteArray())).thenReturn(dir3Name) + whenever(fileNameCryptor.hashDirectoryId(AdditionalMatchers.not(eq("")))).thenReturn("33DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD") + whenever(cloudContentRepository.folder(d, "33")).thenReturn(ddLvl2Dir) + whenever(cloudContentRepository.folder(ddLvl2Dir, "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD")).thenReturn(ddFolder) + whenever(cloudContentRepository.folder(aaFolder, shortenedFileName)).thenReturn(testDir3) + whenever(cloudContentRepository.exists(testDir3)).thenReturn(false) + whenever(dirIdCache.put(eq(cryptoFolder3), any())).thenReturn(DirIdInfo("dir3-id", ddFolder)) + whenever(cloudContentRepository.file(testDir3, "dir.c9r")).thenReturn(testDir3DirFile) + whenever(cloudContentRepository.file(testDir3, "name.c9s", 257L)).thenReturn(testDir3NameFile) + whenever>(cloudContentRepository.list(ddFolder)).thenReturn(ArrayList()) + whenever(fileNameCryptor.hashDirectoryId(AdditionalMatchers.not(eq("")))).thenReturn("33DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD") + + inTest.delete(cryptoFolder3) + + Mockito.verify(cloudContentRepository).delete(ddFolder) + Mockito.verify(cloudContentRepository).delete(testDir3) + Mockito.verify(dirIdCache).evict(cryptoFolder3) + } + + @Test + @DisplayName("move(\"/File 4\", \"/Directory 1/File 4\")") + @Throws(BackendException::class) + fun testMoveShortFileToNewShortFile() { + val bbLvl2Dir = TestFolder(d, "11", "/d/11") + val bbFolder = TestFolder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB") + val testFile4 = TestFile(aaFolder, "file4.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/file4.c9r", null, null) + val testMovedFile4 = TestFile(bbFolder, "file4.c9r", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/file4.c9r", null, null) + val cryptoFile4 = CryptoFile(root, "File 4", "/File 4", null, testFile4) + val cryptoMovedFile4 = CryptoFile(cryptoFolder1, "File 4", "/Directory 1/File 4", null, testMovedFile4) + + whenever(cloudContentRepository.file(aaFolder, "file4.c9r")).thenReturn(testFile4) + whenever(cloudContentRepository.file(bbFolder, "file4.c9r")).thenReturn(testMovedFile4) + whenever(cloudContentRepository.move(testFile4, testMovedFile4)).thenReturn(testMovedFile4) + whenever(cloudContentRepository.folder(rootFolder, "d")).thenReturn(d) + whenever(cloudContentRepository.folder(d, "11")).thenReturn(bbLvl2Dir) + whenever(cloudContentRepository.folder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB")).thenReturn(bbFolder) + whenever(cloudContentRepository.folder(bbFolder, "file4.c9r")).thenReturn(null) + whenever(dirIdCache.put(eq(cryptoFolder1), any())).thenReturn(DirIdInfo(dirId1, bbFolder)) + whenever(dirIdCache[cryptoFolder1]).thenReturn(DirIdInfo(dirId1, bbFolder)) + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), "File 4", dirId1.toByteArray())).thenReturn("file4") + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), "File 4", dirIdRoot.toByteArray())).thenReturn("file4") + + // just for the exits check + val tmpFileExistFolder = TestFolder(bbFolder, testMovedFile4.name, testMovedFile4.path) + whenever(cloudContentRepository.folder(bbFolder, testMovedFile4.name)) + .thenReturn(tmpFileExistFolder) + whenever(cloudContentRepository.file(tmpFileExistFolder, "dir.c9r")) + .thenReturn(TestFile(tmpFileExistFolder, "dir.c9r", tmpFileExistFolder.path + "/dir.c9r", null, null)) + + val result = inTest.move(cryptoFile4, cryptoMovedFile4) + + Assertions.assertEquals("File 4", result.name) + Mockito.verify(cloudContentRepository).move(testFile4, testMovedFile4) + } + + @Test + @DisplayName("move(\"/File 4\", \"/Directory 1/File 4x250\")") + @Throws(BackendException::class) + fun testMoveShortFileToNewLongFile() { + val file4Name = "File " + Strings.repeat("4", 250) + val file4Cipher = "file" + Strings.repeat("4", 250) + val longFilenameBytes = "$file4Cipher.c9r".toByteArray(StandardCharsets.UTF_8) + val hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes) + val shortenedFileName = BaseEncoding.base64Url().encode(hash) + ".c9s" + val bbLvl2Dir = TestFolder(d, "11", "/d/11") + val bbFolder = TestFolder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB") + val testFile4 = TestFile(aaFolder, "file4.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/file4.c9r", null, null) + val cryptoFile4 = CryptoFile(root, "File 4", "/File 4", null, testFile4) + val testDir4 = TestFolder(bbFolder, shortenedFileName, "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/$shortenedFileName") + val testFile4ContentFile = TestFile(testDir4, "contents.c9r", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/$shortenedFileName/contents.c9r", null, null) + val testFile4NameFile = TestFile(testDir4, "name.c9s", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/$shortenedFileName/name.c9s", 258L, null) + val testFile4WhatTheHellCLoudFile = TestFile(bbFolder, shortenedFileName, "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/$shortenedFileName", null, null) // ugly hack + val cryptoMovedFile4 = CryptoFile(cryptoFolder1, file4Name, "/Directory 1/$file4Name", null, testFile4WhatTheHellCLoudFile) + + whenever(cloudContentRepository.file(aaFolder, "file4.c9r")).thenReturn(testFile4) + whenever(cloudContentRepository.file(testDir4, "contents.c9r")).thenReturn(testFile4ContentFile) + whenever(cloudContentRepository.file(testDir4, "name.c9s")).thenReturn(testFile4NameFile) + whenever(cloudContentRepository.file(testDir4, "name.c9s", 258L)).thenReturn(testFile4NameFile) + whenever(cloudContentRepository.file(bbFolder, shortenedFileName, null)).thenReturn(testFile4WhatTheHellCLoudFile) + whenever(cloudContentRepository.move(testFile4, testFile4ContentFile)).thenReturn(testFile4ContentFile) + whenever(cloudContentRepository.folder(rootFolder, "d")).thenReturn(d) + whenever(cloudContentRepository.folder(d, "11")).thenReturn(bbLvl2Dir) + whenever(cloudContentRepository.folder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB")).thenReturn(bbFolder) + whenever(cloudContentRepository.folder(bbFolder, "file4.c9r")).thenReturn(null) + whenever(cloudContentRepository.folder(bbFolder, shortenedFileName)).thenReturn(testDir4) + whenever(cloudContentRepository.create(testDir4)).thenReturn(testDir4) + whenever(cloudContentRepository.write(eq(testFile4NameFile), any(), any(), eq(true), any())).thenAnswer { invocationOnMock: InvocationOnMock -> + val inputStream = invocationOnMock.getArgument(1) + val dirContent = BufferedReader(InputStreamReader(inputStream.open(context)!!, StandardCharsets.UTF_8)).readLine() + MatcherAssert.assertThat(dirContent, CoreMatchers.`is`("$file4Cipher.c9r")) + testFile4NameFile + } + whenever(dirIdCache.put(eq(cryptoFolder1), any())).thenReturn(DirIdInfo(dirId1, bbFolder)) + whenever(dirIdCache[cryptoFolder1]).thenReturn(DirIdInfo(dirId1, bbFolder)) + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), "File 4", dirIdRoot.toByteArray())).thenReturn("file4") + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), file4Name, dirId1.toByteArray())).thenReturn(file4Cipher) + + val targetFile = inTest.file(cryptoFolder1, file4Name) // needed due to ugly side effect + val result = inTest.move(cryptoFile4, cryptoMovedFile4) + + Assertions.assertEquals(file4Name, result.name) + + Mockito.verify(cloudContentRepository).create(testDir4) + Mockito.verify(cloudContentRepository).move(testFile4, testFile4ContentFile) + Mockito.verify(cloudContentRepository).write(eq(testFile4NameFile), any(), any(), eq(true), any()) + } + + @Test + @DisplayName("move(\"/File 4x250\", \"/Directory 1/File 4x250\")") + @Throws(BackendException::class) + fun testMoveLongFileToNewLongFile() { + val file4Name = "File " + Strings.repeat("4", 250) + val file4Cipher = "file" + Strings.repeat("4", 250) + val longFilenameBytes = "$file4Cipher.c9r".toByteArray(StandardCharsets.UTF_8) + val hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes) + val shortenedFileName = BaseEncoding.base64Url().encode(hash) + ".c9s" + val bbLvl2Dir = TestFolder(d, "11", "/d/11") + val bbFolder = TestFolder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB") + val testDir4Old = TestFolder(aaFolder, shortenedFileName, "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/$shortenedFileName") + val testFile4ContentFileOld = TestFile(testDir4Old, "contents.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/$shortenedFileName/contents.c9r", null, null) + val cryptoFile4Old = CryptoFile(root, file4Name, "/$file4Name", null, testFile4ContentFileOld) + val testDir4 = TestFolder(bbFolder, shortenedFileName, "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/$shortenedFileName") + val testFile4ContentFile = TestFile(testDir4, "contents.c9r", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/$shortenedFileName/contents.c9r", null, null) + val testFile4NameFile = TestFile(testDir4, "name.c9s", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/$shortenedFileName/name.c9s", 258L, null) + val testFile4WhatTheHellCLoudFile = TestFile(bbFolder, shortenedFileName, "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/$shortenedFileName", null, null) // ugly hack + val cryptoMovedFile4 = CryptoFile(cryptoFolder1, file4Name, "/Directory 1/$file4Name", null, testFile4WhatTheHellCLoudFile) + + whenever(cloudContentRepository.file(testDir4, "contents.c9r")).thenReturn(testFile4ContentFile) + whenever(cloudContentRepository.file(testDir4, "name.c9s")).thenReturn(testFile4NameFile) + whenever(cloudContentRepository.file(testDir4, "name.c9s", 258L)).thenReturn(testFile4NameFile) + whenever(cloudContentRepository.file(bbFolder, shortenedFileName, null)).thenReturn(testFile4WhatTheHellCLoudFile) + whenever(cloudContentRepository.file(testDir4Old, "contents.c9r")).thenReturn(testFile4ContentFileOld) + whenever(cloudContentRepository.folder(aaFolder, shortenedFileName)).thenReturn(testDir4Old) + whenever(cloudContentRepository.move(testFile4ContentFileOld, testFile4ContentFile)).thenReturn(testFile4ContentFile) + whenever(cloudContentRepository.folder(rootFolder, "d")).thenReturn(d) + whenever(cloudContentRepository.folder(d, "11")).thenReturn(bbLvl2Dir) + whenever(cloudContentRepository.folder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB")).thenReturn(bbFolder) + whenever(cloudContentRepository.folder(bbFolder, "file4.c9r")).thenReturn(null) + whenever(cloudContentRepository.folder(bbFolder, shortenedFileName)).thenReturn(testDir4) + whenever(cloudContentRepository.create(testDir4)).thenReturn(testDir4) + whenever(cloudContentRepository.write(eq(testFile4NameFile), any(), any(), eq(true), any())).thenAnswer { invocationOnMock: InvocationOnMock -> + val inputStream = invocationOnMock.getArgument(1) + val dirContent = BufferedReader(InputStreamReader(inputStream.open(context)!!, StandardCharsets.UTF_8)).readLine() + MatcherAssert.assertThat(dirContent, CoreMatchers.`is`("$file4Cipher.c9r")) + testFile4NameFile + } + whenever(dirIdCache.put(eq(cryptoFolder1), any())).thenReturn(DirIdInfo(dirId1, bbFolder)) + whenever(dirIdCache[cryptoFolder1]).thenReturn(DirIdInfo(dirId1, bbFolder)) + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), file4Name, dirIdRoot.toByteArray())).thenReturn(file4Cipher) + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), file4Name, dirId1.toByteArray())).thenReturn(file4Cipher) + + val targetFile: CloudFile = inTest.file(cryptoFolder1, file4Name) // needed due to ugly side effect + val result = inTest.move(cryptoFile4Old, cryptoMovedFile4) + + Assertions.assertEquals(file4Name, result.name) + Mockito.verify(cloudContentRepository).create(testDir4) + Mockito.verify(cloudContentRepository).write(eq(testFile4NameFile), any(), any(), eq(true), any()) + Mockito.verify(cloudContentRepository).move(testFile4ContentFileOld, testFile4ContentFile) + Mockito.verify(cloudContentRepository).delete(testDir4Old) + } + + @Test + @DisplayName("move(\"/Directory 1/File 4x250\", \"/File 4\")") + @Throws(BackendException::class) + fun testMoveLongFileToNewShortFile() { + val file4Name = "File " + Strings.repeat("4", 250) + val file4Cipher = "file" + Strings.repeat("4", 250) + val longFilenameBytes = "$file4Cipher.c9r".toByteArray(StandardCharsets.UTF_8) + val hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes) + val shortenedFileName = BaseEncoding.base64Url().encode(hash) + ".c9s" + val bbLvl2Dir = TestFolder(d, "11", "/d/11") + val bbFolder = TestFolder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB") + val testFile4 = TestFile(aaFolder, "file4.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/file4.c9r", null, null) + val cryptoFile4 = CryptoFile(root, "File 4", "/File 4", null, testFile4) + val testDir4 = TestFolder(bbFolder, shortenedFileName, "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/$shortenedFileName") + val testFile4ContentFile = TestFile(testDir4, "contents.c9r", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/$shortenedFileName/contents.c9r", null, null) + val testFile4NameFile = TestFile(testDir4, "name.c9s", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/$shortenedFileName/name.c9s", 258L, null) + val testFile4WhatTheHellCLoudFile = TestFile(bbFolder, shortenedFileName, "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/$shortenedFileName", null, null) // ugly hack + val cryptoMovedFile4 = CryptoFile(cryptoFolder1, file4Name, "/Directory 1/$file4Name", null, testFile4ContentFile) + + whenever(cloudContentRepository.file(aaFolder, "file4.c9r")).thenReturn(testFile4) + whenever(cloudContentRepository.file(testDir4, "contents.c9r")).thenReturn(testFile4ContentFile) + whenever(cloudContentRepository.file(testDir4, "name.c9s")).thenReturn(testFile4NameFile) + whenever(cloudContentRepository.file(testDir4, "name.c9s", 258L)).thenReturn(testFile4NameFile) + whenever(cloudContentRepository.file(bbFolder, shortenedFileName, null)).thenReturn(testFile4WhatTheHellCLoudFile) // bad + whenever(cloudContentRepository.move(testFile4ContentFile, testFile4)).thenReturn(testFile4) + whenever(cloudContentRepository.folder(rootFolder, "d")).thenReturn(d) + whenever(cloudContentRepository.folder(d, "11")).thenReturn(bbLvl2Dir) + whenever(cloudContentRepository.folder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB")).thenReturn(bbFolder) + whenever(cloudContentRepository.folder(bbFolder, "file4.c9r")).thenReturn(null) + whenever(cloudContentRepository.folder(bbFolder, shortenedFileName)).thenReturn(testDir4) + whenever(cloudContentRepository.create(testDir4)).thenReturn(testDir4) + whenever(cloudContentRepository.write(eq(testFile4NameFile), any(), any(), eq(true), any())).thenAnswer { invocationOnMock: InvocationOnMock -> + val inputStream = invocationOnMock.getArgument(1) + val dirContent = BufferedReader(InputStreamReader(inputStream.open(context)!!, StandardCharsets.UTF_8)).readLine() + MatcherAssert.assertThat(dirContent, CoreMatchers.`is`("$file4Cipher.c9r")) + testFile4NameFile + } + whenever(dirIdCache.put(eq(cryptoFolder1), any())).thenReturn(DirIdInfo(dirId1, bbFolder)) + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), "File 4", dirIdRoot.toByteArray())).thenReturn("file4") + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), file4Name, dirId1.toByteArray())).thenReturn(file4Cipher) + + // just for the exits check + val tmpFileExistFolder = TestFolder(aaFolder, testFile4.name, testFile4.path) + whenever(cloudContentRepository.folder(aaFolder, testFile4.name)) + .thenReturn(tmpFileExistFolder) + whenever(cloudContentRepository.file(tmpFileExistFolder, "dir.c9r")) + .thenReturn(TestFile(tmpFileExistFolder, "dir.c9r", tmpFileExistFolder.path + "/dir.c9r", null, null)) + + val result = inTest.move(cryptoMovedFile4, cryptoFile4) + + Mockito.verify(cloudContentRepository).delete(testDir4) + Mockito.verify(cloudContentRepository).move(testFile4ContentFile, testFile4) + } + + @Test + @DisplayName("move(\"/Directory 1\", \"/Directory 15\")") + @Throws(BackendException::class) + fun testMoveShortFolderToNewShortFolder() { + val bbLvl2Dir = TestFolder(d, "11", "/d/11") + val bbFolder = TestFolder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB") + val testDir15 = TestFolder(aaFolder, "dir15.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/dir15.c9r") + val testDir15DirFile = TestFile(testDir15, "dir.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/dir15.c9r/dir.c9r", null, null) + val cryptoFolder15 = CryptoFolder(root, "Directory 15", "/Directory 15/", testDir15DirFile) + + whenever(cloudContentRepository.file(aaFolder, "dir15.c9r", null)) + .thenReturn(TestFile(aaFolder, "dir15.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/dir15.c9r", null, null)) + whenever(cloudContentRepository.folder(rootFolder, "d")).thenReturn(d) + whenever(cloudContentRepository.folder(d, "11")).thenReturn(bbLvl2Dir) + whenever(cloudContentRepository.folder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB")).thenReturn(bbFolder) + whenever(dirIdCache.put(eq(cryptoFolder1), any())).thenReturn(DirIdInfo(dirId1, bbFolder)) + whenever(dirIdCache.put(eq(cryptoFolder15), any())).thenReturn(DirIdInfo(dirId1, bbFolder)) + whenever(dirIdCache[cryptoFolder1]).thenReturn(DirIdInfo(dirId1, bbFolder)) + whenever(dirIdCache[cryptoFolder15]).thenReturn(DirIdInfo(dirId1, bbFolder)) + whenever(cloudContentRepository.create(testDir15)).thenReturn(testDir15) + whenever(cloudContentRepository.file(testDir15, "dir.c9r")).thenReturn(testDir15DirFile) + whenever(cloudContentRepository.move(cryptoFolder1.dirFile!!, testDir15DirFile)).thenReturn(testDir15DirFile) + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), "Directory 15", dirIdRoot.toByteArray())).thenReturn("dir15") + + val result = inTest.move(cryptoFolder1, cryptoFolder15) + + Mockito.verify(cloudContentRepository).create(testDir15) + Mockito.verify(cloudContentRepository).move(cryptoFolder1.dirFile!!, testDir15DirFile) + Mockito.verify(cloudContentRepository).delete(cryptoFolder1.dirFile!!.parent!!) + } + + @Test + @DisplayName("move(\"/Directory 1\", \"/Directory 15x200\")") + @Throws(BackendException::class) + fun testMoveShortFolderToNewLongFolder() { + val dir15Name = "Dir " + Strings.repeat("15", 250) + val dir15Cipher = "dir" + Strings.repeat("15", 250) + val longFilenameBytes = "$dir15Cipher.c9r".toByteArray(StandardCharsets.UTF_8) + val hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes) + val shortenedFileName = BaseEncoding.base64Url().encode(hash) + ".c9s" + val bbLvl2Dir = TestFolder(d, "11", "/d/11") + val bbFolder = TestFolder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB") + val testDir15 = TestFolder(aaFolder, shortenedFileName, "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/$shortenedFileName") + val testDir15DirFile = TestFile(testDir15, "dir.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/$shortenedFileName/dir.c9r", null, null) + val testDir15NameFile = TestFile(testDir15, "name.c9s", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/$shortenedFileName/name.c9s", 507L, null) + val cryptoFolder15 = CryptoFolder(root, dir15Name, "/$dir15Name", testDir15DirFile) + + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), dir15Name, dirId1.toByteArray())).thenReturn(dir15Cipher) + whenever(cloudContentRepository.file(aaFolder, "dir15.c9r", null)) + .thenReturn(TestFile(aaFolder, "dir15.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/dir15.c9r", null, null)) + whenever(cloudContentRepository.folder(aaFolder, shortenedFileName)).thenReturn(testDir15) + whenever(cloudContentRepository.folder(rootFolder, "d")).thenReturn(d) + whenever(cloudContentRepository.folder(d, "11")).thenReturn(bbLvl2Dir) + whenever(cloudContentRepository.folder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB")).thenReturn(bbFolder) + whenever(dirIdCache.put(eq(cryptoFolder1), any())).thenReturn(DirIdInfo(dirId1, bbFolder)) + whenever(dirIdCache.put(eq(cryptoFolder15), any())).thenReturn(DirIdInfo(dirId1, bbFolder)) + whenever(cloudContentRepository.create(testDir15)).thenReturn(testDir15) + whenever(cloudContentRepository.file(testDir15, "dir.c9r")).thenReturn(testDir15DirFile) + whenever(cloudContentRepository.file(testDir15, "name.c9s", 507L)).thenReturn(testDir15NameFile) + whenever(cloudContentRepository.move(cryptoFolder1.dirFile!!, testDir15DirFile)).thenReturn(testDir15DirFile) + whenever(cloudContentRepository.create(testDir15)).thenReturn(testDir15) + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), dir15Name, dirIdRoot.toByteArray())).thenReturn(dir15Cipher) + whenever(cloudContentRepository.write(eq(testDir15NameFile), any(), any(), eq(true), any())).thenAnswer { invocationOnMock: InvocationOnMock -> + val inputStream = invocationOnMock.getArgument(1) + val dirContent = BufferedReader(InputStreamReader(inputStream.open(context)!!, StandardCharsets.UTF_8)).readLine() + MatcherAssert.assertThat(dirContent, CoreMatchers.`is`("$dir15Cipher.c9r")) + testDir15NameFile + } + + val targetFile = inTest.folder(root, dir15Name) // needed due to ugly side effect + val result = inTest.move(cryptoFolder1, cryptoFolder15) + + Mockito.verify(cloudContentRepository).create(testDir15) + Mockito.verify(cloudContentRepository).move(cryptoFolder1.dirFile!!, testDir15DirFile) + Mockito.verify(cloudContentRepository).write(eq(testDir15NameFile), any(), any(), eq(true), any()) + Mockito.verify(cloudContentRepository).delete(cryptoFolder1.dirFile!!.parent!!) + } + + @Test + @DisplayName("move(\"/Directory 15x200\", \"/Directory 3000\")") + @Throws(BackendException::class) + fun testMoveLongFolderToNewShortFolder() { + val dir15Name = "Dir " + Strings.repeat("15", 250) + val dir15Cipher = "dir" + Strings.repeat("15", 250) + val longFilenameBytes = "$dir15Cipher.c9r".toByteArray(StandardCharsets.UTF_8) + val hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes) + val shortenedFileName = BaseEncoding.base64Url().encode(hash) + ".c9s" + val bbLvl2Dir = TestFolder(d, "11", "/d/11") + val bbFolder = TestFolder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB") + val testDir15 = TestFolder(aaFolder, shortenedFileName, "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/$shortenedFileName") + val testDir15DirFile = TestFile(testDir15, "dir.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/$shortenedFileName/dir.c9r", null, null) + val testDir15NameFile = TestFile(testDir15, "name.c9s", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/$shortenedFileName/name.c9s", 507L, null) + val cryptoFolder15 = CryptoFolder(root, dir15Name, "/$dir15Name", testDir15DirFile) + + lvl2Dir = TestFolder(d, "33", "/d/33") + + val ddFolder = TestFolder(lvl2Dir, "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD", "/d/33/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD") + val testDir3 = TestFolder(aaFolder, "dir3.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/dir3.c9r") + val testDir3DirFile = TestFile(testDir3, "dir.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/dir3.c9r/dir.c9r", null, null) + val cryptoFolder3 = CryptoFolder(root, "Directory 3", "/Directory 3", testDir3DirFile) + + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), dir15Name, dirId1.toByteArray())).thenReturn(dir15Cipher) + whenever(cloudContentRepository.file(aaFolder, "dir15.c9r", null)) + .thenReturn(TestFile(aaFolder, "dir15.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/dir15.c9r", null, null)) + whenever(cloudContentRepository.folder(aaFolder, shortenedFileName)).thenReturn(testDir15) + whenever(cloudContentRepository.folder(rootFolder, "d")).thenReturn(d) + whenever(cloudContentRepository.folder(d, "11")).thenReturn(bbLvl2Dir) + whenever(cloudContentRepository.folder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB")).thenReturn(bbFolder) + whenever(dirIdCache.put(eq(cryptoFolder1), any())).thenReturn(DirIdInfo(dirId1, bbFolder)) + whenever(dirIdCache.put(eq(cryptoFolder15), any())).thenReturn(DirIdInfo(dirId1, bbFolder)) + whenever(cloudContentRepository.create(testDir15)).thenReturn(testDir15) + whenever(cloudContentRepository.file(testDir15, "dir.c9r")).thenReturn(testDir15DirFile) + whenever(cloudContentRepository.file(testDir15, "name.c9s", 507L)).thenReturn(testDir15NameFile) + whenever(cloudContentRepository.move(testDir15DirFile, cryptoFolder3.dirFile!!)).thenReturn(cryptoFolder3.dirFile) + whenever(cloudContentRepository.create(testDir15)).thenReturn(testDir15) + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), dir15Name, dirIdRoot.toByteArray())).thenReturn(dir15Cipher) + whenever(cloudContentRepository.write(eq(testDir15NameFile), any(), any(), eq(true), any())).thenAnswer { invocationOnMock: InvocationOnMock -> + val inputStream = invocationOnMock.getArgument(1) + val dirContent = BufferedReader(InputStreamReader(inputStream.open(context)!!, StandardCharsets.UTF_8)).readLine() + MatcherAssert.assertThat(dirContent, CoreMatchers.`is`("$dir15Cipher.c9r")) + testDir15NameFile + } + + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), "Directory 3", dirIdRoot.toByteArray())).thenReturn("dir3") + whenever(fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), "dir3", dirIdRoot.toByteArray())).thenReturn("Directory 3") + whenever(fileNameCryptor.hashDirectoryId(AdditionalMatchers.not(eq("")))).thenReturn("33DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD") + whenever(cloudContentRepository.folder(d, "33")).thenReturn(lvl2Dir) + whenever(cloudContentRepository.folder(aaFolder, "dir3.c9r")).thenReturn(testDir3) + whenever(cloudContentRepository.folder(lvl2Dir, "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD")).thenReturn(ddFolder) + whenever(cloudContentRepository.file(testDir3, "dir.c9r")).thenReturn(testDir3DirFile) + whenever(dirIdCache.put(eq(cryptoFolder3), any())).thenReturn(DirIdInfo("dir3-id", ddFolder)) + whenever(dirIdCache[cryptoFolder3]).thenReturn(DirIdInfo("dir3-id", ddFolder)) + whenever(cloudContentRepository.create(lvl2Dir)).thenReturn(lvl2Dir) + whenever(cloudContentRepository.create(ddFolder)).thenReturn(ddFolder) + whenever(cloudContentRepository.create(testDir3)).thenReturn(testDir3) + whenever(cloudContentRepository.write(eq(testDir3DirFile), any(), any(), eq(false), any())).thenReturn(testDir3DirFile) + // just for the exits check + whenever(cloudContentRepository.file(aaFolder, "dir3.c9r", null)) + .thenReturn(TestFile(aaFolder, "dir3.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/dir3.c9r", null, null)) + + val targetFile = inTest.folder(root, cryptoFolder3.name) // needed due to ugly side effect + val result = inTest.move(cryptoFolder15, cryptoFolder3) + + Mockito.verify(cloudContentRepository).create(testDir3) + Mockito.verify(cloudContentRepository).move(testDir15DirFile, cryptoFolder3.dirFile!!) + Mockito.verify(cloudContentRepository).delete(testDir15) + } +} diff --git a/data/src/test/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7Test.java b/data/src/test/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7Test.java deleted file mode 100644 index 915a9a79..00000000 --- a/data/src/test/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7Test.java +++ /dev/null @@ -1,936 +0,0 @@ -package org.cryptomator.data.cloud.crypto; - -import android.content.Context; - -import com.google.common.base.Strings; -import com.google.common.io.BaseEncoding; - -import org.apache.commons.codec.binary.Base32; -import org.cryptomator.cryptolib.api.Cryptor; -import org.cryptomator.cryptolib.api.FileContentCryptor; -import org.cryptomator.cryptolib.api.FileHeader; -import org.cryptomator.cryptolib.api.FileHeaderCryptor; -import org.cryptomator.cryptolib.api.FileNameCryptor; -import org.cryptomator.cryptolib.common.MessageDigestSupplier; -import org.cryptomator.data.util.CopyStream; -import org.cryptomator.domain.Cloud; -import org.cryptomator.domain.CloudFile; -import org.cryptomator.domain.CloudFolder; -import org.cryptomator.domain.CloudNode; -import org.cryptomator.domain.exception.BackendException; -import org.cryptomator.domain.repository.CloudContentRepository; -import org.cryptomator.domain.usecases.ProgressAware; -import org.cryptomator.domain.usecases.cloud.ByteArrayDataSource; -import org.cryptomator.domain.usecases.cloud.DataSource; -import org.cryptomator.util.Encodings; -import org.cryptomator.util.Optional; -import org.hamcrest.Matchers; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.mockito.AdditionalMatchers; -import org.mockito.Answers; -import org.mockito.Mockito; - -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; - -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; - -/** - * - * path/to/vault/d/00 - * ├─ Directory 1 - * │ ├─ Directory 2 - * │ ├─ Directory 3x250 - * │ │ ├─ Directory 4x250 - * │ │ └─ File 5x250 - * │ └─ File 3 - * ├─ File 1 - * ├─ File 2 - * ├─ File 4 - * - */ -class CryptoImplVaultFormatPre7Test { - - private final String dirIdRoot = ""; - private final String dirId1 = "dir1-id"; - private final String dirId2 = "dir2-id"; - - private Cloud cloud; - private CryptoCloud cryptoCloud; - private Context context; - private Cryptor cryptor; - private CloudContentRepository cloudContentRepository; - private DirIdCache dirIdCache; - private FileNameCryptor fileNameCryptor; - private FileContentCryptor fileContentCryptor; - private FileHeaderCryptor fileHeaderCryptor; - - private CryptoImplVaultFormatPre7 inTest; - - private TestFolder rootFolder = new RootTestFolder(cloud); - private TestFolder d = new TestFolder(rootFolder, "d", "/d"); - private TestFolder m = new TestFolder(rootFolder, "m", "/m"); - private TestFolder lvl2Dir = new TestFolder(d, "00", "/d/00"); - private TestFolder aaFolder = new TestFolder(lvl2Dir, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); - - private RootCryptoFolder root; - private CryptoFile cryptoFile1; - private CryptoFile cryptoFile2; - private CryptoFile cryptoFile4; - private CryptoFolder cryptoFolder1; - - @BeforeEach - public void setup() throws BackendException { - cloud = Mockito.mock(Cloud.class); - cryptoCloud = Mockito.mock(CryptoCloud.class); - context = Mockito.mock(Context.class); - cryptor = Mockito.mock(Cryptor.class); - cloudContentRepository = Mockito.mock(CloudContentRepository.class, Answers.RETURNS_DEEP_STUBS); - dirIdCache = Mockito.mock(DirIdCache.class); - fileNameCryptor = Mockito.mock(FileNameCryptor.class); - fileContentCryptor = Mockito.mock(FileContentCryptor.class); - fileHeaderCryptor = Mockito.mock(FileHeaderCryptor.class); - - Mockito.when(cryptor.fileNameCryptor()).thenReturn(fileNameCryptor); - Mockito.when(cryptor.fileNameCryptor()).thenReturn(fileNameCryptor); - Mockito.when(cryptor.fileContentCryptor()).thenReturn(fileContentCryptor); - Mockito.when(cryptor.fileHeaderCryptor()).thenReturn(fileHeaderCryptor); - - root = new RootCryptoFolder(cryptoCloud); - inTest = new CryptoImplVaultFormatPre7(context, () -> cryptor, cloudContentRepository, rootFolder, dirIdCache); - - Mockito.when(fileNameCryptor.hashDirectoryId(dirIdRoot)).thenReturn("00AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); - Mockito.when(fileNameCryptor.hashDirectoryId(dirId1)).thenReturn("11BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"); - Mockito.when(fileNameCryptor.hashDirectoryId(dirId2)).thenReturn("22CCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"); - Mockito.when(fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), "dir1", dirIdRoot.getBytes())).thenReturn("Directory 1"); - Mockito.when(fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), "file1", dirIdRoot.getBytes())).thenReturn("File 1"); - Mockito.when(fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), "file2", dirIdRoot.getBytes())).thenReturn("File 2"); - Mockito.when(fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), "dir2", dirId1.getBytes())).thenReturn("Directory 2"); - Mockito.when(fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), "file3", dirId1.getBytes())).thenReturn("File 3"); - Mockito.when(fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), "file4", dirIdRoot.getBytes())).thenReturn("File 4"); - - TestFile testFile1 = new TestFile(aaFolder, "file1.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/file1.c9r", Optional.empty(), Optional.empty()); - TestFile testFile2 = new TestFile(aaFolder, "file2.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/file2.c9r", Optional.empty(), Optional.empty()); - TestFile testFile4 = new TestFile(aaFolder, "file4.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/file4.c9r", Optional.empty(), Optional.empty()); - TestFile testDir1 = new TestFile(aaFolder, "0dir1", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/0dir1", Optional.empty(), Optional.empty()); - - ArrayList rootItems = new ArrayList() { - { - add(testFile1); - add(testFile2); - add(testFile4); - add(testDir1); - } - }; - - cryptoFile1 = new CryptoFile(root, "File 1", "/File 1", Optional.of(15l), testFile1); - cryptoFile2 = new CryptoFile(root, "File 2", "/File 2", Optional.empty(), testFile2); - cryptoFile4 = new CryptoFile(root, "File 4", "/File 4", Optional.empty(), testFile4); - cryptoFolder1 = new CryptoFolder(root, "Directory 1", "/Directory 1", testDir1); - - Mockito.when(cloudContentRepository.folder(rootFolder, "d")).thenReturn(d); - Mockito.when(cloudContentRepository.folder(d, "00")).thenReturn(lvl2Dir); - Mockito.when(cloudContentRepository.folder(lvl2Dir, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")).thenReturn(aaFolder); - Mockito.when(cloudContentRepository.file(aaFolder, "0dir1")).thenReturn(testDir1); - Mockito.when(cloudContentRepository.exists(testDir1)).thenReturn(true); - Mockito.doAnswer(invocation -> { - OutputStream out = invocation.getArgument(2); - CopyStream.copyStreamToStream(new ByteArrayInputStream(dirId1.getBytes()), out); - return null; - }).when(cloudContentRepository).read(Mockito.eq(cryptoFolder1.getDirFile()), Mockito.any(), Mockito.any(), Mockito.any()); - Mockito.when(cloudContentRepository.list(aaFolder)).thenReturn(rootItems); - Mockito.when(dirIdCache.put(Mockito.eq(root), Mockito.any())).thenReturn(new DirIdCache.DirIdInfo("", aaFolder)); - } - - @Test - @DisplayName("list(\"/\")") - public void testListRoot() throws BackendException { - List rootDirContent = inTest.list(root); - - Matchers.contains(rootDirContent, cryptoFile1); - Matchers.contains(rootDirContent, cryptoFile2); - Matchers.contains(rootDirContent, cryptoFile4); - Matchers.contains(rootDirContent, cryptoFolder1); - } - - @Test - @DisplayName("list(\"/Directory 1/Directory 3x250\")") - public void testListDirectory3x250() throws BackendException { - String dir3Name = "Directory " + Strings.repeat("3", 250); - String dir3Cipher = "dir" + Strings.repeat("3", 250); - - byte[] longFilenameBytes = (dir3Cipher + ".c9r").getBytes(Encodings.UTF_8); - byte[] hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes); - String shortenedFileName = BaseEncoding.base64Url().encode(hash) + ".c9s"; - - TestFolder bbLvl2Dir = new TestFolder(d, "11", "/d/11"); - TestFolder bbFolder = new TestFolder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"); - TestFolder ddLvl2Dir = new TestFolder(d, "33", "/d/33"); - TestFolder ddFolder = new TestFolder(ddLvl2Dir, "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD", "/d/33/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD"); - TestFolder testDir3 = new TestFolder(bbFolder, shortenedFileName, "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/" + shortenedFileName); - TestFile testDir3DirFile = new TestFile(testDir3, "dir.c9r", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/" + shortenedFileName + "/dir.c9r", Optional.empty(), Optional.empty()); - TestFile testDir3NameFile = new TestFile(testDir3, "name.c9s", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/" + shortenedFileName + "/name.c9s", Optional.empty(), Optional.empty()); - - Mockito.when(fileNameCryptor.encryptFilename(dir3Name, dirId1.getBytes())).thenReturn(dir3Cipher); - Mockito.when(fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), dir3Cipher, dirId1.getBytes())).thenReturn(dir3Name); - Mockito.when(fileNameCryptor.hashDirectoryId(AdditionalMatchers.not(Mockito.eq("")))).thenReturn("33DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD"); - - Mockito.when(cloudContentRepository.folder(d, "33")).thenReturn(ddLvl2Dir); - Mockito.when(cloudContentRepository.folder(ddLvl2Dir, "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD")).thenReturn(ddFolder); - Mockito.when(cloudContentRepository.file(testDir3, "dir.c9r")).thenReturn(testDir3DirFile); - Mockito.when(cloudContentRepository.file(testDir3, "name.c9s")).thenReturn(testDir3NameFile); - - Mockito.when(fileNameCryptor.encryptFilename("Directory 1", dirIdRoot.getBytes())).thenReturn("dir1"); - Mockito.when(fileNameCryptor.encryptFilename("Directory 2", dirId1.getBytes())).thenReturn("dir2"); - - CryptoFolder cryptoFolder3 = new CryptoFolder(cryptoFolder1, dir3Name, "/Directory 1/" + dir3Name, testDir3DirFile); - Mockito.doAnswer(invocation -> { - OutputStream out = invocation.getArgument(2); - CopyStream.copyStreamToStream(new ByteArrayInputStream("dir3-id".getBytes()), out); - return null; - }).when(cloudContentRepository).read(Mockito.eq(cryptoFolder3.getDirFile()), Mockito.any(), Mockito.any(), Mockito.any()); - - /* - * │ ├─ Directory 3x250 - * │ │ ├─ Directory 4x250 - * │ │ └─ File 5x250 - */ - - String dir4Name = "Directory " + Strings.repeat("4", 250); - String dir4Cipher = "dir" + Strings.repeat("4", 250); - - byte[] longFilenameBytes4 = (dir4Cipher + ".c9r").getBytes(Encodings.UTF_8); - byte[] hash4 = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes4); - String shortenedFileName4 = BaseEncoding.base64Url().encode(hash4) + ".c9s"; - - TestFolder directory4x250 = new TestFolder(ddFolder, shortenedFileName4, "/d/33/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD" + shortenedFileName4); - TestFile testDir4DirFile = new TestFile(directory4x250, "dir.c9r", "/d/33/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD/" + shortenedFileName4 + "/dir.c9r", Optional.empty(), Optional.empty()); - TestFile testDir4NameFile = new TestFile(directory4x250, "name.c9s", "/d/33/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD/" + shortenedFileName4 + "/name.c9s", Optional.empty(), Optional.empty()); - - Mockito.when(cloudContentRepository.file(directory4x250, "dir.c9r")).thenReturn(testDir4DirFile); - Mockito.when(cloudContentRepository.file(directory4x250, "name.c9s")).thenReturn(testDir4NameFile); - Mockito.when(fileNameCryptor.encryptFilename(dir4Name, "dir3-id".getBytes())).thenReturn(dir4Cipher); - Mockito.when(fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), dir4Cipher, "dir3-id".getBytes())).thenReturn(dir4Name); - Mockito.doAnswer(invocation -> { - OutputStream out = invocation.getArgument(2); - CopyStream.copyStreamToStream(new ByteArrayInputStream(dir4Cipher.getBytes("UTF-8")), out); - return null; - }).when(cloudContentRepository).read(Mockito.eq(testDir4NameFile), Mockito.any(), Mockito.any(), Mockito.any()); - - ArrayList dir4Files = new ArrayList() { - { - add(testDir4DirFile); - add(testDir4NameFile); - } - }; - - String file5Name = "File " + Strings.repeat("5", 250); - String file5Cipher = "file" + Strings.repeat("5", 250); - - byte[] longFilenameBytes5 = (file5Cipher + ".c9r").getBytes(Encodings.UTF_8); - byte[] hash5 = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes5); - String shortenedFileName5 = BaseEncoding.base64Url().encode(hash5) + ".c9s"; - - TestFolder directory5x250 = new TestFolder(ddFolder, shortenedFileName5, "/d/33/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD" + shortenedFileName5); - TestFile testFile5ContentFile = new TestFile(directory5x250, "contents.c9r", "/d/33/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD/" + shortenedFileName5 + "/contents.c9r", Optional.empty(), Optional.empty()); - TestFile testFile5NameFile = new TestFile(directory5x250, "name.c9s", "/d/33/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD/" + shortenedFileName5 + "/name.c9s", Optional.empty(), Optional.empty()); - - Mockito.when(cloudContentRepository.file(directory5x250, "contents.c9r")).thenReturn(testFile5ContentFile); - Mockito.when(cloudContentRepository.file(directory5x250, "name.c9s")).thenReturn(testFile5NameFile); - Mockito.when(fileNameCryptor.encryptFilename(file5Name, "dir3-id".getBytes())).thenReturn(file5Cipher); - Mockito.when(fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), file5Cipher, "dir3-id".getBytes())).thenReturn(file5Name); - Mockito.doAnswer(invocation -> { - OutputStream out = invocation.getArgument(2); - CopyStream.copyStreamToStream(new ByteArrayInputStream(file5Cipher.getBytes("UTF-8")), out); - return null; - }).when(cloudContentRepository).read(Mockito.eq(testFile5NameFile), Mockito.any(), Mockito.any(), Mockito.any()); - - ArrayList dir5Files = new ArrayList() { - { - add(testFile5ContentFile); - add(testFile5NameFile); - } - }; - - ArrayList dir3Items = new ArrayList() { - { - add(directory4x250); - add(directory5x250); - } - }; - - Mockito.when(cloudContentRepository.exists(testDir3DirFile)).thenReturn(true); - Mockito.when(cloudContentRepository.list(ddFolder)).thenReturn(dir3Items); - Mockito.when(cloudContentRepository.list(directory4x250)).thenReturn(dir4Files); - Mockito.when(cloudContentRepository.list(directory5x250)).thenReturn(dir5Files); - Mockito.when(dirIdCache.put(Mockito.eq(cryptoFolder3), Mockito.any())).thenReturn(new DirIdCache.DirIdInfo("dir3-id", ddFolder)); - - List folder3Content = inTest.list(cryptoFolder3); - - Matchers.contains(folder3Content, new CryptoFolder(cryptoFolder3, dir4Name, "/Directory 1/" + dir3Name + "/" + dir4Name, testDir4DirFile)); - Matchers.contains(folder3Content, new CryptoFile(cryptoFolder3, file5Name, "/Directory 1/" + dir3Name + "/" + file5Name, Optional.empty(), testFile5ContentFile)); - } - - @Test - @DisplayName("read(\"/File 1\", NO_PROGRESS_AWARE)") - public void testReadFromShortFile() throws BackendException { - byte[] file1Content = "hhhhhTOPSECRET!TOPSECRET!TOPSECRET!TOPSECRET!".getBytes(); - FileHeader header = Mockito.mock(FileHeader.class); - - Mockito.when(fileContentCryptor.cleartextChunkSize()).thenReturn(8); - Mockito.when(fileContentCryptor.ciphertextChunkSize()).thenReturn(10); - Mockito.when(fileHeaderCryptor.headerSize()).thenReturn(5); - - Mockito.when(fileNameCryptor.encryptFilename("File 1", dirIdRoot.getBytes())).thenReturn("file1"); - Mockito.when(fileHeaderCryptor.decryptHeader(UTF_8.encode("hhhhh"))).thenReturn(header); - Mockito.when(fileContentCryptor.decryptChunk(Mockito.eq(UTF_8.encode("TOPSECRET!")), Mockito.anyLong(), Mockito.eq(header), Mockito.anyBoolean())).then(invocation -> UTF_8.encode("geheim!!")); - - Mockito.doAnswer(invocation -> { - OutputStream out = invocation.getArgument(2); - CopyStream.copyStreamToStream(new ByteArrayInputStream(file1Content), out); - return null; - }).when(cloudContentRepository).read(Mockito.eq(cryptoFile1.getCloudFile()), Mockito.any(), Mockito.any(), Mockito.any()); - - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(1000); - - inTest.read(cryptoFile1, outputStream, ProgressAware.NO_OP_PROGRESS_AWARE); - - assertThat(outputStream.toString(), is("geheim!!geheim!!geheim!!geheim!!")); - } - - @Test - @DisplayName("read(\"/File 15x250\", NO_PROGRESS_AWARE)") - public void testReadFromLongFile() throws BackendException { - String file3Name = "File " + Strings.repeat("15", 250); - - byte[] longFilenameBytes = file3Name.getBytes(Encodings.UTF_8); - byte[] hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes); - String shortenedFileName = BaseEncoding.base64Url().encode(hash) + ".c9s"; - - TestFolder testFile3Folder = new TestFolder(aaFolder, shortenedFileName, "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/" + shortenedFileName); - TestFile testFile3ContentFile = new TestFile(testFile3Folder, "content.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/" + shortenedFileName + "/content.c9r", Optional.empty(), Optional.empty()); - - byte[] file1Content = "hhhhhTOPSECRET!TOPSECRET!TOPSECRET!TOPSECRET!".getBytes(); - FileHeader header = Mockito.mock(FileHeader.class); - - Mockito.when(fileContentCryptor.cleartextChunkSize()).thenReturn(8); - Mockito.when(fileContentCryptor.ciphertextChunkSize()).thenReturn(10); - Mockito.when(fileHeaderCryptor.headerSize()).thenReturn(5); - - Mockito.when(fileHeaderCryptor.decryptHeader(UTF_8.encode("hhhhh"))).thenReturn(header); - Mockito.when(fileContentCryptor.decryptChunk(Mockito.eq(UTF_8.encode("TOPSECRET!")), Mockito.anyLong(), Mockito.eq(header), Mockito.anyBoolean())).then(invocation -> UTF_8.encode("geheim!!")); - - CryptoFile cryptoFile15 = new CryptoFile(root, file3Name, "/" + file3Name, Optional.empty(), testFile3ContentFile); - - Mockito.doAnswer(invocation -> { - OutputStream out = invocation.getArgument(2); - CopyStream.copyStreamToStream(new ByteArrayInputStream(file1Content), out); - return null; - }).when(cloudContentRepository).read(Mockito.eq(cryptoFile15.getCloudFile()), Mockito.any(), Mockito.any(), Mockito.any()); - - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(1000); - - inTest.read(cryptoFile15, outputStream, ProgressAware.NO_OP_PROGRESS_AWARE); - - assertThat(outputStream.toString(), is("geheim!!geheim!!geheim!!geheim!!")); - - } - - @Test - @DisplayName("write(\"/File 1\", text, NO_PROGRESS_AWARE, replace=false, 10bytes)") - public void testWriteToShortFile() throws BackendException { - Mockito.when(fileNameCryptor.encryptFilename("File 1", dirIdRoot.getBytes())).thenReturn("file1"); - - FileHeader header = Mockito.mock(FileHeader.class); - Mockito.when(fileHeaderCryptor.create()).thenReturn(header); - Mockito.when(fileHeaderCryptor.encryptHeader(header)).thenReturn(ByteBuffer.wrap("hhhhh".getBytes())); - Mockito.when(fileHeaderCryptor.headerSize()).thenReturn(5); - Mockito.when(fileContentCryptor.cleartextChunkSize()).thenReturn(10); - Mockito.when(fileContentCryptor.ciphertextChunkSize()).thenReturn(10); - Mockito.when(fileContentCryptor.encryptChunk(Mockito.any(ByteBuffer.class), Mockito.anyLong(), Mockito.any(FileHeader.class))).thenAnswer(invocation -> { - ByteBuffer input = invocation.getArgument(0); - String inStr = UTF_8.decode(input).toString(); - return ByteBuffer.wrap(inStr.toLowerCase().getBytes(UTF_8)); - }); - - Mockito.when(cloudContentRepository.write(Mockito.eq(cryptoFile1.getCloudFile()), Mockito.any(DataSource.class), Mockito.any(), Mockito.eq(false), Mockito.anyLong())).thenAnswer(invocationOnMock -> { - DataSource in = invocationOnMock.getArgument(1); - String encrypted = new BufferedReader(new InputStreamReader(in.open(context), StandardCharsets.UTF_8)).readLine(); - assertThat(encrypted, is("hhhhhtopsecret!")); - return invocationOnMock.getArgument(0); - }); - - CryptoFile cryptoFile = inTest.write(cryptoFile1, ByteArrayDataSource.from("TOPSECRET!".getBytes(UTF_8)), ProgressAware.NO_OP_PROGRESS_AWARE, false, 10l); - assertThat(cryptoFile, is(cryptoFile1)); - } - - @Test - @DisplayName("write(\"/File 15x250\", text, NO_PROGRESS_AWARE, replace=false, 10bytes)") - public void testWriteToLongFile() throws BackendException { - String file15Name = "File " + Strings.repeat("15", 250); - String file15Cipher = "file" + Strings.repeat("15", 250); - - byte[] longFilenameBytes = file15Cipher.getBytes(Encodings.UTF_8); - byte[] hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes); - String shortenedFileName = new Base32().encodeAsString(hash) + ".lng"; - - CloudFile metaDataDFile = new TestFile(aaFolder, shortenedFileName, "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/" + shortenedFileName, Optional.empty(), Optional.empty()); - CloudFile metaDataMFile = metadataFile(shortenedFileName); - - CryptoFile cryptoFile15 = new CryptoFile(root, file15Name, "/" + file15Name, Optional.of(15l), metaDataDFile); - - Mockito.when(fileNameCryptor.encryptFilename(file15Name, dirIdRoot.getBytes())).thenReturn(file15Cipher); - - FileHeader header = Mockito.mock(FileHeader.class); - Mockito.when(fileHeaderCryptor.create()).thenReturn(header); - Mockito.when(fileHeaderCryptor.encryptHeader(header)).thenReturn(ByteBuffer.wrap("hhhhh".getBytes())); - Mockito.when(fileHeaderCryptor.headerSize()).thenReturn(5); - Mockito.when(fileContentCryptor.cleartextChunkSize()).thenReturn(10); - Mockito.when(fileContentCryptor.ciphertextChunkSize()).thenReturn(10); - Mockito.when(fileContentCryptor.encryptChunk(Mockito.any(ByteBuffer.class), Mockito.anyLong(), Mockito.any(FileHeader.class))).thenAnswer(invocation -> { - ByteBuffer input = invocation.getArgument(0); - String inStr = UTF_8.decode(input).toString(); - return ByteBuffer.wrap(inStr.toLowerCase().getBytes(UTF_8)); - }); - - Mockito.when(cloudContentRepository.write(Mockito.eq(metaDataDFile), Mockito.any(DataSource.class), Mockito.any(), Mockito.eq(false), Mockito.anyLong())).thenAnswer(invocationOnMock -> { - DataSource in = invocationOnMock.getArgument(1); - String encrypted = new BufferedReader(new InputStreamReader(in.open(context), StandardCharsets.UTF_8)).readLine(); - assertThat(encrypted, is("hhhhhtopsecret!")); - return invocationOnMock.getArgument(0); - }); - - Mockito.when(cloudContentRepository.write(Mockito.eq(metaDataMFile), Mockito.any(DataSource.class), Mockito.any(), Mockito.eq(true), Mockito.anyLong())).thenAnswer(invocationOnMock -> { - DataSource in = invocationOnMock.getArgument(1); - String encrypted = new BufferedReader(new InputStreamReader(in.open(context), StandardCharsets.UTF_8)).readLine(); - assertThat(encrypted, is(file15Cipher)); - return invocationOnMock.getArgument(0); - }); - - CryptoFile res = inTest.file(root, file15Name); - CryptoFile cryptoFile = inTest.write(cryptoFile15, ByteArrayDataSource.from("TOPSECRET!".getBytes(UTF_8)), ProgressAware.NO_OP_PROGRESS_AWARE, false, 10l); - assertThat(cryptoFile, is(cryptoFile15)); - - Mockito.verify(cloudContentRepository).write(Mockito.eq(metaDataDFile), Mockito.any(DataSource.class), Mockito.any(), Mockito.eq(false), Mockito.anyLong()); - Mockito.verify(cloudContentRepository).write(Mockito.eq(metaDataMFile), Mockito.any(DataSource.class), Mockito.any(), Mockito.eq(true), Mockito.anyLong()); - } - - @Test - @DisplayName("create(\"/Directory 3/\")") - public void testCreateShortFolder() throws BackendException { - /* - * - * path/to/vault/d - * ├─ Directory 1 - * │ ├─ ... - * ├─ Directory 3 - * ├─ ... - * - */ - - lvl2Dir = new TestFolder(d, "33", "/d/33"); - TestFolder ddFolder = new TestFolder(lvl2Dir, "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD", "/d/33/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD"); - TestFile testDir3DirFile = new TestFile(aaFolder, "0dir3", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/0dir3", Optional.empty(), Optional.empty()); - CryptoFolder cryptoFolder3 = new CryptoFolder(root, "Directory 3", "/Directory 3", testDir3DirFile); - - Mockito.when(fileNameCryptor.encryptFilename("Directory 3", dirIdRoot.getBytes())).thenReturn("dir3"); - Mockito.when(fileNameCryptor.decryptFilename("dir3", dirIdRoot.getBytes())).thenReturn("Directory 3"); - Mockito.when(fileNameCryptor.hashDirectoryId(AdditionalMatchers.not(Mockito.eq("")))).thenReturn("33DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD"); - - Mockito.when(cloudContentRepository.folder(d, "33")).thenReturn(lvl2Dir); - Mockito.when(cloudContentRepository.folder(lvl2Dir, "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD")).thenReturn(ddFolder); - Mockito.when(cloudContentRepository.file(aaFolder, "0dir3")).thenReturn(testDir3DirFile); - Mockito.when(dirIdCache.put(Mockito.eq(cryptoFolder3), Mockito.any())).thenReturn(new DirIdCache.DirIdInfo("dir3-id", ddFolder)); - - Mockito.when(cloudContentRepository.create(lvl2Dir)).thenReturn(lvl2Dir); - Mockito.when(cloudContentRepository.create(ddFolder)).thenReturn(ddFolder); - Mockito.when(cloudContentRepository.write(Mockito.eq(testDir3DirFile), Mockito.any(), Mockito.any(), Mockito.eq(false), Mockito.anyLong())).thenReturn(testDir3DirFile); - - Mockito.when(cloudContentRepository.file(aaFolder, "dir3.c9r")).thenReturn(null); - - CloudFolder cloudFolder = inTest.create(cryptoFolder3); - assertThat(cloudFolder, is(cryptoFolder3)); - - Mockito.verify(cloudContentRepository).create(ddFolder); - Mockito.verify(cloudContentRepository).write(Mockito.eq(testDir3DirFile), Mockito.any(), Mockito.any(), Mockito.eq(false), Mockito.anyLong()); - } - - @Test - @DisplayName("create(\"/Directory 3x250/\")") - public void testCreateLongFolder() throws BackendException { - /* - * - * path/to/vault/d - * ├─ Directory 1 - * │ ├─ ... - * ├─ Directory 3x250 - * ├─ ... - * - */ - String dir3Name = "Directory " + Strings.repeat("3", 250); - String dir3Cipher = "dir" + Strings.repeat("3", 250); - byte[] longFilenameBytes = ("0" + dir3Cipher).getBytes(Encodings.UTF_8); - byte[] hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes); - String shortenedFileName = new Base32().encodeAsString(hash) + ".lng"; - - TestFolder ddLvl2Dir = new TestFolder(d, "33", "/d/33"); - TestFolder ddFolder = new TestFolder(ddLvl2Dir, "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD", "/d/33/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD"); - - TestFile testDir3DirFile = new TestFile(aaFolder, shortenedFileName, "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/" + shortenedFileName, Optional.empty(), Optional.empty()); - - CloudFile testDir3NameFile = metadataFile(shortenedFileName); - - Mockito.when(fileNameCryptor.encryptFilename(dir3Name, dirIdRoot.getBytes())).thenReturn(dir3Cipher); - Mockito.when(fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), dir3Cipher, dirIdRoot.getBytes())).thenReturn(dir3Name); - Mockito.when(fileNameCryptor.hashDirectoryId(AdditionalMatchers.not(Mockito.eq("")))).thenReturn("33DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD"); - - Mockito.when(cloudContentRepository.folder(d, "33")).thenReturn(ddLvl2Dir); - Mockito.when(cloudContentRepository.folder(ddLvl2Dir, "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD")).thenReturn(ddFolder); - - CryptoFolder cryptoFolder3 = new CryptoFolder(root, dir3Name, "/" + dir3Name, testDir3DirFile); - - Mockito.when(fileNameCryptor.hashDirectoryId(AdditionalMatchers.not(Mockito.eq("")))).thenReturn("33DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD"); - - Mockito.when(cloudContentRepository.folder(d, "33")).thenReturn(ddLvl2Dir); - Mockito.when(cloudContentRepository.folder(lvl2Dir, "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD")).thenReturn(ddFolder); - Mockito.when(dirIdCache.put(Mockito.eq(cryptoFolder3), Mockito.any())).thenReturn(new DirIdCache.DirIdInfo("dir3-id", ddFolder)); - - Mockito.when(cloudContentRepository.create(ddLvl2Dir)).thenReturn(ddLvl2Dir); - Mockito.when(cloudContentRepository.create(ddFolder)).thenReturn(ddFolder); - Mockito.when(cloudContentRepository.write(Mockito.eq(testDir3DirFile), Mockito.any(), Mockito.any(), Mockito.eq(false), Mockito.anyLong())).thenAnswer(invocationOnMock -> { - DataSource in = invocationOnMock.getArgument(1); - String dirContent = new BufferedReader(new InputStreamReader(in.open(context), StandardCharsets.UTF_8)).readLine(); - assertThat(dirContent, is("dir3-id")); - return testDir3DirFile; - }); - Mockito.when(cloudContentRepository.write(Mockito.eq(testDir3NameFile), Mockito.any(), Mockito.any(), Mockito.eq(true), Mockito.anyLong())).thenAnswer(invocationOnMock -> { - DataSource in = invocationOnMock.getArgument(1); - String nameContent = new BufferedReader(new InputStreamReader(in.open(context), StandardCharsets.UTF_8)).readLine(); - assertThat(nameContent, is("0" + dir3Cipher)); - return testDir3NameFile; - }); - - CloudFolder cloudFolder = inTest.folder(root, dir3Name); - cloudFolder = inTest.create(cryptoFolder3); - - assertThat(cloudFolder, is(cryptoFolder3)); - - Mockito.verify(cloudContentRepository).create(ddFolder); - Mockito.verify(cloudContentRepository).create(testDir3NameFile.getParent()); - Mockito.verify(cloudContentRepository).write(Mockito.eq(testDir3DirFile), Mockito.any(), Mockito.any(), Mockito.eq(false), Mockito.anyLong()); - Mockito.verify(cloudContentRepository).write(Mockito.eq(testDir3NameFile), Mockito.any(), Mockito.any(), Mockito.eq(true), Mockito.anyLong()); - } - - @Test - @DisplayName("delete(\"/File 4\")") - public void testDeleteShortFile() throws BackendException { - inTest.delete(cryptoFile4); - Mockito.verify(cloudContentRepository).delete(cryptoFile4.getCloudFile()); - } - - @Test - @DisplayName("delete(\"/File 15x250\")") - public void testDeleteLongFile() throws BackendException { - String file15Name = "File " + Strings.repeat("15", 250); - String file15Cipher = "file" + Strings.repeat("15", 250); - - byte[] longFilenameBytes = file15Name.getBytes(Encodings.UTF_8); - byte[] hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes); - String shortenedFileName = new Base32().encodeAsString(hash) + ".lng"; - - CloudFile metaDataFile = metadataFile(shortenedFileName); - - Mockito.when(fileNameCryptor.encryptFilename(file15Name, dirIdRoot.getBytes())).thenReturn(file15Cipher); - - CryptoFile cryptoFile15 = new CryptoFile(root, file15Name, "/" + file15Name, Optional.of(15l), metaDataFile); - - inTest.delete(cryptoFile15); - - Mockito.verify(cloudContentRepository).delete(metaDataFile); - } - - @Test - @DisplayName("delete(\"/Directory 1/Directory 2/\")") - public void testDeleteSingleShortFolder() throws BackendException { - TestFolder bbLvl2Dir = new TestFolder(d, "11", "/d/11"); - TestFolder bbFolder = new TestFolder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"); - TestFolder ccLvl2Dir = new TestFolder(d, "22", "/d/22"); - TestFolder ccFolder = new TestFolder(ccLvl2Dir, "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCC", "/d/22/CCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"); - - Mockito.when(fileNameCryptor.encryptFilename("Directory 1", dirIdRoot.getBytes())).thenReturn("dir1"); - Mockito.when(fileNameCryptor.encryptFilename("Directory 2", dirId1.getBytes())).thenReturn("dir2"); - - TestFile testDir2DirFile = new TestFile(bbFolder, "0dir2", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/0dir2", Optional.empty(), Optional.empty()); - - CryptoFolder cryptoFolder2 = new CryptoFolder(cryptoFolder1, "Directory 2", "/Directory 1/Directory 2", testDir2DirFile); - Mockito.doAnswer(invocation -> { - OutputStream out = invocation.getArgument(2); - CopyStream.copyStreamToStream(new ByteArrayInputStream(dirId2.getBytes()), out); - return null; - }).when(cloudContentRepository).read(Mockito.eq(cryptoFolder2.getDirFile()), Mockito.any(), Mockito.any(), Mockito.any()); - - ArrayList dir1Items = new ArrayList() { - { - add(testDir2DirFile); - } - }; - - Mockito.when(cloudContentRepository.folder(rootFolder, "d")).thenReturn(d); - Mockito.when(cloudContentRepository.folder(d, "22")).thenReturn(ccLvl2Dir); - Mockito.when(cloudContentRepository.folder(ccLvl2Dir, "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCC")).thenReturn(ccFolder); - Mockito.when(cloudContentRepository.file(aaFolder, "0dir2")).thenReturn(testDir2DirFile); - Mockito.when(cloudContentRepository.list(bbFolder)).thenReturn(dir1Items); - Mockito.when(dirIdCache.put(Mockito.eq(cryptoFolder2), Mockito.any())).thenReturn(new DirIdCache.DirIdInfo(dirId2, ccFolder)); - Mockito.when(cloudContentRepository.exists(testDir2DirFile)).thenReturn(true); - Mockito.when(cloudContentRepository.list(ccFolder)).thenReturn(new ArrayList()); - - inTest.delete(cryptoFolder2); - - Mockito.verify(cloudContentRepository).delete(ccFolder); - Mockito.verify(cloudContentRepository).delete(testDir2DirFile); - Mockito.verify(dirIdCache).evict(cryptoFolder2); - } - - @Test - @DisplayName("delete(\"/Directory 3x250\")") - public void testDeleteSingleLongFolder() throws BackendException { - /* - * - * path/to/vault/d - * ├─ Directory 1 - * │ ├─ ... - * ├─ Directory 3x250 - * ├─ ... - * - */ - String dir3Name = "Directory " + Strings.repeat("3", 250); - String dir3Cipher = "dir" + Strings.repeat("3", 250); - byte[] longFilenameBytes = ("0" + dir3Cipher).getBytes(Encodings.UTF_8); - byte[] hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes); - String shortenedFileName = new Base32().encodeAsString(hash) + ".lng"; - - TestFolder ddLvl2Dir = new TestFolder(d, "33", "/d/33"); - TestFolder ddFolder = new TestFolder(ddLvl2Dir, "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD", "/d/33/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD"); - - TestFile testDir3DirFile = new TestFile(aaFolder, shortenedFileName, "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/" + shortenedFileName, Optional.empty(), Optional.empty()); - - CloudFile testDir3NameFile = metadataFile(shortenedFileName); - - Mockito.when(fileNameCryptor.encryptFilename(dir3Name, dirIdRoot.getBytes())).thenReturn(dir3Cipher); - Mockito.when(fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), dir3Cipher, dirIdRoot.getBytes())).thenReturn(dir3Name); - Mockito.when(fileNameCryptor.hashDirectoryId(AdditionalMatchers.not(Mockito.eq("")))).thenReturn("33DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD"); - - Mockito.when(cloudContentRepository.folder(d, "33")).thenReturn(ddLvl2Dir); - Mockito.when(cloudContentRepository.folder(ddLvl2Dir, "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD")).thenReturn(ddFolder); - - CryptoFolder cryptoFolder3 = new CryptoFolder(root, dir3Name, "/" + dir3Name, testDir3DirFile); - - Mockito.when(fileNameCryptor.hashDirectoryId(AdditionalMatchers.not(Mockito.eq("")))).thenReturn("33DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD"); - - Mockito.when(cloudContentRepository.folder(d, "33")).thenReturn(ddLvl2Dir); - Mockito.when(cloudContentRepository.folder(lvl2Dir, "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD")).thenReturn(ddFolder); - Mockito.when(dirIdCache.put(Mockito.eq(cryptoFolder3), Mockito.any())).thenReturn(new DirIdCache.DirIdInfo("dir3-id", ddFolder)); - Mockito.when(cloudContentRepository.file(aaFolder, shortenedFileName)).thenReturn(testDir3DirFile); - Mockito.when(cloudContentRepository.file(testDir3NameFile.getParent(), shortenedFileName, Optional.of(257L))).thenReturn(testDir3NameFile); - Mockito.when(cloudContentRepository.list(ddFolder)).thenReturn(new ArrayList()); - - inTest.delete(cryptoFolder3); - - Mockito.verify(cloudContentRepository).delete(ddFolder); - Mockito.verify(cloudContentRepository).delete(testDir3DirFile); - Mockito.verify(dirIdCache).evict(cryptoFolder3); - } - - @Test - @DisplayName("move(\"/File 4\", \"/Directory 1/File 4\")") - public void testMoveShortFileToNewShortFile() throws BackendException { - TestFolder bbLvl2Dir = new TestFolder(d, "11", "/d/11"); - TestFolder bbFolder = new TestFolder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"); - - TestFile testFile4 = new TestFile(aaFolder, "file4", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/file4", Optional.empty(), Optional.empty()); - TestFile testMovedFile4 = new TestFile(bbFolder, "file4", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/file4", Optional.empty(), Optional.empty()); - CryptoFile cryptoFile4 = new CryptoFile(root, "File 4", "/File 4", Optional.empty(), testFile4); - CryptoFile cryptoMovedFile4 = new CryptoFile(cryptoFolder1, "File 4", "/Directory 1/File 4", Optional.empty(), testMovedFile4); - - Mockito.when(cloudContentRepository.file(aaFolder, "file4")).thenReturn(testFile4); - Mockito.when(cloudContentRepository.file(bbFolder, "file4")).thenReturn(testMovedFile4); - Mockito.when(cloudContentRepository.move(testFile4, testMovedFile4)).thenReturn(testMovedFile4); - Mockito.when(cloudContentRepository.folder(rootFolder, "d")).thenReturn(d); - Mockito.when(cloudContentRepository.folder(d, "11")).thenReturn(bbLvl2Dir); - Mockito.when(cloudContentRepository.folder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB")).thenReturn(bbFolder); - Mockito.when(cloudContentRepository.folder(bbFolder, "file4")).thenReturn(null); - Mockito.when(dirIdCache.put(Mockito.eq(cryptoFolder1), Mockito.any())).thenReturn(new DirIdCache.DirIdInfo(dirId1, bbFolder)); - - Mockito.when(fileNameCryptor.encryptFilename("File 4", dirId1.getBytes())).thenReturn("file4"); - Mockito.when(fileNameCryptor.encryptFilename("File 4", dirIdRoot.getBytes())).thenReturn("file4"); - - CryptoFile result = inTest.move(cryptoFile4, cryptoMovedFile4); - - Assertions.assertEquals("File 4", result.getName()); - - Mockito.verify(cloudContentRepository).move(testFile4, testMovedFile4); - } - - @Test - @DisplayName("move(\"/File 4\", \"/Directory 1/File 4x250\")") - public void testMoveShortFileToNewLongFile() throws BackendException { - String file4Name = "File " + Strings.repeat("4", 250); - String file4Cipher = "file" + Strings.repeat("4", 250); - byte[] longFilenameBytes = file4Cipher.getBytes(Encodings.UTF_8); - byte[] hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes); - String shortenedFileName = BaseEncoding.base32().encode(hash) + ".lng"; - - TestFolder bbLvl2Dir = new TestFolder(d, "11", "/d/11"); - TestFolder bbFolder = new TestFolder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"); - - TestFile testFile4 = new TestFile(aaFolder, "file4", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/file4", Optional.empty(), Optional.empty()); - CryptoFile cryptoFile4 = new CryptoFile(root, "File 4", "/File 4", Optional.empty(), testFile4); - - TestFile testFile4ContentFile = new TestFile(bbFolder, shortenedFileName, "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/" + shortenedFileName, Optional.empty(), Optional.empty()); - - CloudFile testFile4NameFile = metadataFile(shortenedFileName); - - CryptoFile cryptoMovedFile4 = new CryptoFile(cryptoFolder1, file4Name, "/Directory 1/" + file4Name, Optional.empty(), testFile4ContentFile); - - Mockito.when(cloudContentRepository.move(testFile4, testFile4ContentFile)).thenReturn(testFile4ContentFile); - Mockito.when(cloudContentRepository.create(testFile4NameFile.getParent())).thenReturn(testFile4NameFile.getParent()); - Mockito.when(cloudContentRepository.write(Mockito.eq(testFile4NameFile), Mockito.any(), Mockito.any(), Mockito.eq(true), Mockito.anyLong())).thenAnswer(invocationOnMock -> { - DataSource in = invocationOnMock.getArgument(1); - String dirContent = new BufferedReader(new InputStreamReader(in.open(context), StandardCharsets.UTF_8)).readLine(); - assertThat(dirContent, is(file4Cipher)); - return testFile4NameFile; - }); - - Mockito.when(dirIdCache.put(Mockito.eq(cryptoFolder1), Mockito.any())).thenReturn(new DirIdCache.DirIdInfo(dirId1, bbFolder)); - - Mockito.when(fileNameCryptor.encryptFilename("File 4", dirIdRoot.getBytes())).thenReturn("file4"); - Mockito.when(fileNameCryptor.encryptFilename(file4Name, dirId1.getBytes())).thenReturn(file4Cipher); - - CloudFile targetFile = inTest.file(cryptoFolder1, file4Name); // needed due to ugly side effect - CryptoFile result = inTest.move(cryptoFile4, cryptoMovedFile4); - - Assertions.assertEquals(file4Name, result.getName()); - - Mockito.verify(cloudContentRepository).create(testFile4NameFile.getParent()); - Mockito.verify(cloudContentRepository).move(testFile4, testFile4ContentFile); - Mockito.verify(cloudContentRepository).write(Mockito.eq(testFile4NameFile), Mockito.any(), Mockito.any(), Mockito.eq(true), Mockito.anyLong()); - } - - @Test - @DisplayName("move(\"/File 4x250\", \"/Directory 1/File 4x250\")") - public void testMoveLongFileToNewLongFile() throws BackendException { - String file4Name = "File " + Strings.repeat("4", 250); - String file4Cipher = "file" + Strings.repeat("4", 250); - byte[] longFilenameBytes = file4Cipher.getBytes(Encodings.UTF_8); - byte[] hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes); - String shortenedFileName = BaseEncoding.base32().encode(hash) + ".lng"; - - TestFolder bbLvl2Dir = new TestFolder(d, "11", "/d/11"); - TestFolder bbFolder = new TestFolder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"); - - TestFile testFile4ContentFileOld = new TestFile(aaFolder, shortenedFileName, "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/" + shortenedFileName, Optional.empty(), Optional.empty()); - - CryptoFile cryptoFile4Old = new CryptoFile(root, file4Name, "/" + file4Name, Optional.empty(), testFile4ContentFileOld); - - TestFile testFile4ContentFile = new TestFile(bbFolder, shortenedFileName, "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/" + shortenedFileName, Optional.empty(), Optional.empty()); - CloudFile testFile4NameFile = metadataFile(shortenedFileName); - - CryptoFile cryptoMovedFile4 = new CryptoFile(cryptoFolder1, file4Name, "/Directory 1/" + file4Name, Optional.empty(), testFile4ContentFile); - - Mockito.when(cloudContentRepository.move(testFile4ContentFileOld, testFile4ContentFile)).thenReturn(testFile4ContentFile); - Mockito.when(cloudContentRepository.create(testFile4NameFile.getParent())).thenReturn(testFile4NameFile.getParent()); - Mockito.when(cloudContentRepository.write(Mockito.eq(testFile4NameFile), Mockito.any(), Mockito.any(), Mockito.eq(true), Mockito.anyLong())).thenAnswer(invocationOnMock -> { - DataSource in = invocationOnMock.getArgument(1); - String dirContent = new BufferedReader(new InputStreamReader(in.open(context), StandardCharsets.UTF_8)).readLine(); - assertThat(dirContent, is(file4Cipher)); - return testFile4NameFile; - }); - - Mockito.when(dirIdCache.put(Mockito.eq(cryptoFolder1), Mockito.any())).thenReturn(new DirIdCache.DirIdInfo(dirId1, bbFolder)); - - Mockito.when(fileNameCryptor.encryptFilename(file4Name, dirIdRoot.getBytes())).thenReturn(file4Cipher); - Mockito.when(fileNameCryptor.encryptFilename(file4Name, dirId1.getBytes())).thenReturn(file4Cipher); - - CloudFile targetFile = inTest.file(cryptoFolder1, file4Name); // needed due to ugly side effect - CryptoFile result = inTest.move(cryptoFile4Old, cryptoMovedFile4); - - Assertions.assertEquals(file4Name, result.getName()); - - Mockito.verify(cloudContentRepository).create(testFile4NameFile.getParent()); - Mockito.verify(cloudContentRepository).write(Mockito.eq(testFile4NameFile), Mockito.any(), Mockito.any(), Mockito.eq(true), Mockito.anyLong()); - Mockito.verify(cloudContentRepository).move(testFile4ContentFileOld, testFile4ContentFile); - } - - @Test - @DisplayName("move(\"/Directory 1/File 4x250\", \"/File 4\")") - public void testMoveLongFileToNewShortFile() throws BackendException { - String file4Name = "File " + Strings.repeat("4", 250); - String file4Cipher = "file" + Strings.repeat("4", 250); - byte[] longFilenameBytes = file4Cipher.getBytes(Encodings.UTF_8); - byte[] hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes); - String shortenedFileName = BaseEncoding.base32().encode(hash) + ".lng"; - - TestFolder bbLvl2Dir = new TestFolder(d, "11", "/d/11"); - TestFolder bbFolder = new TestFolder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"); - - TestFile testFile4 = new TestFile(aaFolder, "file4", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/file4", Optional.empty(), Optional.empty()); - CryptoFile cryptoFile4 = new CryptoFile(root, "File 4", "/File 4", Optional.empty(), testFile4); - - TestFile testFile4DirFile = new TestFile(bbFolder, "contents.c9r", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/" + shortenedFileName, Optional.empty(), Optional.empty()); - CloudFile testFile4NameFile = metadataFile(shortenedFileName); - - CryptoFile cryptoMovedFile4 = new CryptoFile(cryptoFolder1, file4Name, "/Directory 1/" + file4Name, Optional.empty(), testFile4DirFile); - - Mockito.when(cloudContentRepository.file(aaFolder, "file4.c9r")).thenReturn(testFile4); - Mockito.when(cloudContentRepository.move(testFile4DirFile, testFile4)).thenReturn(testFile4); - Mockito.when(cloudContentRepository.write(Mockito.eq(testFile4NameFile), Mockito.any(), Mockito.any(), Mockito.eq(true), Mockito.anyLong())).thenAnswer(invocationOnMock -> { - DataSource in = invocationOnMock.getArgument(1); - String dirContent = new BufferedReader(new InputStreamReader(in.open(context), StandardCharsets.UTF_8)).readLine(); - assertThat(dirContent, is(file4Cipher + ".c9r")); - return testFile4NameFile; - }); - - Mockito.when(dirIdCache.put(Mockito.eq(cryptoFolder1), Mockito.any())).thenReturn(new DirIdCache.DirIdInfo(dirId1, bbFolder)); - - Mockito.when(fileNameCryptor.encryptFilename("File 4", dirIdRoot.getBytes())).thenReturn("file4"); - Mockito.when(fileNameCryptor.encryptFilename(file4Name, dirId1.getBytes())).thenReturn(file4Cipher); - - CryptoFile result = inTest.move(cryptoMovedFile4, cryptoFile4); - Assertions.assertEquals(cryptoFile4, result); - - Mockito.verify(cloudContentRepository).move(testFile4DirFile, testFile4); - } - - @Test - @DisplayName("move(\"/Directory 1\", \"/Directory 15\")") - public void testMoveShortFolderToNewShortFolder() throws BackendException { - TestFolder bbLvl2Dir = new TestFolder(d, "11", "/d/11"); - TestFolder bbFolder = new TestFolder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"); - - TestFile testDir15DirFile = new TestFile(aaFolder, "0dir15", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/0dir15", Optional.empty(), Optional.empty()); - - CryptoFolder cryptoFolder15 = new CryptoFolder(root, "Directory 15", "/Directory 15/", testDir15DirFile); - - Mockito.when(cloudContentRepository.folder(rootFolder, "d")).thenReturn(d); - Mockito.when(cloudContentRepository.folder(d, "11")).thenReturn(bbLvl2Dir); - Mockito.when(cloudContentRepository.folder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB")).thenReturn(bbFolder); - Mockito.when(dirIdCache.put(Mockito.eq(cryptoFolder1), Mockito.any())).thenReturn(new DirIdCache.DirIdInfo(dirId1, bbFolder)); - Mockito.when(dirIdCache.put(Mockito.eq(cryptoFolder15), Mockito.any())).thenReturn(new DirIdCache.DirIdInfo(dirId1, bbFolder)); - - Mockito.when(cloudContentRepository.file(aaFolder, "0dir15")).thenReturn(testDir15DirFile); - Mockito.when(cloudContentRepository.move(cryptoFolder1.getDirFile(), testDir15DirFile)).thenReturn(testDir15DirFile); - - Mockito.when(fileNameCryptor.encryptFilename("Directory 15", dirIdRoot.getBytes())).thenReturn(dirId1); - - CryptoFolder result = inTest.move(cryptoFolder1, cryptoFolder15); - - Mockito.verify(cloudContentRepository).move(cryptoFolder1.getDirFile(), testDir15DirFile); - Mockito.verify(dirIdCache).evict(cryptoFolder1); - Mockito.verify(dirIdCache).evict(cryptoFolder15); - } - - @Test - @DisplayName("move(\"/Directory 1\", \"/Directory 15x200\")") - public void testMoveShortFolderToNewLongFolder() throws BackendException { - String dir15Name = "Dir " + Strings.repeat("15", 250); - String dir15Cipher = "dir" + Strings.repeat("15", 250); - byte[] longFilenameBytes = ("0" + dir15Cipher).getBytes(Encodings.UTF_8); - byte[] hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes); - String shortenedFileName = BaseEncoding.base32().encode(hash) + ".lng"; - - TestFolder bbLvl2Dir = new TestFolder(d, "11", "/d/11"); - TestFolder bbFolder = new TestFolder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"); - - TestFile testDir15DirFile = new TestFile(aaFolder, shortenedFileName, "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/" + shortenedFileName, Optional.empty(), Optional.empty()); - CloudFile testDir15NameFile = metadataFile(shortenedFileName); - - CryptoFolder cryptoFolder15 = new CryptoFolder(root, dir15Name, "/" + dir15Name, testDir15DirFile); - - Mockito.when(fileNameCryptor.encryptFilename(dir15Name, dirId1.getBytes())).thenReturn(dir15Cipher); - - Mockito.when(cloudContentRepository.file(aaFolder, "dir15.c9r", Optional.ofNullable(null))).thenReturn(new TestFile(aaFolder, "dir15.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/dir15.c9r", Optional.empty(), Optional.empty())); - Mockito.when(dirIdCache.put(Mockito.eq(cryptoFolder1), Mockito.any())).thenReturn(new DirIdCache.DirIdInfo(dirId1, bbFolder)); - Mockito.when(dirIdCache.put(Mockito.eq(cryptoFolder15), Mockito.any())).thenReturn(new DirIdCache.DirIdInfo(dirId1, bbFolder)); - - Mockito.when(cloudContentRepository.move(cryptoFolder1.getDirFile(), testDir15DirFile)).thenReturn(testDir15DirFile); - - Mockito.when(fileNameCryptor.encryptFilename(dir15Name, dirIdRoot.getBytes())).thenReturn(dir15Cipher); - - Mockito.when(cloudContentRepository.write(Mockito.eq(testDir15NameFile), Mockito.any(), Mockito.any(), Mockito.eq(true), Mockito.anyLong())).thenAnswer(invocationOnMock -> { - DataSource in = invocationOnMock.getArgument(1); - String dirContent = new BufferedReader(new InputStreamReader(in.open(context), StandardCharsets.UTF_8)).readLine(); - assertThat(dirContent, is("0" + dir15Cipher)); - return testDir15NameFile; - }); - - CryptoFolder targetFile = inTest.folder(root, dir15Name); // needed due to ugly side effect - CryptoFolder result = inTest.move(cryptoFolder1, cryptoFolder15); - Assertions.assertEquals(cryptoFolder15, result); - - Mockito.verify(cloudContentRepository).write(Mockito.eq(testDir15NameFile), Mockito.any(), Mockito.any(), Mockito.eq(true), Mockito.anyLong()); - Mockito.verify(cloudContentRepository).move(cryptoFolder1.getDirFile(), testDir15DirFile); - } - - @Test - @DisplayName("move(\"/Directory 15x200\", \"/Directory 3000\")") - public void testMoveLongFolderToNewShortFolder() throws BackendException { - String dir15Name = "Dir " + Strings.repeat("15", 250); - String dir15Cipher = "dir" + Strings.repeat("15", 250); - byte[] longFilenameBytes = ("0" + dir15Cipher).getBytes(Encodings.UTF_8); - byte[] hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes); - String shortenedFileName = BaseEncoding.base32().encode(hash) + ".lng"; - - TestFolder bbLvl2Dir = new TestFolder(d, "11", "/d/11"); - TestFolder bbFolder = new TestFolder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"); - - TestFile testDir15DirFile = new TestFile(aaFolder, shortenedFileName, "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/" + shortenedFileName, Optional.empty(), Optional.empty()); - - CryptoFolder cryptoFolder15 = new CryptoFolder(root, dir15Name, "/" + dir15Name, testDir15DirFile); - - Mockito.when(fileNameCryptor.encryptFilename(dir15Name, dirId1.getBytes())).thenReturn(dir15Cipher); - - Mockito.when(dirIdCache.put(Mockito.eq(cryptoFolder1), Mockito.any())).thenReturn(new DirIdCache.DirIdInfo(dirId1, bbFolder)); - - Mockito.when(fileNameCryptor.encryptFilename(dir15Name, dirIdRoot.getBytes())).thenReturn(dir15Cipher); - - TestFile testDir3DirFile = new TestFile(aaFolder, "dir3", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/dir3", Optional.empty(), Optional.empty()); - CryptoFolder cryptoFolder3 = new CryptoFolder(root, "Directory 3", "/Directory 3", testDir3DirFile); - - Mockito.when(fileNameCryptor.encryptFilename("Directory 3", dirIdRoot.getBytes())).thenReturn("dir3"); - Mockito.when(fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), "dir3", dirIdRoot.getBytes())).thenReturn("Directory 3"); - Mockito.when(fileNameCryptor.hashDirectoryId(AdditionalMatchers.not(Mockito.eq("")))).thenReturn("33DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD"); - - Mockito.when(cloudContentRepository.create(lvl2Dir)).thenReturn(lvl2Dir); - Mockito.when(cloudContentRepository.write(Mockito.eq(testDir3DirFile), Mockito.any(), Mockito.any(), Mockito.eq(false), Mockito.anyLong())).thenReturn(testDir3DirFile); - Mockito.when(cloudContentRepository.move(testDir15DirFile, testDir3DirFile)).thenReturn(testDir3DirFile); - - CryptoFolder targetFile = inTest.folder(root, cryptoFolder3.getName()); // needed due to ugly side effect - CryptoFolder result = inTest.move(cryptoFolder15, cryptoFolder3); - - Mockito.verify(cloudContentRepository).move(testDir15DirFile, cryptoFolder3.getDirFile()); - } - - private CloudFile metadataFile(String shortFilename) throws BackendException { - Mockito.when(cloudContentRepository.folder(rootFolder, "m")).thenReturn(m); - TestFolder firstLevelFolder = new TestFolder(m, shortFilename.substring(0, 2), "/m/" + shortFilename.substring(0, 2)); - Mockito.when(cloudContentRepository.folder(m, shortFilename.substring(0, 2))).thenReturn(firstLevelFolder); - TestFolder secondLevelFolder = new TestFolder(firstLevelFolder, shortFilename.substring(2, 4), "/m/" + shortFilename.substring(0, 2) + "/" + shortFilename.substring(2, 4)); - Mockito.when(cloudContentRepository.folder(firstLevelFolder, shortFilename.substring(2, 4))).thenReturn(secondLevelFolder); - TestFile file = new TestFile(secondLevelFolder, shortFilename, "/m/" + shortFilename.substring(0, 2) + "/" + shortFilename.substring(2, 4) + "/" + shortFilename, Optional.empty(), Optional.empty()); - Mockito.when(cloudContentRepository.file(secondLevelFolder, shortFilename)).thenReturn(file); - return file; - } - -} diff --git a/data/src/test/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7Test.kt b/data/src/test/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7Test.kt new file mode 100644 index 00000000..50fb9ecf --- /dev/null +++ b/data/src/test/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7Test.kt @@ -0,0 +1,969 @@ +package org.cryptomator.data.cloud.crypto + +import android.content.Context +import com.google.common.base.Strings +import com.google.common.io.BaseEncoding +import org.apache.commons.codec.binary.Base32 +import org.cryptomator.cryptolib.api.Cryptor +import org.cryptomator.cryptolib.api.FileContentCryptor +import org.cryptomator.cryptolib.api.FileHeader +import org.cryptomator.cryptolib.api.FileHeaderCryptor +import org.cryptomator.cryptolib.api.FileNameCryptor +import org.cryptomator.cryptolib.common.MessageDigestSupplier +import org.cryptomator.data.cloud.crypto.DirIdCache.DirIdInfo +import org.cryptomator.data.util.CopyStream.copyStreamToStream +import org.cryptomator.domain.Cloud +import org.cryptomator.domain.CloudFile +import org.cryptomator.domain.CloudFolder +import org.cryptomator.domain.CloudNode +import org.cryptomator.domain.exception.BackendException +import org.cryptomator.domain.repository.CloudContentRepository +import org.cryptomator.domain.usecases.ProgressAware +import org.cryptomator.domain.usecases.cloud.ByteArrayDataSource.Companion.from +import org.cryptomator.domain.usecases.cloud.DataSource +import org.hamcrest.CoreMatchers +import org.hamcrest.MatcherAssert +import org.hamcrest.Matchers +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.mockito.AdditionalMatchers +import org.mockito.Mockito +import org.mockito.invocation.InvocationOnMock +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import java.io.BufferedReader +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.InputStreamReader +import java.io.OutputStream +import java.nio.ByteBuffer +import java.nio.charset.StandardCharsets +import java.util.ArrayList +import java.util.function.Supplier +import kotlin.io.path.createTempDirectory +import kotlin.io.path.deleteExisting + +/** + * ` + * path/to/vault/d/00 + * ├─ Directory 1 + * │ ├─ Directory 2 + * │ ├─ Directory 3x250 + * │ │ ├─ Directory 4x250 + * │ │ └─ File 5x250 + * │ └─ File 3 + * ├─ File 1 + * ├─ File 2 + * ├─ File 4 +` * + */ +internal class CryptoImplVaultFormatPre7Test { + + private var context: Context = mock() + private var cloud: Cloud = mock() + private var cryptoCloud: CryptoCloud = mock() + private var cryptor: Cryptor = mock() + private var cloudContentRepository: CloudContentRepository = mock() + private var dirIdCache: DirIdCache = mock() + private var fileNameCryptor: FileNameCryptor = mock() + private var fileContentCryptor: FileContentCryptor = mock() + private var fileHeaderCryptor: FileHeaderCryptor = mock() + private var tmpDir = createTempDirectory() + + private val dirIdRoot = "" + private val dirId1 = "dir1-id" + private val dirId2 = "dir2-id" + private val rootFolder: TestFolder = RootTestFolder(cloud) + private val d = TestFolder(rootFolder, "d", "/d") + private val m = TestFolder(rootFolder, "m", "/m") + private var lvl2Dir = TestFolder(d, "00", "/d/00") + private val aaFolder = TestFolder(lvl2Dir, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") + + private lateinit var inTest: CryptoImplVaultFormatPre7 + private lateinit var root: RootCryptoFolder + private lateinit var cryptoFile1: CryptoFile + private lateinit var cryptoFile2: CryptoFile + private lateinit var cryptoFile4: CryptoFile + private lateinit var cryptoFolder1: CryptoFolder + + private fun any(type: Class): T = Mockito.any(type) + + @BeforeEach + @Throws(BackendException::class) + fun setup() { + whenever(context.cacheDir).thenReturn(tmpDir.toFile()) + + whenever(cryptor.fileNameCryptor()).thenReturn(fileNameCryptor) + whenever(cryptor.fileNameCryptor()).thenReturn(fileNameCryptor) + whenever(cryptor.fileContentCryptor()).thenReturn(fileContentCryptor) + whenever(cryptor.fileHeaderCryptor()).thenReturn(fileHeaderCryptor) + + root = RootCryptoFolder(cryptoCloud) + inTest = CryptoImplVaultFormatPre7(context, Supplier { cryptor }, cloudContentRepository, rootFolder, dirIdCache) + + whenever(fileNameCryptor.hashDirectoryId(dirIdRoot)).thenReturn("00AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") + whenever(fileNameCryptor.hashDirectoryId(dirId1)).thenReturn("11BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB") + whenever(fileNameCryptor.hashDirectoryId(dirId2)).thenReturn("22CCCCCCCCCCCCCCCCCCCCCCCCCCCCCC") + whenever(fileNameCryptor.decryptFilename(BaseEncoding.base32(), "dir1", dirIdRoot.toByteArray())).thenReturn("Directory 1") + whenever(fileNameCryptor.decryptFilename(BaseEncoding.base32(), "file1", dirIdRoot.toByteArray())).thenReturn("File 1") + whenever(fileNameCryptor.decryptFilename(BaseEncoding.base32(), "file2", dirIdRoot.toByteArray())).thenReturn("File 2") + whenever(fileNameCryptor.decryptFilename(BaseEncoding.base32(), "dir2", dirId1.toByteArray())).thenReturn("Directory 2") + whenever(fileNameCryptor.decryptFilename(BaseEncoding.base32(), "file3", dirId1.toByteArray())).thenReturn("File 3") + whenever(fileNameCryptor.decryptFilename(BaseEncoding.base32(), "file4", dirIdRoot.toByteArray())).thenReturn("File 4") + + val testFile1 = TestFile(aaFolder, "file1.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/file1.c9r", null, null) + val testFile2 = TestFile(aaFolder, "file2.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/file2.c9r", null, null) + val testFile4 = TestFile(aaFolder, "file4.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/file4.c9r", null, null) + val testDir1 = TestFile(aaFolder, "0dir1", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/0dir1", null, null) + val rootItems: ArrayList = object : ArrayList() { + init { + add(testFile1) + add(testFile2) + add(testFile4) + add(testDir1) + } + } + cryptoFile1 = CryptoFile(root, "File 1", "/File 1", 15L, testFile1) + cryptoFile2 = CryptoFile(root, "File 2", "/File 2", null, testFile2) + cryptoFile4 = CryptoFile(root, "File 4", "/File 4", null, testFile4) + cryptoFolder1 = CryptoFolder(root, "Directory 1", "/Directory 1", testDir1) + + whenever(cloudContentRepository.folder(rootFolder, "d")).thenReturn(d) + whenever(cloudContentRepository.folder(d, "00")).thenReturn(lvl2Dir) + whenever(cloudContentRepository.folder(lvl2Dir, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")).thenReturn(aaFolder) + whenever(cloudContentRepository.file(aaFolder, "0dir1")).thenReturn(testDir1) + whenever(cloudContentRepository.exists(testDir1)).thenReturn(true) + Mockito.doAnswer { invocation: InvocationOnMock -> + val out = invocation.getArgument(2) + copyStreamToStream(ByteArrayInputStream(dirId1.toByteArray()), out) + null + }.`when`(cloudContentRepository).read(eq(cryptoFolder1.dirFile!!), any(), any(), any()) + whenever>(cloudContentRepository.list(aaFolder)).thenReturn(rootItems) + whenever(dirIdCache.put(eq(root), any())).thenReturn(DirIdInfo("", aaFolder)) + } + + @AfterEach + fun tearDown() { + tmpDir.deleteExisting() + } + + @Test + @DisplayName("list(\"/\")") + @Throws(BackendException::class) + fun testListRoot() { + val rootDirContent = inTest.list(root) + + Matchers.contains(rootDirContent, cryptoFile1) + Matchers.contains(rootDirContent, cryptoFile2) + Matchers.contains(rootDirContent, cryptoFile4) + Matchers.contains(rootDirContent, cryptoFolder1) + } + + @Test + @DisplayName("list(\"/Directory 1/Directory 3x250\")") + @Throws(BackendException::class) + fun testListDirectory3x250() { + val dir3Name = "Directory " + Strings.repeat("3", 250) + val dir3Cipher = "dir" + Strings.repeat("3", 250) + val longFilenameBytes = "$dir3Cipher.c9r".toByteArray(StandardCharsets.UTF_8) + val hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes) + val shortenedFileName = BaseEncoding.base32().encode(hash) + ".c9s" + val bbLvl2Dir = TestFolder(d, "11", "/d/11") + val bbFolder = TestFolder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB") + val ddLvl2Dir = TestFolder(d, "33", "/d/33") + val ddFolder = TestFolder(ddLvl2Dir, "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD", "/d/33/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD") + val testDir3 = TestFolder(bbFolder, shortenedFileName, "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/$shortenedFileName") + val testDir3DirFile = TestFile(testDir3, "dir.c9r", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/$shortenedFileName/dir.c9r", null, null) + val testDir3NameFile = TestFile(testDir3, "name.c9s", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/$shortenedFileName/name.c9s", null, null) + + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base32(), dir3Name, dirId1.toByteArray())).thenReturn(dir3Cipher) + whenever(fileNameCryptor.decryptFilename(BaseEncoding.base32(), dir3Cipher, dirId1.toByteArray())).thenReturn(dir3Name) + whenever(fileNameCryptor.hashDirectoryId(AdditionalMatchers.not(eq("")))).thenReturn("33DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD") + whenever(cloudContentRepository.folder(d, "33")).thenReturn(ddLvl2Dir) + whenever(cloudContentRepository.folder(ddLvl2Dir, "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD")).thenReturn(ddFolder) + whenever(cloudContentRepository.file(testDir3, "dir.c9r")).thenReturn(testDir3DirFile) + whenever(cloudContentRepository.file(testDir3, "name.c9s")).thenReturn(testDir3NameFile) + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base32(), "Directory 1", dirIdRoot.toByteArray())).thenReturn("dir1") + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base32(), "Directory 2", dirId1.toByteArray())).thenReturn("dir2") + + val cryptoFolder3 = CryptoFolder(cryptoFolder1, dir3Name, "/Directory 1/$dir3Name", testDir3DirFile) + + Mockito.doAnswer { invocation: InvocationOnMock -> + val out = invocation.getArgument(2) + copyStreamToStream(ByteArrayInputStream("dir3-id".toByteArray()), out) + null + }.`when`(cloudContentRepository).read(eq(cryptoFolder3.dirFile!!), any(), any(), any()) + + /* + * │ ├─ Directory 3x250 + * │ │ ├─ Directory 4x250 + * │ │ └─ File 5x250 + */ + val dir4Name = "Directory " + Strings.repeat("4", 250) + val dir4Cipher = "dir" + Strings.repeat("4", 250) + val longFilenameBytes4 = "$dir4Cipher.c9r".toByteArray(StandardCharsets.UTF_8) + val hash4 = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes4) + val shortenedFileName4 = BaseEncoding.base32().encode(hash4) + ".c9s" + val directory4x250 = TestFolder(ddFolder, shortenedFileName4, "/d/33/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD$shortenedFileName4") + val testDir4DirFile = TestFile(directory4x250, "dir.c9r", "/d/33/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD/$shortenedFileName4/dir.c9r", null, null) + val testDir4NameFile = TestFile(directory4x250, "name.c9s", "/d/33/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD/$shortenedFileName4/name.c9s", null, null) + + whenever(cloudContentRepository.file(directory4x250, "dir.c9r")).thenReturn(testDir4DirFile) + whenever(cloudContentRepository.file(directory4x250, "name.c9s")).thenReturn(testDir4NameFile) + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base32(), dir4Name, "dir3-id".toByteArray())).thenReturn(dir4Cipher) + whenever(fileNameCryptor.decryptFilename(BaseEncoding.base32(), dir4Cipher, "dir3-id".toByteArray())).thenReturn(dir4Name) + Mockito.doAnswer { invocation: InvocationOnMock -> + val out = invocation.getArgument(2) + copyStreamToStream(ByteArrayInputStream(dir4Cipher.toByteArray(charset("UTF-8"))), out) + null + }.`when`(cloudContentRepository).read(eq(testDir4NameFile), any(), any(), any()) + + val dir4Files: ArrayList = object : ArrayList() { + init { + add(testDir4DirFile) + add(testDir4NameFile) + } + } + val file5Name = "File " + Strings.repeat("5", 250) + val file5Cipher = "file" + Strings.repeat("5", 250) + val longFilenameBytes5 = "$file5Cipher.c9r".toByteArray(StandardCharsets.UTF_8) + val hash5 = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes5) + val shortenedFileName5 = BaseEncoding.base32().encode(hash5) + ".c9s" + val directory5x250 = TestFolder(ddFolder, shortenedFileName5, "/d/33/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD$shortenedFileName5") + val testFile5ContentFile = TestFile(directory5x250, "contents.c9r", "/d/33/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD/$shortenedFileName5/contents.c9r", null, null) + val testFile5NameFile = TestFile(directory5x250, "name.c9s", "/d/33/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD/$shortenedFileName5/name.c9s", null, null) + + whenever(cloudContentRepository.file(directory5x250, "contents.c9r")).thenReturn(testFile5ContentFile) + whenever(cloudContentRepository.file(directory5x250, "name.c9s")).thenReturn(testFile5NameFile) + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base32(), file5Name, "dir3-id".toByteArray())).thenReturn(file5Cipher) + whenever(fileNameCryptor.decryptFilename(BaseEncoding.base32(), file5Cipher, "dir3-id".toByteArray())).thenReturn(file5Name) + Mockito.doAnswer { invocation: InvocationOnMock -> + val out = invocation.getArgument(2) + copyStreamToStream(ByteArrayInputStream(file5Cipher.toByteArray(charset("UTF-8"))), out) + null + }.`when`(cloudContentRepository).read(eq(testFile5NameFile), any(), any(), any()) + val dir5Files: ArrayList = object : ArrayList() { + init { + add(testFile5ContentFile) + add(testFile5NameFile) + } + } + val dir3Items: ArrayList = object : ArrayList() { + init { + add(directory4x250) + add(directory5x250) + } + } + whenever(cloudContentRepository.exists(testDir3DirFile)).thenReturn(true) + whenever>(cloudContentRepository.list(ddFolder)).thenReturn(dir3Items) + whenever>(cloudContentRepository.list(directory4x250)).thenReturn(dir4Files) + whenever>(cloudContentRepository.list(directory5x250)).thenReturn(dir5Files) + whenever(dirIdCache.put(eq(cryptoFolder3), any())).thenReturn(DirIdInfo("dir3-id", ddFolder)) + whenever(dirIdCache[cryptoFolder3]).thenReturn(DirIdInfo("dir3-id", ddFolder)) + + val folder3Content = inTest.list(cryptoFolder3) + + Matchers.contains(folder3Content, CryptoFolder(cryptoFolder3, dir4Name, "/Directory 1/$dir3Name/$dir4Name", testDir4DirFile)) + Matchers.contains(folder3Content, CryptoFile(cryptoFolder3, file5Name, "/Directory 1/$dir3Name/$file5Name", null, testFile5ContentFile)) + } + + @Test + @DisplayName("read(\"/File 1\", NO_PROGRESS_AWARE)") + @Throws(BackendException::class) + fun testReadFromShortFile() { + val file1Content = "hhhhhTOPSECRET!TOPSECRET!TOPSECRET!TOPSECRET!".toByteArray() + val header: FileHeader = mock() + + whenever(fileContentCryptor.cleartextChunkSize()).thenReturn(8) + whenever(fileContentCryptor.ciphertextChunkSize()).thenReturn(10) + whenever(fileHeaderCryptor.headerSize()).thenReturn(5) + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base32(), "File 1", dirIdRoot.toByteArray())).thenReturn("file1") + whenever(fileHeaderCryptor.decryptHeader(StandardCharsets.UTF_8.encode("hhhhh"))).thenReturn(header) + whenever(fileContentCryptor.decryptChunk(eq(StandardCharsets.UTF_8.encode("TOPSECRET!")), any(), eq(header), any())) + .then { invocation: InvocationOnMock? -> StandardCharsets.UTF_8.encode("geheim!!") } + Mockito.doAnswer { invocation: InvocationOnMock -> + val out = invocation.getArgument(2) + copyStreamToStream(ByteArrayInputStream(file1Content), out) + null + }.`when`(cloudContentRepository).read(eq(cryptoFile1.cloudFile), any(), any(), any()) + + val outputStream = ByteArrayOutputStream(1000) + inTest.read(cryptoFile1, outputStream, ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD) + + MatcherAssert.assertThat(outputStream.toString(), CoreMatchers.`is`("geheim!!geheim!!geheim!!geheim!!")) + } + + @Test + @DisplayName("read(\"/File 15x250\", NO_PROGRESS_AWARE)") + @Throws(BackendException::class) + fun testReadFromLongFile() { + val file3Name = "File " + Strings.repeat("15", 250) + val longFilenameBytes = file3Name.toByteArray(StandardCharsets.UTF_8) + val hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes) + val shortenedFileName = BaseEncoding.base32().encode(hash) + ".c9s" + val testFile3Folder = TestFolder(aaFolder, shortenedFileName, "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/$shortenedFileName") + val testFile3ContentFile = TestFile(testFile3Folder, "content.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/$shortenedFileName/content.c9r", null, null) + val file1Content = "hhhhhTOPSECRET!TOPSECRET!TOPSECRET!TOPSECRET!".toByteArray() + val header: FileHeader = mock() + + whenever(fileContentCryptor.cleartextChunkSize()).thenReturn(8) + whenever(fileContentCryptor.ciphertextChunkSize()).thenReturn(10) + whenever(fileHeaderCryptor.headerSize()).thenReturn(5) + whenever(fileHeaderCryptor.decryptHeader(StandardCharsets.UTF_8.encode("hhhhh"))).thenReturn(header) + whenever(fileContentCryptor.decryptChunk(eq(StandardCharsets.UTF_8.encode("TOPSECRET!")), any(), eq(header), any())) + .then { invocation: InvocationOnMock? -> StandardCharsets.UTF_8.encode("geheim!!") } + val cryptoFile15 = CryptoFile(root, file3Name, "/$file3Name", null, testFile3ContentFile) + Mockito.doAnswer { invocation: InvocationOnMock -> + val out = invocation.getArgument(2) + copyStreamToStream(ByteArrayInputStream(file1Content), out) + null + }.`when`(cloudContentRepository).read(eq(cryptoFile15.cloudFile), any(), any(), any()) + + val outputStream = ByteArrayOutputStream(1000) + inTest.read(cryptoFile15, outputStream, ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD) + + MatcherAssert.assertThat(outputStream.toString(), CoreMatchers.`is`("geheim!!geheim!!geheim!!geheim!!")) + } + + @Test + @DisplayName("write(\"/File 1\", text, NO_PROGRESS_AWARE, replace=false, 10bytes)") + @Throws(BackendException::class) + fun testWriteToShortFile() { + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base32(), "File 1", dirIdRoot.toByteArray())).thenReturn("file1") + val header: FileHeader = mock() + whenever(fileHeaderCryptor.create()).thenReturn(header) + whenever(fileHeaderCryptor.encryptHeader(header)).thenReturn(ByteBuffer.wrap("hhhhh".toByteArray())) + whenever(fileHeaderCryptor.headerSize()).thenReturn(5) + whenever(fileContentCryptor.cleartextChunkSize()).thenReturn(10) + whenever(fileContentCryptor.ciphertextChunkSize()).thenReturn(10) + whenever( + fileContentCryptor.encryptChunk( + any(ByteBuffer::class.java), any(), any( + FileHeader::class.java + ) + ) + ).thenAnswer { invocation: InvocationOnMock -> + val input = invocation.getArgument(0) + val inStr = StandardCharsets.UTF_8.decode(input).toString() + ByteBuffer.wrap(inStr.lowercase().toByteArray(StandardCharsets.UTF_8)) + } + whenever(cloudContentRepository.write(eq(cryptoFile1.cloudFile), any(DataSource::class.java), any(), eq(false), any())) + .thenAnswer { invocationOnMock: InvocationOnMock -> + val inputStream = invocationOnMock.getArgument(1) + val encrypted = BufferedReader(InputStreamReader(inputStream.open(context)!!, StandardCharsets.UTF_8)).readLine() + MatcherAssert.assertThat(encrypted, CoreMatchers.`is`("hhhhhtopsecret!")) + invocationOnMock.getArgument(0) + } + + // just for the exists check + whenever(cloudContentRepository.file(aaFolder, "0file1")).thenReturn(TestFile(rootFolder, "0file1", aaFolder.path + "0file1", null, null)) + + val cryptoFile = inTest.write(cryptoFile1, from("TOPSECRET!".toByteArray(StandardCharsets.UTF_8)), ProgressAware.NO_OP_PROGRESS_AWARE_UPLOAD, false, 10L) + + MatcherAssert.assertThat(cryptoFile, CoreMatchers.`is`(cryptoFile1)) + } + + @Test + @DisplayName("write(\"/File 15x250\", text, NO_PROGRESS_AWARE, replace=false, 10bytes)") + @Throws(BackendException::class) + fun testWriteToLongFile() { + val file15Name = "File " + Strings.repeat("15", 250) + val file15Cipher = "file" + Strings.repeat("15", 250) + val longFilenameBytes = file15Cipher.toByteArray(StandardCharsets.UTF_8) + val hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes) + val shortenedFileName = Base32().encodeAsString(hash) + ".lng" + val metaDataDFile: CloudFile = TestFile(aaFolder, shortenedFileName, "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/$shortenedFileName", null, null) + val metaDataMFile = metadataFile(shortenedFileName) + val cryptoFile15 = CryptoFile(root, file15Name, "/$file15Name", 15L, metaDataDFile) + + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base32(), file15Name, dirIdRoot.toByteArray())).thenReturn(file15Cipher) + val header: FileHeader = mock() + whenever(fileHeaderCryptor.create()).thenReturn(header) + whenever(fileHeaderCryptor.encryptHeader(header)).thenReturn(ByteBuffer.wrap("hhhhh".toByteArray())) + whenever(fileHeaderCryptor.headerSize()).thenReturn(5) + whenever(fileContentCryptor.cleartextChunkSize()).thenReturn(10) + whenever(fileContentCryptor.ciphertextChunkSize()).thenReturn(10) + whenever( + fileContentCryptor.encryptChunk( + any(ByteBuffer::class.java), any(), any( + FileHeader::class.java + ) + ) + ).thenAnswer { invocation: InvocationOnMock -> + val input = invocation.getArgument(0) + val inStr = StandardCharsets.UTF_8.decode(input).toString() + ByteBuffer.wrap(inStr.lowercase().toByteArray(StandardCharsets.UTF_8)) + } + whenever(cloudContentRepository.write(eq(metaDataDFile), any(DataSource::class.java), any(), eq(false), any())) + .thenAnswer { invocationOnMock: InvocationOnMock -> + val inputStream = invocationOnMock.getArgument(1) + val encrypted = BufferedReader(InputStreamReader(inputStream.open(context)!!, StandardCharsets.UTF_8)).readLine() + MatcherAssert.assertThat(encrypted, CoreMatchers.`is`("hhhhhtopsecret!")) + invocationOnMock.getArgument(0) + } + whenever(cloudContentRepository.write(eq(metaDataMFile), any(DataSource::class.java), any(), eq(true), any())) + .thenAnswer { invocationOnMock: InvocationOnMock -> + val inputStream = invocationOnMock.getArgument(1) + val encrypted = BufferedReader(InputStreamReader(inputStream.open(context)!!, StandardCharsets.UTF_8)).readLine() + MatcherAssert.assertThat(encrypted, CoreMatchers.`is`(file15Cipher)) + invocationOnMock.getArgument(0) + } + + // just for the exists check + whenever(cloudContentRepository.file(aaFolder, shortenedFileName, null)).thenReturn(metaDataDFile) + val file15Cipher0 = "0file" + Strings.repeat("15", 250) + val longFilenameBytes1 = file15Cipher0.toByteArray(StandardCharsets.UTF_8) + val hash1 = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes1) + val shortenedFileName1 = Base32().encodeAsString(hash1) + ".lng" + val metaDataMFile1 = metadataFile(shortenedFileName1) + + whenever(cloudContentRepository.file(aaFolder, shortenedFileName1)).thenReturn(metaDataMFile1) + + val res = inTest.file(root, file15Name) + + val cryptoFile = inTest.write(cryptoFile15, from("TOPSECRET!".toByteArray(StandardCharsets.UTF_8)), ProgressAware.NO_OP_PROGRESS_AWARE_UPLOAD, false, 10L) + + MatcherAssert.assertThat(cryptoFile, CoreMatchers.`is`(cryptoFile15)) + Mockito.verify(cloudContentRepository).write( + eq(metaDataDFile), any( + DataSource::class.java + ), any(), eq(false), any() + ) + Mockito.verify(cloudContentRepository).write( + eq(metaDataMFile), any( + DataSource::class.java + ), any(), eq(true), any() + ) + } + + @Test + @DisplayName("create(\"/Directory 3/\")") + @Throws(BackendException::class) + fun testCreateShortFolder() { + /* + * + * path/to/vault/d + * ├─ Directory 1 + * │ ├─ ... + * ├─ Directory 3 + * ├─ ... + * + */ + lvl2Dir = TestFolder(d, "33", "/d/33") + val ddFolder = TestFolder(lvl2Dir, "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD", "/d/33/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD") + val testDir3DirFile = TestFile(aaFolder, "0dir3", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/0dir3", null, null) + val cryptoFolder3 = CryptoFolder(root, "Directory 3", "/Directory 3", testDir3DirFile) + + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base32(), "Directory 3", dirIdRoot.toByteArray())).thenReturn("dir3") + whenever(fileNameCryptor.decryptFilename(BaseEncoding.base32(), "dir3", dirIdRoot.toByteArray())).thenReturn("Directory 3") + whenever(fileNameCryptor.hashDirectoryId(AdditionalMatchers.not(eq("")))).thenReturn("33DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD") + whenever(cloudContentRepository.folder(d, "33")).thenReturn(lvl2Dir) + whenever(cloudContentRepository.folder(lvl2Dir, "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD")).thenReturn(ddFolder) + whenever(cloudContentRepository.file(aaFolder, "0dir3")).thenReturn(testDir3DirFile) + whenever(dirIdCache.put(eq(cryptoFolder3), any())).thenReturn(DirIdInfo("dir3-id", ddFolder)) + whenever(dirIdCache[cryptoFolder3]).thenReturn(DirIdInfo("dir3-id", ddFolder)) + whenever(cloudContentRepository.create(lvl2Dir)).thenReturn(lvl2Dir) + whenever(cloudContentRepository.create(ddFolder)).thenReturn(ddFolder) + whenever(cloudContentRepository.write(eq(testDir3DirFile), any(), any(), eq(false), any())).thenReturn(testDir3DirFile) + + // just for the exists check + whenever(cloudContentRepository.file(aaFolder, "dir3", null)).thenReturn(TestFile(aaFolder, "dir3", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/dir3", null, null)) + + val cloudFolder: CloudFolder = inTest.create(cryptoFolder3) + + MatcherAssert.assertThat(cloudFolder, CoreMatchers.`is`(cryptoFolder3)) + Mockito.verify(cloudContentRepository).create(ddFolder) + Mockito.verify(cloudContentRepository).write(eq(testDir3DirFile), any(), any(), eq(false), any()) + } + + @Test + @DisplayName("create(\"/Directory 3x250/\")") + @Throws(BackendException::class) + fun testCreateLongFolder() { + /* + * + * path/to/vault/d + * ├─ Directory 1 + * │ ├─ ... + * ├─ Directory 3x250 + * ├─ ... + * + */ + val dir3Name = "Directory " + Strings.repeat("3", 250) + val dir3Cipher = "dir" + Strings.repeat("3", 250) + val longFilenameBytes = "0$dir3Cipher".toByteArray(StandardCharsets.UTF_8) + val hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes) + val shortenedFileName = Base32().encodeAsString(hash) + ".lng" + val ddLvl2Dir = TestFolder(d, "33", "/d/33") + val ddFolder = TestFolder(ddLvl2Dir, "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD", "/d/33/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD") + val testDir3DirFile = TestFile(aaFolder, shortenedFileName, "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/$shortenedFileName", null, null) + val testDir3NameFile = metadataFile(shortenedFileName) + + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base32(), dir3Name, dirIdRoot.toByteArray())).thenReturn(dir3Cipher) + whenever(fileNameCryptor.decryptFilename(BaseEncoding.base32(), dir3Cipher, dirIdRoot.toByteArray())).thenReturn(dir3Name) + whenever(fileNameCryptor.hashDirectoryId(AdditionalMatchers.not(eq("")))).thenReturn("33DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD") + whenever(cloudContentRepository.folder(d, "33")).thenReturn(ddLvl2Dir) + whenever(cloudContentRepository.folder(ddLvl2Dir, "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD")).thenReturn(ddFolder) + + val cryptoFolder3 = CryptoFolder(root, dir3Name, "/$dir3Name", testDir3DirFile) + whenever(fileNameCryptor.hashDirectoryId(AdditionalMatchers.not(eq("")))).thenReturn("33DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD") + whenever(cloudContentRepository.folder(d, "33")).thenReturn(ddLvl2Dir) + whenever(cloudContentRepository.folder(lvl2Dir, "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD")).thenReturn(ddFolder) + whenever(dirIdCache.put(eq(cryptoFolder3), any())).thenReturn(DirIdInfo("dir3-id", ddFolder)) + whenever(cloudContentRepository.create(ddLvl2Dir)).thenReturn(ddLvl2Dir) + whenever(cloudContentRepository.create(ddFolder)).thenReturn(ddFolder) + whenever(cloudContentRepository.create(testDir3NameFile.parent!!)).thenReturn(testDir3NameFile.parent!!) + whenever(cloudContentRepository.write(eq(testDir3DirFile), any(), any(), eq(false), any())).thenAnswer { invocationOnMock: InvocationOnMock -> + val inputStream = invocationOnMock.getArgument(1) + val dirContent = BufferedReader(InputStreamReader(inputStream.open(context)!!, StandardCharsets.UTF_8)).readLine() + MatcherAssert.assertThat(dirContent, CoreMatchers.`is`("dir3-id")) + testDir3DirFile + } + whenever(cloudContentRepository.write(eq(testDir3NameFile), any(), any(), eq(true), any())).thenAnswer { invocationOnMock: InvocationOnMock -> + val inputStream = invocationOnMock.getArgument(1) + val nameContent = BufferedReader(InputStreamReader(inputStream.open(context)!!, StandardCharsets.UTF_8)).readLine() + MatcherAssert.assertThat(nameContent, CoreMatchers.`is`("0$dir3Cipher")) + testDir3NameFile + } + + // just for the exists check + whenever(cloudContentRepository.file(aaFolder, shortenedFileName)).thenReturn(TestFile(aaFolder, shortenedFileName, aaFolder.path + "/" + shortenedFileName, null, null)) + + var cloudFolder = inTest.folder(root, dir3Name) + + val longFilenameBytes1 = dir3Cipher.toByteArray(StandardCharsets.UTF_8) + val hash1 = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes1) + val shortenedFileName1 = Base32().encodeAsString(hash1) + ".lng" + val testDir3NameFile1 = metadataFile(shortenedFileName1) + + whenever(cloudContentRepository.file(aaFolder, shortenedFileName1, null)).thenReturn(testDir3NameFile1) + + cloudFolder = inTest.create(cryptoFolder3) + + MatcherAssert.assertThat(cloudFolder, CoreMatchers.`is`(cryptoFolder3)) + Mockito.verify(cloudContentRepository).create(ddFolder) + Mockito.verify(cloudContentRepository).create(testDir3NameFile.parent!!) + Mockito.verify(cloudContentRepository).write(eq(testDir3DirFile), any(), any(), eq(false), any()) + Mockito.verify(cloudContentRepository).write(eq(testDir3NameFile), any(), any(), eq(true), any()) + } + + @Test + @DisplayName("delete(\"/File 4\")") + @Throws(BackendException::class) + fun testDeleteShortFile() { + inTest.delete(cryptoFile4) + Mockito.verify(cloudContentRepository).delete(cryptoFile4.cloudFile) + } + + @Test + @DisplayName("delete(\"/File 15x250\")") + @Throws(BackendException::class) + fun testDeleteLongFile() { + val file15Name = "File " + Strings.repeat("15", 250) + val file15Cipher = "file" + Strings.repeat("15", 250) + val longFilenameBytes = file15Name.toByteArray(StandardCharsets.UTF_8) + val hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes) + val shortenedFileName = Base32().encodeAsString(hash) + ".lng" + val metaDataFile = metadataFile(shortenedFileName) + + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base32(), file15Name, dirIdRoot.toByteArray())).thenReturn(file15Cipher) + val cryptoFile15 = CryptoFile(root, file15Name, "/$file15Name", 15L, metaDataFile) + + inTest.delete(cryptoFile15) + Mockito.verify(cloudContentRepository).delete(metaDataFile) + } + + @Test + @DisplayName("delete(\"/Directory 1/Directory 2/\")") + @Throws(BackendException::class) + fun testDeleteSingleShortFolder() { + val bbLvl2Dir = TestFolder(d, "11", "/d/11") + val bbFolder = TestFolder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB") + val ccLvl2Dir = TestFolder(d, "22", "/d/22") + val ccFolder = TestFolder(ccLvl2Dir, "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCC", "/d/22/CCCCCCCCCCCCCCCCCCCCCCCCCCCCCC") + + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base32(), "Directory 1", dirIdRoot.toByteArray())).thenReturn("dir1") + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base32(), "Directory 2", dirId1.toByteArray())).thenReturn("dir2") + + val testDir2DirFile = TestFile(bbFolder, "0dir2", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/0dir2", null, null) + val cryptoFolder2 = CryptoFolder(cryptoFolder1, "Directory 2", "/Directory 1/Directory 2", testDir2DirFile) + + Mockito.doAnswer { invocation: InvocationOnMock -> + val out = invocation.getArgument(2) + copyStreamToStream(ByteArrayInputStream(dirId2.toByteArray()), out) + null + }.`when`(cloudContentRepository).read(eq(cryptoFolder2.dirFile!!), any(), any(), any()) + + val dir1Items: ArrayList = object : ArrayList() { + init { + add(testDir2DirFile) + } + } + + whenever(cloudContentRepository.folder(rootFolder, "d")).thenReturn(d) + whenever(cloudContentRepository.folder(d, "22")).thenReturn(ccLvl2Dir) + whenever(cloudContentRepository.folder(ccLvl2Dir, "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCC")).thenReturn(ccFolder) + whenever(cloudContentRepository.file(aaFolder, "0dir2")).thenReturn(testDir2DirFile) + whenever>(cloudContentRepository.list(bbFolder)).thenReturn(dir1Items) + whenever(dirIdCache.put(eq(cryptoFolder2), any())).thenReturn(DirIdInfo(dirId2, ccFolder)) + whenever(dirIdCache[cryptoFolder2]).thenReturn(DirIdInfo(dirId2, ccFolder)) + whenever(cloudContentRepository.exists(testDir2DirFile)).thenReturn(true) + whenever>(cloudContentRepository.list(ccFolder)).thenReturn(ArrayList()) + + inTest.delete(cryptoFolder2) + + Mockito.verify(cloudContentRepository).delete(ccFolder) + Mockito.verify(cloudContentRepository).delete(testDir2DirFile) + Mockito.verify(dirIdCache).evict(cryptoFolder2) + } + + @Test + @DisplayName("delete(\"/Directory 3x250\")") + @Throws(BackendException::class) + fun testDeleteSingleLongFolder() { + /* + * + * path/to/vault/d + * ├─ Directory 1 + * │ ├─ ... + * ├─ Directory 3x250 + * ├─ ... + * + */ + val dir3Name = "Directory " + Strings.repeat("3", 250) + val dir3Cipher = "dir" + Strings.repeat("3", 250) + val longFilenameBytes = "0$dir3Cipher".toByteArray(StandardCharsets.UTF_8) + val hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes) + val shortenedFileName = Base32().encodeAsString(hash) + ".lng" + val ddLvl2Dir = TestFolder(d, "33", "/d/33") + val ddFolder = TestFolder(ddLvl2Dir, "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD", "/d/33/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD") + val testDir3DirFile = TestFile(aaFolder, shortenedFileName, "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/$shortenedFileName", null, null) + val testDir3NameFile = metadataFile(shortenedFileName) + + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base32(), dir3Name, dirIdRoot.toByteArray())).thenReturn(dir3Cipher) + whenever(fileNameCryptor.decryptFilename(BaseEncoding.base32(), dir3Cipher, dirIdRoot.toByteArray())).thenReturn(dir3Name) + whenever(fileNameCryptor.hashDirectoryId(AdditionalMatchers.not(eq("")))).thenReturn("33DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD") + whenever(cloudContentRepository.folder(d, "33")).thenReturn(ddLvl2Dir) + whenever(cloudContentRepository.folder(ddLvl2Dir, "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD")).thenReturn(ddFolder) + + val cryptoFolder3 = CryptoFolder(root, dir3Name, "/$dir3Name", testDir3DirFile) + + whenever(fileNameCryptor.hashDirectoryId(AdditionalMatchers.not(eq("")))).thenReturn("33DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD") + whenever(cloudContentRepository.folder(d, "33")).thenReturn(ddLvl2Dir) + whenever(cloudContentRepository.folder(lvl2Dir, "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD")).thenReturn(ddFolder) + whenever(dirIdCache.put(eq(cryptoFolder3), any())).thenReturn(DirIdInfo("dir3-id", ddFolder)) + whenever(cloudContentRepository.file(aaFolder, shortenedFileName)).thenReturn(testDir3DirFile) + whenever(cloudContentRepository.file(testDir3NameFile.parent!!, shortenedFileName, 257L)).thenReturn(testDir3NameFile) + whenever>(cloudContentRepository.list(ddFolder)).thenReturn(ArrayList()) + + inTest.delete(cryptoFolder3) + + Mockito.verify(cloudContentRepository).delete(ddFolder) + Mockito.verify(cloudContentRepository).delete(testDir3DirFile) + Mockito.verify(dirIdCache).evict(cryptoFolder3) + } + + @Test + @DisplayName("move(\"/File 4\", \"/Directory 1/File 4\")") + @Throws(BackendException::class) + fun testMoveShortFileToNewShortFile() { + val bbLvl2Dir = TestFolder(d, "11", "/d/11") + val bbFolder = TestFolder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB") + val testFile4 = TestFile(aaFolder, "file4", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/file4", null, null) + val testMovedFile4 = TestFile(bbFolder, "file4", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/file4", null, null) + val cryptoFile4 = CryptoFile(root, "File 4", "/File 4", null, testFile4) + val cryptoMovedFile4 = CryptoFile(cryptoFolder1, "File 4", "/Directory 1/File 4", null, testMovedFile4) + + whenever(cloudContentRepository.file(aaFolder, "file4")).thenReturn(testFile4) + whenever(cloudContentRepository.file(bbFolder, "file4")).thenReturn(testMovedFile4) + whenever(cloudContentRepository.move(testFile4, testMovedFile4)).thenReturn(testMovedFile4) + whenever(cloudContentRepository.folder(rootFolder, "d")).thenReturn(d) + whenever(cloudContentRepository.folder(d, "11")).thenReturn(bbLvl2Dir) + whenever(cloudContentRepository.folder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB")).thenReturn(bbFolder) + whenever(cloudContentRepository.folder(bbFolder, "file4")).thenReturn(null) + whenever(dirIdCache.put(eq(cryptoFolder1), any())).thenReturn(DirIdInfo(dirId1, bbFolder)) + whenever(dirIdCache[cryptoFolder1]).thenReturn(DirIdInfo(dirId1, bbFolder)) + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base32(), "File 4", dirId1.toByteArray())).thenReturn("file4") + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base32(), "File 4", dirIdRoot.toByteArray())).thenReturn("file4") + + // just for the exists check + whenever(cloudContentRepository.file(bbFolder, "0file4")).thenReturn(TestFile(bbFolder, "0file4", bbFolder.path + "/0file4", null, null)) + + val result = inTest.move(cryptoFile4, cryptoMovedFile4) + + Assertions.assertEquals("File 4", result.name) + Mockito.verify(cloudContentRepository).move(testFile4, testMovedFile4) + } + + @Test + @DisplayName("move(\"/File 4\", \"/Directory 1/File 4x250\")") + @Throws(BackendException::class) + fun testMoveShortFileToNewLongFile() { + val file4Name = "File " + Strings.repeat("4", 250) + val file4Cipher = "file" + Strings.repeat("4", 250) + val longFilenameBytes = file4Cipher.toByteArray(StandardCharsets.UTF_8) + val hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes) + val shortenedFileName = BaseEncoding.base32().encode(hash) + ".lng" + val bbLvl2Dir = TestFolder(d, "11", "/d/11") + val bbFolder = TestFolder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB") + val testFile4 = TestFile(aaFolder, "file4", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/file4", null, null) + val cryptoFile4 = CryptoFile(root, "File 4", "/File 4", null, testFile4) + val testFile4ContentFile = TestFile(bbFolder, shortenedFileName, "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/$shortenedFileName", null, null) + val testFile4NameFile = metadataFile(shortenedFileName) + val cryptoMovedFile4 = CryptoFile(cryptoFolder1, file4Name, "/Directory 1/$file4Name", null, testFile4ContentFile) + + whenever(cloudContentRepository.move(testFile4, testFile4ContentFile)).thenReturn(testFile4ContentFile) + whenever(cloudContentRepository.create(testFile4NameFile.parent!!)).thenReturn(testFile4NameFile.parent) + whenever(cloudContentRepository.write(eq(testFile4NameFile), any(), any(), eq(true), any())).thenAnswer { invocationOnMock: InvocationOnMock -> + val inputStream = invocationOnMock.getArgument(1) + val dirContent = BufferedReader(InputStreamReader(inputStream.open(context)!!, StandardCharsets.UTF_8)).readLine() + MatcherAssert.assertThat(dirContent, CoreMatchers.`is`(file4Cipher)) + testFile4NameFile + } + whenever(dirIdCache.put(eq(cryptoFolder1), any())).thenReturn(DirIdInfo(dirId1, bbFolder)) + whenever(dirIdCache[cryptoFolder1]).thenReturn(DirIdInfo(dirId1, bbFolder)) + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base32(), "File 4", dirIdRoot.toByteArray())).thenReturn("file4") + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base32(), file4Name, dirId1.toByteArray())).thenReturn(file4Cipher) + + // just for the exists check + whenever(cloudContentRepository.file(bbFolder, shortenedFileName, null)).thenReturn(testFile4ContentFile) + whenever(cloudContentRepository.create(testFile4NameFile.parent!!)).thenReturn(testFile4NameFile.parent!!) + val file4Cipher0 = "0file" + Strings.repeat("4", 250) + val longFilenameBytes0 = file4Cipher0.toByteArray(StandardCharsets.UTF_8) + val hash0 = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes0) + val shortenedFileName0 = BaseEncoding.base32().encode(hash0) + ".lng" + val testFile4NameFile0 = metadataFile(shortenedFileName0) + whenever(cloudContentRepository.file(bbFolder, shortenedFileName0)).thenReturn(testFile4NameFile0) + + val targetFile = inTest.file(cryptoFolder1, file4Name) // needed due to ugly side effect + val result = inTest.move(cryptoFile4, cryptoMovedFile4) + + Assertions.assertEquals(file4Name, result.name) + Mockito.verify(cloudContentRepository).create(testFile4NameFile.parent!!) + Mockito.verify(cloudContentRepository).move(testFile4, testFile4ContentFile) + Mockito.verify(cloudContentRepository).write(eq(testFile4NameFile), any(), any(), eq(true), any()) + } + + @Test + @DisplayName("move(\"/File 4x250\", \"/Directory 1/File 4x250\")") + @Throws(BackendException::class) + fun testMoveLongFileToNewLongFile() { + val file4Name = "File " + Strings.repeat("4", 250) + val file4Cipher = "file" + Strings.repeat("4", 250) + val longFilenameBytes = file4Cipher.toByteArray(StandardCharsets.UTF_8) + val hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes) + val shortenedFileName = BaseEncoding.base32().encode(hash) + ".lng" + val bbLvl2Dir = TestFolder(d, "11", "/d/11") + val bbFolder = TestFolder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB") + val testFile4ContentFileOld = TestFile(aaFolder, shortenedFileName, "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/$shortenedFileName", null, null) + val cryptoFile4Old = CryptoFile(root, file4Name, "/$file4Name", null, testFile4ContentFileOld) + val testFile4ContentFile = TestFile(bbFolder, shortenedFileName, "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/$shortenedFileName", null, null) + val testFile4NameFile = metadataFile(shortenedFileName) + val cryptoMovedFile4 = CryptoFile(cryptoFolder1, file4Name, "/Directory 1/$file4Name", null, testFile4ContentFile) + + whenever(cloudContentRepository.move(testFile4ContentFileOld, testFile4ContentFile)).thenReturn(testFile4ContentFile) + whenever(cloudContentRepository.create(testFile4NameFile.parent!!)).thenReturn(testFile4NameFile.parent) + whenever(cloudContentRepository.write(eq(testFile4NameFile), any(), any(), eq(true), any())).thenAnswer { invocationOnMock: InvocationOnMock -> + val inputStream = invocationOnMock.getArgument(1) + val dirContent = BufferedReader(InputStreamReader(inputStream.open(context)!!, StandardCharsets.UTF_8)).readLine() + MatcherAssert.assertThat(dirContent, CoreMatchers.`is`(file4Cipher)) + testFile4NameFile + } + whenever(dirIdCache.put(eq(cryptoFolder1), any())).thenReturn(DirIdInfo(dirId1, bbFolder)) + whenever(dirIdCache[cryptoFolder1]).thenReturn(DirIdInfo(dirId1, bbFolder)) + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base32(), file4Name, dirIdRoot.toByteArray())).thenReturn(file4Cipher) + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base32(), file4Name, dirId1.toByteArray())).thenReturn(file4Cipher) + + whenever(cloudContentRepository.file(bbFolder, shortenedFileName, null)).thenReturn(testFile4NameFile) + + // just for the exists check + val longFilenameBytes0 = "0$file4Cipher".toByteArray(StandardCharsets.UTF_8) + val hash0 = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes0) + val shortenedFileName0 = BaseEncoding.base32().encode(hash0) + ".lng" + val testFile4NameFile0 = metadataFile(shortenedFileName0) + whenever(cloudContentRepository.file(bbFolder, shortenedFileName0)).thenReturn(testFile4NameFile0) + + val targetFile: CloudFile = inTest.file(cryptoFolder1, file4Name) // needed due to ugly side effect + val result = inTest.move(cryptoFile4Old, cryptoMovedFile4) + + Assertions.assertEquals(file4Name, result.name) + Mockito.verify(cloudContentRepository).create(testFile4NameFile.parent!!) + Mockito.verify(cloudContentRepository).write(eq(testFile4NameFile), any(), any(), eq(true), any()) + Mockito.verify(cloudContentRepository).move(testFile4ContentFileOld, testFile4ContentFile) + } + + @Test + @DisplayName("move(\"/Directory 1/File 4x250\", \"/File 4\")") + @Throws(BackendException::class) + fun testMoveLongFileToNewShortFile() { + val file4Name = "File " + Strings.repeat("4", 250) + val file4Cipher = "file" + Strings.repeat("4", 250) + val longFilenameBytes = file4Cipher.toByteArray(StandardCharsets.UTF_8) + val hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes) + val shortenedFileName = BaseEncoding.base32().encode(hash) + ".lng" + val bbLvl2Dir = TestFolder(d, "11", "/d/11") + val bbFolder = TestFolder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB") + val testFile4 = TestFile(aaFolder, "file4", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/file4", null, null) + val cryptoFile4 = CryptoFile(root, "File 4", "/File 4", null, testFile4) + val testFile4DirFile = TestFile(bbFolder, "contents.c9r", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/$shortenedFileName", null, null) + val testFile4NameFile = metadataFile(shortenedFileName) + val cryptoMovedFile4 = CryptoFile(cryptoFolder1, file4Name, "/Directory 1/$file4Name", null, testFile4DirFile) + + whenever(cloudContentRepository.file(aaFolder, "file4.c9r")).thenReturn(testFile4) + whenever(cloudContentRepository.move(testFile4DirFile, testFile4)).thenReturn(testFile4) + whenever(cloudContentRepository.write(eq(testFile4NameFile), any(), any(), eq(true), any())).thenAnswer { invocationOnMock: InvocationOnMock -> + val inputStream = invocationOnMock.getArgument(1) + val dirContent = BufferedReader(InputStreamReader(inputStream.open(context)!!, StandardCharsets.UTF_8)).readLine() + MatcherAssert.assertThat(dirContent, CoreMatchers.`is`("$file4Cipher.c9r")) + testFile4NameFile + } + whenever(dirIdCache.put(eq(cryptoFolder1), any())).thenReturn(DirIdInfo(dirId1, bbFolder)) + whenever(dirIdCache[cryptoFolder1]).thenReturn(DirIdInfo(dirId1, bbFolder)) + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base32(), "File 4", dirIdRoot.toByteArray())).thenReturn("file4") + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base32(), file4Name, dirId1.toByteArray())).thenReturn(file4Cipher) + + // just for the exists check + whenever(cloudContentRepository.file(bbFolder, shortenedFileName, null)).thenReturn(testFile4DirFile) + whenever(cloudContentRepository.file(aaFolder, "0file4")).thenReturn(testFile4NameFile) + + val result = inTest.move(cryptoMovedFile4, cryptoFile4) + Assertions.assertEquals(cryptoFile4, result) + + Mockito.verify(cloudContentRepository).move(testFile4DirFile, testFile4) + } + + @Test + @DisplayName("move(\"/Directory 1\", \"/Directory 15\")") + @Throws(BackendException::class) + fun testMoveShortFolderToNewShortFolder() { + val bbLvl2Dir = TestFolder(d, "11", "/d/11") + val bbFolder = TestFolder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB") + val testDir15DirFile = TestFile(aaFolder, "0dir15", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/0dir15", null, null) + val cryptoFolder15 = CryptoFolder(root, "Directory 15", "/Directory 15/", testDir15DirFile) + + whenever(cloudContentRepository.folder(rootFolder, "d")).thenReturn(d) + whenever(cloudContentRepository.folder(d, "11")).thenReturn(bbLvl2Dir) + whenever(cloudContentRepository.folder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB")).thenReturn(bbFolder) + whenever(dirIdCache.put(eq(cryptoFolder1), any())).thenReturn(DirIdInfo(dirId1, bbFolder)) + whenever(dirIdCache[cryptoFolder15]).thenReturn(DirIdInfo(dirId1, bbFolder)) + whenever(dirIdCache[cryptoFolder1]).thenReturn(DirIdInfo(dirId1, bbFolder)) + whenever(dirIdCache.put(eq(cryptoFolder15), any())).thenReturn(DirIdInfo(dirId1, bbFolder)) + whenever(cloudContentRepository.file(aaFolder, "0dir15")).thenReturn(testDir15DirFile) + whenever(cloudContentRepository.move(cryptoFolder1.dirFile!!, testDir15DirFile)).thenReturn(testDir15DirFile) + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base32(), "Directory 15", dirIdRoot.toByteArray())).thenReturn("dir15") + + // just for the exists check + whenever(cloudContentRepository.file(aaFolder, "dir15", null)).thenReturn(TestFile(rootFolder, "dir15", aaFolder.path + "/dir15", null, null)) + + val result = inTest.move(cryptoFolder1, cryptoFolder15) + + Mockito.verify(cloudContentRepository).move(cryptoFolder1.dirFile!!, testDir15DirFile) + Mockito.verify(dirIdCache).evict(cryptoFolder1) + Mockito.verify(dirIdCache).evict(cryptoFolder15) + } + + @Test + @DisplayName("move(\"/Directory 1\", \"/Directory 15x200\")") + @Throws(BackendException::class) + fun testMoveShortFolderToNewLongFolder() { + val dir15Name = "Dir " + Strings.repeat("15", 250) + val dir15Cipher = "dir" + Strings.repeat("15", 250) + val longFilenameBytes = "0$dir15Cipher".toByteArray(StandardCharsets.UTF_8) + val hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes) + val shortenedFileName = BaseEncoding.base32().encode(hash) + ".lng" + val bbLvl2Dir = TestFolder(d, "11", "/d/11") + val bbFolder = TestFolder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB") + val testDir15DirFile = TestFile(aaFolder, shortenedFileName, "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/$shortenedFileName", null, null) + val testDir15NameFile = metadataFile(shortenedFileName) + val cryptoFolder15 = CryptoFolder(root, dir15Name, "/$dir15Name", testDir15DirFile) + + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base32(), dir15Name, dirId1.toByteArray())).thenReturn(dir15Cipher) + whenever(cloudContentRepository.file(aaFolder, "dir15.c9r", null)) + .thenReturn(TestFile(aaFolder, "dir15.c9r", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/dir15.c9r", null, null)) + whenever(dirIdCache.put(eq(cryptoFolder1), any())).thenReturn(DirIdInfo(dirId1, bbFolder)) + whenever(dirIdCache.put(eq(cryptoFolder15), any())).thenReturn(DirIdInfo(dirId1, bbFolder)) + whenever(dirIdCache[cryptoFolder1]).thenReturn(DirIdInfo(dirId1, bbFolder)) + whenever(dirIdCache[cryptoFolder15]).thenReturn(DirIdInfo(dirId1, bbFolder)) + whenever(cloudContentRepository.move(cryptoFolder1.dirFile!!, testDir15DirFile)).thenReturn(testDir15DirFile) + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base32(), dir15Name, dirIdRoot.toByteArray())).thenReturn(dir15Cipher) + whenever(cloudContentRepository.write(eq(testDir15NameFile), any(), any(), eq(true), any())).thenAnswer { invocationOnMock: InvocationOnMock -> + val inputStream = invocationOnMock.getArgument(1) + val dirContent = BufferedReader(InputStreamReader(inputStream.open(context)!!, StandardCharsets.UTF_8)).readLine() + MatcherAssert.assertThat(dirContent, CoreMatchers.`is`("0$dir15Cipher")) + testDir15NameFile + } + + whenever(cloudContentRepository.file(aaFolder, shortenedFileName)).thenReturn(testDir15DirFile) + + // just for the exists check + val longFilenameBytes0 = dir15Cipher.toByteArray(StandardCharsets.UTF_8) + val hash0 = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes0) + val shortenedFileName0 = BaseEncoding.base32().encode(hash0) + ".lng" + val testDir15DirFile0 = TestFile(aaFolder, shortenedFileName0, "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/$shortenedFileName0", null, null) + val testDir15NameFile0 = metadataFile(shortenedFileName0) + whenever(cloudContentRepository.file(aaFolder, shortenedFileName0, null)).thenReturn(testDir15DirFile0) + + val targetFile = inTest.folder(root, dir15Name) // needed due to ugly side effect + val result = inTest.move(cryptoFolder1, cryptoFolder15) + + Assertions.assertEquals(cryptoFolder15, result) + Mockito.verify(cloudContentRepository).write(eq(testDir15NameFile), any(), any(), eq(true), any()) + Mockito.verify(cloudContentRepository).move(cryptoFolder1.dirFile!!, testDir15DirFile) + } + + @Test + @DisplayName("move(\"/Directory 15x200\", \"/Directory 3000\")") + @Throws(BackendException::class) + fun testMoveLongFolderToNewShortFolder() { + val dir15Name = "Dir " + Strings.repeat("15", 250) + val dir15Cipher = "dir" + Strings.repeat("15", 250) + val longFilenameBytes = "0$dir15Cipher".toByteArray(StandardCharsets.UTF_8) + val hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes) + val shortenedFileName = BaseEncoding.base32().encode(hash) + ".lng" + val bbLvl2Dir = TestFolder(d, "11", "/d/11") + val bbFolder = TestFolder(bbLvl2Dir, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB") + val testDir15DirFile = TestFile(aaFolder, shortenedFileName, "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/$shortenedFileName", null, null) + val cryptoFolder15 = CryptoFolder(root, dir15Name, "/$dir15Name", testDir15DirFile) + + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base32(), dir15Name, dirId1.toByteArray())).thenReturn(dir15Cipher) + whenever(dirIdCache.put(eq(cryptoFolder1), any())).thenReturn(DirIdInfo(dirId1, bbFolder)) + whenever(dirIdCache[cryptoFolder1]).thenReturn(DirIdInfo(dirId1, bbFolder)) + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base32(), dir15Name, dirIdRoot.toByteArray())).thenReturn(dir15Cipher) + + val testDir3DirFile = TestFile(aaFolder, "dir3", "/d/00/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/dir3", null, null) + val cryptoFolder3 = CryptoFolder(root, "Directory 3", "/Directory 3", testDir3DirFile) + + whenever(fileNameCryptor.encryptFilename(BaseEncoding.base32(), "Directory 3", dirIdRoot.toByteArray())).thenReturn("dir3") + whenever(fileNameCryptor.decryptFilename(BaseEncoding.base32(), "dir3", dirIdRoot.toByteArray())).thenReturn("Directory 3") + whenever(fileNameCryptor.hashDirectoryId(AdditionalMatchers.not(eq("")))).thenReturn("33DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD") + whenever(cloudContentRepository.create(lvl2Dir)).thenReturn(lvl2Dir) + whenever(cloudContentRepository.write(eq(testDir3DirFile), any(), any(), eq(false), any())).thenReturn(testDir3DirFile) + whenever(cloudContentRepository.move(testDir15DirFile, testDir3DirFile)).thenReturn(testDir3DirFile) + + // just for the exists check + whenever(cloudContentRepository.file(aaFolder, "0dir3")).thenReturn(TestFile(rootFolder, "0dir3", aaFolder.path + "/0dir3", null, null)) + whenever(cloudContentRepository.file(aaFolder, "dir3", null)).thenReturn(testDir3DirFile) + + val targetFile = inTest.folder(root, cryptoFolder3.name) // needed due to ugly side effect + val result = inTest.move(cryptoFolder15, cryptoFolder3) + + Mockito.verify(cloudContentRepository).move(testDir15DirFile, cryptoFolder3.dirFile!!) + } + + @Throws(BackendException::class) + private fun metadataFile(shortFilename: String): CloudFile { + whenever(cloudContentRepository.folder(rootFolder, "m")).thenReturn(m) + val firstLevelFolder = TestFolder(m, shortFilename.substring(0, 2), "/m/" + shortFilename.substring(0, 2)) + whenever(cloudContentRepository.folder(m, shortFilename.substring(0, 2))).thenReturn(firstLevelFolder) + val secondLevelFolder = TestFolder(firstLevelFolder, shortFilename.substring(2, 4), "/m/" + shortFilename.substring(0, 2) + "/" + shortFilename.substring(2, 4)) + whenever(cloudContentRepository.folder(firstLevelFolder, shortFilename.substring(2, 4))).thenReturn(secondLevelFolder) + val file = TestFile(secondLevelFolder, shortFilename, "/m/" + shortFilename.substring(0, 2) + "/" + shortFilename.substring(2, 4) + "/" + shortFilename, null, null) + whenever(cloudContentRepository.file(secondLevelFolder, shortFilename)).thenReturn(file) + return file + } +} diff --git a/data/src/test/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProviderTest.java b/data/src/test/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProviderTest.java deleted file mode 100644 index 5d137015..00000000 --- a/data/src/test/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProviderTest.java +++ /dev/null @@ -1,336 +0,0 @@ -package org.cryptomator.data.cloud.crypto; - -import android.content.Context; - -import org.cryptomator.cryptolib.api.Cryptor; -import org.cryptomator.cryptolib.api.FileNameCryptor; -import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException; -import org.cryptomator.data.util.CopyStream; -import org.cryptomator.domain.Cloud; -import org.cryptomator.domain.CloudType; -import org.cryptomator.domain.UnverifiedVaultConfig; -import org.cryptomator.domain.Vault; -import org.cryptomator.domain.exception.BackendException; -import org.cryptomator.domain.repository.CloudContentRepository; -import org.cryptomator.domain.usecases.cloud.DataSource; -import org.cryptomator.domain.usecases.vault.UnlockToken; -import org.cryptomator.util.Optional; -import org.hamcrest.MatcherAssert; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.Mockito; - -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.security.SecureRandom; -import java.util.stream.Collectors; - -import static org.cryptomator.cryptolib.api.Masterkey.SUBKEY_LEN_BYTES; -import static org.cryptomator.data.cloud.crypto.CryptoConstants.DATA_DIR_NAME; -import static org.cryptomator.data.cloud.crypto.CryptoConstants.DEFAULT_MAX_FILE_NAME; -import static org.cryptomator.data.cloud.crypto.CryptoConstants.MASTERKEY_BACKUP_FILE_EXT; -import static org.cryptomator.data.cloud.crypto.CryptoConstants.MASTERKEY_FILE_NAME; -import static org.cryptomator.data.cloud.crypto.CryptoConstants.MASTERKEY_SCHEME; -import static org.cryptomator.data.cloud.crypto.CryptoConstants.MAX_VAULT_VERSION; -import static org.cryptomator.data.cloud.crypto.CryptoConstants.MAX_VAULT_VERSION_WITHOUT_VAULT_CONFIG; -import static org.cryptomator.data.cloud.crypto.CryptoConstants.ROOT_DIR_ID; -import static org.cryptomator.data.cloud.crypto.CryptoConstants.VAULT_FILE_NAME; -import static org.cryptomator.data.cloud.crypto.VaultCipherCombo.SIV_CTRMAC; -import static org.cryptomator.domain.usecases.ProgressAware.NO_OP_PROGRESS_AWARE; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; - -class MasterkeyCryptoCloudProviderTest { - - private final String masterkeyV8 = "{ \"version\": 999, \"scryptSalt\": \"AAAAAAAAAAA=\", \"scryptCostParam\": 32768, \"scryptBlockSize\": 8, \"primaryMasterKey\": \"D2kc+xBoAcVY+M7s74YBEy6l7ga2+Nz+HS5o0TQY3JMW1uQ5jTlLIQ==\", \"hmacMasterKey\": \"D2kc+xBoAcVY+M7s74YBEy6l7ga2+Nz+HS5o0TQY3JMW1uQ5jTlLIQ==\", \"versionMac\": \"trDKXqDhu94/VPuoWaQGBm8hwSPYc0D9t6DRRxKZ65k=\"}"; - private final String masterkeyV7 = "{ \"version\": 7, \"scryptSalt\": \"AAAAAAAAAAA=\", \"scryptCostParam\": 32768, \"scryptBlockSize\": 8, \"primaryMasterKey\": \"D2kc+xBoAcVY+M7s74YBEy6l7ga2+Nz+HS5o0TQY3JMW1uQ5jTlLIQ==\", \"hmacMasterKey\": \"D2kc+xBoAcVY+M7s74YBEy6l7ga2+Nz+HS5o0TQY3JMW1uQ5jTlLIQ==\", \"versionMac\": \"cn2sAK6l9p1/w9deJVUuW3h7br056mpv5srvALiYw+g=\"}"; - private final String vaultConfig = "eyJraWQiOiJtYXN0ZXJrZXlmaWxlOm1hc3RlcmtleS5jcnlwdG9tYXRvciIsImFsZyI6IkhTNTEyIn0.eyJmb3JtYXQiOjgsInNob3J0ZW5pbmdUaHJlc2hvbGQiOjIyMCwiY2lwaGVyQ29tYm8iOiJTSVZfQ1RSTUFDIn0.Evt5KXS_35pm53DynIwL3qvXWF56UkfqDZKv12n7SD288jzcdvvmtvu5sQhhqvxU6CPL4Q9v3yFQ_lvBynyrYA"; - - private Context context; - private Cloud cloud; - private CloudContentRepository cloudContentRepository; - private CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory; - private Vault vault; - private VaultConfig.VaultConfigBuilder vaultConfigBuilder; - private Cryptor cryptor; - private FileNameCryptor fileNameCryptor; - private SecureRandom secureRandom; - private MasterkeyCryptoCloudProvider inTest; - - @BeforeEach - public void setUp() { - context = Mockito.mock(Context.class); - cloud = Mockito.mock(Cloud.class); - cloudContentRepository = Mockito.mock(CloudContentRepository.class); - - cryptoCloudContentRepositoryFactory = Mockito.mock(CryptoCloudContentRepositoryFactory.class); - - vault = Mockito.mock(Vault.class); - vaultConfigBuilder = VaultConfig.createVaultConfig().id(""); - - cryptor = Mockito.mock(Cryptor.class); - fileNameCryptor = Mockito.mock(FileNameCryptor.class); - secureRandom = Mockito.mock(SecureRandom.class); - - Mockito.when(cryptor.fileNameCryptor()).thenReturn(fileNameCryptor); - - byte[] key = new byte[SUBKEY_LEN_BYTES + SUBKEY_LEN_BYTES]; - Mockito.doNothing().when(secureRandom).nextBytes(key); - - inTest = Mockito.spy(new MasterkeyCryptoCloudProvider(cloudContentRepository, cryptoCloudContentRepositoryFactory, secureRandom)); - } - - @Test - @DisplayName("create(\"/foo\", \"foo\")") - public void testCreateVault() throws BackendException { - TestFolder rootFolder = new RootTestFolder(cloud); - TestFolder foo = new TestFolder(rootFolder, "foo", "/foo"); - TestFile vaultFile = new TestFile(foo, VAULT_FILE_NAME, "/foo/" + VAULT_FILE_NAME, Optional.empty(), Optional.empty()); - TestFile masterKeyFile = new TestFile(foo, MASTERKEY_FILE_NAME, "/foo/" + MASTERKEY_FILE_NAME, Optional.empty(), Optional.empty()); - - Mockito.when(cloudContentRepository.file(foo, VAULT_FILE_NAME)).thenReturn(vaultFile); - Mockito.when(cloudContentRepository.file(foo, MASTERKEY_FILE_NAME)).thenReturn(masterKeyFile); - - // 1. write masterkey - Mockito.when(cloudContentRepository.write(Mockito.eq(masterKeyFile), Mockito.any(DataSource.class), Mockito.eq(NO_OP_PROGRESS_AWARE), Mockito.eq(false), Mockito.anyLong())).thenAnswer(invocationOnMock -> { - DataSource in = invocationOnMock.getArgument(1); - String masterKeyFileContent = new BufferedReader(new InputStreamReader(in.open(context), StandardCharsets.UTF_8)).lines().collect(Collectors.joining()); - assertThat(masterKeyFileContent, is(masterkeyV8)); - return invocationOnMock.getArgument(0); - }); - - // 2. initialize vault - Mockito.when(cloudContentRepository.write(Mockito.eq(vaultFile), Mockito.any(DataSource.class), Mockito.eq(NO_OP_PROGRESS_AWARE), Mockito.eq(false), Mockito.anyLong())).thenAnswer(invocationOnMock -> { - DataSource in = invocationOnMock.getArgument(1); - String vaultConfigFileContent = new BufferedReader(new InputStreamReader(in.open(context), StandardCharsets.UTF_8)).lines().collect(Collectors.joining()); - assertThat(vaultConfigFileContent, is(vaultConfig)); - return invocationOnMock.getArgument(0); - }); - - // 3. create root folder - String rootDirHash = "KG6TFDGKXGZEGWRZOGTDFDF4YEGAZO6Q"; - - TestFolder dFolder = new TestFolder(foo, "d", "/foo/" + DATA_DIR_NAME); - TestFolder lvl1Dir = new TestFolder(dFolder, rootDirHash.substring(0, 2), "/foo/" + DATA_DIR_NAME + "/" + rootDirHash.substring(0, 2)); - TestFolder lvl2Dir = new TestFolder(lvl1Dir, rootDirHash.substring(2), "/foo/" + DATA_DIR_NAME + "/" + rootDirHash.substring(0, 2) + "/" + rootDirHash.substring(2)); - - - Mockito.when(cloudContentRepository.folder(foo, DATA_DIR_NAME)).thenReturn(dFolder); - Mockito.when(cloudContentRepository.create(dFolder)).thenReturn(dFolder); - - Mockito.when(cryptor.fileNameCryptor().hashDirectoryId(ROOT_DIR_ID)).thenReturn(ROOT_DIR_ID); - - Mockito.when(cloudContentRepository.folder(dFolder, lvl1Dir.getName())).thenReturn(lvl1Dir); - Mockito.when(cloudContentRepository.create(lvl1Dir)).thenReturn(lvl1Dir); - - Mockito.when(cloudContentRepository.folder(lvl1Dir, lvl2Dir.getName())).thenReturn(lvl2Dir); - Mockito.when(cloudContentRepository.create(lvl2Dir)).thenReturn(lvl2Dir); - - inTest.create(foo, "foo", vaultConfigBuilder); - - Mockito.verify(cloudContentRepository).write(Mockito.eq(masterKeyFile), Mockito.any(DataSource.class), Mockito.eq(NO_OP_PROGRESS_AWARE), Mockito.eq(false), Mockito.anyLong()); - Mockito.verify(cloudContentRepository).write(Mockito.eq(vaultFile), Mockito.any(DataSource.class), Mockito.eq(NO_OP_PROGRESS_AWARE), Mockito.eq(false), Mockito.anyLong()); - Mockito.verify(cloudContentRepository).create(dFolder); - Mockito.verify(cloudContentRepository).create(lvl1Dir); - Mockito.verify(cloudContentRepository).create(lvl2Dir); - } - - @Test - @DisplayName("lock(\"foo\")") - public void testLockVault() { - inTest.lock(vault); - Mockito.verify(cryptoCloudContentRepositoryFactory).deregisterCryptor(vault); - } - - @Test - @DisplayName("unlock(\"foo\")") - public void testUnlockVault() throws BackendException, IOException { - CloudType cloudType = Mockito.mock(CloudType.class); - - Mockito.when(cloud.type()).thenReturn(cloudType); - - Mockito.when(vault.getCloud()).thenReturn(cloud); - Mockito.when(vault.getCloudType()).thenReturn(cloudType); - Mockito.when(vault.getFormat()).thenReturn(8); - Mockito.when(vault.getId()).thenReturn(25L); - Mockito.when(vault.getName()).thenReturn("foo"); - Mockito.when(vault.getPath()).thenReturn("/foo"); - Mockito.when(vault.isUnlocked()).thenReturn(true); - - MasterkeyCryptoCloudProvider.UnlockTokenImpl unlockToken = new MasterkeyCryptoCloudProvider.UnlockTokenImpl(vault, masterkeyV7.getBytes(StandardCharsets.UTF_8)); - UnverifiedVaultConfig unverifiedVaultConfig = new UnverifiedVaultConfig(vaultConfig, URI.create(String.format("%s:%s", MASTERKEY_SCHEME, MASTERKEY_FILE_NAME)), MAX_VAULT_VERSION); - - Vault result = inTest.unlock(unlockToken, Optional.of(unverifiedVaultConfig), "foo", () -> false); - - MatcherAssert.assertThat(result.isUnlocked(), is(true)); - MatcherAssert.assertThat(result.getFormat(), is(8)); - MatcherAssert.assertThat(result.getShorteningThreshold(), is(DEFAULT_MAX_FILE_NAME)); - - Mockito.verify(inTest).cryptorFor(unlockToken.getKeyFile("foo"), SIV_CTRMAC); - Mockito.verify(cryptoCloudContentRepositoryFactory).registerCryptor(Mockito.any(Vault.class), Mockito.any(Cryptor.class)); - } - - @Test - @DisplayName("unlockLegacy(\"foo\")") - public void testUnlockLegacyVault() throws BackendException, IOException { - CloudType cloudType = Mockito.mock(CloudType.class); - - Mockito.when(cloud.type()).thenReturn(cloudType); - - Mockito.when(vault.getCloud()).thenReturn(cloud); - Mockito.when(vault.getCloudType()).thenReturn(cloudType); - Mockito.when(vault.getFormat()).thenReturn(7); - Mockito.when(vault.getId()).thenReturn(25L); - Mockito.when(vault.getName()).thenReturn("foo"); - Mockito.when(vault.getPath()).thenReturn("/foo"); - Mockito.when(vault.isUnlocked()).thenReturn(true); - - MasterkeyCryptoCloudProvider.UnlockTokenImpl unlockToken = new MasterkeyCryptoCloudProvider.UnlockTokenImpl(vault, masterkeyV7.getBytes(StandardCharsets.UTF_8)); - - Vault result = inTest.unlock(unlockToken, Optional.empty(), "foo", () -> false); - - MatcherAssert.assertThat(result.isUnlocked(), is(true)); - MatcherAssert.assertThat(result.getFormat(), is(MAX_VAULT_VERSION_WITHOUT_VAULT_CONFIG)); - MatcherAssert.assertThat(result.getShorteningThreshold(), is(DEFAULT_MAX_FILE_NAME)); - - Mockito.verify(inTest).cryptorFor(unlockToken.getKeyFile("foo"), SIV_CTRMAC); - Mockito.verify(cryptoCloudContentRepositoryFactory).registerCryptor(Mockito.any(Vault.class), Mockito.any(Cryptor.class)); - } - - @Test - @DisplayName("unlockLegacyUsingNewVault(\"foo\")") - public void testUnlockLegacyVaultUsingVaultFormat8() { - UnlockToken unlockToken = new MasterkeyCryptoCloudProvider.UnlockTokenImpl(vault, masterkeyV8.getBytes(StandardCharsets.UTF_8)); - Assertions.assertThrows(UnsupportedVaultFormatException.class, () -> inTest.unlock(unlockToken, Optional.empty(), "foo", () -> false)); - } - - @DisplayName("changePassword(\"foo\")") - @ParameterizedTest(name = "Legacy vault format {0}") - @ValueSource(booleans = {true, false}) - public void tesChangePassword(boolean legacy) throws BackendException { - if (legacy) { - testChangePassword(masterkeyV7, Optional.empty()); - } else { - UnverifiedVaultConfig unverifiedVaultConfig = new UnverifiedVaultConfig(vaultConfig, URI.create(String.format("%s:%s", MASTERKEY_SCHEME, MASTERKEY_FILE_NAME)), MAX_VAULT_VERSION); - testChangePassword(masterkeyV8, Optional.of(unverifiedVaultConfig)); - } - } - - private void testChangePassword(String masterkeyContent, Optional unverifiedVaultConfig) throws BackendException { - TestFolder rootFolder = new RootTestFolder(cloud); - TestFolder foo = new TestFolder(rootFolder, "foo", "/foo"); - TestFile vaultFile = new TestFile(foo, VAULT_FILE_NAME, "/foo/" + VAULT_FILE_NAME, Optional.empty(), Optional.empty()); - TestFile masterKeyFile = new TestFile(foo, MASTERKEY_FILE_NAME, "/foo/" + MASTERKEY_FILE_NAME, Optional.empty(), Optional.empty()); - - Mockito.when(cloudContentRepository.file(foo, VAULT_FILE_NAME)).thenReturn(vaultFile); - Mockito.when(cloudContentRepository.file(foo, MASTERKEY_FILE_NAME)).thenReturn(masterKeyFile); - Mockito.when(cloudContentRepository.resolve(vault.getCloud(), vault.getPath())).thenReturn(foo); - - // 1. Read masterkey - Mockito.doAnswer(invocation -> { - OutputStream out = invocation.getArgument(2); - CopyStream.copyStreamToStream(new ByteArrayInputStream(masterkeyContent.getBytes()), out); - return null; - }).when(cloudContentRepository).read(Mockito.eq(masterKeyFile), Mockito.eq(Optional.empty()), Mockito.any(), Mockito.eq(NO_OP_PROGRESS_AWARE)); - - // 2. Create backup - String fileName = masterKeyFile.getName() + BackupFileIdSuffixGenerator.generate(masterkeyContent.getBytes()) + MASTERKEY_BACKUP_FILE_EXT; - TestFile masterKeyBackupFile = new TestFile(foo, fileName, "/foo/" + fileName, Optional.empty(), Optional.empty()); - Mockito.when(cloudContentRepository.file(foo, fileName)).thenReturn(masterKeyBackupFile); - - Mockito.when(cloudContentRepository.write(Mockito.eq(masterKeyBackupFile), Mockito.any(DataSource.class), Mockito.eq(NO_OP_PROGRESS_AWARE), Mockito.eq(true), Mockito.anyLong())).thenAnswer(invocationOnMock -> { - DataSource in = invocationOnMock.getArgument(1); - String masterKeyFileContent = new BufferedReader(new InputStreamReader(in.open(context), StandardCharsets.UTF_8)).lines().collect(Collectors.joining()); - assertThat(masterKeyFileContent, is(masterkeyContent)); - return invocationOnMock.getArgument(0); - }); - - // 3. Create new Masterkey file - String changedMasterkey; - if (unverifiedVaultConfig.isPresent()) { - changedMasterkey = "{ \"version\": 999, \"scryptSalt\": \"AAAAAAAAAAA=\", \"scryptCostParam\": 32768, \"scryptBlockSize\": 8, \"primaryMasterKey\": \"O8Z6ZP+aScORaOrMtWYrXjA5EptZk+IAYjEDEUJ7yIvGOWsR+CiwkA==\", \"hmacMasterKey\": \"O8Z6ZP+aScORaOrMtWYrXjA5EptZk+IAYjEDEUJ7yIvGOWsR+CiwkA==\", \"versionMac\": \"trDKXqDhu94/VPuoWaQGBm8hwSPYc0D9t6DRRxKZ65k=\"}"; - } else { - changedMasterkey = "{ \"version\": 7, \"scryptSalt\": \"AAAAAAAAAAA=\", \"scryptCostParam\": 32768, \"scryptBlockSize\": 8, \"primaryMasterKey\": \"O8Z6ZP+aScORaOrMtWYrXjA5EptZk+IAYjEDEUJ7yIvGOWsR+CiwkA==\", \"hmacMasterKey\": \"O8Z6ZP+aScORaOrMtWYrXjA5EptZk+IAYjEDEUJ7yIvGOWsR+CiwkA==\", \"versionMac\": \"cn2sAK6l9p1/w9deJVUuW3h7br056mpv5srvALiYw+g=\"}"; - } - Mockito.when(cloudContentRepository.write(Mockito.eq(masterKeyFile), Mockito.any(DataSource.class), Mockito.eq(NO_OP_PROGRESS_AWARE), Mockito.eq(true), Mockito.anyLong())).thenAnswer(invocationOnMock -> { - DataSource in = invocationOnMock.getArgument(1); - String masterKeyFileContent = new BufferedReader(new InputStreamReader(in.open(context), StandardCharsets.UTF_8)).lines().collect(Collectors.joining()); - assertThat(masterKeyFileContent, is(changedMasterkey)); - return invocationOnMock.getArgument(0); - }); - - inTest.changePassword(vault, unverifiedVaultConfig, "foo", "bar"); - - Mockito.verify(cloudContentRepository).read(Mockito.eq(masterKeyFile), Mockito.eq(Optional.empty()), Mockito.any(), Mockito.eq(NO_OP_PROGRESS_AWARE)); - Mockito.verify(cloudContentRepository).write(Mockito.eq(masterKeyBackupFile), Mockito.any(DataSource.class), Mockito.eq(NO_OP_PROGRESS_AWARE), Mockito.eq(true), Mockito.anyLong()); - Mockito.verify(cloudContentRepository).write(Mockito.eq(masterKeyFile), Mockito.any(DataSource.class), Mockito.eq(NO_OP_PROGRESS_AWARE), Mockito.eq(true), Mockito.anyLong()); - } - - @DisplayName("isVaultPasswordValid(\"foo\", \"foo\")") - @ParameterizedTest(name = "Legacy vault format {0}") - @ValueSource(booleans = {true, false}) - public void testVaultPasswordVault(boolean legacy) throws BackendException, IOException { - String password = "foo"; - if (legacy) { - assertThat(testVaultPasswordVault(masterkeyV7, Optional.empty(), password), is(true)); - - MasterkeyCryptoCloudProvider.UnlockTokenImpl unlockToken = new MasterkeyCryptoCloudProvider.UnlockTokenImpl(vault, masterkeyV7.getBytes(StandardCharsets.UTF_8)); - Mockito.verify(inTest).cryptorFor(unlockToken.getKeyFile(password), SIV_CTRMAC); - } else { - UnverifiedVaultConfig unverifiedVaultConfig = new UnverifiedVaultConfig(vaultConfig, URI.create(String.format("%s:%s", MASTERKEY_SCHEME, MASTERKEY_FILE_NAME)), MAX_VAULT_VERSION); - assertThat(testVaultPasswordVault(masterkeyV8, Optional.of(unverifiedVaultConfig), password), is(true)); - - MasterkeyCryptoCloudProvider.UnlockTokenImpl unlockToken = new MasterkeyCryptoCloudProvider.UnlockTokenImpl(vault, masterkeyV8.getBytes(StandardCharsets.UTF_8)); - Mockito.verify(inTest).cryptorFor(unlockToken.getKeyFile(password), SIV_CTRMAC); - } - } - - @DisplayName("isVaultPasswordValid(\"foo\", \"bar\")") - @ParameterizedTest(name = "Legacy vault format {0}") - @ValueSource(booleans = {true, false}) - public void testVaultPasswordVaultInvalidPassword(boolean legacy) throws BackendException, IOException { - String password = "bar"; - if (legacy) { - assertThat(testVaultPasswordVault(masterkeyV7, Optional.empty(), password), is(false)); - } else { - UnverifiedVaultConfig unverifiedVaultConfig = new UnverifiedVaultConfig(vaultConfig, URI.create(String.format("%s:%s", MASTERKEY_SCHEME, MASTERKEY_FILE_NAME)), MAX_VAULT_VERSION); - assertThat(testVaultPasswordVault(masterkeyV8, Optional.of(unverifiedVaultConfig), password), is(false)); - } - } - - - private boolean testVaultPasswordVault(String masterkeyContent, Optional unverifiedVaultConfig, String password) throws BackendException { - TestFolder rootFolder = new RootTestFolder(cloud); - TestFolder foo = new TestFolder(rootFolder, "foo", "/foo"); - TestFile vaultFile = new TestFile(foo, VAULT_FILE_NAME, "/foo/" + VAULT_FILE_NAME, Optional.empty(), Optional.empty()); - TestFile masterKeyFile = new TestFile(foo, MASTERKEY_FILE_NAME, "/foo/" + MASTERKEY_FILE_NAME, Optional.empty(), Optional.empty()); - - Mockito.when(cloudContentRepository.file(foo, VAULT_FILE_NAME)).thenReturn(vaultFile); - Mockito.when(cloudContentRepository.file(foo, MASTERKEY_FILE_NAME)).thenReturn(masterKeyFile); - Mockito.when(cloudContentRepository.resolve(vault.getCloud(), vault.getPath())).thenReturn(foo); - - - Mockito.when(cloudContentRepository.file(foo, VAULT_FILE_NAME)).thenReturn(vaultFile); - Mockito.when(cloudContentRepository.file(foo, MASTERKEY_FILE_NAME)).thenReturn(masterKeyFile); - Mockito.when(cloudContentRepository.resolve(vault.getCloud(), vault.getPath())).thenReturn(foo); - - // 1. Read masterkey - Mockito.doAnswer(invocation -> { - OutputStream out = invocation.getArgument(2); - CopyStream.copyStreamToStream(new ByteArrayInputStream(masterkeyContent.getBytes()), out); - return null; - }).when(cloudContentRepository).read(Mockito.eq(masterKeyFile), Mockito.eq(Optional.empty()), Mockito.any(), Mockito.eq(NO_OP_PROGRESS_AWARE)); - - return inTest.isVaultPasswordValid(vault, unverifiedVaultConfig, password); - } - -} diff --git a/data/src/test/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProviderTest.kt b/data/src/test/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProviderTest.kt new file mode 100644 index 00000000..dbb3ff99 --- /dev/null +++ b/data/src/test/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProviderTest.kt @@ -0,0 +1,366 @@ +package org.cryptomator.data.cloud.crypto + +import android.content.Context +import com.google.common.base.Optional +import org.cryptomator.cryptolib.api.Cryptor +import org.cryptomator.cryptolib.api.CryptorProvider +import org.cryptomator.cryptolib.api.FileNameCryptor +import org.cryptomator.cryptolib.api.Masterkey +import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException +import org.cryptomator.data.cloud.crypto.BackupFileIdSuffixGenerator.generate +import org.cryptomator.data.cloud.crypto.MasterkeyCryptoCloudProvider.UnlockTokenImpl +import org.cryptomator.data.cloud.crypto.VaultConfig.VaultConfigBuilder +import org.cryptomator.data.util.CopyStream.copyStreamToStream +import org.cryptomator.domain.Cloud +import org.cryptomator.domain.CloudFile +import org.cryptomator.domain.CloudFolder +import org.cryptomator.domain.CloudNode +import org.cryptomator.domain.CloudType +import org.cryptomator.domain.UnverifiedVaultConfig +import org.cryptomator.domain.Vault +import org.cryptomator.domain.exception.BackendException +import org.cryptomator.domain.repository.CloudContentRepository +import org.cryptomator.domain.usecases.ProgressAware +import org.cryptomator.domain.usecases.cloud.DataSource +import org.cryptomator.domain.usecases.vault.UnlockToken +import org.hamcrest.CoreMatchers +import org.hamcrest.MatcherAssert +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import org.mockito.Mockito +import org.mockito.invocation.InvocationOnMock +import org.mockito.kotlin.any +import org.mockito.kotlin.doNothing +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.spy +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import java.io.BufferedReader +import java.io.ByteArrayInputStream +import java.io.IOException +import java.io.InputStreamReader +import java.io.OutputStream +import java.net.URI +import java.nio.charset.StandardCharsets +import java.security.SecureRandom +import java.util.stream.Collectors + +internal class MasterkeyCryptoCloudProviderTest { + + private val masterkeyV8 = + "{ \"version\": 999, \"scryptSalt\": \"AAAAAAAAAAA=\", \"scryptCostParam\": 32768, \"scryptBlockSize\": 8, \"primaryMasterKey\": \"D2kc+xBoAcVY+M7s74YBEy6l7ga2+Nz+HS5o0TQY3JMW1uQ5jTlLIQ==\", \"hmacMasterKey\": \"D2kc+xBoAcVY+M7s74YBEy6l7ga2+Nz+HS5o0TQY3JMW1uQ5jTlLIQ==\", \"versionMac\": \"trDKXqDhu94/VPuoWaQGBm8hwSPYc0D9t6DRRxKZ65k=\"}" + private val masterkeyV7 = + "{ \"version\": 7, \"scryptSalt\": \"AAAAAAAAAAA=\", \"scryptCostParam\": 32768, \"scryptBlockSize\": 8, \"primaryMasterKey\": \"D2kc+xBoAcVY+M7s74YBEy6l7ga2+Nz+HS5o0TQY3JMW1uQ5jTlLIQ==\", \"hmacMasterKey\": \"D2kc+xBoAcVY+M7s74YBEy6l7ga2+Nz+HS5o0TQY3JMW1uQ5jTlLIQ==\", \"versionMac\": \"cn2sAK6l9p1/w9deJVUuW3h7br056mpv5srvALiYw+g=\"}" + private val vaultConfig = + "eyJraWQiOiJtYXN0ZXJrZXlmaWxlOm1hc3RlcmtleS5jcnlwdG9tYXRvciIsImFsZyI6IkhTNTEyIn0.eyJmb3JtYXQiOjgsInNob3J0ZW5pbmdUaHJlc2hvbGQiOjIyMCwiY2lwaGVyQ29tYm8iOiJTSVZfQ1RSTUFDIn0.Evt5KXS_35pm53DynIwL3qvXWF56UkfqDZKv12n7SD288jzcdvvmtvu5sQhhqvxU6CPL4Q9v3yFQ_lvBynyrYA" + + private var context: Context = mock() + private var cloud: Cloud = mock() + private var cloudContentRepository: CloudContentRepository = mock() + private var cryptoCloudContentRepositoryFactory: CryptoCloudContentRepositoryFactory = mock() + private var vault: Vault = mock() + private var cryptor: Cryptor = mock() + private var fileNameCryptor: FileNameCryptor = mock() + private var secureRandom: SecureRandom = mock() + + private lateinit var inTest: MasterkeyCryptoCloudProvider + + private fun any(type: Class): T = Mockito.any(type) + + @BeforeEach + fun setUp() { + whenever(cryptor.fileNameCryptor()).thenReturn(fileNameCryptor) + + val key = ByteArray(Masterkey.SUBKEY_LEN_BYTES + Masterkey.SUBKEY_LEN_BYTES) + doNothing().whenever(secureRandom).nextBytes(key) + + inTest = spy(MasterkeyCryptoCloudProvider(cloudContentRepository, cryptoCloudContentRepositoryFactory, secureRandom)) + } + + @Test + @DisplayName("create(\"/foo\", \"foo\")") + @Throws(BackendException::class) + fun testCreateVault() { + val rootFolder = RootTestFolder(cloud) + val foo = TestFolder(rootFolder, "foo", "/foo") + val vaultFile = TestFile(foo, CryptoConstants.VAULT_FILE_NAME, "/foo/" + CryptoConstants.VAULT_FILE_NAME, null, null) + val masterKeyFile = TestFile(foo, CryptoConstants.MASTERKEY_FILE_NAME, "/foo/" + CryptoConstants.MASTERKEY_FILE_NAME, null, null) + whenever(cloudContentRepository.file(foo, CryptoConstants.VAULT_FILE_NAME)).thenReturn(vaultFile) + whenever(cloudContentRepository.file(foo, CryptoConstants.MASTERKEY_FILE_NAME)).thenReturn(masterKeyFile) + + // 1. write masterkey + whenever(cloudContentRepository.write(eq(masterKeyFile), any(DataSource::class.java), eq(ProgressAware.NO_OP_PROGRESS_AWARE_UPLOAD), eq(false), any())) + .thenAnswer { invocationOnMock: InvocationOnMock -> + val inputStream = invocationOnMock.getArgument(1) + val masterKeyFileContent = BufferedReader(InputStreamReader(inputStream.open(context)!!, StandardCharsets.UTF_8)).lines().collect(Collectors.joining()) + MatcherAssert.assertThat(masterKeyFileContent, CoreMatchers.`is`(masterkeyV8)) + invocationOnMock.getArgument(0) + } + + // 2. initialize vault + whenever(cloudContentRepository.write(eq(vaultFile), any(DataSource::class.java), eq(ProgressAware.NO_OP_PROGRESS_AWARE_UPLOAD), eq(false), any())) + .thenAnswer { invocationOnMock: InvocationOnMock -> + val inputStream = invocationOnMock.getArgument(1) + val vaultConfigFileContent = BufferedReader(InputStreamReader(inputStream.open(context)!!, StandardCharsets.UTF_8)).lines().collect(Collectors.joining()) + MatcherAssert.assertThat(vaultConfigFileContent, CoreMatchers.`is`(vaultConfig)) + invocationOnMock.getArgument(0) + } + + // 3. create root folder + val rootDirHash = "KG6TFDGKXGZEGWRZOGTDFDF4YEGAZO6Q" + val dFolder = TestFolder(foo, "d", "/foo/" + CryptoConstants.DATA_DIR_NAME) + val lvl1Dir = TestFolder(dFolder, rootDirHash.substring(0, 2), "/foo/" + CryptoConstants.DATA_DIR_NAME + "/" + rootDirHash.substring(0, 2)) + val lvl2Dir = TestFolder(lvl1Dir, rootDirHash.substring(2), "/foo/" + CryptoConstants.DATA_DIR_NAME + "/" + rootDirHash.substring(0, 2) + "/" + rootDirHash.substring(2)) + + whenever(cloudContentRepository.folder(foo, CryptoConstants.DATA_DIR_NAME)).thenReturn(dFolder) + whenever(cloudContentRepository.create(dFolder)).thenReturn(dFolder) + whenever(cryptor.fileNameCryptor().hashDirectoryId(CryptoConstants.ROOT_DIR_ID)).thenReturn(CryptoConstants.ROOT_DIR_ID) + whenever(cloudContentRepository.folder(dFolder, lvl1Dir.name)).thenReturn(lvl1Dir) + whenever(cloudContentRepository.create(lvl1Dir)).thenReturn(lvl1Dir) + whenever(cloudContentRepository.folder(lvl1Dir, lvl2Dir.name)).thenReturn(lvl2Dir) + whenever(cloudContentRepository.create(lvl2Dir)).thenReturn(lvl2Dir) + + inTest.create(foo, "foo", VaultConfigBuilder().id("")) + + verify(cloudContentRepository).write( + eq(masterKeyFile), any( + DataSource::class.java + ), eq(ProgressAware.NO_OP_PROGRESS_AWARE_UPLOAD), eq(false), any() + ) + verify(cloudContentRepository).write( + eq(vaultFile), any( + DataSource::class.java + ), eq(ProgressAware.NO_OP_PROGRESS_AWARE_UPLOAD), eq(false), any() + ) + + verify(cloudContentRepository).create(dFolder) + verify(cloudContentRepository).create(lvl1Dir) + verify(cloudContentRepository).create(lvl2Dir) + } + + @Test + @DisplayName("lock(\"foo\")") + fun testLockVault() { + inTest.lock(vault) + verify(cryptoCloudContentRepositoryFactory).deregisterCryptor(vault) + } + + @Test + @DisplayName("unlock(\"foo\")") + @Throws(BackendException::class, IOException::class) + fun testUnlockVault() { + val cloudType : CloudType = mock() + + whenever(cloud.type()).thenReturn(cloudType) + whenever(vault.cloud).thenReturn(cloud) + whenever(vault.cloudType).thenReturn(cloudType) + whenever(vault.format).thenReturn(8) + whenever(vault.id).thenReturn(25L) + whenever(vault.name).thenReturn("foo") + whenever(vault.path).thenReturn("/foo") + whenever(vault.isUnlocked).thenReturn(true) + + val unlockToken = UnlockTokenImpl(vault, masterkeyV7.toByteArray(StandardCharsets.UTF_8)) + val unverifiedVaultConfig = UnverifiedVaultConfig(vaultConfig, URI.create(String.format("%s:%s", CryptoConstants.MASTERKEY_SCHEME, CryptoConstants.MASTERKEY_FILE_NAME)), CryptoConstants.MAX_VAULT_VERSION) + val result: Vault = inTest.unlock(unlockToken, Optional.of(unverifiedVaultConfig), "foo") { false } + + MatcherAssert.assertThat(result.isUnlocked, CoreMatchers.`is`(true)) + MatcherAssert.assertThat(result.format, CoreMatchers.`is`(8)) + MatcherAssert.assertThat(result.shorteningThreshold, CoreMatchers.`is`(CryptoConstants.DEFAULT_MAX_FILE_NAME)) + + verify(inTest).cryptorFor(unlockToken.getKeyFile("foo"), CryptorProvider.Scheme.SIV_CTRMAC) + verify(cryptoCloudContentRepositoryFactory).registerCryptor(any(Vault::class.java), any(Cryptor::class.java)) + } + + @Test + @DisplayName("unlockLegacy(\"foo\")") + @Throws(BackendException::class, IOException::class) + fun testUnlockLegacyVault() { + val cloudType : CloudType = mock() + + whenever(cloud.type()).thenReturn(cloudType) + whenever(vault.cloud).thenReturn(cloud) + whenever(vault.cloudType).thenReturn(cloudType) + whenever(vault.format).thenReturn(7) + whenever(vault.id).thenReturn(25L) + whenever(vault.name).thenReturn("foo") + whenever(vault.path).thenReturn("/foo") + whenever(vault.isUnlocked).thenReturn(true) + + val unlockToken = UnlockTokenImpl(vault, masterkeyV7.toByteArray(StandardCharsets.UTF_8)) + val result = inTest.unlock(unlockToken, Optional.absent(), "foo", { false }) + + MatcherAssert.assertThat(result.isUnlocked, CoreMatchers.`is`(true)) + MatcherAssert.assertThat(result.format, CoreMatchers.`is`(CryptoConstants.MAX_VAULT_VERSION_WITHOUT_VAULT_CONFIG)) + MatcherAssert.assertThat(result.shorteningThreshold, CoreMatchers.`is`(CryptoConstants.DEFAULT_MAX_FILE_NAME)) + + verify(inTest).cryptorFor(unlockToken.getKeyFile("foo"), CryptorProvider.Scheme.SIV_CTRMAC) + verify(cryptoCloudContentRepositoryFactory).registerCryptor(any(Vault::class.java), any(Cryptor::class.java)) + } + + @Test + @DisplayName("unlockLegacyUsingNewVault(\"foo\")") + fun testUnlockLegacyVaultUsingVaultFormat8() { + val unlockToken: UnlockToken = UnlockTokenImpl(vault, masterkeyV8.toByteArray(StandardCharsets.UTF_8)) + Assertions.assertThrows(UnsupportedVaultFormatException::class.java) { inTest.unlock(unlockToken, Optional.absent(), "foo", { false }) } + } + + @DisplayName("changePassword(\"foo\")") + @ParameterizedTest(name = "Legacy vault format {0}") + @ValueSource(booleans = [true, false]) + @Throws(BackendException::class) + fun tesChangePassword(legacy: Boolean) { + val cloudType : CloudType = mock() + + whenever(cloud.type()).thenReturn(cloudType) + whenever(vault.cloud).thenReturn(cloud) + whenever(vault.cloudType).thenReturn(cloudType) + whenever(vault.format).thenReturn(7) + whenever(vault.id).thenReturn(25L) + whenever(vault.name).thenReturn("foo") + whenever(vault.path).thenReturn("/foo") + whenever(vault.isUnlocked).thenReturn(true) + + if (legacy) { + testChangePassword(masterkeyV7, Optional.absent()) + } else { + val unverifiedVaultConfig = UnverifiedVaultConfig(vaultConfig, URI.create(String.format("%s:%s", CryptoConstants.MASTERKEY_SCHEME, CryptoConstants.MASTERKEY_FILE_NAME)), CryptoConstants.MAX_VAULT_VERSION) + testChangePassword(masterkeyV8, Optional.of(unverifiedVaultConfig)) + } + } + + @Throws(BackendException::class) + private fun testChangePassword(masterkeyContent: String, unverifiedVaultConfig: Optional) { + val rootFolder: TestFolder = RootTestFolder(cloud) + val foo = TestFolder(rootFolder, "foo", "/foo") + val vaultFile = TestFile(foo, CryptoConstants.VAULT_FILE_NAME, "/foo/" + CryptoConstants.VAULT_FILE_NAME, null, null) + val masterKeyFile = TestFile(foo, CryptoConstants.MASTERKEY_FILE_NAME, "/foo/" + CryptoConstants.MASTERKEY_FILE_NAME, null, null) + whenever(cloudContentRepository.file(foo, CryptoConstants.VAULT_FILE_NAME)).thenReturn(vaultFile) + whenever(cloudContentRepository.file(foo, CryptoConstants.MASTERKEY_FILE_NAME)).thenReturn(masterKeyFile) + whenever(cloudContentRepository.resolve(vault.cloud, vault.path)).thenReturn(foo) + + // 1. Read masterkey + Mockito.doAnswer { invocation: InvocationOnMock -> + val out = invocation.getArgument(2) + copyStreamToStream(ByteArrayInputStream(masterkeyContent.toByteArray()), out) + null + }.`when`(cloudContentRepository).read(eq(masterKeyFile), eq(null), any(), eq(ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD)) + + // 2. Create backup + val fileName: String = masterKeyFile.name + generate(masterkeyContent.toByteArray()) + CryptoConstants.MASTERKEY_BACKUP_FILE_EXT + val masterKeyBackupFile = TestFile(foo, fileName, "/foo/$fileName", null, null) + + whenever(cloudContentRepository.file(foo, fileName)).thenReturn(masterKeyBackupFile) + whenever(cloudContentRepository.write(eq(masterKeyBackupFile), any(DataSource::class.java), eq(ProgressAware.NO_OP_PROGRESS_AWARE_UPLOAD), eq(true), any())) + .thenAnswer { invocationOnMock: InvocationOnMock -> + val inputStream = invocationOnMock.getArgument(1) + val masterKeyFileContent = BufferedReader(InputStreamReader(inputStream.open(context)!!, StandardCharsets.UTF_8)).lines().collect(Collectors.joining()) + MatcherAssert.assertThat(masterKeyFileContent, CoreMatchers.`is`(masterkeyContent)) + invocationOnMock.getArgument(0) + } + + // 3. Create new Masterkey file + val changedMasterkey = if (unverifiedVaultConfig.isPresent) { + "{ \"version\": 999, \"scryptSalt\": \"AAAAAAAAAAA=\", \"scryptCostParam\": 32768, \"scryptBlockSize\": 8, \"primaryMasterKey\": \"O8Z6ZP+aScORaOrMtWYrXjA5EptZk+IAYjEDEUJ7yIvGOWsR+CiwkA==\", \"hmacMasterKey\": \"O8Z6ZP+aScORaOrMtWYrXjA5EptZk+IAYjEDEUJ7yIvGOWsR+CiwkA==\", \"versionMac\": \"trDKXqDhu94/VPuoWaQGBm8hwSPYc0D9t6DRRxKZ65k=\"}" + } else { + "{ \"version\": 7, \"scryptSalt\": \"AAAAAAAAAAA=\", \"scryptCostParam\": 32768, \"scryptBlockSize\": 8, \"primaryMasterKey\": \"O8Z6ZP+aScORaOrMtWYrXjA5EptZk+IAYjEDEUJ7yIvGOWsR+CiwkA==\", \"hmacMasterKey\": \"O8Z6ZP+aScORaOrMtWYrXjA5EptZk+IAYjEDEUJ7yIvGOWsR+CiwkA==\", \"versionMac\": \"cn2sAK6l9p1/w9deJVUuW3h7br056mpv5srvALiYw+g=\"}" + } + whenever(cloudContentRepository.write(eq(masterKeyFile), any(DataSource::class.java), eq(ProgressAware.NO_OP_PROGRESS_AWARE_UPLOAD), eq(true), any())) + .thenAnswer { invocationOnMock: InvocationOnMock -> + val inputStream = invocationOnMock.getArgument(1) + val masterKeyFileContent = BufferedReader(InputStreamReader(inputStream.open(context)!!, StandardCharsets.UTF_8)).lines().collect(Collectors.joining()) + MatcherAssert.assertThat(masterKeyFileContent, CoreMatchers.`is`(changedMasterkey)) + invocationOnMock.getArgument(0) + } + + inTest.changePassword(vault, unverifiedVaultConfig, "foo", "bar") + + Mockito.verify(cloudContentRepository).read(eq(masterKeyFile), eq(null), any(), eq(ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD)) + Mockito.verify(cloudContentRepository).write( + eq(masterKeyBackupFile), any( + DataSource::class.java + ), eq(ProgressAware.NO_OP_PROGRESS_AWARE_UPLOAD), eq(true), any() + ) + Mockito.verify(cloudContentRepository).write( + eq(masterKeyFile), any( + DataSource::class.java + ), eq(ProgressAware.NO_OP_PROGRESS_AWARE_UPLOAD), eq(true), any() + ) + } + + @DisplayName("isVaultPasswordValid(\"foo\", \"foo\")") + @ParameterizedTest(name = "Legacy vault format {0}") + @ValueSource(booleans = [true, false]) + @Throws(BackendException::class, IOException::class) + fun testVaultPasswordVault(legacy: Boolean) { + val password = "foo" + if (legacy) { + MatcherAssert.assertThat(testVaultPasswordVault(masterkeyV7, Optional.absent(), password), CoreMatchers.`is`(true)) + + val unlockToken = UnlockTokenImpl(vault, masterkeyV7.toByteArray(StandardCharsets.UTF_8)) + + Mockito.verify(inTest).cryptorFor(unlockToken.getKeyFile(password), CryptorProvider.Scheme.SIV_CTRMAC) + } else { + val unverifiedVaultConfig = UnverifiedVaultConfig(vaultConfig, URI.create(String.format("%s:%s", CryptoConstants.MASTERKEY_SCHEME, CryptoConstants.MASTERKEY_FILE_NAME)), CryptoConstants.MAX_VAULT_VERSION) + + MatcherAssert.assertThat(testVaultPasswordVault(masterkeyV8, Optional.of(unverifiedVaultConfig), password), CoreMatchers.`is`(true)) + + val unlockToken = UnlockTokenImpl(vault, masterkeyV8.toByteArray(StandardCharsets.UTF_8)) + + Mockito.verify(inTest).cryptorFor(unlockToken.getKeyFile(password), CryptorProvider.Scheme.SIV_CTRMAC) + } + } + + @DisplayName("isVaultPasswordValid(\"foo\", \"bar\")") + @ParameterizedTest(name = "Legacy vault format {0}") + @ValueSource(booleans = [true, false]) + @Throws(BackendException::class, IOException::class) + fun testVaultPasswordVaultInvalidPassword(legacy: Boolean) { + val password = "bar" + if (legacy) { + MatcherAssert.assertThat(testVaultPasswordVault(masterkeyV7, Optional.absent(), password), CoreMatchers.`is`(false)) + } else { + val unverifiedVaultConfig = UnverifiedVaultConfig(vaultConfig, URI.create(String.format("%s:%s", CryptoConstants.MASTERKEY_SCHEME, CryptoConstants.MASTERKEY_FILE_NAME)), CryptoConstants.MAX_VAULT_VERSION) + MatcherAssert.assertThat(testVaultPasswordVault(masterkeyV8, Optional.of(unverifiedVaultConfig), password), CoreMatchers.`is`(false)) + } + } + + @Throws(BackendException::class) + private fun testVaultPasswordVault(masterkeyContent: String, unverifiedVaultConfig: Optional, password: String): Boolean { + val cloudType : CloudType = mock() + + whenever(cloud.type()).thenReturn(cloudType) + whenever(vault.cloud).thenReturn(cloud) + whenever(vault.cloudType).thenReturn(cloudType) + whenever(vault.format).thenReturn(7) + whenever(vault.id).thenReturn(25L) + whenever(vault.name).thenReturn("foo") + whenever(vault.path).thenReturn("/foo") + whenever(vault.isUnlocked).thenReturn(true) + + val rootFolder: TestFolder = RootTestFolder(cloud) + val foo = TestFolder(rootFolder, "foo", "/foo") + val vaultFile = TestFile(foo, CryptoConstants.VAULT_FILE_NAME, "/foo/" + CryptoConstants.VAULT_FILE_NAME, null, null) + val masterKeyFile = TestFile(foo, CryptoConstants.MASTERKEY_FILE_NAME, "/foo/" + CryptoConstants.MASTERKEY_FILE_NAME, null, null) + + whenever(cloudContentRepository.file(foo, CryptoConstants.VAULT_FILE_NAME)).thenReturn(vaultFile) + whenever(cloudContentRepository.file(foo, CryptoConstants.MASTERKEY_FILE_NAME)).thenReturn(masterKeyFile) + whenever(cloudContentRepository.resolve(vault.cloud, vault.path)).thenReturn(foo) + whenever(cloudContentRepository.file(foo, CryptoConstants.VAULT_FILE_NAME)).thenReturn(vaultFile) + whenever(cloudContentRepository.file(foo, CryptoConstants.MASTERKEY_FILE_NAME)).thenReturn(masterKeyFile) + whenever(cloudContentRepository.resolve(vault.cloud, vault.path)).thenReturn(foo) + + // 1. Read masterkey + Mockito.doAnswer { invocation: InvocationOnMock -> + val out = invocation.getArgument(2) + copyStreamToStream(ByteArrayInputStream(masterkeyContent.toByteArray()), out) + null + }.`when`(cloudContentRepository).read(eq(masterKeyFile), eq(null), any(), eq(ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD)) + return inTest.isVaultPasswordValid(vault, unverifiedVaultConfig, password) + } +} diff --git a/data/src/test/java/org/cryptomator/data/cloud/crypto/TestFile.java b/data/src/test/java/org/cryptomator/data/cloud/crypto/TestFile.java index 17585c50..e22aee5a 100644 --- a/data/src/test/java/org/cryptomator/data/cloud/crypto/TestFile.java +++ b/data/src/test/java/org/cryptomator/data/cloud/crypto/TestFile.java @@ -2,7 +2,6 @@ package org.cryptomator.data.cloud.crypto; import org.cryptomator.domain.Cloud; import org.cryptomator.domain.CloudFile; -import org.cryptomator.util.Optional; import java.util.Date; import java.util.Objects; @@ -12,10 +11,10 @@ class TestFile implements CloudFile { private final TestFolder parent; private final String name; private final String path; - private final Optional size; - private final Optional modified; + private final Long size; + private final Date modified; - public TestFile(TestFolder parent, String name, String path, Optional size, Optional modified) { + public TestFile(TestFolder parent, String name, String path, Long size, Date modified) { this.parent = parent; this.name = name; this.path = path; @@ -44,12 +43,12 @@ class TestFile implements CloudFile { } @Override - public Optional getSize() { + public Long getSize() { return size; } @Override - public Optional getModified() { + public Date getModified() { return modified; } diff --git a/data/src/test/java/org/cryptomator/data/cloud/webdav/network/PropfindResponseParserTest.java b/data/src/test/java/org/cryptomator/data/cloud/webdav/network/PropfindResponseParserTest.java index 9a8fa2ac..71df4b48 100644 --- a/data/src/test/java/org/cryptomator/data/cloud/webdav/network/PropfindResponseParserTest.java +++ b/data/src/test/java/org/cryptomator/data/cloud/webdav/network/PropfindResponseParserTest.java @@ -5,7 +5,6 @@ import org.cryptomator.data.cloud.webdav.WebDavFile; import org.cryptomator.data.cloud.webdav.WebDavFolder; import org.cryptomator.domain.CloudNode; import org.cryptomator.domain.WebDavCloud; -import org.cryptomator.util.Optional; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; @@ -96,8 +95,8 @@ public class PropfindResponseParserTest { assertThat(nodes.get(0), is(cloudFile(new WebDavFile(PARENT_FOLDER, // "0ZRGQYTW7FFHOJDJWIJYVR3M6MOME5EAR", // "/asdasdasd/d/OC/0ZRGQYTW7FFHOJDJWIJYVR3M6MOME5EAR", // - Optional.of(36L), // - Optional.of(new Date("Thu, 30 Mar 2017 10:14:39 GMT")))))); + 36L, // + new Date("Thu, 30 Mar 2017 10:14:39 GMT"))))); } @Test @@ -120,8 +119,8 @@ public class PropfindResponseParserTest { assertThat(nodes.size(), is(2)); assertThat(nodes, // containsInAnyOrder( // - cloudFolder(new WebDavFolder(webDavFolder, "Gelöschte Dateien")), // - cloudFile(new WebDavFile(webDavFolder, "0.txt", Optional.of(54175L), Optional.of(new Date("Thu, 18 May 2017 9:49:41 GMT")))))); + cloudFolder(new WebDavFolder(webDavFolder, "Gelöschte Dateien", "/Gelöschte Dateien")), // + cloudFile(new WebDavFile(webDavFolder, "0.txt", 54175L, new Date("Thu, 18 May 2017 9:49:41 GMT"))))); } @Test diff --git a/domain/build.gradle b/domain/build.gradle index 4629f84c..de703a84 100644 --- a/domain/build.gradle +++ b/domain/build.gradle @@ -69,6 +69,7 @@ dependencies { testRuntimeOnly dependencies.junit4Engine testImplementation dependencies.mockito + testImplementation dependencies.mockitoKotlin testImplementation dependencies.mockitoInline testImplementation dependencies.hamcrest } diff --git a/domain/src/main/java/org/cryptomator/domain/Cloud.java b/domain/src/main/java/org/cryptomator/domain/Cloud.java deleted file mode 100644 index 1ede6d25..00000000 --- a/domain/src/main/java/org/cryptomator/domain/Cloud.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.cryptomator.domain; - -import java.io.Serializable; - -public interface Cloud extends Serializable { - - Long id(); - - CloudType type(); - - boolean configurationMatches(Cloud cloud); - - boolean predefined(); - - boolean persistent(); - - boolean requiresNetwork(); -} diff --git a/domain/src/main/java/org/cryptomator/domain/Cloud.kt b/domain/src/main/java/org/cryptomator/domain/Cloud.kt new file mode 100644 index 00000000..ee1aba13 --- /dev/null +++ b/domain/src/main/java/org/cryptomator/domain/Cloud.kt @@ -0,0 +1,13 @@ +package org.cryptomator.domain + +import java.io.Serializable + +interface Cloud : Serializable { + + fun id(): Long? + fun type(): CloudType? + fun configurationMatches(cloud: Cloud?): Boolean + fun predefined(): Boolean + fun persistent(): Boolean + fun requiresNetwork(): Boolean +} diff --git a/domain/src/main/java/org/cryptomator/domain/CloudFile.java b/domain/src/main/java/org/cryptomator/domain/CloudFile.java deleted file mode 100644 index 9fcdb7ab..00000000 --- a/domain/src/main/java/org/cryptomator/domain/CloudFile.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.cryptomator.domain; - -import org.cryptomator.util.Optional; - -import java.util.Date; - -public interface CloudFile extends CloudNode { - - Optional getSize(); - - Optional getModified(); - -} diff --git a/domain/src/main/java/org/cryptomator/domain/CloudFile.kt b/domain/src/main/java/org/cryptomator/domain/CloudFile.kt new file mode 100644 index 00000000..d7497238 --- /dev/null +++ b/domain/src/main/java/org/cryptomator/domain/CloudFile.kt @@ -0,0 +1,10 @@ +package org.cryptomator.domain + +import java.util.Date + +interface CloudFile : CloudNode { + + val size: Long? + val modified: Date? + override val parent: CloudFolder +} diff --git a/domain/src/main/java/org/cryptomator/domain/CloudFolder.java b/domain/src/main/java/org/cryptomator/domain/CloudFolder.java deleted file mode 100644 index 675a4dc8..00000000 --- a/domain/src/main/java/org/cryptomator/domain/CloudFolder.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.cryptomator.domain; - -public interface CloudFolder extends CloudNode { - - CloudFolder withCloud(Cloud cloud); - -} diff --git a/domain/src/main/java/org/cryptomator/domain/CloudFolder.kt b/domain/src/main/java/org/cryptomator/domain/CloudFolder.kt new file mode 100644 index 00000000..9a34418b --- /dev/null +++ b/domain/src/main/java/org/cryptomator/domain/CloudFolder.kt @@ -0,0 +1,6 @@ +package org.cryptomator.domain + +interface CloudFolder : CloudNode { + + fun withCloud(cloud: Cloud?): CloudFolder? +} diff --git a/domain/src/main/java/org/cryptomator/domain/CloudNode.java b/domain/src/main/java/org/cryptomator/domain/CloudNode.java deleted file mode 100755 index 1f83a945..00000000 --- a/domain/src/main/java/org/cryptomator/domain/CloudNode.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.cryptomator.domain; - -import java.io.Serializable; - -public interface CloudNode extends Serializable { - - Cloud getCloud(); - - String getName(); - - String getPath(); - - CloudFolder getParent(); -} diff --git a/domain/src/main/java/org/cryptomator/domain/CloudNode.kt b/domain/src/main/java/org/cryptomator/domain/CloudNode.kt new file mode 100755 index 00000000..ba808829 --- /dev/null +++ b/domain/src/main/java/org/cryptomator/domain/CloudNode.kt @@ -0,0 +1,11 @@ +package org.cryptomator.domain + +import java.io.Serializable + +interface CloudNode : Serializable { + + val cloud: Cloud? + val name: String + val path: String + val parent: CloudFolder? +} diff --git a/domain/src/main/java/org/cryptomator/domain/CloudType.java b/domain/src/main/java/org/cryptomator/domain/CloudType.kt similarity index 55% rename from domain/src/main/java/org/cryptomator/domain/CloudType.java rename to domain/src/main/java/org/cryptomator/domain/CloudType.kt index 5e7aafa7..a003c025 100644 --- a/domain/src/main/java/org/cryptomator/domain/CloudType.java +++ b/domain/src/main/java/org/cryptomator/domain/CloudType.kt @@ -1,7 +1,5 @@ -package org.cryptomator.domain; - -public enum CloudType { +package org.cryptomator.domain +enum class CloudType { DROPBOX, GOOGLE_DRIVE, ONEDRIVE, PCLOUD, WEBDAV, LOCAL, S3, CRYPTO - } diff --git a/domain/src/main/java/org/cryptomator/domain/exception/ParentFolderDoesNotExistException.java b/domain/src/main/java/org/cryptomator/domain/exception/ParentFolderDoesNotExistException.java index 030600d4..c9395f55 100644 --- a/domain/src/main/java/org/cryptomator/domain/exception/ParentFolderDoesNotExistException.java +++ b/domain/src/main/java/org/cryptomator/domain/exception/ParentFolderDoesNotExistException.java @@ -2,4 +2,12 @@ package org.cryptomator.domain.exception; public class ParentFolderDoesNotExistException extends BackendException { + public ParentFolderDoesNotExistException() { + super(); + } + + public ParentFolderDoesNotExistException(String name) { + super(name); + } + } diff --git a/domain/src/main/java/org/cryptomator/domain/exception/ParentFolderIsNullException.java b/domain/src/main/java/org/cryptomator/domain/exception/ParentFolderIsNullException.java new file mode 100644 index 00000000..e4f0f8b7 --- /dev/null +++ b/domain/src/main/java/org/cryptomator/domain/exception/ParentFolderIsNullException.java @@ -0,0 +1,9 @@ +package org.cryptomator.domain.exception; + +public class ParentFolderIsNullException extends BackendException { + + public ParentFolderIsNullException(String name) { + super(name); + } + +} diff --git a/domain/src/main/java/org/cryptomator/domain/exception/authentication/AuthenticationException.java b/domain/src/main/java/org/cryptomator/domain/exception/authentication/AuthenticationException.java index 0df773af..2ac36218 100644 --- a/domain/src/main/java/org/cryptomator/domain/exception/authentication/AuthenticationException.java +++ b/domain/src/main/java/org/cryptomator/domain/exception/authentication/AuthenticationException.java @@ -2,8 +2,9 @@ package org.cryptomator.domain.exception.authentication; import android.content.Intent; +import com.google.common.base.Optional; + import org.cryptomator.domain.Cloud; -import org.cryptomator.util.Optional; public abstract class AuthenticationException extends RuntimeException { @@ -24,7 +25,7 @@ public abstract class AuthenticationException extends RuntimeException { } public Optional getRecoveryAction() { - return Optional.empty(); + return Optional.absent(); } } diff --git a/domain/src/main/java/org/cryptomator/domain/exception/authentication/UserRecoverableAuthenticationException.java b/domain/src/main/java/org/cryptomator/domain/exception/authentication/UserRecoverableAuthenticationException.java index 36b04af2..2a1a78d5 100644 --- a/domain/src/main/java/org/cryptomator/domain/exception/authentication/UserRecoverableAuthenticationException.java +++ b/domain/src/main/java/org/cryptomator/domain/exception/authentication/UserRecoverableAuthenticationException.java @@ -2,8 +2,9 @@ package org.cryptomator.domain.exception.authentication; import android.content.Intent; +import com.google.common.base.Optional; + import org.cryptomator.domain.Cloud; -import org.cryptomator.util.Optional; public class UserRecoverableAuthenticationException extends AuthenticationException { @@ -15,7 +16,7 @@ public class UserRecoverableAuthenticationException extends AuthenticationExcept } public Optional getRecoveryAction() { - return Optional.ofNullable(recoveryAction); + return Optional.fromNullable(recoveryAction); } } diff --git a/domain/src/main/java/org/cryptomator/domain/repository/CloudContentRepository.java b/domain/src/main/java/org/cryptomator/domain/repository/CloudContentRepository.java deleted file mode 100644 index 836f9c84..00000000 --- a/domain/src/main/java/org/cryptomator/domain/repository/CloudContentRepository.java +++ /dev/null @@ -1,77 +0,0 @@ -package org.cryptomator.domain.repository; - -import org.cryptomator.domain.Cloud; -import org.cryptomator.domain.CloudFile; -import org.cryptomator.domain.CloudFolder; -import org.cryptomator.domain.CloudNode; -import org.cryptomator.domain.exception.BackendException; -import org.cryptomator.domain.exception.authentication.AuthenticationException; -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.OutputStream; -import java.util.List; - -/** - *

- * An interface to retrieve the contents of a cloud. - *

- * A CloudContentRepository will throw {@link AuthenticationException AuthenticationExceptions} - * from any operation if AuthenticationExceptions occur to allow correct handling in the UI. - */ -public interface CloudContentRepository { - - DirType root(CloudType cloud) throws BackendException; - - DirType resolve(CloudType cloud, String path) throws BackendException; - - FileType file(DirType parent, String name) throws BackendException; - - FileType file(DirType parent, String name, Optional size) throws BackendException; - - DirType folder(DirType parent, String name) throws BackendException; - - boolean exists(NodeType node) throws BackendException; - - List list(DirType folder) throws BackendException; - - /** - * Creates a cloud folder and maybe intermediate directories. - * - * @return created cloud folder (migth be different from target) - * @throws org.cryptomator.domain.exception.CloudNodeAlreadyExistsException If a cloud node with the same folder name already exists - */ - DirType create(DirType folder) throws BackendException; - - /** - * @return moved cloud folder (might be different from target) - * @throws org.cryptomator.domain.exception.CloudNodeAlreadyExistsException If a cloud node with the same target name already exists - */ - DirType move(DirType source, DirType target) throws BackendException; - - /** - * @return moved cloud file (might be different from target) - * @throws org.cryptomator.domain.exception.CloudNodeAlreadyExistsException If a cloud node with the same target name already exists - */ - FileType move(FileType source, FileType target) throws BackendException; - - /** - * @throws org.cryptomator.domain.exception.CloudNodeAlreadyExistsException If a cloud node with the same file name already exists - */ - FileType write(FileType file, DataSource data, ProgressAware progressAware, boolean replace, long size) throws BackendException; - - void read(FileType file, Optional encryptedTmpFile, OutputStream data, ProgressAware progressAware) throws BackendException; - - void delete(NodeType node) throws BackendException; - - String checkAuthenticationAndRetrieveCurrentAccount(CloudType cloud) throws BackendException; - - /** - * Performs a logout. After a call to this method further usage of this cloud will cause {@link org.cryptomator.cryptolib.api.AuthenticationFailedException AuthenticationFailedExceptions}. - */ - void logout(CloudType cloud) throws BackendException; -} diff --git a/domain/src/main/java/org/cryptomator/domain/repository/CloudContentRepository.kt b/domain/src/main/java/org/cryptomator/domain/repository/CloudContentRepository.kt new file mode 100644 index 00000000..f4ca070c --- /dev/null +++ b/domain/src/main/java/org/cryptomator/domain/repository/CloudContentRepository.kt @@ -0,0 +1,90 @@ +package org.cryptomator.domain.repository + +import org.cryptomator.domain.Cloud +import org.cryptomator.domain.CloudFile +import org.cryptomator.domain.CloudFolder +import org.cryptomator.domain.CloudNode +import org.cryptomator.domain.exception.BackendException +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 java.io.File +import java.io.OutputStream + +/** + * + * + * An interface to retrieve the contents of a cloud. + * + * + * A CloudContentRepository will throw [AuthenticationExceptions][AuthenticationException] + * from any operation if AuthenticationExceptions occur to allow correct handling in the UI. + */ +interface CloudContentRepository { + + @Throws(BackendException::class) + fun root(cloud: CloudType): DirType + + @Throws(BackendException::class) + fun resolve(cloud: CloudType, path: String): DirType + + @Throws(BackendException::class) + fun file(parent: DirType, name: String): FileType + + @Throws(BackendException::class) + fun file(parent: DirType, name: String, size: Long?): FileType + + @Throws(BackendException::class) + fun folder(parent: DirType, name: String): DirType + + @Throws(BackendException::class) + fun exists(node: NodeType): Boolean + + @Throws(BackendException::class) + fun list(folder: DirType): List + + /** + * Creates a cloud folder and maybe intermediate directories. + * + * @return created cloud folder (migth be different from target) + * @throws org.cryptomator.domain.exception.CloudNodeAlreadyExistsException If a cloud node with the same folder name already exists + */ + @Throws(BackendException::class) + fun create(folder: DirType): DirType + + /** + * @return moved cloud folder (might be different from target) + * @throws org.cryptomator.domain.exception.CloudNodeAlreadyExistsException If a cloud node with the same target name already exists + */ + @Throws(BackendException::class) + fun move(source: DirType, target: DirType): DirType + + /** + * @return moved cloud file (might be different from target) + * @throws org.cryptomator.domain.exception.CloudNodeAlreadyExistsException If a cloud node with the same target name already exists + */ + @Throws(BackendException::class) + fun move(source: FileType, target: FileType): FileType + + /** + * @throws org.cryptomator.domain.exception.CloudNodeAlreadyExistsException If a cloud node with the same file name already exists + */ + @Throws(BackendException::class) + fun write(file: FileType, data: DataSource, progressAware: ProgressAware, replace: Boolean, size: Long): FileType + + @Throws(BackendException::class) + fun read(file: FileType, encryptedTmpFile: File?, data: OutputStream, progressAware: ProgressAware) + + @Throws(BackendException::class) + fun delete(node: NodeType) + + @Throws(BackendException::class) + fun checkAuthenticationAndRetrieveCurrentAccount(cloud: CloudType): String + + /** + * Performs a logout. After a call to this method further usage of this cloud will cause [AuthenticationFailedExceptions][org.cryptomator.cryptolib.api.AuthenticationFailedException]. + */ + @Throws(BackendException::class) + fun logout(cloud: CloudType) +} diff --git a/domain/src/main/java/org/cryptomator/domain/repository/CloudRepository.java b/domain/src/main/java/org/cryptomator/domain/repository/CloudRepository.java index 55e71c98..469b2ac8 100644 --- a/domain/src/main/java/org/cryptomator/domain/repository/CloudRepository.java +++ b/domain/src/main/java/org/cryptomator/domain/repository/CloudRepository.java @@ -8,7 +8,7 @@ import org.cryptomator.domain.Vault; import org.cryptomator.domain.exception.BackendException; import org.cryptomator.domain.usecases.cloud.Flag; import org.cryptomator.domain.usecases.vault.UnlockToken; -import org.cryptomator.util.Optional; +import com.google.common.base.Optional; import java.util.List; diff --git a/domain/src/main/java/org/cryptomator/domain/repository/UpdateCheckRepository.java b/domain/src/main/java/org/cryptomator/domain/repository/UpdateCheckRepository.java index f1b0588f..f4395763 100644 --- a/domain/src/main/java/org/cryptomator/domain/repository/UpdateCheckRepository.java +++ b/domain/src/main/java/org/cryptomator/domain/repository/UpdateCheckRepository.java @@ -3,7 +3,7 @@ package org.cryptomator.domain.repository; import org.cryptomator.domain.exception.BackendException; import org.cryptomator.domain.exception.update.GeneralUpdateErrorException; import org.cryptomator.domain.usecases.UpdateCheck; -import org.cryptomator.util.Optional; +import com.google.common.base.Optional; import java.io.File; diff --git a/domain/src/main/java/org/cryptomator/domain/usecases/DoUpdateCheck.java b/domain/src/main/java/org/cryptomator/domain/usecases/DoUpdateCheck.java index 1ce94e50..01f158ac 100644 --- a/domain/src/main/java/org/cryptomator/domain/usecases/DoUpdateCheck.java +++ b/domain/src/main/java/org/cryptomator/domain/usecases/DoUpdateCheck.java @@ -4,7 +4,7 @@ import org.cryptomator.domain.exception.BackendException; import org.cryptomator.domain.repository.UpdateCheckRepository; import org.cryptomator.generator.Parameter; import org.cryptomator.generator.UseCase; -import org.cryptomator.util.Optional; +import com.google.common.base.Optional; @UseCase public class DoUpdateCheck { diff --git a/domain/src/main/java/org/cryptomator/domain/usecases/ProgressAware.java b/domain/src/main/java/org/cryptomator/domain/usecases/ProgressAware.java index 647bdd85..f8abdb76 100644 --- a/domain/src/main/java/org/cryptomator/domain/usecases/ProgressAware.java +++ b/domain/src/main/java/org/cryptomator/domain/usecases/ProgressAware.java @@ -1,11 +1,16 @@ package org.cryptomator.domain.usecases; +import org.cryptomator.domain.usecases.cloud.DownloadState; import org.cryptomator.domain.usecases.cloud.Progress; import org.cryptomator.domain.usecases.cloud.ProgressState; +import org.cryptomator.domain.usecases.cloud.UploadState; public interface ProgressAware { - ProgressAware NO_OP_PROGRESS_AWARE = progress -> { + ProgressAware NO_OP_PROGRESS_AWARE_DOWNLOAD = progress -> { + }; + + ProgressAware NO_OP_PROGRESS_AWARE_UPLOAD = progress -> { }; void onProgress(Progress progress); diff --git a/domain/src/main/java/org/cryptomator/domain/usecases/cloud/ByteArrayDataSource.java b/domain/src/main/java/org/cryptomator/domain/usecases/cloud/ByteArrayDataSource.java deleted file mode 100644 index 4cdb810c..00000000 --- a/domain/src/main/java/org/cryptomator/domain/usecases/cloud/ByteArrayDataSource.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.cryptomator.domain.usecases.cloud; - -import android.content.Context; - -import org.cryptomator.util.Optional; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -public class ByteArrayDataSource implements DataSource { - - private final byte[] bytes; - - private ByteArrayDataSource(byte[] bytes) { - this.bytes = bytes; - } - - public static DataSource from(byte[] bytes) { - return new ByteArrayDataSource(bytes); - } - - @Override - public Optional size(Context context) { - long size = bytes.length; - return Optional.of(size); - } - - @Override - public InputStream open(Context context) throws IOException { - return new ByteArrayInputStream(bytes); - } - - @Override - public DataSource decorate(DataSource delegate) { - return delegate; - } - - @Override - public void close() throws IOException { - // do nothing because ByteArrayInputStream need no close - } -} diff --git a/domain/src/main/java/org/cryptomator/domain/usecases/cloud/ByteArrayDataSource.kt b/domain/src/main/java/org/cryptomator/domain/usecases/cloud/ByteArrayDataSource.kt new file mode 100644 index 00000000..7fb94639 --- /dev/null +++ b/domain/src/main/java/org/cryptomator/domain/usecases/cloud/ByteArrayDataSource.kt @@ -0,0 +1,35 @@ +package org.cryptomator.domain.usecases.cloud + +import android.content.Context +import java.io.ByteArrayInputStream +import java.io.IOException +import java.io.InputStream + +class ByteArrayDataSource private constructor(private val bytes: ByteArray) : DataSource { + + override fun size(context: Context): Long { + return bytes.size.toLong() + } + + @Throws(IOException::class) + override fun open(context: Context): InputStream { + return ByteArrayInputStream(bytes) + } + + override fun decorate(delegate: DataSource): DataSource { + return delegate + } + + @Throws(IOException::class) + override fun close() { + // do nothing because ByteArrayInputStream need no close + } + + companion object { + + @JvmStatic + fun from(bytes: ByteArray): DataSource { + return ByteArrayDataSource(bytes) + } + } +} diff --git a/domain/src/main/java/org/cryptomator/domain/usecases/cloud/CancelAwareDataSource.java b/domain/src/main/java/org/cryptomator/domain/usecases/cloud/CancelAwareDataSource.java deleted file mode 100644 index 3791e9d1..00000000 --- a/domain/src/main/java/org/cryptomator/domain/usecases/cloud/CancelAwareDataSource.java +++ /dev/null @@ -1,49 +0,0 @@ -package org.cryptomator.domain.usecases.cloud; - -import android.content.Context; - -import org.cryptomator.domain.exception.CancellationException; -import org.cryptomator.util.Optional; - -import java.io.IOException; -import java.io.InputStream; - -public class CancelAwareDataSource implements DataSource { - - private final DataSource delegate; - private final Flag cancelled; - - private CancelAwareDataSource(DataSource delegate, Flag cancelled) { - this.delegate = delegate; - this.cancelled = cancelled; - } - - public static CancelAwareDataSource wrap(DataSource delegate, Flag cancelled) { - return new CancelAwareDataSource(delegate, cancelled); - } - - @Override - public Optional size(Context context) { - if (cancelled.get()) { - throw new CancellationException(); - } - return delegate.size(context); - } - - @Override - public InputStream open(Context context) throws IOException { - if (cancelled.get()) { - throw new CancellationException(); - } - return CancelAwareInputStream.wrap(delegate.open(context), cancelled); - } - - public CancelAwareDataSource decorate(DataSource delegate) { - return new CancelAwareDataSource(delegate, cancelled); - } - - @Override - public void close() throws IOException { - delegate.close(); - } -} diff --git a/domain/src/main/java/org/cryptomator/domain/usecases/cloud/CancelAwareDataSource.kt b/domain/src/main/java/org/cryptomator/domain/usecases/cloud/CancelAwareDataSource.kt new file mode 100644 index 00000000..5c1a82e2 --- /dev/null +++ b/domain/src/main/java/org/cryptomator/domain/usecases/cloud/CancelAwareDataSource.kt @@ -0,0 +1,41 @@ +package org.cryptomator.domain.usecases.cloud + +import android.content.Context +import org.cryptomator.domain.exception.CancellationException +import java.io.IOException +import java.io.InputStream + +class CancelAwareDataSource private constructor(private val delegate: DataSource, private val cancelled: Flag) : DataSource { + + override fun size(context: Context): Long? { + if (cancelled.get()) { + throw CancellationException() + } + return delegate.size(context) + } + + @Throws(IOException::class) + override fun open(context: Context): InputStream { + if (cancelled.get()) { + throw CancellationException() + } + return CancelAwareInputStream.wrap(delegate.open(context), cancelled) + } + + override fun decorate(delegate: DataSource): CancelAwareDataSource { + return CancelAwareDataSource(delegate, cancelled) + } + + @Throws(IOException::class) + override fun close() { + delegate.close() + } + + companion object { + + @JvmStatic + fun wrap(delegate: DataSource, cancelled: Flag): CancelAwareDataSource { + return CancelAwareDataSource(delegate, cancelled) + } + } +} diff --git a/domain/src/main/java/org/cryptomator/domain/usecases/cloud/DataSource.java b/domain/src/main/java/org/cryptomator/domain/usecases/cloud/DataSource.java deleted file mode 100644 index 460c68b9..00000000 --- a/domain/src/main/java/org/cryptomator/domain/usecases/cloud/DataSource.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.cryptomator.domain.usecases.cloud; - -import android.content.Context; - -import org.cryptomator.util.Optional; - -import java.io.Closeable; -import java.io.IOException; -import java.io.InputStream; -import java.io.Serializable; - -public interface DataSource extends Serializable, Closeable { - - Optional size(Context context); - - InputStream open(Context context) throws IOException; - - DataSource decorate(DataSource delegate); - -} diff --git a/domain/src/main/java/org/cryptomator/domain/usecases/cloud/DataSource.kt b/domain/src/main/java/org/cryptomator/domain/usecases/cloud/DataSource.kt new file mode 100644 index 00000000..9f965d2d --- /dev/null +++ b/domain/src/main/java/org/cryptomator/domain/usecases/cloud/DataSource.kt @@ -0,0 +1,18 @@ +package org.cryptomator.domain.usecases.cloud + +import android.content.Context +import java.io.Closeable +import java.io.IOException +import java.io.InputStream +import java.io.Serializable + +interface DataSource : Serializable, Closeable { + + fun size(context: Context): Long? + + @Throws(IOException::class) + fun open(context: Context): InputStream? + + fun decorate(delegate: DataSource): DataSource + +} diff --git a/domain/src/main/java/org/cryptomator/domain/usecases/cloud/DownloadFiles.java b/domain/src/main/java/org/cryptomator/domain/usecases/cloud/DownloadFiles.java index 8b8d26c7..6d557336 100644 --- a/domain/src/main/java/org/cryptomator/domain/usecases/cloud/DownloadFiles.java +++ b/domain/src/main/java/org/cryptomator/domain/usecases/cloud/DownloadFiles.java @@ -7,7 +7,6 @@ import org.cryptomator.domain.usecases.DownloadFile; import org.cryptomator.domain.usecases.ProgressAware; import org.cryptomator.generator.Parameter; import org.cryptomator.generator.UseCase; -import org.cryptomator.util.Optional; import java.io.Closeable; import java.io.IOException; @@ -30,7 +29,7 @@ class DownloadFiles { List downloadedFiles = new ArrayList<>(); for (DownloadFile file : downloadFiles) { try { - cloudContentRepository.read(file.getDownloadFile(), Optional.empty(), file.getDataSink(), progressAware); + cloudContentRepository.read(file.getDownloadFile(), null, file.getDataSink(), progressAware); downloadedFiles.add(file.getDownloadFile()); } finally { closeQuietly(file.getDataSink()); diff --git a/domain/src/main/java/org/cryptomator/domain/usecases/cloud/FileBasedDataSource.java b/domain/src/main/java/org/cryptomator/domain/usecases/cloud/FileBasedDataSource.java deleted file mode 100644 index fea189d5..00000000 --- a/domain/src/main/java/org/cryptomator/domain/usecases/cloud/FileBasedDataSource.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.cryptomator.domain.usecases.cloud; - -import android.content.Context; - -import org.cryptomator.util.Optional; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; - -public class FileBasedDataSource implements DataSource { - - private final File file; - - private FileBasedDataSource(File file) { - this.file = file; - } - - public static FileBasedDataSource from(File file) { - return new FileBasedDataSource(file); - } - - @Override - public Optional size(Context context) { - return Optional.of(file.length()); - } - - @Override - public InputStream open(Context context) throws IOException { - return new FileInputStream(file); - } - - @Override - public DataSource decorate(DataSource delegate) { - return delegate; - } - - @Override - public void close() throws IOException { - // Do nothing - } -} diff --git a/domain/src/main/java/org/cryptomator/domain/usecases/cloud/FileBasedDataSource.kt b/domain/src/main/java/org/cryptomator/domain/usecases/cloud/FileBasedDataSource.kt new file mode 100644 index 00000000..49964568 --- /dev/null +++ b/domain/src/main/java/org/cryptomator/domain/usecases/cloud/FileBasedDataSource.kt @@ -0,0 +1,36 @@ +package org.cryptomator.domain.usecases.cloud + +import android.content.Context +import java.io.File +import java.io.FileInputStream +import java.io.IOException +import java.io.InputStream + +class FileBasedDataSource private constructor(private val file: File) : DataSource { + + override fun size(context: Context): Long { + return file.length() + } + + @Throws(IOException::class) + override fun open(context: Context): InputStream { + return FileInputStream(file) + } + + override fun decorate(delegate: DataSource): DataSource { + return delegate + } + + @Throws(IOException::class) + override fun close() { + // Do nothing + } + + companion object { + + @JvmStatic + fun from(file: File): FileBasedDataSource { + return FileBasedDataSource(file) + } + } +} diff --git a/domain/src/main/java/org/cryptomator/domain/usecases/cloud/UploadFiles.java b/domain/src/main/java/org/cryptomator/domain/usecases/cloud/UploadFiles.java index f56c6c8a..284e2a88 100644 --- a/domain/src/main/java/org/cryptomator/domain/usecases/cloud/UploadFiles.java +++ b/domain/src/main/java/org/cryptomator/domain/usecases/cloud/UploadFiles.java @@ -11,7 +11,6 @@ import org.cryptomator.domain.repository.CloudContentRepository; import org.cryptomator.domain.usecases.ProgressAware; import org.cryptomator.generator.Parameter; import org.cryptomator.generator.UseCase; -import org.cryptomator.util.Optional; import java.io.Closeable; import java.io.File; @@ -79,7 +78,7 @@ class UploadFiles { private CloudFile upload(UploadFile uploadFile, ProgressAware progressAware) throws BackendException { DataSource dataSource = uploadFile.getDataSource(); - if (dataSource.size(context).isPresent()) { + if (dataSource.size(context) != null) { return upload(uploadFile, dataSource, progressAware); } else { File file = copyDataToFile(dataSource); @@ -113,14 +112,14 @@ class UploadFiles { } private CloudFile writeCloudFile(String fileName, CancelAwareDataSource dataSource, boolean replacing, ProgressAware progressAware) throws BackendException { - Optional size = dataSource.size(context); + Long size = dataSource.size(context); CloudFile source = cloudContentRepository.file(parent, fileName, size); return cloudContentRepository.write( // source, // dataSource, // progressAware, // replacing, // - size.get()); + size); } private void copy(InputStream in, OutputStream out) throws IOException { diff --git a/domain/src/main/java/org/cryptomator/domain/usecases/vault/ChangePassword.java b/domain/src/main/java/org/cryptomator/domain/usecases/vault/ChangePassword.java index c4321e5f..198002d1 100644 --- a/domain/src/main/java/org/cryptomator/domain/usecases/vault/ChangePassword.java +++ b/domain/src/main/java/org/cryptomator/domain/usecases/vault/ChangePassword.java @@ -9,7 +9,7 @@ import org.cryptomator.domain.exception.NoSuchVaultException; import org.cryptomator.domain.repository.CloudRepository; import org.cryptomator.generator.Parameter; import org.cryptomator.generator.UseCase; -import org.cryptomator.util.Optional; +import com.google.common.base.Optional; import static org.cryptomator.util.ExceptionUtil.contains; diff --git a/domain/src/main/java/org/cryptomator/domain/usecases/vault/CheckVaultPassword.java b/domain/src/main/java/org/cryptomator/domain/usecases/vault/CheckVaultPassword.java index cc201583..994f3f9e 100644 --- a/domain/src/main/java/org/cryptomator/domain/usecases/vault/CheckVaultPassword.java +++ b/domain/src/main/java/org/cryptomator/domain/usecases/vault/CheckVaultPassword.java @@ -6,7 +6,7 @@ import org.cryptomator.domain.exception.BackendException; import org.cryptomator.domain.repository.CloudRepository; import org.cryptomator.generator.Parameter; import org.cryptomator.generator.UseCase; -import org.cryptomator.util.Optional; +import com.google.common.base.Optional; @UseCase class CheckVaultPassword { diff --git a/domain/src/main/java/org/cryptomator/domain/usecases/vault/GetUnverifiedVaultConfig.java b/domain/src/main/java/org/cryptomator/domain/usecases/vault/GetUnverifiedVaultConfig.java index 4922008e..6aadcf44 100644 --- a/domain/src/main/java/org/cryptomator/domain/usecases/vault/GetUnverifiedVaultConfig.java +++ b/domain/src/main/java/org/cryptomator/domain/usecases/vault/GetUnverifiedVaultConfig.java @@ -1,5 +1,7 @@ package org.cryptomator.domain.usecases.vault; +import com.google.common.base.Optional; + import org.cryptomator.domain.UnverifiedVaultConfig; import org.cryptomator.domain.Vault; import org.cryptomator.domain.exception.BackendException; @@ -7,7 +9,6 @@ import org.cryptomator.domain.exception.NoSuchCloudFileException; import org.cryptomator.domain.repository.CloudRepository; import org.cryptomator.generator.Parameter; import org.cryptomator.generator.UseCase; -import org.cryptomator.util.Optional; import static org.cryptomator.util.ExceptionUtil.contains; @@ -27,7 +28,7 @@ public class GetUnverifiedVaultConfig { return cloudRepository.unverifiedVaultConfig(vault); } catch (BackendException e) { if (contains(e, NoSuchCloudFileException.class)) { - return Optional.empty(); + return Optional.absent(); } throw e; } diff --git a/domain/src/main/java/org/cryptomator/domain/usecases/vault/PrepareUnlock.java b/domain/src/main/java/org/cryptomator/domain/usecases/vault/PrepareUnlock.java index 931d312d..ca2a2464 100644 --- a/domain/src/main/java/org/cryptomator/domain/usecases/vault/PrepareUnlock.java +++ b/domain/src/main/java/org/cryptomator/domain/usecases/vault/PrepareUnlock.java @@ -8,7 +8,7 @@ import org.cryptomator.domain.exception.NoSuchVaultException; import org.cryptomator.domain.repository.CloudRepository; import org.cryptomator.generator.Parameter; import org.cryptomator.generator.UseCase; -import org.cryptomator.util.Optional; +import com.google.common.base.Optional; import static org.cryptomator.util.ExceptionUtil.contains; diff --git a/domain/src/main/java/org/cryptomator/domain/usecases/vault/UnlockVaultUsingMasterkey.java b/domain/src/main/java/org/cryptomator/domain/usecases/vault/UnlockVaultUsingMasterkey.java index aadb6d01..4703ceff 100644 --- a/domain/src/main/java/org/cryptomator/domain/usecases/vault/UnlockVaultUsingMasterkey.java +++ b/domain/src/main/java/org/cryptomator/domain/usecases/vault/UnlockVaultUsingMasterkey.java @@ -7,7 +7,7 @@ import org.cryptomator.domain.repository.CloudRepository; import org.cryptomator.domain.usecases.cloud.Flag; import org.cryptomator.generator.Parameter; import org.cryptomator.generator.UseCase; -import org.cryptomator.util.Optional; +import com.google.common.base.Optional; @UseCase class UnlockVaultUsingMasterkey { diff --git a/domain/src/main/java/org/cryptomator/domain/usecases/vault/VaultOrUnlockToken.java b/domain/src/main/java/org/cryptomator/domain/usecases/vault/VaultOrUnlockToken.java index 1a8efb51..170bd6e6 100644 --- a/domain/src/main/java/org/cryptomator/domain/usecases/vault/VaultOrUnlockToken.java +++ b/domain/src/main/java/org/cryptomator/domain/usecases/vault/VaultOrUnlockToken.java @@ -1,7 +1,8 @@ package org.cryptomator.domain.usecases.vault; +import com.google.common.base.Optional; + import org.cryptomator.domain.Vault; -import org.cryptomator.util.Optional; import java.io.Serializable; @@ -24,11 +25,11 @@ public class VaultOrUnlockToken implements Serializable { } public Optional getVault() { - return Optional.ofNullable(vault); + return Optional.fromNullable(vault); } public Optional getUnlockToken() { - return Optional.ofNullable(unlockToken); + return Optional.fromNullable(unlockToken); } } diff --git a/domain/src/test/java/org/cryptomator/domain/usecases/cloud/DataSourceCapturingAnswer.java b/domain/src/test/java/org/cryptomator/domain/usecases/cloud/DataSourceCapturingAnswer.java deleted file mode 100644 index bd488556..00000000 --- a/domain/src/test/java/org/cryptomator/domain/usecases/cloud/DataSourceCapturingAnswer.java +++ /dev/null @@ -1,44 +0,0 @@ -package org.cryptomator.domain.usecases.cloud; - -import org.mockito.invocation.InvocationOnMock; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; - -class DataSourceCapturingAnswer implements org.mockito.stubbing.Answer { - - private final T result; - private final int argIndex; - private ByteArrayOutputStream out; - - DataSourceCapturingAnswer(T result, int argIndex) { - this.result = result; - this.argIndex = argIndex; - } - - @Override - public T answer(InvocationOnMock invocation) throws Throwable { - InputStream in = ((DataSource) invocation.getArguments()[argIndex]).open(null); - out = new ByteArrayOutputStream(); - copy(in, out); - return result; - } - - private void copy(InputStream in, ByteArrayOutputStream out) { - byte[] buffer = new byte[4096]; - int read; - try { - while ((read = in.read(buffer)) != -1) { - out.write(buffer, 0, read); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - public byte[] toByteArray() { - return out.toByteArray(); - } - -} diff --git a/domain/src/test/java/org/cryptomator/domain/usecases/cloud/DataSourceCapturingAnswer.kt b/domain/src/test/java/org/cryptomator/domain/usecases/cloud/DataSourceCapturingAnswer.kt new file mode 100644 index 00000000..514b9865 --- /dev/null +++ b/domain/src/test/java/org/cryptomator/domain/usecases/cloud/DataSourceCapturingAnswer.kt @@ -0,0 +1,40 @@ +package org.cryptomator.domain.usecases.cloud + +import android.content.Context +import org.mockito.invocation.InvocationOnMock +import org.mockito.kotlin.mock +import org.mockito.stubbing.Answer +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.InputStream + +internal class DataSourceCapturingAnswer(private val result: T, private val argIndex: Int) : Answer { + + private lateinit var out: ByteArrayOutputStream + + private val context : Context = mock() + + @Throws(Throwable::class) + override fun answer(invocation: InvocationOnMock): T { + val inputStream = (invocation.arguments[argIndex] as DataSource).open(context)!! + out = ByteArrayOutputStream() + copy(inputStream, out) + return result + } + + private fun copy(inputStream: InputStream, out: ByteArrayOutputStream) { + val buffer = ByteArray(4096) + var read: Int + try { + while (inputStream.read(buffer).also { read = it } != -1) { + out.write(buffer, 0, read) + } + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + fun toByteArray(): ByteArray { + return out.toByteArray() + } +} diff --git a/domain/src/test/java/org/cryptomator/domain/usecases/cloud/DownloadFileTest.java b/domain/src/test/java/org/cryptomator/domain/usecases/cloud/DownloadFileTest.java index ff237736..481d8fa1 100644 --- a/domain/src/test/java/org/cryptomator/domain/usecases/cloud/DownloadFileTest.java +++ b/domain/src/test/java/org/cryptomator/domain/usecases/cloud/DownloadFileTest.java @@ -5,7 +5,6 @@ import org.cryptomator.domain.exception.BackendException; import org.cryptomator.domain.repository.CloudContentRepository; import org.cryptomator.domain.usecases.DownloadFile; import org.cryptomator.domain.usecases.ProgressAware; -import org.cryptomator.util.Optional; import org.junit.jupiter.api.Test; import java.io.OutputStream; @@ -39,7 +38,7 @@ public class DownloadFileTest { List results = inTest.execute(progressAware); - verify(cloudContentRepository).read(downloadFile, Optional.empty(), dataSink, progressAware); + verify(cloudContentRepository).read(downloadFile, null, dataSink, progressAware); verifyNoMoreInteractions(cloudContentRepository); assertThat(results, is(singletonList(downloadFile))); @@ -57,7 +56,7 @@ public class DownloadFileTest { List results = inTest.execute(progressAware); verify(cloudContentRepository, times(downloadFiles.size())) // - .read(downloadFile, Optional.empty(), dataSink, progressAware); + .read(downloadFile, null, dataSink, progressAware); verifyNoMoreInteractions(cloudContentRepository); assertThat(results, is(asList(downloadFile, downloadFile))); diff --git a/domain/src/test/java/org/cryptomator/domain/usecases/cloud/UploadFileTest.java b/domain/src/test/java/org/cryptomator/domain/usecases/cloud/UploadFileTest.java deleted file mode 100644 index 441355b8..00000000 --- a/domain/src/test/java/org/cryptomator/domain/usecases/cloud/UploadFileTest.java +++ /dev/null @@ -1,122 +0,0 @@ -package org.cryptomator.domain.usecases.cloud; - -import android.content.Context; - -import org.cryptomator.domain.CloudFile; -import org.cryptomator.domain.CloudFolder; -import org.cryptomator.domain.exception.BackendException; -import org.cryptomator.domain.repository.CloudContentRepository; -import org.cryptomator.domain.usecases.ProgressAware; -import org.cryptomator.util.Optional; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.List; - -import static java.util.Arrays.fill; -import static java.util.Collections.singletonList; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; -import static org.mockito.Matchers.same; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class UploadFileTest { - - private final Context context = mock(Context.class); - private final CloudContentRepository cloudContentRepository = mock(CloudContentRepository.class); - private final CloudFolder parent = mock(CloudFolder.class); - private final CloudFile targetFile = mock(CloudFile.class); - private final CloudFile resultFile = mock(CloudFile.class); - private final String fileName = "fileName"; - private final ProgressAware progressAware = mock(ProgressAware.class); - - @ParameterizedTest - @ValueSource(booleans = {true, false}) - public void testInvocationWithFileSizeDelegatesToCloudContentRepository(Boolean replacing) throws BackendException { - long fileSize = 1337; - DataSource dataSource = dataSourceWithBytes(0, fileSize, Optional.of(fileSize)); - UploadFiles inTest = testCandidate(dataSource, replacing); - when(cloudContentRepository.file(parent, fileName, Optional.of(fileSize))).thenReturn(targetFile); - when(cloudContentRepository.write(same(targetFile), any(DataSource.class), same(progressAware), eq(replacing), eq(fileSize))).thenReturn(resultFile); - - List result = inTest.execute(progressAware); - - assertThat(result.size(), is(1)); - assertThat(result.get(0), is(resultFile)); - } - - @ParameterizedTest - @ValueSource(booleans = {true, false}) - public void testInvocationWithoutFileSizeDelegatesToCloudContentRepository(Boolean replacing) throws BackendException, IOException { - long fileSize = 8893; - try (DataSource dataSource = dataSourceWithBytes(85, fileSize, Optional.empty())) { - UploadFiles inTest = testCandidate(dataSource, replacing); - when(cloudContentRepository.file(parent, fileName, Optional.of(fileSize))).thenReturn(targetFile); - DataSourceCapturingAnswer capturedStreamData = new DataSourceCapturingAnswer(resultFile, 1); - when(cloudContentRepository.write(same(targetFile), any(DataSource.class), same(progressAware), eq(replacing), eq(fileSize))).thenAnswer(capturedStreamData); - - List result = inTest.execute(progressAware); - - assertThat(result.size(), is(1)); - assertThat(result.get(0), is(resultFile)); - assertThat(capturedStreamData.toByteArray(), is(bytes(85, fileSize))); - } - } - - private DataSource dataSourceWithBytes(int value, long amount, final Optional size) { - if (amount > Integer.MAX_VALUE) { - throw new IllegalStateException("Can not use values > Integer.MAX_VALUE"); - } - final byte[] bytes = bytes(value, (int) amount); - return new DataSource() { - - @Override - public Optional size(Context context) { - return size; - } - - @Override - public InputStream open(Context context) throws IOException { - return new ByteArrayInputStream(bytes); - } - - @Override - public DataSource decorate(DataSource delegate) { - return delegate; - } - - @Override - public void close() throws IOException { - // do nothing - } - }; - } - - private byte[] bytes(int value, long amount) { - if (amount > Integer.MAX_VALUE) { - throw new IllegalStateException("Can not use values > Integer.MAX_VALUE"); - } - byte[] data = new byte[(int) amount]; - fill(data, (byte) value); - return data; - } - - private UploadFiles testCandidate(DataSource dataSource, Boolean replacing) { - return new UploadFiles( // - context, // - cloudContentRepository, // - parent, // - singletonList(new UploadFile.Builder() // - .withFileName(fileName) // - .withDataSource(dataSource) // - .thatIsReplacing(replacing) // - .build())); - } - -} diff --git a/domain/src/test/java/org/cryptomator/domain/usecases/cloud/UploadFileTest.kt b/domain/src/test/java/org/cryptomator/domain/usecases/cloud/UploadFileTest.kt new file mode 100644 index 00000000..23c2f327 --- /dev/null +++ b/domain/src/test/java/org/cryptomator/domain/usecases/cloud/UploadFileTest.kt @@ -0,0 +1,136 @@ +package org.cryptomator.domain.usecases.cloud + +import android.content.Context +import org.cryptomator.domain.Cloud +import org.cryptomator.domain.CloudFile +import org.cryptomator.domain.CloudFolder +import org.cryptomator.domain.CloudNode +import org.cryptomator.domain.exception.BackendException +import org.cryptomator.domain.repository.CloudContentRepository +import org.cryptomator.domain.usecases.ProgressAware +import org.hamcrest.CoreMatchers +import org.hamcrest.MatcherAssert +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import org.mockito.Mockito +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.same +import org.mockito.kotlin.whenever +import java.io.ByteArrayInputStream +import java.io.IOException +import java.io.InputStream +import java.util.Arrays + +class UploadFileTest { + + private val context :Context = mock() + private var cloudContentRepository: CloudContentRepository = mock() + private val parent :CloudFolder = mock() + private val targetFile :CloudFile = mock() + private val resultFile :CloudFile = mock() + + private val progressAware: ProgressAware = mock() + + private val fileName = "fileName" + + private fun any(type: Class): T = Mockito.any(type) + + @ParameterizedTest + @ValueSource(booleans = [true, false]) + @Throws(BackendException::class) + fun testInvocationWithFileSizeDelegatesToCloudContentRepository(replacing: Boolean) { + val fileSize: Long = 1337 + val dataSource = dataSourceWithBytes(0, fileSize, fileSize) + val inTest = testCandidate(dataSource, replacing) + + whenever(cloudContentRepository.file(parent, fileName, fileSize)).thenReturn(targetFile) + whenever( + cloudContentRepository.write( + same(targetFile), + any(DataSource::class.java), + same(progressAware), + eq(replacing), + eq(fileSize) + ) + ).thenReturn(resultFile) + + val result = inTest.execute(progressAware) + + MatcherAssert.assertThat(result.size, CoreMatchers.`is`(1)) + MatcherAssert.assertThat(result[0], CoreMatchers.`is`(resultFile)) + } + + @ParameterizedTest + @ValueSource(booleans = [true, false]) + @Throws(BackendException::class, IOException::class) + fun testInvocationWithoutFileSizeDelegatesToCloudContentRepository(replacing: Boolean) { + val fileSize: Long = 8893 + dataSourceWithBytes(85, fileSize, null).use { dataSource -> + val inTest = testCandidate(dataSource, replacing) + whenever(cloudContentRepository.file(parent, fileName, fileSize)).thenReturn(targetFile) + val capturedStreamData = DataSourceCapturingAnswer(resultFile, 1) + whenever( + cloudContentRepository.write( + same(targetFile), + any(DataSource::class.java), + same(progressAware), + eq(replacing), + eq(fileSize) + ) + ).thenAnswer(capturedStreamData) + + val result = inTest.execute(progressAware) + + MatcherAssert.assertThat(result.size, CoreMatchers.`is`(1)) + MatcherAssert.assertThat(result[0], CoreMatchers.`is`(resultFile)) + MatcherAssert.assertThat(capturedStreamData.toByteArray(), CoreMatchers.`is`(bytes(85, fileSize))) + } + } + + private fun dataSourceWithBytes(value: Int, amount: Long, size: Long?): DataSource { + check(amount <= Int.MAX_VALUE) { "Can not use values > Integer.MAX_VALUE" } + val bytes = bytes(value, amount) + return object : DataSource { + override fun size(context: Context): Long? { + return size + } + + @Throws(IOException::class) + override fun open(context: Context): InputStream { + return ByteArrayInputStream(bytes) + } + + override fun decorate(delegate: DataSource): DataSource { + return delegate + } + + @Throws(IOException::class) + override fun close() { + // do nothing + } + } + } + + private fun bytes(value: Int, amount: Long): ByteArray { + check(amount <= Int.MAX_VALUE) { "Can not use values > Integer.MAX_VALUE" } + val data = ByteArray(amount.toInt()) + Arrays.fill(data, value.toByte()) + return data + } + + private fun testCandidate(dataSource: DataSource, replacing: Boolean): UploadFiles { + return UploadFiles( // + context, // + cloudContentRepository, // + parent, // + listOf( + UploadFile.Builder() // + .withFileName(fileName) // + .withDataSource(dataSource) // + .thatIsReplacing(replacing) // + .build() + ) + ) + } +} diff --git a/domain/src/test/java/org/cryptomator/domain/usecases/vault/UnlockVaultUsingMasterkeyTest.java b/domain/src/test/java/org/cryptomator/domain/usecases/vault/UnlockVaultUsingMasterkeyTest.java index c3cce335..de43bd7e 100644 --- a/domain/src/test/java/org/cryptomator/domain/usecases/vault/UnlockVaultUsingMasterkeyTest.java +++ b/domain/src/test/java/org/cryptomator/domain/usecases/vault/UnlockVaultUsingMasterkeyTest.java @@ -1,10 +1,11 @@ package org.cryptomator.domain.usecases.vault; +import com.google.common.base.Optional; + import org.cryptomator.domain.UnverifiedVaultConfig; import org.cryptomator.domain.Vault; import org.cryptomator.domain.exception.BackendException; import org.cryptomator.domain.repository.CloudRepository; -import org.cryptomator.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; diff --git a/presentation/src/androidTest/java/org/cryptomator/presentation/CloudContentRepositoryBlackboxTest.java b/presentation/src/androidTest/java/org/cryptomator/presentation/CloudContentRepositoryBlackboxTest.java index c87faf61..e2400dc6 100644 --- a/presentation/src/androidTest/java/org/cryptomator/presentation/CloudContentRepositoryBlackboxTest.java +++ b/presentation/src/androidTest/java/org/cryptomator/presentation/CloudContentRepositoryBlackboxTest.java @@ -12,6 +12,7 @@ import org.cryptomator.domain.LocalStorageCloud; import org.cryptomator.domain.exception.BackendException; import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException; import org.cryptomator.domain.repository.CloudContentRepository; +import org.cryptomator.domain.usecases.ProgressAware; import org.cryptomator.domain.usecases.cloud.ByteArrayDataSource; import org.cryptomator.presentation.di.component.ApplicationComponent; import org.cryptomator.presentation.testCloud.CryptoTestCloud; @@ -23,7 +24,6 @@ import org.cryptomator.presentation.testCloud.OnedriveTestCloud; import org.cryptomator.presentation.testCloud.TestCloud; import org.cryptomator.presentation.testCloud.WebdavTestCloud; import org.cryptomator.presentation.ui.activity.SplashActivity; -import org.cryptomator.util.Optional; import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -38,7 +38,6 @@ import java.util.List; import java.util.UUID; import static androidx.test.InstrumentationRegistry.getTargetContext; -import static org.cryptomator.domain.usecases.ProgressAware.NO_OP_PROGRESS_AWARE; import static org.cryptomator.presentation.CloudNodeMatchers.aFile; import static org.cryptomator.presentation.CloudNodeMatchers.folder; import static org.hamcrest.CoreMatchers.is; @@ -397,7 +396,7 @@ public class CloudContentRepositoryBlackboxTest { private byte[] read(CloudFile file) throws BackendException { ByteArrayOutputStream out = new ByteArrayOutputStream(); - inTest.read(file, Optional.empty(), out, NO_OP_PROGRESS_AWARE); + inTest.read(file, null, out, ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD); return out.toByteArray(); } @@ -417,7 +416,7 @@ public class CloudContentRepositoryBlackboxTest { if (!inTest.exists(parent)) { parent = inTest.create(parent); } - CloudFile file = inTest.file(parent, name, Optional.of(new Long(data.length))); - return inTest.write(file, ByteArrayDataSource.from(data), NO_OP_PROGRESS_AWARE, repalce, data.length); + CloudFile file = inTest.file(parent, name, new Long(data.length)); + return inTest.write(file, ByteArrayDataSource.from(data), ProgressAware.NO_OP_PROGRESS_AWARE_UPLOAD, repalce, data.length); } } diff --git a/presentation/src/androidTest/java/org/cryptomator/presentation/CloudNodeMatchers.java b/presentation/src/androidTest/java/org/cryptomator/presentation/CloudNodeMatchers.java index 686fb187..9557d5f0 100644 --- a/presentation/src/androidTest/java/org/cryptomator/presentation/CloudNodeMatchers.java +++ b/presentation/src/androidTest/java/org/cryptomator/presentation/CloudNodeMatchers.java @@ -3,7 +3,7 @@ package org.cryptomator.presentation; import org.cryptomator.domain.CloudFile; import org.cryptomator.domain.CloudFolder; import org.cryptomator.domain.CloudNode; -import org.cryptomator.util.Optional; +import com.google.common.base.Optional; import org.hamcrest.Description; import org.hamcrest.Matcher; import org.hamcrest.TypeSafeDiagnosingMatcher; diff --git a/presentation/src/foss/java/org/cryptomator/presentation/presenter/AuthenticateCloudPresenter.kt b/presentation/src/foss/java/org/cryptomator/presentation/presenter/AuthenticateCloudPresenter.kt index 689d2fc9..abfe6e74 100644 --- a/presentation/src/foss/java/org/cryptomator/presentation/presenter/AuthenticateCloudPresenter.kt +++ b/presentation/src/foss/java/org/cryptomator/presentation/presenter/AuthenticateCloudPresenter.kt @@ -223,7 +223,7 @@ class AuthenticateCloudPresenter @Inject constructor( // private fun startAuthentication(cloud: CloudModel) { authenticationStarted = true - val authenticationAdapter = OnedriveClientFactory.instance(context(), (cloud.toCloud() as OnedriveCloud).accessToken()).authenticationAdapter + val authenticationAdapter = OnedriveClientFactory.getAuthAdapter(context(), (cloud.toCloud() as OnedriveCloud).accessToken()) authenticationAdapter.login(activity(), object : ICallback { override fun success(accessToken: String?) { if (accessToken == null) { @@ -377,7 +377,7 @@ class AuthenticateCloudPresenter @Inject constructor( // } } - fun onAcceptWebDavCertificateClicked(cloud: WebDavCloud?, certificate: X509Certificate?) { + fun onAcceptWebDavCertificateClicked(cloud: WebDavCloud, certificate: X509Certificate) { try { val webDavCloudWithAcceptedCert = WebDavCloud.aCopyOf(cloud) // .withCertificate(X509CertificateHelper.convertToPem(certificate)) // diff --git a/presentation/src/main/java/org/cryptomator/presentation/CryptomatorApp.kt b/presentation/src/main/java/org/cryptomator/presentation/CryptomatorApp.kt index 202780c6..630b07bc 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/CryptomatorApp.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/CryptomatorApp.kt @@ -170,7 +170,7 @@ class CryptomatorApp : MultiDexApplication(), HasComponent } fun allVaultsLocked(): Boolean { - return appCryptors.isEmpty + return appCryptors.isEmpty() } fun suspendLock() { diff --git a/presentation/src/main/java/org/cryptomator/presentation/logging/FormattedTime.kt b/presentation/src/main/java/org/cryptomator/presentation/logging/FormattedTime.kt index 148113c0..fb67cd71 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/logging/FormattedTime.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/logging/FormattedTime.kt @@ -9,11 +9,9 @@ internal class FormattedTime private constructor(private val timestamp: Long) { @Volatile private var formatted: String? = null + @Synchronized override fun toString(): String { - if (formatted == null) { - formatted = SimpleDateFormat(FORMAT, Locale.getDefault()).format(Date(timestamp)) - } - return formatted!! + return formatted ?: SimpleDateFormat(FORMAT, Locale.getDefault()).format(Date(timestamp)).also { formatted = it } } companion object { diff --git a/presentation/src/main/java/org/cryptomator/presentation/model/CloudFileModel.kt b/presentation/src/main/java/org/cryptomator/presentation/model/CloudFileModel.kt index 34029dbb..e2fa8a71 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/model/CloudFileModel.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/model/CloudFileModel.kt @@ -3,13 +3,12 @@ package org.cryptomator.presentation.model import org.cryptomator.domain.CloudFile import org.cryptomator.domain.usecases.ResultRenamed import org.cryptomator.presentation.util.FileIcon -import org.cryptomator.util.Optional import java.util.Date class CloudFileModel(cloudFile: CloudFile, val icon: FileIcon) : CloudNodeModel(cloudFile) { - val modified: Optional = cloudFile.modified - val size: Optional = cloudFile.size + val modified: Date? = cloudFile.modified + val size: Long? = cloudFile.size constructor(cloudFileRenamed: ResultRenamed, icon: FileIcon) : this(cloudFileRenamed.value(), icon) { oldName = cloudFileRenamed.oldName diff --git a/presentation/src/main/java/org/cryptomator/presentation/model/CloudFolderModel.kt b/presentation/src/main/java/org/cryptomator/presentation/model/CloudFolderModel.kt index 3a8bc18e..1d53cea8 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/model/CloudFolderModel.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/model/CloudFolderModel.kt @@ -17,7 +17,7 @@ class CloudFolderModel(cloudFolder: CloudFolder) : CloudNodeModel(c get() = true fun vault(): VaultModel? { - return if (toCloudNode().cloud.type() == CloudType.CRYPTO) { + return if (toCloudNode().cloud?.type() == CloudType.CRYPTO) { VaultModel((toCloudNode().cloud as CryptoCloud).vault) } else { null diff --git a/presentation/src/main/java/org/cryptomator/presentation/model/CloudNodeModel.kt b/presentation/src/main/java/org/cryptomator/presentation/model/CloudNodeModel.kt index 38e53a2b..4e07dd4a 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/model/CloudNodeModel.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/model/CloudNodeModel.kt @@ -1,13 +1,12 @@ package org.cryptomator.presentation.model import org.cryptomator.domain.CloudNode -import org.cryptomator.util.Optional import java.io.Serializable abstract class CloudNodeModel internal constructor(private val cloudNode: T) : Serializable { var oldName: String? = null - var progress: Optional = Optional.empty() + var progress: ProgressModel? = null var isSelected = false val name: String get() = cloudNode.name @@ -15,8 +14,8 @@ abstract class CloudNodeModel internal constructor(private val cl get() = cloudNode.name.substring(0, cloudNode.name.lastIndexOf(".")) val path: String get() = cloudNode.path - val parent: CloudFolderModel - get() = CloudFolderModel(cloudNode.parent) + val parent: CloudFolderModel? + get() = cloudNode.parent?.let { CloudFolderModel(it) } abstract val isFile: Boolean abstract val isFolder: Boolean diff --git a/presentation/src/main/java/org/cryptomator/presentation/model/LocalStorageModel.kt b/presentation/src/main/java/org/cryptomator/presentation/model/LocalStorageModel.kt index 3a0a3612..c53270e7 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/model/LocalStorageModel.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/model/LocalStorageModel.kt @@ -3,8 +3,8 @@ package org.cryptomator.presentation.model import org.cryptomator.domain.Cloud import org.cryptomator.domain.LocalStorageCloud import org.cryptomator.presentation.R -import org.cryptomator.util.Encodings import java.net.URLDecoder +import java.nio.charset.StandardCharsets class LocalStorageModel(cloud: Cloud) : CloudModel(cloud) { @@ -12,7 +12,7 @@ class LocalStorageModel(cloud: Cloud) : CloudModel(cloud) { return R.string.cloud_names_local_storage } - override fun username(): String? { + override fun username(): String { return "" } @@ -36,7 +36,7 @@ class LocalStorageModel(cloud: Cloud) : CloudModel(cloud) { } private fun prepareTokenForDisplay(): String { - return URLDecoder.decode(cloud().rootUri(), Encodings.UTF_8.name()) + return URLDecoder.decode(cloud().rootUri(), StandardCharsets.UTF_8.name()) } fun uri(): String { diff --git a/presentation/src/main/java/org/cryptomator/presentation/model/PCloudModel.kt b/presentation/src/main/java/org/cryptomator/presentation/model/PCloudModel.kt index 3660e5d6..1233991a 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/model/PCloudModel.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/model/PCloudModel.kt @@ -18,7 +18,7 @@ class PCloudModel(cloud: Cloud) : CloudModel(cloud) { return cloud().url() } - fun id(): Long { + fun id(): Long? { return cloud().id() } diff --git a/presentation/src/main/java/org/cryptomator/presentation/model/S3CloudModel.kt b/presentation/src/main/java/org/cryptomator/presentation/model/S3CloudModel.kt index 86356dac..1c94e69c 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/model/S3CloudModel.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/model/S3CloudModel.kt @@ -18,7 +18,7 @@ class S3CloudModel(cloud: Cloud) : CloudModel(cloud) { return CloudTypeModel.S3 } - fun id(): Long { + fun id(): Long? { return cloud().id() } diff --git a/presentation/src/main/java/org/cryptomator/presentation/model/WebDavCloudModel.kt b/presentation/src/main/java/org/cryptomator/presentation/model/WebDavCloudModel.kt index 69431f23..96a0d7b8 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/model/WebDavCloudModel.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/model/WebDavCloudModel.kt @@ -26,7 +26,7 @@ class WebDavCloudModel(cloud: Cloud) : CloudModel(cloud) { return cloud().password() } - fun id(): Long { + fun id(): Long? { return cloud().id() } diff --git a/presentation/src/main/java/org/cryptomator/presentation/model/comparator/CloudNodeModelDateNewestFirstComparator.kt b/presentation/src/main/java/org/cryptomator/presentation/model/comparator/CloudNodeModelDateNewestFirstComparator.kt index cfb81d5a..ea9fd84c 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/model/comparator/CloudNodeModelDateNewestFirstComparator.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/model/comparator/CloudNodeModelDateNewestFirstComparator.kt @@ -17,11 +17,11 @@ class CloudNodeModelDateNewestFirstComparator : Comparator> { val o1ModifyDate = (o1 as CloudFileModel).modified val o2ModifyDate = (o2 as CloudFileModel).modified - return if (o1ModifyDate.isPresent && o2ModifyDate.isPresent) { - o2ModifyDate.get().compareTo(o1ModifyDate.get()) - } else if (o2ModifyDate.isPresent) { + return if (o1ModifyDate != null && o2ModifyDate != null) { + o2ModifyDate.compareTo(o1ModifyDate) + } else if (o2ModifyDate != null) { -1 - } else if (o1ModifyDate.isPresent) { + } else if (o1ModifyDate != null) { 1 } else { 0 diff --git a/presentation/src/main/java/org/cryptomator/presentation/model/comparator/CloudNodeModelDateOldestFirstComparator.kt b/presentation/src/main/java/org/cryptomator/presentation/model/comparator/CloudNodeModelDateOldestFirstComparator.kt index 0d189dde..3909a2c6 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/model/comparator/CloudNodeModelDateOldestFirstComparator.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/model/comparator/CloudNodeModelDateOldestFirstComparator.kt @@ -17,11 +17,11 @@ class CloudNodeModelDateOldestFirstComparator : Comparator> { val o1ModifyDate = (o1 as CloudFileModel).modified val o2ModifyDate = (o2 as CloudFileModel).modified - return if (o1ModifyDate.isPresent && o2ModifyDate.isPresent) { - o1ModifyDate.get().compareTo(o2ModifyDate.get()) - } else if (o1ModifyDate.isPresent) { + return if (o1ModifyDate != null && o2ModifyDate != null) { + o1ModifyDate.compareTo(o2ModifyDate) + } else if (o1ModifyDate != null) { -1 - } else if (o2ModifyDate.isPresent) { + } else if (o2ModifyDate != null) { 1 } else { 0 diff --git a/presentation/src/main/java/org/cryptomator/presentation/model/comparator/CloudNodeModelSizeBiggestFirstComparator.kt b/presentation/src/main/java/org/cryptomator/presentation/model/comparator/CloudNodeModelSizeBiggestFirstComparator.kt index 58db65c6..3568e1c6 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/model/comparator/CloudNodeModelSizeBiggestFirstComparator.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/model/comparator/CloudNodeModelSizeBiggestFirstComparator.kt @@ -17,11 +17,11 @@ class CloudNodeModelSizeBiggestFirstComparator : Comparator> { val o1Size = (o1 as CloudFileModel).size val o2Size = (o2 as CloudFileModel).size - return if (o2Size.isPresent && o1Size.isPresent) { - o2Size.get().compareTo(o1Size.get()) - } else if (o2Size.isPresent) { + return if (o2Size != null && o1Size != null) { + o2Size.compareTo(o1Size) + } else if (o2Size != null) { -1 - } else if (o1Size.isPresent) { + } else if (o1Size != null) { 1 } else { 0 diff --git a/presentation/src/main/java/org/cryptomator/presentation/model/comparator/CloudNodeModelSizeSmallestFirstComparator.kt b/presentation/src/main/java/org/cryptomator/presentation/model/comparator/CloudNodeModelSizeSmallestFirstComparator.kt index 3772b7a9..4d316912 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/model/comparator/CloudNodeModelSizeSmallestFirstComparator.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/model/comparator/CloudNodeModelSizeSmallestFirstComparator.kt @@ -17,11 +17,11 @@ class CloudNodeModelSizeSmallestFirstComparator : Comparator> val o1Size = (o1 as CloudFileModel).size val o2Size = (o2 as CloudFileModel).size - return if (o1Size.isPresent && o2Size.isPresent) { - o1Size.get().compareTo(o2Size.get()) - } else if (o1Size.isPresent) { + return if (o1Size != null && o2Size != null) { + o1Size.compareTo(o2Size) + } else if (o1Size != null) { -1 - } else if (o2Size.isPresent) { + } else if (o2Size != null) { 1 } else { 0 diff --git a/presentation/src/main/java/org/cryptomator/presentation/model/mappers/CloudModelMapper.kt b/presentation/src/main/java/org/cryptomator/presentation/model/mappers/CloudModelMapper.kt index 89b53ca7..0006b457 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/model/mappers/CloudModelMapper.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/model/mappers/CloudModelMapper.kt @@ -22,7 +22,7 @@ class CloudModelMapper @Inject constructor() : ModelMapper() } override fun toModel(domainObject: Cloud): CloudModel { - return when (CloudTypeModel.valueOf(domainObject.type())) { + return when (domainObject.type()?.let { CloudTypeModel.valueOf(it) }) { CloudTypeModel.DROPBOX -> DropboxCloudModel(domainObject) CloudTypeModel.GOOGLE_DRIVE -> GoogleDriveCloudModel(domainObject) CloudTypeModel.LOCAL -> LocalStorageModel(domainObject) @@ -31,6 +31,7 @@ class CloudModelMapper @Inject constructor() : ModelMapper() CloudTypeModel.S3 -> S3CloudModel(domainObject) CloudTypeModel.CRYPTO -> CryptoCloudModel(domainObject) CloudTypeModel.WEBDAV -> WebDavCloudModel(domainObject) + null -> throw IllegalStateException("The type of the object shouldn't be null") } } } diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/AutoUploadChooseVaultPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/AutoUploadChooseVaultPresenter.kt index 2ec1bd71..64ed5926 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/AutoUploadChooseVaultPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/AutoUploadChooseVaultPresenter.kt @@ -90,10 +90,7 @@ class AutoUploadChooseVaultPresenter @Inject constructor( // @Callback fun vaultUnlockedAutoUpload(result: ActivityResult) { val cloud = result.intent().getSerializableExtra(SINGLE_RESULT) as Cloud - when { - result.isResultOk -> rootFolderFor(cloud) - else -> TODO("Not yet implemented") - } + rootFolderFor(cloud) } diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/BiometricAuthSettingsPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/BiometricAuthSettingsPresenter.kt index 86b6070b..51f156f5 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/BiometricAuthSettingsPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/BiometricAuthSettingsPresenter.kt @@ -79,23 +79,16 @@ class BiometricAuthSettingsPresenter @Inject constructor( // val cloud = result.intent().getSerializableExtra(SINGLE_RESULT) as Cloud val password = result.intent().getStringExtra(UnlockVaultPresenter.PASSWORD) val vault = Vault.aCopyOf(vaultModel.toVault()).withCloud(cloud).withSavedPassword(password).build() - when { - result.isResultOk -> requestActivityResult( // - ActivityResultCallbacks.encryptVaultPassword(vaultModel), // - Intents.unlockVaultIntent().withVaultModel(VaultModel(vault)).withVaultAction(UnlockVaultIntent.VaultAction.ENCRYPT_PASSWORD) - ) - else -> TODO("Not yet implemented") - } + requestActivityResult( // + ActivityResultCallbacks.encryptVaultPassword(vaultModel), // + Intents.unlockVaultIntent().withVaultModel(VaultModel(vault)).withVaultAction(UnlockVaultIntent.VaultAction.ENCRYPT_PASSWORD)) } @Callback fun encryptVaultPassword(result: ActivityResult, vaultModel: VaultModel) { val tmpVault = result.intent().getSerializableExtra(SINGLE_RESULT) as VaultModel val vault = Vault.aCopyOf(vaultModel.toVault()).withSavedPassword(tmpVault.password).build() - when { - result.isResultOk -> saveVault(vault) - else -> TODO("Not yet implemented") - } + saveVault(vault) } private fun saveVault(vault: Vault?) { diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt index 89a6e577..8c93685d 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt @@ -37,9 +37,7 @@ import org.cryptomator.presentation.ui.dialog.FileNameDialog import org.cryptomator.presentation.util.* import org.cryptomator.presentation.workflow.* import org.cryptomator.util.ExceptionUtil -import org.cryptomator.util.Optional import org.cryptomator.util.SharedPreferencesHandler -import org.cryptomator.util.Supplier import org.cryptomator.util.file.FileCacheUtils import org.cryptomator.util.file.MimeType import org.cryptomator.util.file.MimeTypes @@ -47,6 +45,7 @@ import java.io.* import java.security.DigestInputStream import java.security.MessageDigest import java.util.* +import java.util.function.Supplier import javax.inject.Inject import kotlin.collections.ArrayList import kotlin.reflect.KClass @@ -108,9 +107,10 @@ class BrowseFilesPresenter @Inject constructor( // @JvmField @InstanceState - var openedCloudFileMd5: Optional = Optional.empty() + var openedCloudFileMd5: ByteArray? = null - private var openWritableFileNotification: Optional = Optional.empty() + @JvmField + var openWritableFileNotification: OpenWritableFileNotification? = null override fun workflows(): Iterable> { return listOf(addExistingVaultWorkflow, createNewVaultWorkflow) @@ -189,7 +189,9 @@ class BrowseFilesPresenter @Inject constructor( // @Callback fun getCloudListAfterAuthentication(result: ActivityResult, cloudFolderModel: CloudFolderModel) { val cloudModel = result.getSingleResult(CloudModel::class.java) - getCloudList(cloudFolderModelMapper.toModel(cloudFolderModel.toCloudNode().withCloud(cloudModel.toCloud()))) + cloudFolderModel.toCloudNode().withCloud(cloudModel.toCloud())?.let { + getCloudList(cloudFolderModelMapper.toModel(it)) + } ?: throw FatalBackendException("cloudFolderModel with updated Cloud shouldn't be null") } fun onCreateFolderPressed(cloudFolder: CloudFolderModel, folderName: String?) { @@ -431,10 +433,7 @@ class BrowseFilesPresenter @Inject constructor( // Intents.textEditorIntent() // .withTextFile(cloudFile) ) - } else if (!lowerFileName.endsWith(".gif") && mimeTypes.fromFilename(cloudFile.name) // - .orElse(MimeType.WILDCARD_MIME_TYPE) // - .mediatype == "image" - ) { + } else if (!lowerFileName.endsWith(".gif") && mimeTypes.fromFilename(cloudFile.name) ?: (MimeType.WILDCARD_MIME_TYPE).mediatype == "image") { val cloudFileNodes = previewCloudFileNodes val imagePreviewStore = ImagePreviewFilesStore( // cloudFileNodes, // @@ -457,12 +456,12 @@ class BrowseFilesPresenter @Inject constructor( // openedCloudFileMd5 = calculateDigestFromUri(it) viewFileIntent.setDataAndType( // uriToOpenedFile, // - mimeTypes.fromFilename(cloudFile.name).map(MimeType.TO_STRING).orElse(null) + mimeTypes.fromFilename(cloudFile.name)?.toString() ) viewFileIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) if (sharedPreferencesHandler.keepUnlockedWhileEditing()) { - openWritableFileNotification = Optional.of(OpenWritableFileNotification(context(), it)) - openWritableFileNotification.ifPresent { obj: OpenWritableFileNotification -> obj.show() } + openWritableFileNotification = OpenWritableFileNotification(context(), it) + openWritableFileNotification?.show() val cryptomatorApp = activity().application as CryptomatorApp cryptomatorApp.suspendLock() } @@ -477,8 +476,7 @@ class BrowseFilesPresenter @Inject constructor( // ?.filterIsInstance() ?.filterTo(previewCloudFiles) { !it.name.endsWith(".gif") // - && mimeTypes.fromFilename(it.name) // - .orElse(MimeType.WILDCARD_MIME_TYPE).mediatype == "image" + && mimeTypes.fromFilename(it.name) ?: (MimeType.WILDCARD_MIME_TYPE).mediatype == "image" } return previewCloudFiles } @@ -495,7 +493,7 @@ class BrowseFilesPresenter @Inject constructor( // private fun combinedMimeType(shareFiles: List): MimeType { var result: MimeType? = null shareFiles.forEach { file -> - val type = mimeTypes.fromFilename(file.name).orElse(MimeType.WILDCARD_MIME_TYPE) + val type = mimeTypes.fromFilename(file.name) ?: MimeType.WILDCARD_MIME_TYPE result = result?.combine(type) ?: type } return result ?: MimeType.WILDCARD_MIME_TYPE @@ -728,7 +726,6 @@ class BrowseFilesPresenter @Inject constructor( // requestActivityResult(ActivityResultCallbacks.exportFileToUserSelectedLocation(fileToExport, exportOperation), intent) } - @RequiresApi(Build.VERSION_CODES.LOLLIPOP) private fun exportNodesToUserSelectedLocation(nodesToExport: ArrayList>, exportOperation: ExportOperation) { try { requestActivityResult( // @@ -880,8 +877,7 @@ class BrowseFilesPresenter @Inject constructor( // @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) @Throws(IllegalFileNameException::class, NoSuchCloudFileException::class) private fun createNewDocumentUri(parentUri: Uri, fileName: String): Uri { - val mimeType = mimeTypes.fromFilename(fileName) // - .orElse(MimeType.APPLICATION_OCTET_STREAM) + val mimeType = mimeTypes.fromFilename(fileName) ?: MimeType.APPLICATION_OCTET_STREAM return try { DocumentsContract.createDocument( // context().contentResolver, // @@ -1148,13 +1144,14 @@ class BrowseFilesPresenter @Inject constructor( // uriToOpenedFile?.let { try { - val hashAfterEdit = calculateDigestFromUri(it) - if (hashAfterEdit.isPresent && openedCloudFileMd5.isPresent // - && Arrays.equals(hashAfterEdit.get(), openedCloudFileMd5.get()) - ) { - Timber.tag("BrowseFilesPresenter").i("Opened app finished, file not changed") - } else { - uploadChangedFile() + calculateDigestFromUri(it)?.let { hashAfterEdit -> + openedCloudFileMd5?.let { hashBeforeEdit -> + if (hashAfterEdit.contentEquals(hashBeforeEdit)) { + Timber.tag("BrowseFilesPresenter").i("Opened app finished, file not changed") + } else { + uploadChangedFile() + } + } } } catch (e: FileNotFoundException) { Timber.tag("BrowseFilesPresenter").e(e, "Failed to read back changes, file isn't present anymore") @@ -1166,47 +1163,46 @@ class BrowseFilesPresenter @Inject constructor( // private fun uploadChangedFile() { view?.showUploadDialog(1) openedCloudFile?.let { openedCloudFile -> - uriToOpenedFile?.let { uriToOpenedFile -> - uploadFilesUseCase // - .withParent(openedCloudFile.parent.toCloudNode()) // - .andFiles(listOf(createUploadFile(openedCloudFile.name, uriToOpenedFile, true))) // - .run(object : DefaultProgressAwareResultHandler, UploadState>() { - override fun onProgress(progress: Progress) { - view?.showProgress(progressModelMapper.toModel(progress)) - } - - override fun onSuccess(files: List) { - files.forEach { file -> - view?.addOrUpdateCloudNode(cloudFileModelMapper.toModel(file)) + openedCloudFile.parent?.let { openedCloudFilesParent -> + uriToOpenedFile?.let { uriToOpenedFile -> + uploadFilesUseCase // + .withParent(openedCloudFilesParent.toCloudNode()) // + .andFiles(listOf(createUploadFile(openedCloudFile.name, uriToOpenedFile, true))) // + .run(object : DefaultProgressAwareResultHandler, UploadState>() { + override fun onProgress(progress: Progress) { + view?.showProgress(progressModelMapper.toModel(progress)) } - onFileUploadCompleted() - } - override fun onError(e: Throwable) { - onFileUploadError() - if (ExceptionUtil.contains(e, CloudNodeAlreadyExistsException::class.java)) { - ExceptionUtil.extract(e, CloudNodeAlreadyExistsException::class.java).get().message?.let { - onCloudNodeAlreadyExists(it) - } ?: super.onError(e) - } else { - super.onError(e) + override fun onSuccess(files: List) { + files.forEach { file -> + view?.addOrUpdateCloudNode(cloudFileModelMapper.toModel(file)) + } + onFileUploadCompleted() } - } - }) + + override fun onError(e: Throwable) { + onFileUploadError() + if (ExceptionUtil.contains(e, CloudNodeAlreadyExistsException::class.java)) { + ExceptionUtil.extract(e, CloudNodeAlreadyExistsException::class.java).get().message?.let { + onCloudNodeAlreadyExists(it) + } ?: super.onError(e) + } else { + super.onError(e) + } + } + }) + } } } } private fun hideWritableNotification() { // openWritableFileNotification can not be made serializable because of this, can be null after Activity resumed - if (openWritableFileNotification.isAbsent) { - openWritableFileNotification = Optional.of(OpenWritableFileNotification(context(), Uri.EMPTY)) - } - openWritableFileNotification.ifPresent { obj: OpenWritableFileNotification -> obj.hide() } + openWritableFileNotification?.hide() ?: OpenWritableFileNotification(context(), Uri.EMPTY).hide() } @Throws(FileNotFoundException::class) - private fun calculateDigestFromUri(uri: Uri): Optional { + private fun calculateDigestFromUri(uri: Uri): ByteArray? { val digest = MessageDigest.getInstance("MD5") DigestInputStream(context().contentResolver.openInputStream(uri), digest).use { dis -> val buffer = ByteArray(8192) @@ -1214,7 +1210,7 @@ class BrowseFilesPresenter @Inject constructor( // while (dis.read(buffer) > -1) { } } - return Optional.ofNullable(digest.digest()) + return digest.digest() } interface ExportOperation : Serializable { @@ -1225,7 +1221,7 @@ class BrowseFilesPresenter @Inject constructor( // private val enableRefreshOnBackpressSupplier = RefreshSupplier() - class RefreshSupplier : Supplier { + class RefreshSupplier : Supplier { private var inSelectionMode = false private var inAction = false 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 ded8e98c..1fa66b76 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudConnectionListPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudConnectionListPresenter.kt @@ -145,7 +145,6 @@ class CloudConnectionListPresenter @Inject constructor( // } } - @RequiresApi(Build.VERSION_CODES.LOLLIPOP) private fun openDocumentTree() { try { requestActivityResult( // diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/Presenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/Presenter.kt index 2cdba135..860fd3a8 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/Presenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/Presenter.kt @@ -21,9 +21,9 @@ import org.cryptomator.presentation.workflow.ActivityResult import org.cryptomator.presentation.workflow.AsyncResult import org.cryptomator.presentation.workflow.PermissionsResult import org.cryptomator.presentation.workflow.Workflow -import org.cryptomator.util.Supplier import java.io.Serializable import java.util.Collections +import java.util.function.Supplier import timber.log.Timber abstract class Presenter protected constructor(private val exceptionMappings: ExceptionHandlers) : ActivityHolder { @@ -64,13 +64,11 @@ abstract class Presenter protected constructor(private val exceptionMa fun finishWithResultAndExtra(result: Serializable?, extraName: String?, extraResult: Serializable?) { val data = Intent() - if (result == null) { - activity().setResult(Activity.RESULT_CANCELED) - } else { - data.putExtra(SINGLE_RESULT, result) + result?.let { + data.putExtra(SINGLE_RESULT, it) data.putExtra(extraName, extraResult) activity().setResult(Activity.RESULT_OK, data) - } + } ?: activity().setResult(Activity.RESULT_CANCELED) finish() } diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/SettingsPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/SettingsPresenter.kt index 7ca400dc..f40390d1 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/SettingsPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/SettingsPresenter.kt @@ -6,6 +6,7 @@ import android.content.Intent import android.os.AsyncTask import android.os.Build import android.widget.Toast +import com.google.common.base.Optional import org.cryptomator.data.util.NetworkConnectionCheck import org.cryptomator.domain.di.PerView import org.cryptomator.domain.usecases.DoUpdateCheckUseCase @@ -26,7 +27,6 @@ import org.cryptomator.presentation.ui.dialog.UpdateAppDialog import org.cryptomator.presentation.util.EmailBuilder import org.cryptomator.presentation.util.FileUtil import org.cryptomator.presentation.workflow.PermissionsResult -import org.cryptomator.util.Optional import org.cryptomator.util.SharedPreferencesHandler import java.io.File import java.io.FileInputStream @@ -110,10 +110,10 @@ class SettingsPresenter @Inject internal constructor( if (networkConnectionCheck.isPresent) { updateCheckUseCase // .withVersion(BuildConfig.VERSION_NAME) - .run(object : NoOpResultHandler?>() { - override fun onSuccess(result: Optional?) { - if (result?.isPresent == true) { - result.get()?.let { updateStatusRetrieved(it, context()) } + .run(object : NoOpResultHandler>() { + override fun onSuccess(result: Optional) { + if (result.isPresent) { + updateStatusRetrieved(result.get(), context()) } else { Timber.tag("SettingsPresenter").i("UpdateCheck finished, latest version") Toast.makeText(context(), getString(R.string.notification_update_check_finished_latest), Toast.LENGTH_SHORT).show() @@ -219,12 +219,12 @@ class SettingsPresenter @Inject internal constructor( val entry = ZipEntry(logfile.name) entry.time = logfile.lastModified() logs.putNextEntry(entry) - FileInputStream(logfile).use { `in` -> + FileInputStream(logfile).use { inputStream -> val buffer = ByteArray(4096) var count = 0 while (count != EOF) { logs.write(buffer, 0, count) - count = `in`.read(buffer) + count = inputStream.read(buffer) } } } diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/SharedFilesPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/SharedFilesPresenter.kt index a1d3ad6b..4ed7b75f 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/SharedFilesPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/SharedFilesPresenter.kt @@ -24,7 +24,6 @@ import org.cryptomator.presentation.util.FileNameValidator.Companion.isInvalidNa import org.cryptomator.presentation.workflow.ActivityResult import org.cryptomator.presentation.workflow.AuthenticationExceptionHandler import org.cryptomator.presentation.workflow.PermissionsResult -import org.cryptomator.util.Optional import org.cryptomator.util.file.FileCacheUtils import java.util.* import javax.inject.Inject @@ -140,10 +139,7 @@ class SharedFilesPresenter @Inject constructor( // @Callback fun vaultUnlockedSharedFiles(result: ActivityResult) { val cloud = result.intent().getSerializableExtra(SINGLE_RESULT) as Cloud - when { - result.isResultOk -> rootFolderFor(cloud) - else -> TODO("Not yet implemented") - } + rootFolderFor(cloud) } private fun decryptedCloudFor(vault: Vault) { @@ -263,29 +259,25 @@ class SharedFilesPresenter @Inject constructor( // private fun hasUsedFileNamesAtLocation(currentCloudNodes: List): Boolean { existingFilesForUpload.clear() currentCloudNodes.forEach { cloudNode -> - val uploadFileWithName = fileForUploadWithName(cloudNode.name) - if (uploadFileWithName.isPresent) { + fileForUploadWithName(cloudNode.name)?.let { if (cloudNode is CloudFile) { - filesForUpload.remove(uploadFileWithName.get()) + filesForUpload.remove(it) existingFilesForUpload.add( // - UploadFile.aCopyOf(uploadFileWithName.get()) // + UploadFile.aCopyOf(it) // .thatIsReplacing(true) // .build() ) } else { // remove file when name is used by a folder - filesForUpload.remove(uploadFileWithName.get()) + filesForUpload.remove(it) } } } return existingFilesForUpload.isNotEmpty() } - private fun fileForUploadWithName(name: String): Optional { - return filesForUpload - .firstOrNull { it.fileName == name } - ?.let { Optional.of(it) } - ?: Optional.empty() + private fun fileForUploadWithName(name: String): UploadFile? { + return filesForUpload.firstOrNull { it.fileName == name } } private fun checkForExistingFilesOrUploadFiles(folder: CloudFolder, currentCloudNodes: List) { diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/TextEditorPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/TextEditorPresenter.kt index 6003c52d..7633a138 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/TextEditorPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/TextEditorPresenter.kt @@ -2,6 +2,7 @@ package org.cryptomator.presentation.presenter import org.cryptomator.domain.CloudFile import org.cryptomator.domain.di.PerView +import org.cryptomator.domain.exception.ParentFolderIsNullException import org.cryptomator.domain.usecases.cloud.DataSource import org.cryptomator.domain.usecases.cloud.UploadFile import org.cryptomator.domain.usecases.cloud.UploadFilesUseCase @@ -72,29 +73,30 @@ class TextEditorPresenter @Inject constructor( // } private fun uploadFile(fileName: String, dataSource: DataSource) { - uploadFilesUseCase // - .withParent(textFile.get().parent.toCloudNode()) // - .andFiles( - listOf( // - UploadFile.anUploadFile() // - .withFileName(fileName) // - .withDataSource(dataSource) // - .thatIsReplacing(true) // - .build() // - ) - ) // - .run(object : DefaultProgressAwareResultHandler, UploadState>() { - override fun onFinished() { - view?.showProgress(ProgressModel.COMPLETED) - view?.finish() - view?.showMessage(R.string.screen_text_editor_save_success) - } + textFile.get().parent?.let { + uploadFilesUseCase // + .withParent(it.toCloudNode()) // + .andFiles( + listOf( // + UploadFile.anUploadFile() // + .withFileName(fileName) // + .withDataSource(dataSource) // + .thatIsReplacing(true) // + .build() // + ) + ).run(object : DefaultProgressAwareResultHandler, UploadState>() { + override fun onFinished() { + view?.showProgress(ProgressModel.COMPLETED) + view?.finish() + view?.showMessage(R.string.screen_text_editor_save_success) + } - override fun onError(e: Throwable) { - view?.showProgress(ProgressModel.COMPLETED) - showError(e) - } - }) + override fun onError(e: Throwable) { + view?.showProgress(ProgressModel.COMPLETED) + showError(e) + } + }) + } ?: throw ParentFolderIsNullException(textFile.get().name) } fun loadFileContent() { diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/UnlockVaultPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/UnlockVaultPresenter.kt index e874cfb7..f5b327b2 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/UnlockVaultPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/UnlockVaultPresenter.kt @@ -2,6 +2,7 @@ package org.cryptomator.presentation.presenter import android.os.Handler import androidx.biometric.BiometricManager +import com.google.common.base.Optional import org.cryptomator.data.cloud.crypto.CryptoConstants import org.cryptomator.domain.Cloud import org.cryptomator.domain.UnverifiedVaultConfig @@ -32,7 +33,6 @@ import org.cryptomator.presentation.ui.activity.view.UnlockVaultView import org.cryptomator.presentation.ui.dialog.EnterPasswordDialog import org.cryptomator.presentation.workflow.ActivityResult import org.cryptomator.presentation.workflow.AuthenticationExceptionHandler -import org.cryptomator.util.Optional import org.cryptomator.util.SharedPreferencesHandler import java.io.Serializable import javax.inject.Inject @@ -116,15 +116,14 @@ class UnlockVaultPresenter @Inject constructor( } private fun onUnverifiedVaultConfigRetrieved(unverifiedVaultConfig: Optional) { - if (unverifiedVaultConfig.isAbsent || unverifiedVaultConfig.get().keyId.scheme == CryptoConstants.MASTERKEY_SCHEME) { + if (!unverifiedVaultConfig.isPresent || unverifiedVaultConfig.get().keyId.scheme == CryptoConstants.MASTERKEY_SCHEME) { when (intent.vaultAction()) { UnlockVaultIntent.VaultAction.UNLOCK, UnlockVaultIntent.VaultAction.UNLOCK_FOR_BIOMETRIC_AUTH -> { startedUsingPrepareUnlock = sharedPreferencesHandler.backgroundUnlockPreparation() - pendingUnlockFor(intent.vaultModel().toVault())?.unverifiedVaultConfig = unverifiedVaultConfig.orElse(null) + pendingUnlockFor(intent.vaultModel().toVault())?.unverifiedVaultConfig = unverifiedVaultConfig.orNull() unlockVault(intent.vaultModel()) } - UnlockVaultIntent.VaultAction.CHANGE_PASSWORD -> view?.showChangePasswordDialog(intent.vaultModel(), unverifiedVaultConfig.orElse(null)) - else -> TODO("Not yet implemented") + UnlockVaultIntent.VaultAction.CHANGE_PASSWORD -> view?.showChangePasswordDialog(intent.vaultModel(), unverifiedVaultConfig.orNull()) } } } @@ -176,7 +175,7 @@ class UnlockVaultPresenter @Inject constructor( fun startPrepareUnlockUseCase(vault: Vault) { prepareUnlockUseCase // .withVault(vault) // - .andUnverifiedVaultConfig(Optional.ofNullable(pendingUnlockFor(intent.vaultModel().toVault())?.unverifiedVaultConfig)) + .andUnverifiedVaultConfig(Optional.fromNullable(pendingUnlockFor(intent.vaultModel().toVault())?.unverifiedVaultConfig)) .run(object : DefaultResultHandler() { override fun onSuccess(unlockToken: UnlockToken) { if (!startedUsingPrepareUnlock && vault.password != null) { @@ -258,7 +257,7 @@ class UnlockVaultPresenter @Inject constructor( private fun doUnlock(token: UnlockToken, password: String, unverifiedVaultConfig: UnverifiedVaultConfig?) { unlockVaultUsingMasterkeyUseCase // .withVaultOrUnlockToken(VaultOrUnlockToken.from(token)) // - .andUnverifiedVaultConfig(Optional.ofNullable(unverifiedVaultConfig)) // + .andUnverifiedVaultConfig(Optional.fromNullable(unverifiedVaultConfig)) // .andPassword(password) // .run(object : DefaultResultHandler() { override fun onSuccess(cloud: Cloud) { @@ -267,7 +266,6 @@ class UnlockVaultPresenter @Inject constructor( handleUnlockVaultSuccess(token.vault, cloud, password) } UnlockVaultIntent.VaultAction.UNLOCK -> finishWithResult(cloud) - else -> TODO("Not yet implemented") } } @@ -331,14 +329,13 @@ class UnlockVaultPresenter @Inject constructor( } }) } - else -> TODO("Not yet implemented") } } fun onChangePasswordClick(vaultModel: VaultModel, unverifiedVaultConfig: UnverifiedVaultConfig?, oldPassword: String, newPassword: String) { view?.showProgress(ProgressModel(ProgressStateModel.CHANGING_PASSWORD)) changePasswordUseCase.withVault(vaultModel.toVault()) // - .andUnverifiedVaultConfig(Optional.ofNullable(unverifiedVaultConfig)) // + .andUnverifiedVaultConfig(Optional.fromNullable(unverifiedVaultConfig)) // .andOldPassword(oldPassword) // .andNewPassword(newPassword) // .run(object : DefaultResultHandler() { diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/UriBasedDataSource.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/UriBasedDataSource.kt index 3015272a..39280129 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/UriBasedDataSource.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/UriBasedDataSource.kt @@ -4,14 +4,13 @@ import android.content.Context import android.net.Uri import org.cryptomator.domain.usecases.cloud.DataSource import org.cryptomator.presentation.util.ContentResolverUtil -import org.cryptomator.util.Optional import java.io.IOException import java.io.InputStream class UriBasedDataSource private constructor(private val uri: Uri) : DataSource { - override fun size(context: Context): Optional { - return Optional.ofNullable(ContentResolverUtil(context).fileSize(uri)) + override fun size(context: Context): Long? { + return ContentResolverUtil(context).fileSize(uri) } @Throws(IOException::class) diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt index 5342f347..37a44667 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt @@ -7,6 +7,7 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.widget.Toast +import com.google.common.base.Optional import org.cryptomator.data.cloud.crypto.CryptoCloud import org.cryptomator.data.util.NetworkConnectionCheck import org.cryptomator.domain.Cloud @@ -54,7 +55,6 @@ import org.cryptomator.presentation.workflow.AddExistingVaultWorkflow import org.cryptomator.presentation.workflow.AuthenticationExceptionHandler import org.cryptomator.presentation.workflow.CreateNewVaultWorkflow import org.cryptomator.presentation.workflow.Workflow -import org.cryptomator.util.Optional import org.cryptomator.util.SharedPreferencesHandler import javax.inject.Inject import timber.log.Timber @@ -368,10 +368,7 @@ class VaultListPresenter @Inject constructor( // @Callback fun vaultUnlockedVaultList(result: ActivityResult) { val cloud = result.intent().getSerializableExtra(SINGLE_RESULT) as Cloud - when { - result.isResultOk -> navigateToVaultContent(cloud) - else -> TODO("Not yet implemented") - } + navigateToVaultContent(cloud) } private fun navigateToVaultContent(cloud: Cloud) { diff --git a/presentation/src/main/java/org/cryptomator/presentation/service/AutoUploadService.java b/presentation/src/main/java/org/cryptomator/presentation/service/AutoUploadService.java index 716023aa..a56c9050 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/service/AutoUploadService.java +++ b/presentation/src/main/java/org/cryptomator/presentation/service/AutoUploadService.java @@ -30,7 +30,6 @@ import org.cryptomator.presentation.model.AutoUploadFilesStore; import org.cryptomator.presentation.presenter.UriBasedDataSource; import org.cryptomator.presentation.util.ContentResolverUtil; import org.cryptomator.presentation.util.FileUtil; -import org.cryptomator.util.Optional; import org.cryptomator.util.SharedPreferencesHandler; import java.io.File; @@ -204,14 +203,9 @@ public class AutoUploadService extends Service { } private CloudFile writeCloudFile(String fileName, CancelAwareDataSource dataSource, boolean replacing, ProgressAware progressAware) throws BackendException { - Optional size = dataSource.size(context); + Long size = dataSource.size(context); CloudFile source = cloudContentRepository.file(parent, fileName, size); - return cloudContentRepository.write( // - source, // - dataSource, // - progressAware, // - replacing, // - size.get()); + return cloudContentRepository.write(source, dataSource, progressAware, replacing, size); } @Override diff --git a/presentation/src/main/java/org/cryptomator/presentation/service/CryptorsService.java b/presentation/src/main/java/org/cryptomator/presentation/service/CryptorsService.java index 9b55fba8..d28905f7 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/service/CryptorsService.java +++ b/presentation/src/main/java/org/cryptomator/presentation/service/CryptorsService.java @@ -12,13 +12,13 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager; import org.cryptomator.data.cloud.crypto.Cryptors; import org.cryptomator.presentation.util.FileUtil; -import org.cryptomator.util.Consumer; import org.cryptomator.util.LockTimeout; import org.cryptomator.util.SharedPreferencesHandler; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; import timber.log.Timber; diff --git a/presentation/src/main/java/org/cryptomator/presentation/service/PhotoContentJob.kt b/presentation/src/main/java/org/cryptomator/presentation/service/PhotoContentJob.kt index cf1655d6..f70972bb 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/service/PhotoContentJob.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/service/PhotoContentJob.kt @@ -10,20 +10,17 @@ import android.content.Context import android.database.Cursor import android.database.MergeCursor import android.net.Uri -import android.os.Build import android.os.Handler import android.provider.MediaStore -import androidx.annotation.RequiresApi import org.cryptomator.domain.exception.FatalBackendException import org.cryptomator.presentation.R import org.cryptomator.presentation.util.FileUtil import org.cryptomator.presentation.util.ResourceHelper import org.cryptomator.util.SharedPreferencesHandler -import org.cryptomator.util.file.MimeTypeMap_Factory +import org.cryptomator.util.file.MimeTypeMap import org.cryptomator.util.file.MimeTypes import timber.log.Timber -@RequiresApi(api = Build.VERSION_CODES.N) class PhotoContentJob : JobService() { private val handler = Handler() @@ -37,7 +34,7 @@ class PhotoContentJob : JobService() { override fun onStartJob(params: JobParameters): Boolean { Timber.tag("PhotoContentJob").i("Job started!") - val fileUtil = FileUtil(baseContext, MimeTypes(MimeTypeMap_Factory.newInstance())) + val fileUtil = FileUtil(baseContext, MimeTypes(MimeTypeMap())) runningParams = params 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 410599f7..cfdc2b25 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 @@ -11,6 +11,7 @@ import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment import androidx.localbroadcastmanager.content.LocalBroadcastManager import org.cryptomator.domain.CloudNode +import org.cryptomator.domain.exception.ParentFolderIsNullException import org.cryptomator.generator.Activity import org.cryptomator.generator.InjectIntent import org.cryptomator.presentation.R @@ -141,7 +142,9 @@ class BrowseFilesActivity : BaseActivity(), // supportFragmentManager.popBackStack() } hasCloudNodeSettings() && isNavigationMode(MOVE_CLOUD_NODE) && browseFilesFragment().folder.hasParent() -> { - createBackStackFor(browseFilesFragment().folder.parent) + browseFilesFragment().folder.parent?.let { + createBackStackFor(it) + } ?: throw ParentFolderIsNullException(browseFilesFragment().folder.name) } else -> { super.onBackPressed() diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/ImagePreviewActivity.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/ImagePreviewActivity.kt index 3910607b..209f27ea 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/ImagePreviewActivity.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/ImagePreviewActivity.kt @@ -24,7 +24,6 @@ import org.cryptomator.presentation.presenter.ImagePreviewPresenter import org.cryptomator.presentation.ui.activity.view.ImagePreviewView import org.cryptomator.presentation.ui.dialog.ConfirmDeleteCloudNodeDialog import org.cryptomator.presentation.ui.fragment.ImagePreviewFragment -import org.cryptomator.util.Optional import javax.inject.Inject import kotlinx.android.synthetic.main.activity_image_preview.controlView import kotlinx.android.synthetic.main.activity_image_preview.deleteImage @@ -172,25 +171,17 @@ class ImagePreviewActivity : BaseActivity(), ImagePreviewView, ConfirmDeleteClou } override fun showImagePreview(imagePreviewFile: ImagePreviewFile) { - val imagePreviewFragmentOptional = fragmentFor(imagePreviewFile) - if (imagePreviewFragmentOptional.isPresent) { - imagePreviewFragmentOptional.get().showAndUpdateImage(imagePreviewFile) - } + fragmentFor(imagePreviewFile)?.showAndUpdateImage(imagePreviewFile) } - private fun fragmentFor(imagePreviewFile: ImagePreviewFile): Optional { + private fun fragmentFor(imagePreviewFile: ImagePreviewFile): ImagePreviewFragment? { return supportFragmentManager.fragments .map { it as ImagePreviewFragment } .firstOrNull { it.imagePreviewFile() == imagePreviewFile } - ?.let { Optional.of(it) } - ?: Optional.empty() } override fun hideProgressBar(imagePreviewFile: ImagePreviewFile) { - val imagePreviewFragmentOptional = fragmentFor(imagePreviewFile) - if (imagePreviewFragmentOptional.isPresent) { - imagePreviewFragmentOptional.get().hideProgressBar() - } + fragmentFor(imagePreviewFile)?.hideProgressBar() } override fun vaultExpectedToBeUnlocked() { diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt index aba523ce..4079d888 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt @@ -26,7 +26,6 @@ import org.cryptomator.presentation.util.FileIcon import org.cryptomator.presentation.util.FileSizeHelper import org.cryptomator.presentation.util.FileUtil import org.cryptomator.presentation.util.ResourceHelper.Companion.getDrawable -import org.cryptomator.util.Optional import org.cryptomator.util.SharedPreferencesHandler import java.util.Comparator import javax.inject.Inject @@ -226,10 +225,7 @@ constructor( } private fun bindProgressIfPresent(node: CloudNodeModel<*>) { - val progress = node.progress - if (progress.isPresent) { - showProgress(progress.get()) - } + node.progress?.let { showProgress(it) } } private fun bindFolder(folder: CloudFolderModel) { @@ -265,21 +261,17 @@ constructor( val formattedFileSize = fileSizeHelper.getFormattedFileSize(cloudFile.size) val formattedModifiedDate = dateHelper.getFormattedModifiedDate(cloudFile.modified) - return if (formattedFileSize.isPresent) { - if (formattedModifiedDate.isPresent) { - formattedFileSize.get() + " • " + formattedModifiedDate.get() + return if (formattedFileSize != null) { + if (formattedModifiedDate != null) { + "$formattedFileSize • $formattedModifiedDate" } else { - formattedFileSize.get() + formattedFileSize } - } else if (formattedModifiedDate.isPresent) { - formattedModifiedDate.get() - } else { - "" - } + } else formattedModifiedDate ?: "" } fun showProgress(progress: ProgressModel?) { - bound?.progress = Optional.of(progress) + bound?.progress = progress when { progress?.state() === COMPLETED -> hideProgress() progress?.progress() == ProgressModel.UNKNOWN_PROGRESS_PERCENTAGE -> showIndeterminateProgress(progress) @@ -334,7 +326,7 @@ constructor( fun hideProgress() { uiState?.let { switchTo(it.details()) } - bound?.progress = Optional.empty() + bound?.progress = null } private fun switchTo(state: UiStateTest) { @@ -500,8 +492,8 @@ constructor( val formattedModifiedDate = dateHelper.getFormattedModifiedDate(node.modified) return when (comparator) { - is CloudNodeModelDateNewestFirstComparator, is CloudNodeModelDateOldestFirstComparator -> formattedModifiedDate.orElse(node.name.first().toString()) - is CloudNodeModelSizeBiggestFirstComparator, is CloudNodeModelSizeSmallestFirstComparator -> formattedFileSize.orElse(node.name.first().toString()) + is CloudNodeModelDateNewestFirstComparator, is CloudNodeModelDateOldestFirstComparator -> formattedModifiedDate ?: node.name.first().toString() + is CloudNodeModelSizeBiggestFirstComparator, is CloudNodeModelSizeSmallestFirstComparator -> formattedFileSize ?: node.name.first().toString() else -> all[position].name.first().toString() } } diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/BaseDialog.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/BaseDialog.kt index 2f26a5d5..5ded788c 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/BaseDialog.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/BaseDialog.kt @@ -18,7 +18,7 @@ import androidx.fragment.app.DialogFragment import androidx.fragment.app.FragmentManager import org.cryptomator.presentation.util.KeyboardHelper import org.cryptomator.util.SharedPreferencesHandler -import org.cryptomator.util.Supplier +import java.util.function.Supplier abstract class BaseDialog : DialogFragment() { 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 97a802a0..036916f1 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 @@ -56,12 +56,11 @@ class ChangePasswordDialog : BaseProgressErrorDialog - et_new_retype_password.nextFocusForwardId = button.id + changePasswordButton?.let { + et_new_retype_password.nextFocusForwardId = it.id + registerOnEditorDoneActionAndPerformButtonClick(et_new_retype_password) { it } } - registerOnEditorDoneActionAndPerformButtonClick(et_new_retype_password) { changePasswordButton } - PasswordStrengthUtil() // .startUpdatingPasswordStrengthMeter( et_new_password, // diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/CloudNodeRenameDialog.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/CloudNodeRenameDialog.kt index f75f6af5..4fa6259b 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/CloudNodeRenameDialog.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/CloudNodeRenameDialog.kt @@ -67,14 +67,14 @@ class CloudNodeRenameDialog : BaseProgressErrorDialog et_rename.setText(cloudNodeModel.name) - registerOnEditorDoneActionAndPerformButtonClick(et_rename) { renameConfirmButton } + renameConfirmButton?.let { registerOnEditorDoneActionAndPerformButtonClick(et_rename) { it } } et_rename.addTextChangedListener(object : TextWatcher { override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} override fun afterTextChanged(s: Editable) { - if (renameConfirmButton != null) { + renameConfirmButton?.let { validateInput(s.toString()) - renameConfirmButton?.isEnabled = s.toString().isNotEmpty() && !hasInvalidInput(s.toString()) + it.isEnabled = s.toString().isNotEmpty() && !hasInvalidInput(s.toString()) } } }) diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/CreateFolderDialog.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/CreateFolderDialog.kt index 48d5ebfd..44ad566b 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/CreateFolderDialog.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/CreateFolderDialog.kt @@ -51,14 +51,16 @@ class CreateFolderDialog : BaseProgressErrorDialog( override fun setupView() { et_folder_name.requestFocus() - registerOnEditorDoneActionAndPerformButtonClick(et_folder_name) { createFolderButton } + createFolderButton?.let { + registerOnEditorDoneActionAndPerformButtonClick(et_folder_name) { it } + } et_folder_name.addTextChangedListener(object : TextWatcher { override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} override fun afterTextChanged(s: Editable) { - if (createFolderButton != null) { + createFolderButton?.let { validateInput(s.toString()) - createFolderButton?.isEnabled = s.toString().isNotEmpty() && !hasInvalidInput(s.toString()) + it.isEnabled = s.toString().isNotEmpty() && !hasInvalidInput(s.toString()) } } }) diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/EnterPasswordDialog.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/EnterPasswordDialog.kt index ea7556eb..af1edc37 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/EnterPasswordDialog.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/EnterPasswordDialog.kt @@ -64,7 +64,7 @@ class EnterPasswordDialog : BaseProgressErrorDialog() { } override fun setupView() { - registerOnEditorDoneActionAndPerformButtonClick(file_name) { createFileButton } + createFileButton?.let { registerOnEditorDoneActionAndPerformButtonClick(file_name) { it } } file_name.addTextChangedListener(object : TextWatcher { override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/UpdateLicenseDialog.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/UpdateLicenseDialog.kt index d82c085a..58dee01e 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/UpdateLicenseDialog.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/UpdateLicenseDialog.kt @@ -62,7 +62,7 @@ class UpdateLicenseDialog : BaseProgressErrorDialog() public override fun setupView() { val vaultModel = requireArguments().getSerializable(VAULT_ARG) as VaultModel - registerOnEditorDoneActionAndPerformButtonClick(et_rename) { renameConfirmButton } + renameConfirmButton?.let { registerOnEditorDoneActionAndPerformButtonClick(et_rename) { it } } et_rename.setText(vaultModel.name) et_rename.addTextChangedListener(object : TextWatcher { override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/BrowseFilesFragment.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/BrowseFilesFragment.kt index 7fcac79b..e6c6c3ec 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/BrowseFilesFragment.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/BrowseFilesFragment.kt @@ -26,8 +26,8 @@ import org.cryptomator.presentation.model.ProgressModel import org.cryptomator.presentation.presenter.BrowseFilesPresenter import org.cryptomator.presentation.ui.adapter.BrowseFilesAdapter import org.cryptomator.presentation.util.ResourceHelper.Companion.getPixelOffset -import org.cryptomator.util.Optional import java.util.Comparator +import java.util.Optional import javax.inject.Inject import kotlinx.android.synthetic.main.floating_action_button_layout.floatingActionButton import kotlinx.android.synthetic.main.fragment_browse_files.slidingCoordinatorLayout @@ -198,7 +198,7 @@ class BrowseFilesFragment : BaseFragment() { if (viewHolder.isPresent) { viewHolder.get().showProgress(progress) } else { - node?.progress = Optional.of(progress) + node?.progress = progress node?.let { addOrUpdate(it) } } } @@ -214,7 +214,7 @@ class BrowseFilesFragment : BaseFragment() { if (viewHolder.isPresent) { viewHolder.get().hideProgress() } else { - cloudNode?.progress = Optional.of(ProgressModel.COMPLETED) + cloudNode?.progress = ProgressModel.COMPLETED cloudNode?.let { addOrUpdate(it) } } } diff --git a/presentation/src/main/java/org/cryptomator/presentation/util/BiometricAuthentication.kt b/presentation/src/main/java/org/cryptomator/presentation/util/BiometricAuthentication.kt index 0871f54f..2782c83f 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/util/BiometricAuthentication.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/util/BiometricAuthentication.kt @@ -15,7 +15,6 @@ import org.cryptomator.util.crypto.UnrecoverableStorageKeyException import java.util.concurrent.Executor import timber.log.Timber -@RequiresApi(Build.VERSION_CODES.M) class BiometricAuthentication(val callback: Callback, val context: Context, val cryptoMode: CryptoMode, private val useConfirmationInFaceUnlockAuth: Boolean) { interface Callback { diff --git a/presentation/src/main/java/org/cryptomator/presentation/util/DateHelper.kt b/presentation/src/main/java/org/cryptomator/presentation/util/DateHelper.kt index 10068d52..575c5743 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/util/DateHelper.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/util/DateHelper.kt @@ -1,23 +1,17 @@ package org.cryptomator.presentation.util import org.cryptomator.presentation.R -import org.cryptomator.util.Optional import java.util.Date import java.util.concurrent.TimeUnit import javax.inject.Inject class DateHelper @Inject constructor() { - fun getFormattedModifiedDate(modified: Optional): Optional { - if (modified.isAbsent) { - return Optional.empty() + fun getFormattedModifiedDate(modified: Date?): String? { + return modified?.let { + val modifiedAgo = currentDate().time - it.time + return String.format(ResourceHelper.getString(R.string.screen_file_browser_file_info_label_date), convert(modifiedAgo)) } - val modifiedAgo = currentDate().time - modified.get().time - return wrap(convert(modifiedAgo)) - } - - private fun wrap(value: String): Optional { - return Optional.of(String.format(ResourceHelper.getString(R.string.screen_file_browser_file_info_label_date), value)) } private fun convert(time: Long): String { diff --git a/presentation/src/main/java/org/cryptomator/presentation/util/FileIcon.java b/presentation/src/main/java/org/cryptomator/presentation/util/FileIcon.java index a5718972..a75f140d 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/util/FileIcon.java +++ b/presentation/src/main/java/org/cryptomator/presentation/util/FileIcon.java @@ -2,8 +2,9 @@ package org.cryptomator.presentation.util; import org.cryptomator.presentation.R; import org.cryptomator.presentation.util.FileUtil.FileInfo; -import org.cryptomator.util.Optional; -import org.cryptomator.util.Predicate; + +import java.util.Optional; +import java.util.function.Predicate; import static java.lang.Boolean.FALSE; import static java.lang.Boolean.TRUE; @@ -57,14 +58,18 @@ public enum FileIcon { } private static Predicate forExtensions(final String... extensions) { - return fileInfo -> fileInfo.getExtension().map(fileExtension -> { + return fileInfo -> { + if(fileInfo.getExtension() == null) { + return FALSE; + } + for (String extension : extensions) { - if (fileExtension.equalsIgnoreCase(extension)) { + if (fileInfo.getExtension().equalsIgnoreCase(extension)) { return TRUE; } } return FALSE; - }).orElse(FALSE); + }; } private static Predicate forMediatype(final String mediatype) { @@ -72,14 +77,7 @@ public enum FileIcon { } private static Predicate forMediaTypeOrExtensions(final String mediatype, final String... extensions) { - return fileInfo -> fileInfo.getMimeType().getMediatype().equalsIgnoreCase(mediatype) || fileInfo.getExtension().map(fileExtension -> { - for (String extension : extensions) { - if (fileExtension.equalsIgnoreCase(extension)) { - return TRUE; - } - } - return FALSE; - }).orElse(FALSE); + return forMediatype(mediatype).or(forExtensions(extensions)); } private boolean matches(FileInfo fileInfo) { diff --git a/presentation/src/main/java/org/cryptomator/presentation/util/FileSizeHelper.kt b/presentation/src/main/java/org/cryptomator/presentation/util/FileSizeHelper.kt index 0558b31f..00cc1f11 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/util/FileSizeHelper.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/util/FileSizeHelper.kt @@ -2,7 +2,6 @@ package org.cryptomator.presentation.util import android.content.Context import org.cryptomator.presentation.R -import org.cryptomator.util.Optional import java.text.DecimalFormat import javax.inject.Inject import kotlin.math.log10 @@ -19,24 +18,19 @@ class FileSizeHelper @Inject constructor(private val context: Context) { context.getString(R.string.file_size_unit_tera_bytes) ) - fun getFormattedFileSize(size: Optional): Optional { - return when { - size.isAbsent -> { - Optional.empty() + fun getFormattedFileSize(size: Long?): String? { + return when (size) { + null -> { + null } - size.get() == 0L -> { - wrap(context.getString(R.string.file_size_zero)) + 0L -> { + String.format(ResourceHelper.getString(R.string.screen_file_browser_file_info_label_size), context.getString(R.string.file_size_zero)) } else -> { - val digitGroups = min(log10(size.get().toDouble()) / log10(1000.0), units.size - 1.toDouble()).toInt() + val digitGroups = min(log10(size.toDouble()) / log10(1000.0), units.size - 1.toDouble()).toInt() val formatPattern = if (digitGroups < 2) "##0" else "##0.#" - wrap(DecimalFormat(formatPattern).format(size.get() / 1000.0.pow(digitGroups.toDouble())) + " " + units[digitGroups]) + String.format(ResourceHelper.getString(R.string.screen_file_browser_file_info_label_size), DecimalFormat(formatPattern).format(size / 1000.0.pow(digitGroups.toDouble())) + " " + units[digitGroups]) } } } - - private fun wrap(value: String): Optional { - return Optional.of(String.format(ResourceHelper.getString(R.string.screen_file_browser_file_info_label_size), value)) - } - } diff --git a/presentation/src/main/java/org/cryptomator/presentation/util/FileUtil.kt b/presentation/src/main/java/org/cryptomator/presentation/util/FileUtil.kt index f70df428..8b95e265 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/util/FileUtil.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/util/FileUtil.kt @@ -8,7 +8,6 @@ import org.cryptomator.domain.exception.FatalBackendException import org.cryptomator.presentation.model.AutoUploadFilesStore import org.cryptomator.presentation.model.CloudFileModel import org.cryptomator.presentation.model.ImagePreviewFilesStore -import org.cryptomator.util.Optional import org.cryptomator.util.file.LruFileCacheUtil import org.cryptomator.util.file.MimeType import org.cryptomator.util.file.MimeTypes @@ -249,17 +248,17 @@ class FileUtil @Inject constructor(private val context: Context, private val mim class FileInfo(val name: String, mimeTypes: MimeTypes) { - var extension: Optional + var extension: String? var mimeType: MimeType init { val lastDot = name.lastIndexOf('.') if (lastDot == -1 || lastDot == name.length - 1) { - extension = Optional.empty() + extension = null mimeType = MimeType.APPLICATION_OCTET_STREAM } else { - extension = Optional.of(name.substring(lastDot + 1)) - mimeType = mimeTypes.fromExtension(extension.get()).orElse(MimeType.APPLICATION_OCTET_STREAM) + extension = name.substring(lastDot + 1) + mimeType = extension?.let { mimeTypes.fromExtension(it) } ?: MimeType.APPLICATION_OCTET_STREAM } } } diff --git a/presentation/src/main/java/org/cryptomator/presentation/util/PasswordStrength.kt b/presentation/src/main/java/org/cryptomator/presentation/util/PasswordStrength.kt index 80d8b2c4..faaea094 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/util/PasswordStrength.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/util/PasswordStrength.kt @@ -2,7 +2,6 @@ package org.cryptomator.presentation.util import com.nulabinc.zxcvbn.Zxcvbn import org.cryptomator.presentation.R -import org.cryptomator.util.Optional enum class PasswordStrength(val score: Int, val description: Int, val color: Int) { EMPTY(-1, R.string.empty, R.color.password_strength_empty), // @@ -27,16 +26,13 @@ enum class PasswordStrength(val score: Int, val description: Int, val color: Int EXTREMELY_WEAK } else -> { - forScore(zxcvbn.measure(password, sanitizedInputs).score).orElse(EMPTY) + forScore(zxcvbn.measure(password, sanitizedInputs).score) ?: EMPTY } } } - private fun forScore(score: Int): Optional { - return values() - .firstOrNull { score == it.score } - ?.let { Optional.of(it) } - ?: Optional.empty() + private fun forScore(score: Int): PasswordStrength? { + return values().firstOrNull { score == it.score } } } } diff --git a/presentation/src/main/java/org/cryptomator/presentation/util/ShareFileHelper.kt b/presentation/src/main/java/org/cryptomator/presentation/util/ShareFileHelper.kt index 153f3afb..02472807 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/util/ShareFileHelper.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/util/ShareFileHelper.kt @@ -27,7 +27,7 @@ class ShareFileHelper @Inject constructor( // } private fun mimeTypeFromFileName(fileName: String): String { - return mimeTypes.fromFilename(fileName).orElse(MimeType.WILDCARD_MIME_TYPE).toString() + return (mimeTypes.fromFilename(fileName) ?: MimeType.WILDCARD_MIME_TYPE).toString() } private fun shareFile(activityHolder: ActivityHolder, fileUri: Uri, mimeType: String) { diff --git a/presentation/src/notFoss/java/org/cryptomator/presentation/presenter/AuthenticateCloudPresenter.kt b/presentation/src/notFoss/java/org/cryptomator/presentation/presenter/AuthenticateCloudPresenter.kt index c0508190..8f33fa66 100644 --- a/presentation/src/notFoss/java/org/cryptomator/presentation/presenter/AuthenticateCloudPresenter.kt +++ b/presentation/src/notFoss/java/org/cryptomator/presentation/presenter/AuthenticateCloudPresenter.kt @@ -269,7 +269,7 @@ class AuthenticateCloudPresenter @Inject constructor( // private fun startAuthentication(cloud: CloudModel) { authenticationStarted = true - val authenticationAdapter = OnedriveClientFactory.instance(context(), (cloud.toCloud() as OnedriveCloud).accessToken()).authenticationAdapter + val authenticationAdapter = OnedriveClientFactory.getAuthAdapter(context(), (cloud.toCloud() as OnedriveCloud).accessToken()) authenticationAdapter.login(activity(), object : ICallback { override fun success(accessToken: String?) { if (accessToken == null) { @@ -423,7 +423,7 @@ class AuthenticateCloudPresenter @Inject constructor( // } } - fun onAcceptWebDavCertificateClicked(cloud: WebDavCloud?, certificate: X509Certificate?) { + fun onAcceptWebDavCertificateClicked(cloud: WebDavCloud, certificate: X509Certificate) { try { val webDavCloudWithAcceptedCert = WebDavCloud.aCopyOf(cloud) // .withCertificate(X509CertificateHelper.convertToPem(certificate)) // diff --git a/presentation/src/test/java/org/cryptomator/presentation/SvgValidationTest.java b/presentation/src/test/java/org/cryptomator/presentation/SvgValidationTest.java index 63eec290..46548c13 100644 --- a/presentation/src/test/java/org/cryptomator/presentation/SvgValidationTest.java +++ b/presentation/src/test/java/org/cryptomator/presentation/SvgValidationTest.java @@ -8,7 +8,7 @@ import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; import java.io.StringWriter; -import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; @@ -65,7 +65,7 @@ public class SvgValidationTest { private String toString(File file) throws IOException { StringWriter out = new StringWriter(); - Reader in = new InputStreamReader(new FileInputStream(file), Charset.forName("UTF-8")); + Reader in = new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8); char[] buffer = new char[4096]; int read = 0; while (read != EOF) { diff --git a/util/build.gradle b/util/build.gradle index 3f5ebfd5..88ff9466 100644 --- a/util/build.gradle +++ b/util/build.gradle @@ -69,6 +69,7 @@ dependencies { testImplementation dependencies.hamcrest testImplementation dependencies.mockito + testImplementation dependencies.mockitoKotlin testImplementation dependencies.mockitoInline implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" diff --git a/util/src/main/java/org/cryptomator/util/Consumer.java b/util/src/main/java/org/cryptomator/util/Consumer.java deleted file mode 100644 index a8da0dd0..00000000 --- a/util/src/main/java/org/cryptomator/util/Consumer.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.cryptomator.util; - -public interface Consumer { - - void accept(T value); - -} diff --git a/util/src/main/java/org/cryptomator/util/Encodings.kt b/util/src/main/java/org/cryptomator/util/Encodings.kt deleted file mode 100644 index 7d4f2ea8..00000000 --- a/util/src/main/java/org/cryptomator/util/Encodings.kt +++ /dev/null @@ -1,13 +0,0 @@ -package org.cryptomator.util - -import java.nio.charset.Charset - -object Encodings { - - @JvmField - val ISO_8859_1: Charset = Charset.forName("ISO-8859-1") - - @JvmField - val UTF_8: Charset = Charset.forName("UTF-8") - -} diff --git a/util/src/main/java/org/cryptomator/util/ExceptionUtil.java b/util/src/main/java/org/cryptomator/util/ExceptionUtil.java index f7f71a1e..3bb4d413 100644 --- a/util/src/main/java/org/cryptomator/util/ExceptionUtil.java +++ b/util/src/main/java/org/cryptomator/util/ExceptionUtil.java @@ -1,5 +1,10 @@ package org.cryptomator.util; +import com.google.common.base.Predicates; + +import com.google.common.base.Optional; +import java.util.function.Predicate; + public class ExceptionUtil { public static Predicate thatContainsMessage(final String message) { @@ -18,12 +23,12 @@ public class ExceptionUtil { public static Optional extract(final Throwable e, final Class type, final Predicate test) { if (type.isInstance(e) && test.test(type.cast(e))) { - return Optional.of(type.cast(e)); + return Optional.fromNullable(type.cast(e)); } if (e.getCause() != null) { return extract(e.getCause(), type, test); } - return Optional.empty(); + return Optional.absent(); } public static Optional extract(final Throwable e, final Class type) { diff --git a/util/src/main/java/org/cryptomator/util/Function.java b/util/src/main/java/org/cryptomator/util/Function.java deleted file mode 100644 index efb3af51..00000000 --- a/util/src/main/java/org/cryptomator/util/Function.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.cryptomator.util; - -public interface Function { - - B apply(A value); - -} diff --git a/util/src/main/java/org/cryptomator/util/Optional.java b/util/src/main/java/org/cryptomator/util/Optional.java index 31fb1a69..8d936035 100644 --- a/util/src/main/java/org/cryptomator/util/Optional.java +++ b/util/src/main/java/org/cryptomator/util/Optional.java @@ -1,6 +1,9 @@ package org.cryptomator.util; import java.io.Serializable; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; public class Optional implements Serializable { diff --git a/util/src/main/java/org/cryptomator/util/Predicate.java b/util/src/main/java/org/cryptomator/util/Predicate.java deleted file mode 100644 index 04be5f6d..00000000 --- a/util/src/main/java/org/cryptomator/util/Predicate.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.cryptomator.util; - -public interface Predicate { - - boolean test(T value); - -} diff --git a/util/src/main/java/org/cryptomator/util/Predicates.java b/util/src/main/java/org/cryptomator/util/Predicates.java deleted file mode 100644 index f0ff6da9..00000000 --- a/util/src/main/java/org/cryptomator/util/Predicates.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.cryptomator.util; - -public class Predicates { - - public static Predicate not(final Predicate predicate) { - return value -> !predicate.test(value); - } - - public static Predicate alwaysTrue() { - return value -> true; - } - -} diff --git a/util/src/main/java/org/cryptomator/util/SharedPreferencesHandler.kt b/util/src/main/java/org/cryptomator/util/SharedPreferencesHandler.kt index ce9f2b89..5bc32de3 100644 --- a/util/src/main/java/org/cryptomator/util/SharedPreferencesHandler.kt +++ b/util/src/main/java/org/cryptomator/util/SharedPreferencesHandler.kt @@ -6,11 +6,13 @@ import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_NO import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES import androidx.preference.PreferenceManager +import com.google.common.base.Optional import org.cryptomator.util.LockTimeout.ONE_MINUTE import java.text.SimpleDateFormat import java.util.Date import java.util.Locale import java.util.WeakHashMap +import java.util.function.Consumer import javax.inject.Inject import kotlin.math.abs @@ -185,7 +187,7 @@ constructor(context: Context) : SharedPreferences.OnSharedPreferenceChangeListen val updateInterval = defaultSharedPreferences.getValue(UPDATE_INTERVAL, "7") if (updateInterval == "Never") { - return Optional.empty() + return Optional.absent() } return Optional.of(Integer.parseInt(updateInterval)) @@ -206,7 +208,7 @@ constructor(context: Context) : SharedPreferences.OnSharedPreferenceChangeListen fun doUpdate(): Boolean { val updateIntervalInDays = updateIntervalInDays() - if (updateIntervalInDays.isAbsent) { + if (!updateIntervalInDays.isPresent) { return false } diff --git a/util/src/main/java/org/cryptomator/util/Supplier.java b/util/src/main/java/org/cryptomator/util/Supplier.java deleted file mode 100644 index ff0cc14f..00000000 --- a/util/src/main/java/org/cryptomator/util/Supplier.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.cryptomator.util; - -public interface Supplier { - - T get(); - -} diff --git a/util/src/main/java/org/cryptomator/util/concurrent/CompletableFuture.java b/util/src/main/java/org/cryptomator/util/concurrent/CompletableFuture.java deleted file mode 100644 index c44c3cdb..00000000 --- a/util/src/main/java/org/cryptomator/util/concurrent/CompletableFuture.java +++ /dev/null @@ -1,116 +0,0 @@ -package org.cryptomator.util.concurrent; - -import androidx.annotation.NonNull; - -import java.util.concurrent.CancellationException; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.locks.Condition; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; - -public class CompletableFuture implements Future { - - private final Lock lock = new ReentrantLock(); - private final Condition completeOrCancelled = lock.newCondition(); - - private volatile boolean cancelled; - private volatile boolean done; - - private T result; - private ExecutionException error; - - @Override - public boolean cancel(boolean mayInterruptIfRunning) { - lock.lock(); - try { - if (!isDone()) { - cancelled = true; - completeOrCancelled.signal(); - } - } finally { - lock.unlock(); - } - return cancelled; - } - - public void complete(T result) { - lock.lock(); - try { - if (isDone()) { - throw new IllegalStateException("Already completed"); - } - done = true; - this.result = result; - completeOrCancelled.signal(); - } finally { - lock.unlock(); - } - } - - public void fail(Throwable e) { - lock.lock(); - try { - if (isDone()) { - throw new IllegalStateException("Already completed"); - } - done = true; - this.error = new ExecutionException(e); - completeOrCancelled.signal(); - } finally { - lock.unlock(); - } - } - - @Override - public boolean isCancelled() { - return cancelled; - } - - @Override - public boolean isDone() { - return done || cancelled; - } - - @Override - public T get() throws InterruptedException, ExecutionException { - lock.lock(); - try { - if (!isDone()) { - completeOrCancelled.await(); - } - return getInternal(); - } finally { - lock.unlock(); - } - } - - @Override - public T get(long timeout, @NonNull TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { - lock.lock(); - try { - if (!isDone() && !completeOrCancelled.await(timeout, unit)) { - throw new TimeoutException(); - } - return getInternal(); - } finally { - lock.unlock(); - } - } - - private T getInternal() throws ExecutionException { - if (cancelled) { - throw new CancellationException(); - } - if (done) { - if (error != null) { - throw error; - } - return result; - } - throw new IllegalStateException("Future not done but should be"); - } - -} diff --git a/util/src/main/java/org/cryptomator/util/crypto/BiometricAuthCryptor.java b/util/src/main/java/org/cryptomator/util/crypto/BiometricAuthCryptor.java index 79a593fd..deb77c42 100644 --- a/util/src/main/java/org/cryptomator/util/crypto/BiometricAuthCryptor.java +++ b/util/src/main/java/org/cryptomator/util/crypto/BiometricAuthCryptor.java @@ -4,6 +4,7 @@ import android.content.Context; import org.cryptomator.util.ByteArrayUtils; +import java.nio.charset.StandardCharsets; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.KeyStore; @@ -11,9 +12,6 @@ import java.security.KeyStore; import javax.crypto.BadPaddingException; import javax.crypto.IllegalBlockSizeException; -import static org.cryptomator.util.Encodings.ISO_8859_1; -import static org.cryptomator.util.Encodings.UTF_8; - public class BiometricAuthCryptor { private static final String BIOMETRIC_AUTH_KEY_ALIAS = "fingerprintCryptoKeyAccessToken"; @@ -38,7 +36,7 @@ public class BiometricAuthCryptor { } public javax.crypto.Cipher getDecryptCipher(String decrypted) throws InvalidAlgorithmParameterException, InvalidKeyException { - return cipher.getDecryptCipher(decrypted.getBytes(ISO_8859_1)); + return cipher.getDecryptCipher(decrypted.getBytes(StandardCharsets.ISO_8859_1)); } public javax.crypto.Cipher getEncryptCipher() throws InvalidKeyException { @@ -46,13 +44,13 @@ public class BiometricAuthCryptor { } public String encrypt(javax.crypto.Cipher cipher, String password) throws IllegalBlockSizeException, BadPaddingException { - byte[] encrypted = cipher.doFinal(password.getBytes(UTF_8)); + byte[] encrypted = cipher.doFinal(password.getBytes(StandardCharsets.UTF_8)); byte[] encryptedPasswordAndIv = ByteArrayUtils.join(cipher.getIV(), encrypted); - return new String(encryptedPasswordAndIv, ISO_8859_1); + return new String(encryptedPasswordAndIv, StandardCharsets.ISO_8859_1); } public String decrypt(javax.crypto.Cipher cipher, String password) throws IllegalBlockSizeException, BadPaddingException { - byte[] ciphered = cipher.doFinal(CipherImpl.getBytes(password.getBytes(ISO_8859_1))); - return new String(ciphered, UTF_8); + byte[] ciphered = cipher.doFinal(CipherImpl.getBytes(password.getBytes(StandardCharsets.ISO_8859_1))); + return new String(ciphered, StandardCharsets.UTF_8); } } diff --git a/util/src/main/java/org/cryptomator/util/crypto/CredentialCryptor.java b/util/src/main/java/org/cryptomator/util/crypto/CredentialCryptor.java index 49ba4de5..53712697 100644 --- a/util/src/main/java/org/cryptomator/util/crypto/CredentialCryptor.java +++ b/util/src/main/java/org/cryptomator/util/crypto/CredentialCryptor.java @@ -2,11 +2,9 @@ package org.cryptomator.util.crypto; import android.content.Context; +import java.nio.charset.StandardCharsets; import java.security.KeyStore; -import static org.cryptomator.util.Encodings.ISO_8859_1; -import static org.cryptomator.util.Encodings.UTF_8; - public class CredentialCryptor { private static final String DEFAULT_KEY_ALIAS = "cryptoKeyAccessToken"; @@ -31,7 +29,7 @@ public class CredentialCryptor { } public String encrypt(String decrypted) { - return new String(encrypt(decrypted.getBytes(UTF_8)), ISO_8859_1); + return new String(encrypt(decrypted.getBytes(StandardCharsets.UTF_8)), StandardCharsets.ISO_8859_1); } public byte[] decrypt(byte[] encrypted) { @@ -39,7 +37,7 @@ public class CredentialCryptor { } public String decrypt(String encrypted) { - return new String(decrypt(encrypted.getBytes(ISO_8859_1)), UTF_8); + return new String(decrypt(encrypted.getBytes(StandardCharsets.ISO_8859_1)), StandardCharsets.UTF_8); } } diff --git a/util/src/main/java/org/cryptomator/util/file/FileCacheUtils.kt b/util/src/main/java/org/cryptomator/util/file/FileCacheUtils.kt index 10775d29..0581cc52 100644 --- a/util/src/main/java/org/cryptomator/util/file/FileCacheUtils.kt +++ b/util/src/main/java/org/cryptomator/util/file/FileCacheUtils.kt @@ -2,7 +2,6 @@ package org.cryptomator.util.file import android.content.Context import android.net.Uri -import org.cryptomator.util.Encodings import java.io.BufferedReader import java.io.File import java.io.FileNotFoundException @@ -13,6 +12,7 @@ import java.io.InputStreamReader import java.io.OutputStreamWriter import java.io.PrintWriter import java.io.StringWriter +import java.nio.charset.StandardCharsets import java.util.UUID import javax.inject.Inject @@ -21,8 +21,8 @@ class FileCacheUtils @Inject constructor(context: Context) { private val cacheDir: File = context.cacheDir @Throws(IOException::class) - fun read(`in`: InputStream): String { - BufferedReader(InputStreamReader(`in`, Encodings.UTF_8)).use { reader -> + fun read(inputStream: InputStream): String { + BufferedReader(InputStreamReader(inputStream, StandardCharsets.UTF_8)).use { reader -> StringWriter().use { writer -> val buffer = CharArray(1024 * 4) var line: Int @@ -71,7 +71,7 @@ class FileCacheUtils @Inject constructor(context: Context) { private fun open(tmpFile: File): PrintWriter { return try { - PrintWriter(OutputStreamWriter(FileOutputStream(tmpFile), Encodings.UTF_8)) + PrintWriter(OutputStreamWriter(FileOutputStream(tmpFile), StandardCharsets.UTF_8)) } catch (e: FileNotFoundException) { throw IllegalStateException("Opening ", e) } 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 abac83df..d3fa5dc6 100644 --- a/util/src/main/java/org/cryptomator/util/file/LruFileCacheUtil.kt +++ b/util/src/main/java/org/cryptomator/util/file/LruFileCacheUtil.kt @@ -128,10 +128,10 @@ class LruFileCacheUtil(context: Context) { @Throws(IOException::class) private fun writeFileContentInData(cacheFile: File, data: OutputStream) { - BufferedInputStream(FileInputStream(cacheFile)).use { `in` -> + BufferedInputStream(FileInputStream(cacheFile)).use { inputStream -> val buffer = ByteArray(2048) var lengthRead: Int - while (`in`.read(buffer).also { lengthRead = it } > 0) { + while (inputStream.read(buffer).also { lengthRead = it } > 0) { data.write(buffer, 0, lengthRead) data.flush() } diff --git a/util/src/main/java/org/cryptomator/util/file/MimeType.kt b/util/src/main/java/org/cryptomator/util/file/MimeType.kt index 98b6b8e0..e9b46a1a 100644 --- a/util/src/main/java/org/cryptomator/util/file/MimeType.kt +++ b/util/src/main/java/org/cryptomator/util/file/MimeType.kt @@ -1,7 +1,5 @@ package org.cryptomator.util.file -import org.cryptomator.util.Function - class MimeType internal constructor(val mediatype: String, val subtype: String) { fun combine(other: MimeType): MimeType { @@ -45,7 +43,7 @@ class MimeType internal constructor(val mediatype: String, val subtype: String) companion object { - val TO_STRING = Function { obj: MimeType -> obj.toString() } + val TO_STRING = java.util.function.Function { obj: MimeType -> obj.toString() } const val WILDCARD_MEDIATYPE = "*" const val WILDCARD_SUBTYPE = "*" diff --git a/util/src/main/java/org/cryptomator/util/file/MimeTypes.kt b/util/src/main/java/org/cryptomator/util/file/MimeTypes.kt index 12f68146..ea289b96 100644 --- a/util/src/main/java/org/cryptomator/util/file/MimeTypes.kt +++ b/util/src/main/java/org/cryptomator/util/file/MimeTypes.kt @@ -1,28 +1,26 @@ package org.cryptomator.util.file -import org.cryptomator.util.Optional import javax.inject.Inject class MimeTypes @Inject constructor(private val mimeTypeMap: MimeTypeMap) { - fun fromExtension(fileExtension: String): Optional { + fun fromExtension(fileExtension: String): MimeType? { val mimeType = mimeTypeMap.getMimeTypeFromExtension(fileExtension.lowercase()) - ?: return Optional.empty() + ?: return null val firstSlash = mimeType.indexOf('/') return if (firstSlash == -1) { - Optional.empty() - } else Optional.of( + null + } else MimeType( // mimeType.substring(0, firstSlash), // mimeType.substring(firstSlash + 1) ) - ) } - fun fromFilename(filename: String): Optional { + fun fromFilename(filename: String): MimeType? { val lastDot = filename.lastIndexOf('.') return if (lastDot == -1) { - Optional.empty() + null } else fromExtension(filename.substring(lastDot + 1)) } } diff --git a/util/src/test/java/org/cryptomator/util/concurrent/CompletableFutureTest.java b/util/src/test/java/org/cryptomator/util/concurrent/CompletableFutureTest.java deleted file mode 100644 index 4dff7868..00000000 --- a/util/src/test/java/org/cryptomator/util/concurrent/CompletableFutureTest.java +++ /dev/null @@ -1,251 +0,0 @@ -package org.cryptomator.util.concurrent; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.concurrent.CancellationException; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; - -public class CompletableFutureTest { - - private static final long ASYNC_DELAY = 50; // [ms] - private static final long DELAY_BEFORE_ASYNC_COMPLETED = ASYNC_DELAY - 40; // [ms] - private static final long DELAY_AFTER_ASYNC_COMPLETD = ASYNC_DELAY + 50; // [ms] - - private static final Object AN_OBJECT = new Object(); - - private final Collection asyncOperations = new ArrayList<>(); - - private final CompletableFuture inTest = new CompletableFuture<>(); - - @AfterEach - public void tearDown() { - for (AsyncOperation asyncOperation : asyncOperations) { - asyncOperation.await(); - } - } - - @Test - public void testCompletedFutureCanNotBeCompleted() { - inTest.complete(AN_OBJECT); - - Assertions.assertThrows(IllegalStateException.class, () -> inTest.complete(AN_OBJECT)); - } - - @Test - public void testFailedFutureCanNotBeCompleted() { - inTest.fail(new Throwable()); - - Assertions.assertThrows(IllegalStateException.class, () -> inTest.complete(AN_OBJECT)); - } - - @Test - public void testCancelledFutureCanNotBeCompleted() { - inTest.cancel(false); - - Assertions.assertThrows(IllegalStateException.class, () -> inTest.complete(AN_OBJECT)); - } - - @Test - public void testCompletedFutureCanNotBeFailed() { - inTest.complete(AN_OBJECT); - - Assertions.assertThrows(IllegalStateException.class, () -> inTest.fail(new Throwable())); - } - - @Test - public void testFailedFutureCanNotBeFailed() { - inTest.fail(new Throwable()); - - Assertions.assertThrows(IllegalStateException.class, () -> inTest.fail(new Throwable())); - } - - @Test - public void testCancelledFutureCanNotBeFailed() { - inTest.cancel(false); - - Assertions.assertThrows(IllegalStateException.class, () -> inTest.fail(new Throwable())); - } - - @Test - public void testFutureReturnsTrueWhenCancelled() { - assertThat(inTest.cancel(false), is(true)); - } - - @Test - public void testCompletedFutureReturnsFalseWhenCancelled() { - inTest.complete(AN_OBJECT); - - assertThat(inTest.cancel(false), is(false)); - } - - @Test - public void testFailedFutureReturnsFalseWhenCancelled() { - inTest.fail(new Throwable()); - - assertThat(inTest.cancel(false), is(false)); - } - - @Test - public void testCancelledFutureReturnsTrueWhenCancelled() { - inTest.cancel(false); - - assertThat(inTest.cancel(false), is(true)); - } - - @Test - public void testCompletedFutureReturnsResultFromGet() throws ExecutionException, InterruptedException { - inTest.complete(AN_OBJECT); - - Object result = inTest.get(); - - assertThat(result, is(AN_OBJECT)); - } - - @Test - public void testFailedFutureThrowsExceptionFromGet() { - Throwable expectedCause = new Throwable(); - inTest.fail(expectedCause); - - ExecutionException exception = Assertions.assertThrows(ExecutionException.class, inTest::get); - assertThat(expectedCause, is(exception.getCause())); - } - - @Test - public void testCancelledFutureThrowsExceptionFromGet() { - inTest.cancel(false); - - Assertions.assertThrows(CancellationException.class, inTest::get); - } - - @Test - public void testCompletedFutureReturnsResultFromGetWithTimeout() throws ExecutionException, InterruptedException, TimeoutException { - inTest.complete(AN_OBJECT); - - Object result = inTest.get(1, TimeUnit.MILLISECONDS); - - assertThat(result, is(AN_OBJECT)); - } - - @Test - public void testFailedFutureThrowsExceptionFromGetWithTimeout() { - Throwable expectedCause = new Throwable(); - inTest.fail(expectedCause); - - ExecutionException exception = Assertions.assertThrows(ExecutionException.class, () -> inTest.get(1, TimeUnit.MILLISECONDS)); - assertThat(expectedCause, is(exception.getCause())); - } - - @Test - public void testCancelledFutureThrowsExceptionFromGetWithTimeout() { - inTest.cancel(false); - - Assertions.assertThrows(CancellationException.class, () -> inTest.get(1, TimeUnit.MILLISECONDS)); - } - - @Test - public void testFutureCompletedAsyncReturnsResultFromGet() throws ExecutionException, InterruptedException { - runAsyncAndDelayed(() -> inTest.complete(AN_OBJECT)); - - Object result = inTest.get(); - - assertThat(result, is(AN_OBJECT)); - } - - @Test - public void testFutureFailedAsyncThrowsExceptionFromGet() { - final Throwable expectedCause = new Throwable(); - runAsyncAndDelayed(() -> inTest.fail(expectedCause)); - - ExecutionException exception = Assertions.assertThrows(ExecutionException.class, inTest::get); - assertThat(expectedCause, is(exception.getCause())); - } - - @Test - public void testFutureCancelledAsyncThrowsExceptionFromGet() { - runAsyncAndDelayed(() -> inTest.cancel(false)); - - Assertions.assertThrows(CancellationException.class, inTest::get); - } - - @Test - public void testFutureCompletedAsyncReturnsResultFromGetWithTimeout() throws ExecutionException, InterruptedException, TimeoutException { - runAsyncAndDelayed(() -> inTest.complete(AN_OBJECT)); - - Object result = inTest.get(DELAY_AFTER_ASYNC_COMPLETD, TimeUnit.MILLISECONDS); - - assertThat(result, is(AN_OBJECT)); - } - - @Test - public void testFutureFailedAsyncThrowsExceptionFromGetWithTimeout() { - final Throwable expectedCause = new Throwable(); - runAsyncAndDelayed(() -> inTest.fail(expectedCause)); - - ExecutionException exception = Assertions.assertThrows(ExecutionException.class, () -> inTest.get(DELAY_AFTER_ASYNC_COMPLETD, TimeUnit.MILLISECONDS)); - assertThat(expectedCause, is(exception.getCause())); - } - - @Test - public void testFutureCancelledAsyncThrowsExceptionFromGetWithTimeout() { - runAsyncAndDelayed(() -> inTest.cancel(false)); - - Assertions.assertThrows(CancellationException.class, () -> inTest.get(DELAY_AFTER_ASYNC_COMPLETD, TimeUnit.MILLISECONDS)); - } - - @Test - public void testFutureCompletedNotYetThrowsTimeoutExceptionFromGetWithTimeout() { - runAsyncAndDelayed(() -> inTest.complete(AN_OBJECT)); - - Assertions.assertThrows(TimeoutException.class, () -> inTest.get(DELAY_BEFORE_ASYNC_COMPLETED, TimeUnit.MILLISECONDS)); - } - - @Test - public void testFutureFailedNotYetThrowsTimeoutExceptionFromGetWithTimeout() { - final Throwable expectedCause = new Throwable(); - runAsyncAndDelayed(() -> inTest.fail(expectedCause)); - - Assertions.assertThrows(TimeoutException.class, () -> inTest.get(DELAY_BEFORE_ASYNC_COMPLETED, TimeUnit.MILLISECONDS)); - } - - @Test - public void testFutureCancelledNotYetThrowsTimeoutExceptionFromGetWithTimeout() { - runAsyncAndDelayed(() -> inTest.cancel(false)); - - Assertions.assertThrows(TimeoutException.class, () -> inTest.get(DELAY_BEFORE_ASYNC_COMPLETED, TimeUnit.MILLISECONDS)); - } - - private void runAsyncAndDelayed(final Runnable task) { - final Thread thread = new Thread(() -> { - try { - Thread.sleep(ASYNC_DELAY); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - task.run(); - }); - thread.start(); - asyncOperations.add(() -> { - try { - thread.join(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - }); - } - - private interface AsyncOperation { - - void await(); - - } - -} diff --git a/util/src/test/java/org/cryptomator/util/file/MimeTypesTest.java b/util/src/test/java/org/cryptomator/util/file/MimeTypesTest.java deleted file mode 100644 index b87caacb..00000000 --- a/util/src/test/java/org/cryptomator/util/file/MimeTypesTest.java +++ /dev/null @@ -1,105 +0,0 @@ -package org.cryptomator.util.file; - -import org.cryptomator.util.Optional; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; - -import static org.cryptomator.util.matchers.MimeTypeMatchers.hasMediatype; -import static org.cryptomator.util.matchers.MimeTypeMatchers.hasSubtype; -import static org.cryptomator.util.matchers.OptionalMatchers.anEmptyOptional; -import static org.cryptomator.util.matchers.OptionalMatchers.anOptionalWithValueThat; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.allOf; -import static org.hamcrest.Matchers.is; -import static org.mockito.Mockito.when; - -public class MimeTypesTest { - - private static final String AN_EXTENSION = "exe"; - private static final java.lang.String INVALID_MIME_TYPE = "notAMimeTypeBecaseSlashIsMissing"; - - private static final java.lang.String MEDIATYPE_OF_VALID_MIME_TYPE = "foo"; - private static final java.lang.String SUBTYPE_OF_VALID_MIME_TYPE = "bar"; - private static final java.lang.String VALID_MIME_TYPE = MEDIATYPE_OF_VALID_MIME_TYPE + '/' + SUBTYPE_OF_VALID_MIME_TYPE; - - private static final String A_FILENAME_WITHOUT_EXTENSION = "fooBarBaz"; - private static final String A_FILENAME_WITH_AN_EXTENSION = "fooBarBaz." + AN_EXTENSION; - - private MimeTypeMap mimeTypeMap = Mockito.mock(MimeTypeMap.class); - - private MimeTypes inTest; - - @BeforeEach - public void setup() { - inTest = new MimeTypes(mimeTypeMap); - } - - @Test - public void testFromExtensionReturnsEmptyOptionalIfMimeTypeMapReturnsNull() { - when(mimeTypeMap.getMimeTypeFromExtension(AN_EXTENSION)).thenReturn(null); - - Optional result = inTest.fromExtension(AN_EXTENSION); - - assertThat(result, is(anEmptyOptional())); - } - - @Test - public void testFromExtensionReturnsEmptyOptionalIfMimeTypeMapReturnsInvalidMimeType() { - when(mimeTypeMap.getMimeTypeFromExtension(AN_EXTENSION)).thenReturn(INVALID_MIME_TYPE); - - Optional result = inTest.fromExtension(AN_EXTENSION); - - assertThat(result, is(anEmptyOptional())); - } - - @Test - public void testFromExtensionReturnsOptionalContainingCorrectMimeType() { - when(mimeTypeMap.getMimeTypeFromExtension(AN_EXTENSION)).thenReturn(VALID_MIME_TYPE); - - Optional result = inTest.fromExtension(AN_EXTENSION); - - assertThat(result, is( // - anOptionalWithValueThat(allOf( // - hasMediatype(MEDIATYPE_OF_VALID_MIME_TYPE), // - hasSubtype(SUBTYPE_OF_VALID_MIME_TYPE))))); - } - - @Test - public void testFromFilenameReturnsEmptyOptionalIfMimeTypeMapReturnsNull() { - when(mimeTypeMap.getMimeTypeFromExtension(AN_EXTENSION)).thenReturn(null); - - Optional result = inTest.fromFilename(A_FILENAME_WITH_AN_EXTENSION); - - assertThat(result, is(anEmptyOptional())); - } - - @Test - public void testFromFilenameReturnsEmptyOptionalIfMimeTypeMapReturnsInvalidMimeType() { - when(mimeTypeMap.getMimeTypeFromExtension(AN_EXTENSION)).thenReturn(INVALID_MIME_TYPE); - - Optional result = inTest.fromFilename(A_FILENAME_WITH_AN_EXTENSION); - - assertThat(result, is(anEmptyOptional())); - } - - @Test - public void testFromFilenameReturnsEmptyOptionalIfFilenameHasNoExtension() { - Optional result = inTest.fromFilename(A_FILENAME_WITHOUT_EXTENSION); - - assertThat(result, is(anEmptyOptional())); - } - - @Test - public void testFromFilenameReturnsOptionalContainingCorrectMimeType() { - when(mimeTypeMap.getMimeTypeFromExtension(AN_EXTENSION)).thenReturn(VALID_MIME_TYPE); - - Optional result = inTest.fromFilename(A_FILENAME_WITH_AN_EXTENSION); - - assertThat(result, is( // - anOptionalWithValueThat(allOf( // - hasMediatype(MEDIATYPE_OF_VALID_MIME_TYPE), // - hasSubtype(SUBTYPE_OF_VALID_MIME_TYPE))))); - } - -} diff --git a/util/src/test/java/org/cryptomator/util/file/MimeTypesTest.kt b/util/src/test/java/org/cryptomator/util/file/MimeTypesTest.kt new file mode 100644 index 00000000..a35e76d4 --- /dev/null +++ b/util/src/test/java/org/cryptomator/util/file/MimeTypesTest.kt @@ -0,0 +1,95 @@ +package org.cryptomator.util.file + +import org.cryptomator.util.matchers.MimeTypeMatchers +import org.hamcrest.MatcherAssert +import org.hamcrest.Matchers +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + + +class MimeTypesTest { + + private val mimeTypeMap: MimeTypeMap = mock() + private lateinit var inTest: MimeTypes + + @BeforeEach + fun setup() { + inTest = MimeTypes(mimeTypeMap) + } + + @Test + fun testFromExtensionReturnsEmptyOptionalIfMimeTypeMapReturnsNull() { + whenever(mimeTypeMap.getMimeTypeFromExtension(AN_EXTENSION)).thenReturn(null) + val result = inTest.fromExtension(AN_EXTENSION) + assertNull(result) + } + + @Test + fun testFromExtensionReturnsEmptyOptionalIfMimeTypeMapReturnsInvalidMimeType() { + whenever(mimeTypeMap.getMimeTypeFromExtension(AN_EXTENSION)).thenReturn(INVALID_MIME_TYPE) + val result = inTest.fromExtension(AN_EXTENSION) + assertNull(result) + } + + @Test + fun testFromExtensionReturnsOptionalContainingCorrectMimeType() { + whenever(mimeTypeMap.getMimeTypeFromExtension(AN_EXTENSION)).thenReturn(VALID_MIME_TYPE) + val result = inTest.fromExtension(AN_EXTENSION) + MatcherAssert.assertThat( + result, Matchers.`is`( // + Matchers.allOf( // + MimeTypeMatchers.hasMediatype(MEDIATYPE_OF_VALID_MIME_TYPE), // + MimeTypeMatchers.hasSubtype(SUBTYPE_OF_VALID_MIME_TYPE) + ) + ) + ) + } + + @Test + fun testFromFilenameReturnsEmptyOptionalIfMimeTypeMapReturnsNull() { + whenever(mimeTypeMap.getMimeTypeFromExtension(AN_EXTENSION)).thenReturn(null) + val result = inTest.fromFilename(A_FILENAME_WITH_AN_EXTENSION) + assertNull(result) + } + + @Test + fun testFromFilenameReturnsEmptyOptionalIfMimeTypeMapReturnsInvalidMimeType() { + whenever(mimeTypeMap.getMimeTypeFromExtension(AN_EXTENSION)).thenReturn(INVALID_MIME_TYPE) + val result = inTest.fromFilename(A_FILENAME_WITH_AN_EXTENSION) + assertNull(result) + } + + @Test + fun testFromFilenameReturnsEmptyOptionalIfFilenameHasNoExtension() { + val result = inTest.fromFilename(A_FILENAME_WITHOUT_EXTENSION) + assertNull(result) + } + + @Test + fun testFromFilenameReturnsOptionalContainingCorrectMimeType() { + whenever(mimeTypeMap.getMimeTypeFromExtension(AN_EXTENSION)).thenReturn(VALID_MIME_TYPE) + val result = inTest.fromFilename(A_FILENAME_WITH_AN_EXTENSION) + MatcherAssert.assertThat( + result, Matchers.`is`( // + Matchers.allOf( // + MimeTypeMatchers.hasMediatype(MEDIATYPE_OF_VALID_MIME_TYPE), // + MimeTypeMatchers.hasSubtype(SUBTYPE_OF_VALID_MIME_TYPE) + ) + ) + ) + } + + companion object { + + private const val AN_EXTENSION = "exe" + private const val INVALID_MIME_TYPE = "notAMimeTypeBecaseSlashIsMissing" + private const val MEDIATYPE_OF_VALID_MIME_TYPE = "foo" + private const val SUBTYPE_OF_VALID_MIME_TYPE = "bar" + private const val VALID_MIME_TYPE = MEDIATYPE_OF_VALID_MIME_TYPE + '/' + SUBTYPE_OF_VALID_MIME_TYPE + private const val A_FILENAME_WITHOUT_EXTENSION = "fooBarBaz" + private const val A_FILENAME_WITH_AN_EXTENSION = "fooBarBaz." + AN_EXTENSION + } +} diff --git a/util/src/test/java/org/cryptomator/util/matchers/OptionalMatchers.java b/util/src/test/java/org/cryptomator/util/matchers/OptionalMatchers.java index eb8b7917..619dd9a1 100644 --- a/util/src/test/java/org/cryptomator/util/matchers/OptionalMatchers.java +++ b/util/src/test/java/org/cryptomator/util/matchers/OptionalMatchers.java @@ -1,17 +1,18 @@ package org.cryptomator.util.matchers; -import org.cryptomator.util.Optional; import org.hamcrest.Description; import org.hamcrest.Matcher; import org.hamcrest.TypeSafeDiagnosingMatcher; +import com.google.common.base.Optional; + public class OptionalMatchers { public static Matcher> anEmptyOptional() { return new TypeSafeDiagnosingMatcher>() { @Override protected boolean matchesSafely(Optional value, Description description) { - if (value.isAbsent()) { + if (!value.isPresent()) { return true; } else { description.appendText("non empty Optional"); @@ -30,7 +31,7 @@ public class OptionalMatchers { return new TypeSafeDiagnosingMatcher>() { @Override protected boolean matchesSafely(Optional value, Description description) { - if (value.isAbsent()) { + if (!value.isPresent()) { description.appendText("empty Optional"); return false; } else if (subMatcher.matches(value.get())) { From d82bcaa3e8f1a2bd555bed5121562b4ab2184165 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Tue, 6 Jul 2021 12:00:29 +0200 Subject: [PATCH 2/6] Update dependencies --- buildsystem/dependencies.gradle | 34 +++++++++---------- .../webdav/network/SSLSocketFactories.kt | 2 +- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/buildsystem/dependencies.gradle b/buildsystem/dependencies.gradle index 12da0d11..405da723 100644 --- a/buildsystem/dependencies.gradle +++ b/buildsystem/dependencies.gradle @@ -17,10 +17,10 @@ ext { // support lib androidSupportAnnotationsVersion = '1.2.0' - androidSupportAppcompatVersion = '1.2.0' - androidSupportDesignVersion = '1.3.0' + androidSupportAppcompatVersion = '1.3.0' + androidSupportDesignVersion = '1.4.0' - coreDesugaringVersion = '1.0.9' + coreDesugaringVersion = '1.1.5' // app frameworks and utilities @@ -28,7 +28,7 @@ ext { rxAndroidVersion = '2.1.1' rxBindingVersion = '2.2.0' - daggerVersion = '2.36' + daggerVersion = '2.37' gsonVersion = '2.8.7' @@ -39,7 +39,7 @@ ext { timberVersion = '4.7.1' - zxcvbnVersion = '1.5.0' + zxcvbnVersion = '1.5.2' scaleImageViewVersion = '3.10.0' @@ -49,19 +49,17 @@ ext { greenDaoVersion = '3.3.0' // cloud provider libs - - // do not update to 1.4.0 until minsdk is 7.x (or desugaring works better) otherwise it will crash on 6.x cryptolibVersion = '2.0.0-rc6' dropboxVersion = '4.0.0' googleApiServicesVersion = 'v3-rev197-1.25.0' googlePlayServicesVersion = '19.0.0' - googleClientVersion = '1.31.5' + googleClientVersion = '1.32.1' msgraphVersion = '2.10.0' - minIoVersion = '8.2.1' + minIoVersion = '8.2.2' staxVersion = '1.2.0' // needed for minIO commonsCodecVersion = '1.15' @@ -73,26 +71,26 @@ ext { jUnitVersion = '5.7.1' jUnit4Version = '4.13.1' assertJVersion = '1.7.1' - mockitoVersion = '3.10.0' + mockitoVersion = '3.11.2' mockitoKotlinVersion = '3.2.0' hamcrestVersion = '1.3' dexmakerVersion = '1.0' - espressoVersion = '3.3.0' + espressoVersion = '3.4.0' testingSupportLibVersion = '0.1' - runnerVersion = '1.3.0' - rulesVersion = '1.3.0' - contributionVersion = '3.3.0' + runnerVersion = '1.4.0' + rulesVersion = '1.4.0' + contributionVersion = '3.4.0' uiautomatorVersion = '2.2.0' - androidxCoreVersion = '1.3.2' - androidxFragmentVersion = '1.3.3' + androidxCoreVersion = '1.6.0' + androidxFragmentVersion = '1.3.5' androidxViewpagerVersion = '1.0.0' androidxSwiperefreshVersion = '1.1.0' androidxPreferenceVersion = '1.1.1' - androidxRecyclerViewVersion = '1.2.0' + androidxRecyclerViewVersion = '1.2.1' androidxDocumentfileVersion = '1.0.1' androidxBiometricVersion = '1.1.0' - androidxTestCoreVersion = '1.3.0' + androidxTestCoreVersion = '1.4.0' jsonWebTokenApiVersion = '0.11.2' diff --git a/data/src/main/java/org/cryptomator/data/cloud/webdav/network/SSLSocketFactories.kt b/data/src/main/java/org/cryptomator/data/cloud/webdav/network/SSLSocketFactories.kt index b2aba76c..a26ae086 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/webdav/network/SSLSocketFactories.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/webdav/network/SSLSocketFactories.kt @@ -11,7 +11,7 @@ internal object SSLSocketFactories { fun from(trustManager: X509TrustManager): SSLSocketFactory { return try { - val sslContext = SSLContext.getInstance("TLS") + val sslContext = SSLContext.getInstance("TLSv1.2") sslContext.init(null, arrayOf(trustManager), null) sslContext.socketFactory } catch (e: GeneralSecurityException) { From 210d0fa7f4974588efe443e1be86b489b9662d13 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Tue, 6 Jul 2021 12:49:25 +0200 Subject: [PATCH 3/6] Handle less generic exceptions in catch clauses --- .../data/cloud/crypto/VaultConfig.kt | 3 +- .../OnedriveCloudContentRepository.kt | 4 +-- .../repository/UpdateCheckRepositoryImpl.java | 4 +-- .../ui/fragment/S3AddOrChangeFragment.kt | 3 +- .../ui/fragment/WebDavAddOrChangeFragment.kt | 3 +- .../cryptomator/presentation/util/FileUtil.kt | 30 ++++++++----------- .../cryptomator/util/crypto/CipherImpl.java | 26 +++++++--------- .../util/crypto/CryptoOperationsImpl.java | 12 +++++--- .../util/crypto/FatalCryptoException.java | 10 +++++++ 9 files changed, 50 insertions(+), 45 deletions(-) create mode 100644 util/src/main/java/org/cryptomator/util/crypto/FatalCryptoException.java diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/VaultConfig.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/VaultConfig.kt index d361fdd7..a9f3e190 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/VaultConfig.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/VaultConfig.kt @@ -116,11 +116,10 @@ class VaultConfig private constructor(builder: VaultConfigBuilder) { .shorteningThreshold(parser.body[JSON_KEY_SHORTENING_THRESHOLD] as Int) VaultConfig(vaultConfigBuilder) - } catch (e: Exception) { + } catch (e: JwtException) { when (e) { is MissingClaimException, is IncorrectClaimException -> throw VaultVersionMismatchException("Vault config not for version " + unverifiedVaultConfig.vaultFormat) is SignatureException -> throw VaultKeyInvalidException() - is JwtException -> throw VaultConfigLoadException("Failed to verify vault config", e) else -> throw VaultConfigLoadException(e) } } diff --git a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveCloudContentRepository.kt b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveCloudContentRepository.kt index 850324f0..6db19a32 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveCloudContentRepository.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveCloudContentRepository.kt @@ -110,9 +110,9 @@ internal class OnedriveCloudContentRepository(private val cloud: OnedriveCloud, } @Throws(BackendException::class) - override fun read(file: OnedriveFile, tmpEncryptedFile: File?, data: OutputStream, progressAware: ProgressAware) { + override fun read(file: OnedriveFile, encryptedTmpFile: File?, data: OutputStream, progressAware: ProgressAware) { try { - oneDriveImpl.read(file, tmpEncryptedFile, data, progressAware) + oneDriveImpl.read(file, encryptedTmpFile, data, progressAware) } catch (e: IOException) { when { ExceptionUtil.contains(e, NoSuchCloudFileException::class.java) -> { diff --git a/data/src/main/java/org/cryptomator/data/repository/UpdateCheckRepositoryImpl.java b/data/src/main/java/org/cryptomator/data/repository/UpdateCheckRepositoryImpl.java index 962ad968..048a18ff 100644 --- a/data/src/main/java/org/cryptomator/data/repository/UpdateCheckRepositoryImpl.java +++ b/data/src/main/java/org/cryptomator/data/repository/UpdateCheckRepositoryImpl.java @@ -144,7 +144,7 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository { } } return new String(Hex.encodeHex(digest.digest())); - } catch (Exception e) { + } catch (NoSuchAlgorithmException | IOException e) { throw new GeneralUpdateErrorException(e); } } @@ -272,7 +272,7 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository { urlApk = jws.get("url", String.class); apkSha256 = jws.get("apk_sha_256", String.class); urlReleaseNote = jws.get("release_notes", String.class); - } catch (Exception e) { + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { throw new GeneralUpdateErrorException("Failed to parse latest version", e); } } diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/S3AddOrChangeFragment.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/S3AddOrChangeFragment.kt index b442cb5f..d0981f1a 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/S3AddOrChangeFragment.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/S3AddOrChangeFragment.kt @@ -7,6 +7,7 @@ import org.cryptomator.presentation.R import org.cryptomator.presentation.model.S3CloudModel import org.cryptomator.presentation.presenter.S3AddOrChangePresenter import org.cryptomator.util.crypto.CredentialCryptor +import org.cryptomator.util.crypto.FatalCryptoException import javax.inject.Inject import kotlinx.android.synthetic.main.fragment_setup_s3.accessKeyEditText import kotlinx.android.synthetic.main.fragment_setup_s3.bucketEditText @@ -58,7 +59,7 @@ class S3AddOrChangeFragment : BaseFragment() { CredentialCryptor // .getInstance(activity?.applicationContext) // .decrypt(text) - } catch (e: RuntimeException) { + } catch (e: FatalCryptoException) { Timber.tag("S3AddOrChangeFragment").e(e, "Unable to decrypt password, clearing it") "" } diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/WebDavAddOrChangeFragment.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/WebDavAddOrChangeFragment.kt index 42073a1a..a53152b6 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/WebDavAddOrChangeFragment.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/WebDavAddOrChangeFragment.kt @@ -7,6 +7,7 @@ import org.cryptomator.presentation.R import org.cryptomator.presentation.model.WebDavCloudModel import org.cryptomator.presentation.presenter.WebDavAddOrChangePresenter import org.cryptomator.util.crypto.CredentialCryptor +import org.cryptomator.util.crypto.FatalCryptoException import javax.inject.Inject import kotlinx.android.synthetic.main.fragment_setup_webdav.createCloudButton import kotlinx.android.synthetic.main.fragment_setup_webdav.passwordEditText @@ -55,7 +56,7 @@ class WebDavAddOrChangeFragment : BaseFragment() { CredentialCryptor // .getInstance(activity?.applicationContext) // .decrypt(password) - } catch (e: RuntimeException) { + } catch (e: FatalCryptoException) { Timber.tag("WebdavAddOrCangeFragmnt").e(e, "Unable to decrypt password, clearing it") "" } diff --git a/presentation/src/main/java/org/cryptomator/presentation/util/FileUtil.kt b/presentation/src/main/java/org/cryptomator/presentation/util/FileUtil.kt index 8b95e265..900b128f 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/util/FileUtil.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/util/FileUtil.kt @@ -208,24 +208,20 @@ class FileUtil @Inject constructor(private val context: Context, private val mim */ private fun tryRecoverAutoUploadFilesStoreDueToFileObfuscation(file: File): AutoUploadFilesStore { Timber.tag("FileUtil").i("Try to recover AutoUploadFilesStore using class c or a") - try { - ObjectInputStream(FileInputStream(file)).use { objectInputStream -> - val uploadPaths = when (val obj = objectInputStream.readObject()) { - is org.cryptomator.presentation.e.c -> obj.mE() // version 1.5.10 - is org.cryptomator.presentation.i.a -> obj.b() // version 1.5.11-beta1 - else -> null - } - when { - uploadPaths != null -> { - Timber.tag("FileUtil").i("Nailed it! Successfully recovered AutoUploadFilesStore!") - file.delete() - return AutoUploadFilesStore(uploadPaths) - } - else -> throw FatalBackendException("Failed to recover AutoUploadFilesStore") - } + ObjectInputStream(FileInputStream(file)).use { objectInputStream -> + val uploadPaths = when (val obj = objectInputStream.readObject()) { + is org.cryptomator.presentation.e.c -> obj.mE() // version 1.5.10 + is org.cryptomator.presentation.i.a -> obj.b() // version 1.5.11-beta1 + else -> null + } + when { + uploadPaths != null -> { + Timber.tag("FileUtil").i("Nailed it! Successfully recovered AutoUploadFilesStore!") + file.delete() + return AutoUploadFilesStore(uploadPaths) + } + else -> throw FatalBackendException("Failed to recover AutoUploadFilesStore") } - } catch (e: Exception) { - throw FatalBackendException("Failed to recover AutoUploadFilesStore", e) } } diff --git a/util/src/main/java/org/cryptomator/util/crypto/CipherImpl.java b/util/src/main/java/org/cryptomator/util/crypto/CipherImpl.java index 2ba69fc6..e0432f3e 100644 --- a/util/src/main/java/org/cryptomator/util/crypto/CipherImpl.java +++ b/util/src/main/java/org/cryptomator/util/crypto/CipherImpl.java @@ -3,6 +3,8 @@ package org.cryptomator.util.crypto; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; import javax.crypto.SecretKey; import javax.crypto.spec.IvParameterSpec; @@ -22,20 +24,14 @@ class CipherImpl implements Cipher { private static byte[] mergeIvAndEncryptedData(byte[] encrypted, byte[] iv) { byte[] mergedIvAndEncrypted = new byte[encrypted.length + iv.length]; - arraycopy( // - iv, 0, // - mergedIvAndEncrypted, 0, IV_LENGTH); - arraycopy( // - encrypted, 0, // - mergedIvAndEncrypted, IV_LENGTH, encrypted.length); + arraycopy(iv, 0, mergedIvAndEncrypted, 0, IV_LENGTH); + arraycopy(encrypted, 0, mergedIvAndEncrypted, IV_LENGTH, encrypted.length); return mergedIvAndEncrypted; } static byte[] getBytes(byte[] encryptedBytesWithIv) { byte[] bytes = new byte[encryptedBytesWithIv.length - IV_LENGTH]; - arraycopy( // - encryptedBytesWithIv, IV_LENGTH, // - bytes, 0, bytes.length); + arraycopy(encryptedBytesWithIv, IV_LENGTH, bytes, 0, bytes.length); return bytes; } @@ -45,8 +41,8 @@ class CipherImpl implements Cipher { cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, key); byte[] encrypted = cipher.doFinal(data); return mergeIvAndEncryptedData(encrypted, cipher.getIV()); - } catch (Exception e) { - throw new RuntimeException(e); + } catch (InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) { + throw new FatalCryptoException(e); } } @@ -58,8 +54,8 @@ class CipherImpl implements Cipher { IvParameterSpec ivspec = new IvParameterSpec(iv); cipher.init(javax.crypto.Cipher.DECRYPT_MODE, key, ivspec); return cipher.doFinal(bytes); - } catch (Exception e) { - throw new RuntimeException(e); + } catch (InvalidKeyException | BadPaddingException | IllegalBlockSizeException | InvalidAlgorithmParameterException e) { + throw new FatalCryptoException(e); } } @@ -79,9 +75,7 @@ class CipherImpl implements Cipher { private byte[] getIv(byte[] encryptedBytesWithIv) { byte[] iv = new byte[IV_LENGTH]; - arraycopy( // - encryptedBytesWithIv, 0, // - iv, 0, IV_LENGTH); + arraycopy(encryptedBytesWithIv, 0, iv, 0, IV_LENGTH); return iv; } diff --git a/util/src/main/java/org/cryptomator/util/crypto/CryptoOperationsImpl.java b/util/src/main/java/org/cryptomator/util/crypto/CryptoOperationsImpl.java index 14c7e810..85b11a79 100644 --- a/util/src/main/java/org/cryptomator/util/crypto/CryptoOperationsImpl.java +++ b/util/src/main/java/org/cryptomator/util/crypto/CryptoOperationsImpl.java @@ -6,8 +6,12 @@ import android.security.keystore.KeyGenParameterSpec; import android.security.keystore.KeyProperties; import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; import java.security.UnrecoverableKeyException; +import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; class CryptoOperationsImpl implements CryptoOperations { @@ -22,8 +26,8 @@ class CryptoOperationsImpl implements CryptoOperations { return new CipherImpl(cipher, key); } catch (UnrecoverableKeyException e) { throw new UnrecoverableStorageKeyException(e); - } catch (Exception e) { - throw new RuntimeException(e); + } catch (NoSuchPaddingException | NoSuchAlgorithmException | KeyStoreException e) { + throw new FatalCryptoException(e); } } @@ -32,8 +36,8 @@ class CryptoOperationsImpl implements CryptoOperations { final javax.crypto.KeyGenerator generator; try { generator = javax.crypto.KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KeyStoreBuilder.DEFAULT_KEYSTORE_NAME); - } catch (Exception e) { - throw new RuntimeException(e); + } catch (NoSuchAlgorithmException | NoSuchProviderException e) { + throw new FatalCryptoException(e); } return requireUserAuthentication -> { KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec // diff --git a/util/src/main/java/org/cryptomator/util/crypto/FatalCryptoException.java b/util/src/main/java/org/cryptomator/util/crypto/FatalCryptoException.java new file mode 100644 index 00000000..7e564f30 --- /dev/null +++ b/util/src/main/java/org/cryptomator/util/crypto/FatalCryptoException.java @@ -0,0 +1,10 @@ +package org.cryptomator.util.crypto; + +import java.security.GeneralSecurityException; + +public class FatalCryptoException extends RuntimeException { + + public FatalCryptoException(GeneralSecurityException e) { + super(e); + } +} From c23a5a2cfcf121160dcd7537480985bfa37b8d48 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Tue, 6 Jul 2021 15:30:34 +0200 Subject: [PATCH 4/6] Handle further exceptions in a less generic way --- .../data/cloud/crypto/CryptoImplDecorator.kt | 7 +-- .../cloud/crypto/CryptoImplVaultFormat7.kt | 2 - .../okhttplogging/HttpLoggingInterceptor.kt | 2 +- .../org/cryptomator/data/cloud/s3/S3Impl.kt | 52 ++++++++----------- .../network/WebDavCompatibleHttpClient.kt | 3 +- .../googledrive/GoogleDriveClientFactory.kt | 16 +++--- .../cloud/DataSourceCapturingAnswer.kt | 3 +- 7 files changed, 36 insertions(+), 49 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt index 646099c0..8c9cbaf1 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt @@ -420,8 +420,7 @@ abstract class CryptoImplDecorator( assertCryptoFileAlreadyExists(cryptoFile) } try { - data.open(context).use { stream -> - requireNotNull(stream) + data.open(context)?.use { stream -> requireNotNull(cryptoFile.size) val encryptedTmpFile = File.createTempFile(UUID.randomUUID().toString(), ".crypto", internalCache) try { @@ -444,12 +443,10 @@ abstract class CryptoImplDecorator( return writeFromTmpFile(data, cryptoFile, encryptedTmpFile, progressAware, replace) } } - } catch (e: Throwable) { - throw e } finally { encryptedTmpFile.delete() } - } + } ?: throw IllegalStateException("InputStream shouldn't be null") } catch (e: IOException) { throw FatalBackendException(e) } diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt index 87698d10..bc085a87 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt @@ -481,8 +481,6 @@ open class CryptoImplVaultFormat7 : CryptoImplDecorator { } ?: throw FatalBackendException("CloudFile size shouldn't be null") } } - } catch (e: Throwable) { - throw e } finally { encryptedTmpFile.delete() } diff --git a/data/src/main/java/org/cryptomator/data/cloud/okhttplogging/HttpLoggingInterceptor.kt b/data/src/main/java/org/cryptomator/data/cloud/okhttplogging/HttpLoggingInterceptor.kt index 558eb6f0..b388fbb8 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/okhttplogging/HttpLoggingInterceptor.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/okhttplogging/HttpLoggingInterceptor.kt @@ -49,7 +49,7 @@ class HttpLoggingInterceptor(private val logger: Logger, private val context: Co private fun getResponseLoggingExceptions(request: Request, chain: Interceptor.Chain): Response { return try { chain.proceed(request) - } catch (e: Exception) { + } catch (e: IOException) { logger.log("<-- HTTP FAILED: $e") throw e } diff --git a/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.kt b/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.kt index fbd13777..a5ff3f28 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.kt @@ -88,8 +88,6 @@ internal class S3Impl(context: Context, cloud: S3Cloud) { return false } throw FatalBackendException(e) - } catch (ex: Exception) { - throw handleApiError(ex, node.path) } } @@ -114,8 +112,8 @@ internal class S3Impl(context: Context, cloud: S3Cloud) { } } } - } catch (ex: Exception) { - throw handleApiError(ex, folder.path) + } catch (e: ErrorResponseException) { + throw handleApiError(e, folder.path) } } @@ -140,8 +138,8 @@ internal class S3Impl(context: Context, cloud: S3Cloud) { .build() client().putObject(putObjectArgs) - } catch (ex: Exception) { - throw handleApiError(ex, folder.path) + } catch (e: ErrorResponseException) { + throw handleApiError(e, folder.path) } return S3CloudNodeFactory.folder(parentFolder, folder.name) @@ -181,8 +179,8 @@ internal class S3Impl(context: Context, cloud: S3Cloud) { val copyObjectArgs = CopyObjectArgs.builder().bucket(cloud.s3Bucket()).`object`(targetKey).source(copySource).build() try { client().copyObject(copyObjectArgs) - } catch (ex: Exception) { - throw handleApiError(ex, source.path) + } catch (e: ErrorResponseException) { + throw handleApiError(e, source.path) } } @@ -191,8 +189,8 @@ internal class S3Impl(context: Context, cloud: S3Cloud) { for (result in client().removeObjects(removeObjectsArgs)) { try { result.get() - } catch (ex: Exception) { - throw handleApiError(ex, source.path) + } catch (e: ErrorResponseException) { + throw handleApiError(e, source.path) } } @@ -209,8 +207,8 @@ internal class S3Impl(context: Context, cloud: S3Cloud) { delete(source) val lastModified = result.headers().getDate("Last-Modified") return S3CloudNodeFactory.file(target.parent, target.name, source.size, lastModified) - } catch (ex: Exception) { - throw handleApiError(ex, source.path) + } catch (e: ErrorResponseException) { + throw handleApiError(e, source.path) } } @@ -257,8 +255,8 @@ internal class S3Impl(context: Context, cloud: S3Cloud) { progressAware.onProgress(Progress.completed(UploadState.upload(file))) return S3CloudNodeFactory.file(file.parent, file.name, size, lastModified) - } catch (ex: Exception) { - throw handleApiError(ex, file.path) + } catch (e: ErrorResponseException) { + throw handleApiError(e, file.path) } } } ?: throw FatalBackendException("InputStream shouldn't bee null") @@ -281,8 +279,8 @@ internal class S3Impl(context: Context, cloud: S3Cloud) { } }.use { out -> CopyStream.copyStreamToStream(response, out) } } - } catch (ex: Exception) { - throw handleApiError(ex, file.path) + } catch (e: ErrorResponseException) { + throw handleApiError(e, file.path) } progressAware.onProgress(Progress.completed(DownloadState.download(file))) } @@ -307,7 +305,7 @@ internal class S3Impl(context: Context, cloud: S3Cloud) { DeleteObject(item.objectName()) } } - } catch (e: Exception) { + } catch (e: ErrorResponseException) { throw handleApiError(e, node.path) } @@ -317,18 +315,18 @@ internal class S3Impl(context: Context, cloud: S3Cloud) { try { val error = result.get() Timber.tag("S3Impl").e("Error in deleting object " + error.objectName() + "; " + error.message()) - } catch (e: Exception) { + } catch (e: ErrorResponseException) { throw handleApiError(e, node.path) } } } - @Throws(IOException::class, BackendException::class) + //@Throws(IOException::class, BackendException::class) private fun deleteFile(node: S3File) { val removeObjectArgs = RemoveObjectArgs.builder().bucket(cloud.s3Bucket()).`object`(node.key).build() try { client().removeObject(removeObjectArgs) - } catch (e: Exception) { + } catch (e: ErrorResponseException) { throw handleApiError(e, "") } } @@ -340,15 +338,14 @@ internal class S3Impl(context: Context, cloud: S3Cloud) { throw NoSuchBucketException(cloud.s3Bucket()) } "" - } catch (e: Exception) { + } catch (e: ErrorResponseException) { throw handleApiError(e, "") } } - private fun handleApiError(ex: Exception, name: String): Exception { - return if (ex is ErrorResponseException) { - val errorCode = ex.errorResponse().code() - when { + private fun handleApiError(e: ErrorResponseException, name: String): Exception { + val errorCode = e.errorResponse().code() + return when { isAccessProblem(errorCode) -> { ForbiddenException() } @@ -359,12 +356,9 @@ internal class S3Impl(context: Context, cloud: S3Cloud) { NoSuchCloudFileException(name) } else -> { - FatalBackendException(ex) + FatalBackendException(e) } } - } else { - FatalBackendException(ex) - } } companion object { diff --git a/data/src/main/java/org/cryptomator/data/cloud/webdav/network/WebDavCompatibleHttpClient.kt b/data/src/main/java/org/cryptomator/data/cloud/webdav/network/WebDavCompatibleHttpClient.kt index c0dea8cf..b6367775 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/webdav/network/WebDavCompatibleHttpClient.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/webdav/network/WebDavCompatibleHttpClient.kt @@ -16,6 +16,7 @@ import org.cryptomator.domain.WebDavCloud import org.cryptomator.domain.exception.UnableToDecryptWebdavPasswordException import org.cryptomator.util.SharedPreferencesHandler import org.cryptomator.util.crypto.CredentialCryptor +import org.cryptomator.util.crypto.FatalCryptoException import org.cryptomator.util.file.LruFileCacheUtil import java.io.IOException import java.nio.charset.StandardCharsets @@ -128,7 +129,7 @@ internal class WebDavCompatibleHttpClient(cloud: WebDavCloud, context: Context) CredentialCryptor // .getInstance(context) // .decrypt(password) - } catch (e: RuntimeException) { + } catch (e: FatalCryptoException) { throw UnableToDecryptWebdavPasswordException(e) } } diff --git a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveClientFactory.kt b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveClientFactory.kt index fe8c13ec..8cdb15f1 100644 --- a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveClientFactory.kt +++ b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveClientFactory.kt @@ -26,7 +26,7 @@ class GoogleDriveClientFactory internal constructor() { @Throws(FatalBackendException::class) fun createClient(accountName: String, context: Context): Drive { - if ( SharedPreferencesHandler(context).debugMode()) { + if (SharedPreferencesHandler(context).debugMode()) { Logger.getLogger("com.google.api.client").level = Level.CONFIG Logger.getLogger("com.google.api.client").addHandler(object : Handler() { override fun publish(record: LogRecord) { @@ -45,15 +45,11 @@ class GoogleDriveClientFactory internal constructor() { } }) } - return try { - val credential: FixedGoogleAccountCredential = FixedGoogleAccountCredential.usingOAuth2(context, setOf(DriveScopes.DRIVE)) - credential.setAccountName(accountName) - Drive.Builder(NetHttpTransport(), JacksonFactory.getDefaultInstance(), credential) // - .setApplicationName("Cryptomator-Android/" + BuildConfig.VERSION_NAME) // - .build() - } catch (e: Exception) { - throw FatalBackendException(e) - } + val credential = FixedGoogleAccountCredential.usingOAuth2(context, setOf(DriveScopes.DRIVE)).also { it.setAccountName(accountName) } + return Drive.Builder(NetHttpTransport(), JacksonFactory.getDefaultInstance(), credential) // + .setApplicationName("Cryptomator-Android/" + BuildConfig.VERSION_NAME) // + .build() + } } } diff --git a/domain/src/test/java/org/cryptomator/domain/usecases/cloud/DataSourceCapturingAnswer.kt b/domain/src/test/java/org/cryptomator/domain/usecases/cloud/DataSourceCapturingAnswer.kt index 514b9865..7975ae63 100644 --- a/domain/src/test/java/org/cryptomator/domain/usecases/cloud/DataSourceCapturingAnswer.kt +++ b/domain/src/test/java/org/cryptomator/domain/usecases/cloud/DataSourceCapturingAnswer.kt @@ -1,6 +1,7 @@ package org.cryptomator.domain.usecases.cloud import android.content.Context +import org.cryptomator.domain.exception.FatalBackendException import org.mockito.invocation.InvocationOnMock import org.mockito.kotlin.mock import org.mockito.stubbing.Answer @@ -30,7 +31,7 @@ internal class DataSourceCapturingAnswer(private val result: T, private val a out.write(buffer, 0, read) } } catch (e: IOException) { - throw RuntimeException(e) + throw FatalBackendException(e) } } From 10e03d3df2817a40a9ae138466f7f659bddb2548 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Tue, 6 Jul 2021 17:38:10 +0200 Subject: [PATCH 5/6] Remove some unwraps --- .../cloud/crypto/CryptoImplVaultFormatPre7.kt | 14 ++--- .../cryptomator/data/cloud/crypto/Cryptors.kt | 5 +- .../LocalStorageAccessFolder.kt | 2 +- .../onedrive/OnedriveCloudNodeFactory.kt | 9 ++-- .../data/cloud/onedrive/OnedriveIdCache.kt | 2 +- .../data/cloud/onedrive/OnedriveImpl.kt | 51 +++++++++---------- .../data/cloud/pcloud/PCloudNodeFactory.kt | 4 +- .../data/cloud/googledrive/GoogleDriveImpl.kt | 9 ++-- 8 files changed, 48 insertions(+), 48 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt index f3514360..4222f8f3 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt @@ -17,6 +17,7 @@ import org.cryptomator.domain.exception.AlreadyExistException import org.cryptomator.domain.exception.BackendException import org.cryptomator.domain.exception.EmptyDirFileException import org.cryptomator.domain.exception.NoSuchCloudFileException +import org.cryptomator.domain.exception.ParentFolderIsNullException import org.cryptomator.domain.repository.CloudContentRepository import org.cryptomator.domain.usecases.ProgressAware import org.cryptomator.domain.usecases.cloud.ByteArrayDataSource.Companion.from @@ -214,14 +215,15 @@ internal class CryptoImplVaultFormatPre7( @Throws(BackendException::class) override fun move(source: CryptoFolder, target: CryptoFolder): CryptoFolder { - requireNotNull(target.parent) requireNotNull(source.dirFile) requireNotNull(target.dirFile) - assertCryptoFolderAlreadyExists(target) - val result = folder(target.parent!!, target.name, cloudContentRepository.move(source.dirFile, target.dirFile)) - evictFromCache(source) - evictFromCache(target) - return result + target.parent?.let { + assertCryptoFolderAlreadyExists(target) + val result = folder(it, target.name, cloudContentRepository.move(source.dirFile, target.dirFile)) + evictFromCache(source) + evictFromCache(target) + return result + } ?: throw ParentFolderIsNullException(target.name) } @Throws(BackendException::class) diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/Cryptors.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/Cryptors.kt index e20117f9..485c5078 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/Cryptors.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/Cryptors.kt @@ -35,8 +35,9 @@ abstract class Cryptors internal constructor() { @Synchronized fun removeDelegate() { - fallback.putAll(delegate!!.cryptors) - delegate = null + delegate?.let { + fallback.putAll(it.cryptors) + }.also { delegate = null } } @Synchronized diff --git a/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFolder.kt b/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFolder.kt index 2416f61b..af3d8027 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFolder.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFolder.kt @@ -35,6 +35,6 @@ open class LocalStorageAccessFolder(override val parent: LocalStorageAccessFolde } override fun withCloud(cloud: Cloud?): LocalStorageAccessFolder? { - return LocalStorageAccessFolder(parent!!.withCloud(cloud), name, path, documentId, documentUri) + return LocalStorageAccessFolder(parent?.withCloud(cloud), name, path, documentId, documentUri) } } diff --git a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveCloudNodeFactory.kt b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveCloudNodeFactory.kt index c2bab99e..85aa7b45 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveCloudNodeFactory.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveCloudNodeFactory.kt @@ -53,9 +53,12 @@ internal object OnedriveCloudNodeFactory { } @JvmStatic - fun getDriveId(item: DriveItem): String { - return if (item.remoteItem != null) item.remoteItem.parentReference.driveId - else (if (item.parentReference != null) item.parentReference.driveId else null)!! + fun getDriveId(item: DriveItem): String? { + return when { + item.remoteItem != null -> item.remoteItem.parentReference.driveId + item.parentReference != null -> item.parentReference.driveId + else -> null + } } @JvmStatic diff --git a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveIdCache.kt b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveIdCache.kt index 38ac5eff..9580991a 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveIdCache.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveIdCache.kt @@ -43,7 +43,7 @@ internal class OnedriveIdCache @Inject constructor() { } } - internal class NodeInfo(val id: String, val driveId: String, val isFolder: Boolean, private val cTag: String?) { + internal class NodeInfo(val id: String, val driveId: String?, val isFolder: Boolean, private val cTag: String?) { constructor(node: OnedriveIdCloudNode) : this(node.id, node.driveId, node is CloudFolder, "") fun getcTag(): String? { diff --git a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveImpl.kt b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveImpl.kt index ba1a9cee..692a260a 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveImpl.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveImpl.kt @@ -28,7 +28,6 @@ import org.cryptomator.domain.exception.BackendException import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException import org.cryptomator.domain.exception.FatalBackendException import org.cryptomator.domain.exception.NoSuchCloudFileException -import org.cryptomator.domain.exception.ParentFolderDoesNotExistException import org.cryptomator.domain.exception.ParentFolderIsNullException import org.cryptomator.domain.exception.authentication.NoAuthenticationProvidedException import org.cryptomator.domain.usecases.ProgressAware @@ -121,7 +120,7 @@ internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, nodeInfoCach fun exists(node: OnedriveNode): Boolean { node.parent?.let { val parentNodeInfo = nodeInfo(it) - if (parentNodeInfo == null) { + if (parentNodeInfo?.driveId == null) { removeNodeInfo(node) return false } @@ -161,22 +160,22 @@ internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, nodeInfoCach @Throws(NoSuchCloudFileException::class) fun create(folder: OnedriveFolder): OnedriveFolder { var parent = folder.parent - if (nodeInfo(parent!!) == null) { //FIXME - if (parent == null) { - throw ParentFolderDoesNotExistException() - } else { - parent = create(parent) + parent?.let { parentFolder -> + if (nodeInfo(parentFolder) == null) { + parent = create(parentFolder) } - } - val folderToCreate = DriveItem() - folderToCreate.name = folder.name - folderToCreate.folder = Folder() - val parentNodeInfo = requireNodeInfo(parent) - val createdFolder = drive(parentNodeInfo.driveId) // - .items(parentNodeInfo.id).children() // - .buildRequest() // - .post(folderToCreate) - return cacheNodeInfo(folder(parent, createdFolder), createdFolder) + } ?: throw ParentFolderIsNullException(folder.name) + parent?.let { parentFolder -> + val folderToCreate = DriveItem() + folderToCreate.name = folder.name + folderToCreate.folder = Folder() + val parentNodeInfo = requireNodeInfo(parentFolder) + val createdFolder = drive(parentNodeInfo.driveId) // + .items(parentNodeInfo.id).children() // + .buildRequest() // + .post(folderToCreate) + return cacheNodeInfo(folder(parentFolder, createdFolder), createdFolder) + } ?: throw ParentFolderIsNullException(folder.name) } @Throws(NoSuchCloudFileException::class, CloudNodeAlreadyExistsException::class) @@ -405,7 +404,7 @@ internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, nodeInfoCach } private fun nodeInfo(node: OnedriveNode): OnedriveIdCache.NodeInfo? { - var result: OnedriveIdCache.NodeInfo? = nodeInfoCache[node.path] + var result = nodeInfoCache[node.path] if (result == null) { result = loadNodeInfo(node) if (result == null) { @@ -419,15 +418,8 @@ internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, nodeInfoCach } else result } - private fun cacheNodeInfo(node: T, item: DriveItem): T { - nodeInfoCache.add( // - node?.path!!, OnedriveIdCache.NodeInfo( // - getId(item), // - getDriveId(item), // - isFolder(item), // - item.cTag // - ) // - ) + private fun cacheNodeInfo(node: T, item: DriveItem): T { + nodeInfoCache.add(node.path, OnedriveIdCache.NodeInfo(getId(item), getDriveId(item), isFolder(item), item.cTag)) return node } @@ -454,7 +446,10 @@ internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, nodeInfoCach private fun loadNonRootNodeInfo(node: OnedriveNode): OnedriveIdCache.NodeInfo? { node.parent?.let { targetsParent -> - val parentNodeInfo = nodeInfo(targetsParent) ?: return null + val parentNodeInfo = nodeInfo(targetsParent) + if (parentNodeInfo?.driveId == null) { + return null + } val item = childByName(parentNodeInfo.id, parentNodeInfo.driveId, node.name) return if (item == null) { null diff --git a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudNodeFactory.kt b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudNodeFactory.kt index 4a718ba6..e761210c 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudNodeFactory.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudNodeFactory.kt @@ -28,8 +28,8 @@ internal object PCloudNodeFactory { } @JvmStatic - fun folder(parent: PCloudFolder?, name: String, path: String?): PCloudFolder { - return PCloudFolder(parent, name, path!!) + fun folder(parent: PCloudFolder?, name: String, path: String): PCloudFolder { + return PCloudFolder(parent, name, path) } fun getNodePath(parent: PCloudFolder, name: String): String { diff --git a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveImpl.kt b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveImpl.kt index c01f1541..2e1c107b 100644 --- a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveImpl.kt +++ b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveImpl.kt @@ -114,12 +114,11 @@ internal class GoogleDriveImpl(context: Context, googleDriveCloud: GoogleDriveCl @Throws(IOException::class) fun exists(node: GoogleDriveNode): Boolean { return try { - requireNotNull(node.parent) - val file = findFile(node.parent!!.driveId, node.name) - file?.let { idCache.add(GoogleDriveCloudNodeFactory.from(node.parent!!, it)) } - file != null + node.parent?.let { nodesParent -> + findFile(nodesParent.driveId, node.name)?.let { idCache.add(GoogleDriveCloudNodeFactory.from(nodesParent, it)) } != null + } ?: throw ParentFolderIsNullException(node.name) } catch (e: GoogleJsonResponseException) { - if(e.statusCode == 404) { + if (e.statusCode == 404) { return false } throw e From d18e08f8a4babb6d50b0bcc5dcb0721a92aeb300 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Tue, 6 Jul 2021 18:58:50 +0200 Subject: [PATCH 6/6] Fix move in GoogleDrive --- .../cloud/crypto/CryptoImplVaultFormatPre7.kt | 8 ++--- .../org/cryptomator/data/cloud/s3/S3Impl.kt | 1 - .../data/cloud/googledrive/GoogleDriveImpl.kt | 30 ++++++++++--------- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt index 4222f8f3..bdac767f 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt @@ -219,10 +219,10 @@ internal class CryptoImplVaultFormatPre7( requireNotNull(target.dirFile) target.parent?.let { assertCryptoFolderAlreadyExists(target) - val result = folder(it, target.name, cloudContentRepository.move(source.dirFile, target.dirFile)) - evictFromCache(source) - evictFromCache(target) - return result + return folder(it, target.name, cloudContentRepository.move(source.dirFile, target.dirFile)).also { + evictFromCache(source) + evictFromCache(target) + } } ?: throw ParentFolderIsNullException(target.name) } diff --git a/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.kt b/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.kt index a5ff3f28..f283d6eb 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.kt @@ -127,7 +127,6 @@ internal class S3Impl(context: Context, cloud: S3Cloud) { } } ?: throw ParentFolderIsNullException(folder.name) - folder.parent?.let { parentFolder -> try { val putObjectArgs = PutObjectArgs // diff --git a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveImpl.kt b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveImpl.kt index 2e1c107b..efd281c0 100644 --- a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveImpl.kt +++ b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveImpl.kt @@ -183,20 +183,22 @@ internal class GoogleDriveImpl(context: Context, googleDriveCloud: GoogleDriveCl throw CloudNodeAlreadyExistsException(target.name) } - target.parent?.let { targetsParent -> - val metadata = File() - metadata.name = target.name - val movedFile = client() // - .files() // - .update(source.driveId, metadata) // - .setFields("id,mimeType,modifiedTime,name,size") // - .setAddParents(targetsParent.driveId) // - .setRemoveParents(targetsParent.driveId) // - .setSupportsAllDrives(true) // - .execute() - idCache.remove(source) - return idCache.cache(GoogleDriveCloudNodeFactory.from(targetsParent, movedFile)) - } ?: throw ParentFolderIsNullException(target.name) + source.parent?.let { sourcesParent -> + target.parent?.let { targetsParent -> + val metadata = File() + metadata.name = target.name + val movedFile = client() // + .files() // + .update(source.driveId, metadata) // + .setFields("id,mimeType,modifiedTime,name,size") // + .setAddParents(targetsParent.driveId) // + .setRemoveParents(sourcesParent.driveId) // + .setSupportsAllDrives(true) // + .execute() + idCache.remove(source) + return idCache.cache(GoogleDriveCloudNodeFactory.from(targetsParent, movedFile)) + } ?: throw ParentFolderIsNullException(target.name) + } ?: throw ParentFolderIsNullException(source.name) } @Throws(IOException::class, BackendException::class)