From 62dbd68e45c9b72cf063fa6df1e3dc4929b26d06 Mon Sep 17 00:00:00 2001 From: Majid Achhoud Date: Tue, 26 Mar 2024 09:03:39 +0100 Subject: [PATCH 01/21] Implemented the basic structure and key functionalities of BoxCloudProvider --- .../project.pbxproj | 72 +++ .../xcshareddata/swiftpm/Package.resolved | 12 +- Package.resolved | 9 + Package.swift | 6 +- .../Box/BoxAuthenticator.swift | 33 ++ .../Box/BoxCloudProvider.swift | 547 ++++++++++++++++++ .../Box/BoxCredential.swift | 18 + .../Box/BoxIdentifierCache.swift | 45 ++ .../CryptomatorCloudAccess/Box/BoxItem.swift | 55 ++ .../CryptomatorCloudAccess/Box/BoxSetup.swift | 24 + .../BoxCloudProviderIntegrationTests.swift | 44 ++ 11 files changed, 862 insertions(+), 3 deletions(-) create mode 100644 Sources/CryptomatorCloudAccess/Box/BoxAuthenticator.swift create mode 100644 Sources/CryptomatorCloudAccess/Box/BoxCloudProvider.swift create mode 100644 Sources/CryptomatorCloudAccess/Box/BoxCredential.swift create mode 100644 Sources/CryptomatorCloudAccess/Box/BoxIdentifierCache.swift create mode 100644 Sources/CryptomatorCloudAccess/Box/BoxItem.swift create mode 100644 Sources/CryptomatorCloudAccess/Box/BoxSetup.swift create mode 100644 Tests/CryptomatorCloudAccessIntegrationTests/Box/BoxCloudProviderIntegrationTests.swift diff --git a/CryptomatorCloudAccess.xcodeproj/project.pbxproj b/CryptomatorCloudAccess.xcodeproj/project.pbxproj index 4d1d310..9242474 100644 --- a/CryptomatorCloudAccess.xcodeproj/project.pbxproj +++ b/CryptomatorCloudAccess.xcodeproj/project.pbxproj @@ -183,6 +183,14 @@ 9ED0E624246198F600FDB438 /* VaultFormat7CloudProviderMockTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ED0E623246198F600FDB438 /* VaultFormat7CloudProviderMockTests.swift */; }; 9EE62A0D247D54760089DAF7 /* CloudProvider+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EE62A0C247D54760089DAF7 /* CloudProvider+Convenience.swift */; }; 9EE62A10247D54E90089DAF7 /* CloudProvider+ConvenienceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EE62A0F247D54E90089DAF7 /* CloudProvider+ConvenienceTests.swift */; }; + B3D513912BA9A32200DE0D36 /* BoxAuthenticator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3D5138D2BA9A32200DE0D36 /* BoxAuthenticator.swift */; }; + B3D513922BA9A32200DE0D36 /* BoxCredential.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3D5138E2BA9A32200DE0D36 /* BoxCredential.swift */; }; + B3D513932BA9A32200DE0D36 /* BoxSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3D5138F2BA9A32200DE0D36 /* BoxSetup.swift */; }; + B3D513972BA9A44000DE0D36 /* BoxCloudProviderIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3D513952BA9A3BB00DE0D36 /* BoxCloudProviderIntegrationTests.swift */; }; + B3FC94A42BA9A98200D1ECFD /* BoxSDK in Frameworks */ = {isa = PBXBuildFile; productRef = B3FC94A32BA9A98200D1ECFD /* BoxSDK */; }; + B3FC94A62BA9AA4400D1ECFD /* BoxCloudProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FC94A52BA9AA4400D1ECFD /* BoxCloudProvider.swift */; }; + B3FC94A82BA9AEEC00D1ECFD /* BoxIdentifierCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FC94A72BA9AEEC00D1ECFD /* BoxIdentifierCache.swift */; }; + B3FC94AA2BA9AEFC00D1ECFD /* BoxItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FC94A92BA9AEFC00D1ECFD /* BoxItem.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -368,6 +376,13 @@ 9ED0E623246198F600FDB438 /* VaultFormat7CloudProviderMockTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultFormat7CloudProviderMockTests.swift; sourceTree = ""; }; 9EE62A0C247D54760089DAF7 /* CloudProvider+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CloudProvider+Convenience.swift"; sourceTree = ""; }; 9EE62A0F247D54E90089DAF7 /* CloudProvider+ConvenienceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CloudProvider+ConvenienceTests.swift"; sourceTree = ""; }; + B3D5138D2BA9A32200DE0D36 /* BoxAuthenticator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BoxAuthenticator.swift; sourceTree = ""; }; + B3D5138E2BA9A32200DE0D36 /* BoxCredential.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BoxCredential.swift; sourceTree = ""; }; + B3D5138F2BA9A32200DE0D36 /* BoxSetup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BoxSetup.swift; sourceTree = ""; }; + B3D513952BA9A3BB00DE0D36 /* BoxCloudProviderIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxCloudProviderIntegrationTests.swift; sourceTree = ""; }; + B3FC94A52BA9AA4400D1ECFD /* BoxCloudProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxCloudProvider.swift; sourceTree = ""; }; + B3FC94A72BA9AEEC00D1ECFD /* BoxIdentifierCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxIdentifierCache.swift; sourceTree = ""; }; + B3FC94A92BA9AEFC00D1ECFD /* BoxItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxItem.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -376,6 +391,7 @@ buildActionMask = 2147483647; files = ( 4A75E1C428806F5800952FE6 /* MSGraphClientModels in Frameworks */, + B3FC94A42BA9A98200D1ECFD /* BoxSDK in Frameworks */, 4A75E1C728806FA100952FE6 /* MSGraphClientSDK in Frameworks */, 4AF0AA7C2844DDD200C20B75 /* AWSS3 in Frameworks */, 74F9355D251F67A3001F4ADA /* CryptomatorCryptoLib in Frameworks */, @@ -418,6 +434,7 @@ 74F9354B251F66EE001F4ADA /* Sources */, 74F9354C251F66F8001F4ADA /* Tests */, 4A058FF224519FFC008831F9 /* Products */, + B3FC94A22BA9A98200D1ECFD /* Frameworks */, ); sourceTree = ""; }; @@ -437,6 +454,7 @@ 4A058FF424519FFC008831F9 /* CryptomatorCloudAccess.h */, 4A058FF524519FFC008831F9 /* Info.plist */, 4A0590162451A1BB008831F9 /* API */, + B3D513902BA9A32200DE0D36 /* Box */, 4A741FF0287C696F00489C23 /* Common */, 7416F22424F658160074DA8E /* Crypto */, 4A567AF02615C2DE002C4D82 /* Dropbox */, @@ -596,6 +614,7 @@ 4ACA63B02615FE8000D19304 /* CloudAccessIntegrationTestWithAuthentication.swift */, 4ACA63A42615FE5700D19304 /* IntegrationTestError.swift */, 4ACA64252616054F00D19304 /* IntegrationTestSecrets.swift */, + B3D513942BA9A37A00DE0D36 /* Box */, 4ACA63BF2615FEB200D19304 /* CryptoDecorator */, 4ACA63F02615FF9700D19304 /* Dropbox */, 4ACA63F92615FF9700D19304 /* Extensions */, @@ -959,6 +978,35 @@ path = API; sourceTree = ""; }; + B3D513902BA9A32200DE0D36 /* Box */ = { + isa = PBXGroup; + children = ( + B3D5138D2BA9A32200DE0D36 /* BoxAuthenticator.swift */, + B3D5138E2BA9A32200DE0D36 /* BoxCredential.swift */, + B3D5138F2BA9A32200DE0D36 /* BoxSetup.swift */, + B3FC94A52BA9AA4400D1ECFD /* BoxCloudProvider.swift */, + B3FC94A72BA9AEEC00D1ECFD /* BoxIdentifierCache.swift */, + B3FC94A92BA9AEFC00D1ECFD /* BoxItem.swift */, + ); + name = Box; + path = Sources/CryptomatorCloudAccess/Box; + sourceTree = SOURCE_ROOT; + }; + B3D513942BA9A37A00DE0D36 /* Box */ = { + isa = PBXGroup; + children = ( + B3D513952BA9A3BB00DE0D36 /* BoxCloudProviderIntegrationTests.swift */, + ); + path = Box; + sourceTree = ""; + }; + B3FC94A22BA9A98200D1ECFD /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -1005,6 +1053,7 @@ 4A75E1C628806FA100952FE6 /* MSGraphClientSDK */, 4A75E1C928806FF000952FE6 /* ObjectiveDropboxOfficial */, 4A8B872E287D7E77002D676E /* CocoaLumberjackSwift */, + B3FC94A32BA9A98200D1ECFD /* BoxSDK */, ); productName = CloudAccess; productReference = 4A058FF124519FFC008831F9 /* CryptomatorCloudAccess.framework */; @@ -1095,6 +1144,7 @@ 4A75E1C528806FA100952FE6 /* XCRemoteSwiftPackageReference "msgraph-sdk-objc-spm" */, 4A75E1C828806FF000952FE6 /* XCRemoteSwiftPackageReference "dropbox-sdk-obj-c-spm" */, 4A8B872D287D7E77002D676E /* XCRemoteSwiftPackageReference "CocoaLumberjack" */, + B3FC94A12BA9A8E600D1ECFD /* XCRemoteSwiftPackageReference "box-ios-sdk" */, ); productRefGroup = 4A058FF224519FFC008831F9 /* Products */; projectDirPath = ""; @@ -1215,9 +1265,12 @@ 74C0FB2729B209B6008EF811 /* S3Authenticator.swift in Sources */, 747A77FF2698577E005E5AD4 /* GTLRDrive_File+CloudItemType.swift in Sources */, 4A1A1183262B078E00DAF62F /* OneDriveError.swift in Sources */, + B3D513922BA9A32200DE0D36 /* BoxCredential.swift in Sources */, 740C144E249B4F2B008CA3E0 /* VaultFormat7ShorteningProviderDecorator.swift in Sources */, 74FD6C4824F6F3AA00C8D3C4 /* VaultFormat6ShorteningProviderDecorator.swift in Sources */, 4A567B222615CA24002C4D82 /* GoogleDriveIdentifierCache.swift in Sources */, + B3D513912BA9A32200DE0D36 /* BoxAuthenticator.swift in Sources */, + B3FC94AA2BA9AEFC00D1ECFD /* BoxItem.swift in Sources */, 4A0421822642B9260033144A /* VaultProviderFactory.swift in Sources */, 4A567B102615C6F3002C4D82 /* DropboxError.swift in Sources */, 4A567AED2615C2D7002C4D82 /* DropboxAuthenticator.swift in Sources */, @@ -1233,6 +1286,7 @@ 9E969456249B8493000DB743 /* VaultFormat7ShortenedNameCache.swift in Sources */, 7471BDAE24865B6F000D05FC /* LocalFileSystemProvider.swift in Sources */, 7484608729795421009933D8 /* VaultConfigHelper.swift in Sources */, + B3FC94A62BA9AA4400D1ECFD /* BoxCloudProvider.swift in Sources */, 74073D1927C9406000A86C9A /* Task+Promises.swift in Sources */, 4A1A1194262EC46E00DAF62F /* OneDriveIdentifierCache.swift in Sources */, 746F091327BC0DA2003FCD9F /* PCloudCredential.swift in Sources */, @@ -1273,6 +1327,7 @@ 4A1A11792629ACD500DAF62F /* OneDriveAuthenticator.swift in Sources */, 748BD4CA24B4B1D50001CA8C /* PropfindResponseParser.swift in Sources */, 4A567B322615CA6E002C4D82 /* GoogleDriveError.swift in Sources */, + B3FC94A82BA9AEEC00D1ECFD /* BoxIdentifierCache.swift in Sources */, 4A0785302859F4FE0015DAE1 /* S3Credential.swift in Sources */, 748A42B824AA231D00DEB6D0 /* WebDAVAuthenticator.swift in Sources */, 74F4AA1525ED3D2A00FDF2C6 /* VaultConfig.swift in Sources */, @@ -1280,6 +1335,7 @@ 4AD55339263ABA4200126046 /* MSGraphDriveItem+CloudItemType.swift in Sources */, 747A77FD269854A6005E5AD4 /* GoogleDriveItem.swift in Sources */, 748A42C024AB424500DEB6D0 /* WebDAVClient.swift in Sources */, + B3D513932BA9A32200DE0D36 /* BoxSetup.swift in Sources */, 4A567B082615C6AF002C4D82 /* DropboxCloudProvider.swift in Sources */, 74C596E824F022AF00FFD17E /* CloudPath.swift in Sources */, 4A05900C2451A107008831F9 /* CloudProvider.swift in Sources */, @@ -1358,6 +1414,7 @@ 7467A0D627DF9A8000BCFDF8 /* VaultFormat6PCloudIntegrationTests.swift in Sources */, 4AC75F9C2861A6DE002731FE /* VaultFormat6S3IntegrationTests.swift in Sources */, 4ACA63A02615FE2C00D19304 /* CloudAccessIntegrationTest.swift in Sources */, + B3D513972BA9A44000DE0D36 /* BoxCloudProviderIntegrationTests.swift in Sources */, 4ACA64262616054F00D19304 /* IntegrationTestSecrets.swift in Sources */, 4A41D2432641938A00B5D787 /* VaultFormat6OneDriveIntegrationTests.swift in Sources */, 7470C54B26569A7E00E361B8 /* MSAuthenticationProviderMock.swift in Sources */, @@ -1526,6 +1583,7 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Sources/CryptomatorCloudAccess/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1552,6 +1610,7 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Sources/CryptomatorCloudAccess/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1803,6 +1862,14 @@ minimumVersion = 2.3.0; }; }; + B3FC94A12BA9A8E600D1ECFD /* XCRemoteSwiftPackageReference "box-ios-sdk" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/box/box-ios-sdk.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 5.5.0; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -1881,6 +1948,11 @@ package = 74F93565251F6863001F4ADA /* XCRemoteSwiftPackageReference "promises" */; productName = Promises; }; + B3FC94A32BA9A98200D1ECFD /* BoxSDK */ = { + isa = XCSwiftPackageProductDependency; + package = B3FC94A12BA9A8E600D1ECFD /* XCRemoteSwiftPackageReference "box-ios-sdk" */; + productName = BoxSDK; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 4A058FE824519FFC008831F9 /* Project object */; diff --git a/CryptomatorCloudAccess.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/CryptomatorCloudAccess.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index f6c0615..24acdb3 100644 --- a/CryptomatorCloudAccess.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/CryptomatorCloudAccess.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,4 +1,5 @@ { + "originHash" : "e5917b23d4b3d376d037268c2d81d7b09bc24bae3352b91337ce008605fe3761", "pins" : [ { "identity" : "appauth-ios", @@ -27,6 +28,15 @@ "version" : "0.9.0" } }, + { + "identity" : "box-ios-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/box/box-ios-sdk.git", + "state" : { + "revision" : "daffd86b861a5f5882655bf7a01b891f6b808c1f", + "version" : "5.5.0" + } + }, { "identity" : "cocoalumberjack", "kind" : "remoteSourceControl", @@ -154,5 +164,5 @@ } } ], - "version" : 2 + "version" : 3 } diff --git a/Package.resolved b/Package.resolved index 2d058a4..ed20f18 100644 --- a/Package.resolved +++ b/Package.resolved @@ -27,6 +27,15 @@ "version" : "0.9.0" } }, + { + "identity" : "box-ios-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/box/box-ios-sdk.git", + "state" : { + "revision" : "daffd86b861a5f5882655bf7a01b891f6b808c1f", + "version" : "5.5.0" + } + }, { "identity" : "cocoalumberjack", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index c157f88..ed021c3 100644 --- a/Package.swift +++ b/Package.swift @@ -41,7 +41,8 @@ let package = Package( .package(url: "https://github.com/pCloud/pcloud-sdk-swift.git", .upToNextMinor(from: "3.2.0")), .package(url: "https://github.com/phil1995/dropbox-sdk-obj-c-spm.git", .upToNextMinor(from: "7.2.0")), .package(url: "https://github.com/phil1995/msgraph-sdk-objc-spm.git", .upToNextMinor(from: "1.0.0")), - .package(url: "https://github.com/phil1995/msgraph-sdk-objc-models-spm.git", .upToNextMinor(from: "1.3.0")) + .package(url: "https://github.com/phil1995/msgraph-sdk-objc-models-spm.git", .upToNextMinor(from: "1.3.0")), + .package(url: "https://github.com/box/box-ios-sdk.git", .upToNextMinor(from: "5.5.0")), ], targets: [ .target( @@ -61,7 +62,8 @@ let package = Package( .product(name: "MSGraphClientSDK", package: "msgraph-sdk-objc-models-spm"), .product(name: "ObjectiveDropboxOfficial", package: "dropbox-sdk-obj-c-spm"), .product(name: "PCloudSDKSwift", package: "pcloud-sdk-swift"), - .product(name: "Promises", package: "promises") + .product(name: "Promises", package: "promises"), + .product(name: "BoxSDK", package: "box-ios-sdk") ], path: "Sources/CryptomatorCloudAccess", exclude: appExtensionUnsafeSources diff --git a/Sources/CryptomatorCloudAccess/Box/BoxAuthenticator.swift b/Sources/CryptomatorCloudAccess/Box/BoxAuthenticator.swift new file mode 100644 index 0000000..a3258a1 --- /dev/null +++ b/Sources/CryptomatorCloudAccess/Box/BoxAuthenticator.swift @@ -0,0 +1,33 @@ +// +// BoxAuthenticator.swift +// +// +// Created by Majid Achhoud on 18.03.24. +// + +import Foundation +import BoxSDK +import Promises +import UIKit + +public enum BoxAuthenticatorError: Error { + case authenticationFailed +} + +public struct BoxAuthenticator { + + private static let sdk = BoxSDK(clientId: BoxSetup.constants.clientId, clientSecret: BoxSetup.constants.clientSecret) + + public static func authenticate(from viewController: UIViewController) -> Promise { + return Promise { fulfill, reject in + sdk.getOAuth2Client() { result in + switch result { + case let .success(client): + fulfill(client) + case .failure(_): + reject(BoxAuthenticatorError.authenticationFailed) + } + } + } + } +} diff --git a/Sources/CryptomatorCloudAccess/Box/BoxCloudProvider.swift b/Sources/CryptomatorCloudAccess/Box/BoxCloudProvider.swift new file mode 100644 index 0000000..e91a9b6 --- /dev/null +++ b/Sources/CryptomatorCloudAccess/Box/BoxCloudProvider.swift @@ -0,0 +1,547 @@ +// +// BoxCloudProvider.swift +// CryptomatorCloudAccess +// +// Created by Majid Achhoud on 19.03.24. +// Copyright © 2024 Skymatic GmbH. All rights reserved. +// + +import BoxSDK +import Foundation +import Promises + +public class BoxCloudProvider: CloudProvider { + private let client: BoxClient + private let identifierCache: BoxIdentifierCache + + public init(credential: BoxCredential) throws { + self.client = credential.client + self.identifierCache = try BoxIdentifierCache() + } + + public func fetchItemMetadata(at cloudPath: CloudPath) -> Promise { + return resolvePath(forItemAt: cloudPath).then { item in + self.fetchItemMetadata(for: item) + } + } + + public func fetchItemList(forFolderAt cloudPath: CloudPath, withPageToken pageToken: String?) -> Promise { + guard pageToken == nil else { + return Promise(CloudProviderError.pageTokenInvalid) + } + return resolvePath(forItemAt: cloudPath).then { item in + self.fetchItemList(for: item, pageToken: pageToken) + } + } + + public func downloadFile(from cloudPath: CloudPath, to localURL: URL, onTaskCreation: ((URLSessionDownloadTask?) -> Void)?) -> Promise { + precondition(localURL.isFileURL) + if FileManager.default.fileExists(atPath: localURL.path) { + return Promise(CloudProviderError.itemAlreadyExists) + } + return resolvePath(forItemAt: cloudPath).then { item in + self.downloadFile(for: item, to: localURL) + } + } + + public func uploadFile(from localURL: URL, to cloudPath: CloudPath, replaceExisting: Bool, onTaskCreation: ((URLSessionUploadTask?) -> Void)?) -> Promise { + precondition(localURL.isFileURL) + var isDirectory: ObjCBool = false + let fileExists = FileManager.default.fileExists(atPath: localURL.path, isDirectory: &isDirectory) + if !fileExists { + return Promise(CloudProviderError.itemNotFound) + } + if isDirectory.boolValue { + return Promise(CloudProviderError.itemTypeMismatch) + } + return fetchItemMetadata(at: cloudPath).then { metadata -> Void in + if !replaceExisting || (replaceExisting && metadata.itemType == .folder) { + throw CloudProviderError.itemAlreadyExists + } + }.recover { error -> Void in + guard case CloudProviderError.itemNotFound = error else { + throw error + } + }.then { _ -> Promise in + return self.resolveParentPath(forItemAt: cloudPath) + }.then { parentItem in + return self.uploadFile(for: parentItem, from: localURL, to: cloudPath) + } + } + + public func createFolder(at cloudPath: CloudPath) -> Promise { + return checkForItemExistence(at: cloudPath).then { itemExists in + if itemExists { + throw CloudProviderError.itemAlreadyExists + } + }.then { _ -> Promise in + return self.resolveParentPath(forItemAt: cloudPath) + }.then { parentItem in + return self.createFolder(for: parentItem, with: cloudPath.lastPathComponent) + } + } + + public func deleteFile(at cloudPath: CloudPath) -> Promise { + return resolvePath(forItemAt: cloudPath).then { item in + self.deleteFile(for: item) + } + } + + public func deleteFolder(at cloudPath: CloudPath) -> Promise { + return resolvePath(forItemAt: cloudPath).then { item in + self.deleteFolder(for: item) + } + } + + public func moveFile(from sourceCloudPath: CloudPath, to targetCloudPath: CloudPath) -> Promise { + return checkForItemExistence(at: targetCloudPath).then { itemExists -> Void in + if itemExists { + throw CloudProviderError.itemAlreadyExists + } + }.then { + return all(self.resolvePath(forItemAt: sourceCloudPath), self.resolveParentPath(forItemAt: targetCloudPath)) + }.then { item, targetParentItem in + return self.moveFile(from: item, toParent: targetParentItem, targetCloudPath: targetCloudPath) + } + } + + public func moveFolder(from sourceCloudPath: CloudPath, to targetCloudPath: CloudPath) -> Promise { + return checkForItemExistence(at: targetCloudPath).then { itemExists -> Void in + if itemExists { + throw CloudProviderError.itemAlreadyExists + } + }.then { + return all(self.resolvePath(forItemAt: sourceCloudPath), self.resolveParentPath(forItemAt: targetCloudPath)) + }.then { item, targetParentItem in + return self.moveFolder(from: item, toParent: targetParentItem, targetCloudPath: targetCloudPath) + } + } + + // MARK: - Operations + + private func fetchItemMetadata(for item: BoxItem) -> Promise { + if item.itemType == .file { + return fetchFileMetadata(for: item) + } else if item.itemType == .folder { + return fetchFolderMetadata(for: item) + } else { + let error = CloudProviderError.itemTypeMismatch + CloudAccessDDLogDebug("PCloudCloudProvider: fetchItemMetadata(for: \(item.identifier)) failed with error: \(error)") + return Promise(error) + } + } + + private func fetchFileMetadata(for item: BoxItem) -> Promise { + assert(item.itemType == .file) + CloudAccessDDLogDebug("BoxCloudProvider: fetchFileMetadata(for: \(item.identifier)) called") + return Promise { fulfill, reject in + self.client.files.get(fileId: item.identifier, fields: ["name", "size", "modified_at"]) { result in + switch result { + case let .success(file): + do { + let metadata = try self.convertToCloudItemMetadata(file, at: item.cloudPath) + try self.identifierCache.addOrUpdate(item) + CloudAccessDDLogDebug("BoxCloudProvider: fetchFileMetadata(for: \(item.identifier)) successful") + fulfill(metadata) + } catch { + CloudAccessDDLogDebug("BoxCloudProvider: fetchFileMetadata(for: \(item.identifier)) error: \(error)") + reject(error) + } + case let .failure(error): + CloudAccessDDLogDebug("BoxCloudProvider: fetchFileMetadata(for: \(item.identifier)) failed with error: \(error)") + reject(error) + } + } + } + } + + private func fetchFolderMetadata(for item: BoxItem) -> Promise { + assert(item.itemType == .folder) + CloudAccessDDLogDebug("BoxCloudProvider: fetchFolderMetadata(for: \(item.identifier)) called") + return Promise { fulfill, reject in + self.client.folders.get(folderId: item.identifier, fields: ["name", "modified_at"]) { result in + switch result { + case let .success(folder): + do { + let metadata = try self.convertToCloudItemMetadata(folder, at: item.cloudPath) + try self.identifierCache.addOrUpdate(item) + CloudAccessDDLogDebug("BoxCloudProvider: fetchFolderMetadata(for: \(item.identifier)) successful") + fulfill(metadata) + } catch { + CloudAccessDDLogDebug("BoxCloudProvider: fetchFolderMetadata(for: \(item.identifier)) error: \(error)") + reject(error) + } + case let .failure(error): + CloudAccessDDLogDebug("BoxCloudProvider: fetchFolderMetadata(for: \(item.identifier)) failed with error: \(error)") + reject(error) + } + } + } + } + + private func fetchItemList(for item: BoxItem, pageToken: String?) -> Promise { + CloudAccessDDLogDebug("BoxCloudProvider: fetchItemList(forFolderAt: \(item.identifier)) called") + guard item.itemType == .folder else { + let error = CloudProviderError.itemTypeMismatch + CloudAccessDDLogDebug("BoxCloudProvider: fetchItemList(forFolderAt: \(item.identifier)) failed with error: \(error)") + return Promise(error) + } + + return Promise { fulfill, reject in + let iterator = self.client.folders.listItems(folderId: item.identifier, usemarker: true, marker: pageToken) + var allItems: [CloudItemMetadata] = [] + + iterator.next { result in + switch result { + case let .success(page): + for folderItem in page.entries { + do { + let childCloudPath: CloudPath + let childItemMetadata: CloudItemMetadata + + switch folderItem { + case let .file(file): + childCloudPath = item.cloudPath.appendingPathComponent(file.name ?? "") + childItemMetadata = try self.convertToCloudItemMetadata(file, at: childCloudPath) + case let .folder(folder): + childCloudPath = item.cloudPath.appendingPathComponent(folder.name ?? "") + childItemMetadata = try self.convertToCloudItemMetadata(folder, at: childCloudPath) + case .webLink: + continue + } + + allItems.append(childItemMetadata) + + let newItem = try BoxItem(cloudPath: childCloudPath, folderItem: folderItem) + try self.identifierCache.addOrUpdate(newItem) + } catch { + reject(error) + return + } + } + + fulfill(CloudItemList(items: allItems, nextPageToken: page.nextMarker)) + + case let .failure(error): + reject(error) + } + } + } + } + + private func downloadFile(for item: BoxItem, to localURL: URL) -> Promise { + CloudAccessDDLogDebug("BoxCloudProvider: downloadFile(for: \(item.identifier), to: \(localURL)) called") + return Promise { fulfill, reject in + let task = self.client.files.download(fileId: item.identifier, destinationURL: localURL) { result in + switch result { + case .success: + CloudAccessDDLogDebug("BoxCloudProvider: downloadFile(for: \(item.identifier), to: \(localURL)) finished downloading") + fulfill(()) + case let .failure(error): + CloudAccessDDLogDebug("BoxCloudProvider: downloadFile(for: \(item.identifier), to: \(localURL)) failed with error: \(error)") + reject(error) + } + } + } + } + + private func uploadFile(for parentItem: BoxItem, from localURL: URL, to cloudPath: CloudPath) -> Promise { + CloudAccessDDLogDebug("BoxCloudProvider: uploadFile(for: \(parentItem.identifier), from: \(localURL), to: \(cloudPath.path)) called") + return Promise { fulfill, reject in + do { + let fileData = try Data(contentsOf: localURL) //Refactor + let progress = Progress(totalUnitCount: Int64(fileData.count)) + self.client.files.upload(data: fileData, name: cloudPath.lastPathComponent, parentId: parentItem.identifier, progress: { progressUpdate in + print("Upload progress: \(progressUpdate.fractionCompleted)") + }) { (result: Result) in + switch result { + case let .success(file): + CloudAccessDDLogDebug("BoxCloudProvider: uploadFile successful with file ID: \(file.id)") + let metadata = CloudItemMetadata(name: file.name ?? "", cloudPath: cloudPath, itemType: .file, lastModifiedDate: file.modifiedAt, size: file.size) + do { + let boxItem = BoxItem(cloudPath: cloudPath, identifier: file.id, itemType: .file) + try self.identifierCache.addOrUpdate(boxItem) + fulfill(metadata) + } catch { + reject(error) + } + case let .failure(error): + CloudAccessDDLogDebug("BoxCloudProvider: uploadFile failed with error: \(error.localizedDescription)") + reject(error) + } + } + } catch { + reject(error) + } + } + } + + private func createFolder(for parentItem: BoxItem, with name: String) -> Promise { + CloudAccessDDLogDebug("BoxCloudProvider: createFolder(for: \(parentItem.identifier), with: \(name)) called") + return Promise { fulfill, reject in + let cloudPath = parentItem.cloudPath.appendingPathComponent(name) + self.resolveParentPath(forItemAt: cloudPath.deletingLastPathComponent()).then { parentItem -> Void in + self.client.folders.create(name: name, parentId: parentItem.identifier) { result in + switch result { + case let .success(folder): + CloudAccessDDLogDebug("BoxCloudProvider: createFolder successful with folder ID: \(folder.id)") + let newItemMetadata = CloudItemMetadata(name: folder.name ?? "", cloudPath: cloudPath.appendingPathComponent(name), itemType: .folder, lastModifiedDate: folder.modifiedAt, size: nil) + do { + let newItem = BoxItem(cloudPath: cloudPath.appendingPathComponent(name), identifier: folder.id, itemType: .folder) + try self.identifierCache.addOrUpdate(newItem) + fulfill(()) + } catch { + reject(error) + } + case let .failure(error): + CloudAccessDDLogDebug("BoxCloudProvider: createFolder failed with error: \(error.localizedDescription)") + reject(error) + } + } + }.catch { error in + reject(error) + } + } + } + + private func deleteFile(for item: BoxItem) -> Promise { + CloudAccessDDLogDebug("BoxCloudProvider: deleteFile(for: \(item.identifier)) called") + guard item.itemType == .file else { + return Promise(CloudProviderError.itemTypeMismatch) + } + return Promise { fulfill, reject in + self.client.files.delete(fileId: item.identifier) { result in + switch result { + case .success: + CloudAccessDDLogDebug("BoxCloudProvider: deleteFile(for: \(item.identifier)) succeeded") + do { + try self.identifierCache.invalidate(item) + fulfill(()) + } catch { + CloudAccessDDLogDebug("BoxCloudProvider: Cache update failed with error: \(error)") + reject(error) + } + case let .failure(error): + CloudAccessDDLogDebug("BoxCloudProvider: deleteFolder(for: \(item.identifier)) failed with error: \(error)") + if case BoxSDKErrorEnum.notFound = error.message { + reject(CloudProviderError.itemNotFound) + } else { + reject(error) + } + } + } + } + } + + private func deleteFolder(for item: BoxItem) -> Promise { + CloudAccessDDLogDebug("BoxCloudProvider: deleteFile(for: \(item.identifier)) called") + guard item.itemType == .folder else { + return Promise(CloudProviderError.itemTypeMismatch) + } + return Promise { fulfill, reject in + self.client.folders.delete(folderId: item.identifier) { result in + switch result { + case .success: + CloudAccessDDLogDebug("BoxCloudProvider: deleteFile(for: \(item.identifier)) succeeded") + do { + try self.identifierCache.invalidate(item) + fulfill(()) + } catch { + CloudAccessDDLogDebug("BoxCloudProvider: Cache update failed with error: \(error)") + reject(error) + } + case let .failure(error): + CloudAccessDDLogDebug("BoxCloudProvider: deleteFolder(for: \(item.identifier)) failed with error: \(error)") + if case BoxSDKErrorEnum.notFound = error.message { + reject(CloudProviderError.itemNotFound) + } else { + reject(error) + } + } + } + } + } + + private func moveFile(from sourceItem: BoxItem, toParent targetParentItem: BoxItem, targetCloudPath: CloudPath) -> Promise { + CloudAccessDDLogDebug("BoxCloudProvider: moveFile(from: \(sourceItem.identifier), toParent: \(targetParentItem.identifier), targetCloudPath: \(targetCloudPath.path)) called") + + return Promise { fulfill, reject in + let newName = targetCloudPath.lastPathComponent + self.client.files.update(fileId: sourceItem.identifier, name: newName, parentId: targetParentItem.identifier) { result in + switch result { + case .success: + CloudAccessDDLogDebug("BoxCloudProvider: moveFile succeeded for \(sourceItem.identifier) to \(targetCloudPath.path)") + do { + try self.identifierCache.invalidate(sourceItem) + let newItem = BoxItem(cloudPath: targetCloudPath, identifier: sourceItem.identifier, itemType: sourceItem.itemType) + try self.identifierCache.addOrUpdate(newItem) + fulfill(()) + } catch { + reject(error) + } + case let .failure(error): + CloudAccessDDLogDebug("BoxCloudProvider: moveFile failed for \(sourceItem.identifier) with error: \(error)") + reject(error) + } + } + } + } + + private func moveFolder(from sourceItem: BoxItem, toParent targetParentItem: BoxItem, targetCloudPath: CloudPath) -> Promise { + CloudAccessDDLogDebug("BoxCloudProvider: moveFolder(from: \(sourceItem.identifier), toParent: \(targetParentItem.identifier), targetCloudPath: \(targetCloudPath.path)) called") + + return Promise { fulfill, reject in + let newName = targetCloudPath.lastPathComponent + self.client.folders.update(folderId: sourceItem.identifier, name: newName, parentId: targetParentItem.identifier) { result in + switch result { + case .success: + CloudAccessDDLogDebug("BoxCloudProvider: moveFolder succeeded for \(sourceItem.identifier) to \(targetCloudPath.path)") + do { + try self.identifierCache.invalidate(sourceItem) + let newItem = BoxItem(cloudPath: targetCloudPath, identifier: sourceItem.identifier, itemType: sourceItem.itemType) + try self.identifierCache.addOrUpdate(newItem) + fulfill(()) + } catch { + reject(error) + } + case let .failure(error): + CloudAccessDDLogDebug("BoxCloudProvider: moveFolder failed for \(sourceItem.identifier) with error: \(error)") + reject(error) + } + } + } + } + + // MARK: - Resolve Path + + private func resolvePath(forItemAt cloudPath: CloudPath) -> Promise { + var pathToCheckForCache = cloudPath + var cachedItem = identifierCache.get(pathToCheckForCache) + while cachedItem == nil, !pathToCheckForCache.pathComponents.isEmpty { + pathToCheckForCache = pathToCheckForCache.deletingLastPathComponent() + cachedItem = identifierCache.get(pathToCheckForCache) + } + guard let item = cachedItem else { + return Promise(PCloudError.inconsistentCache) + } + if pathToCheckForCache != cloudPath { + return traverseThroughPath(from: pathToCheckForCache, to: cloudPath, withStartItem: item) + } + return Promise(item) + } + + private func resolveParentPath(forItemAt cloudPath: CloudPath) -> Promise { + let parentCloudPath = cloudPath.deletingLastPathComponent() + return resolvePath(forItemAt: parentCloudPath).recover { error -> BoxItem in + if case CloudProviderError.itemNotFound = error { + throw CloudProviderError.parentFolderDoesNotExist + } else { + throw error + } + } + } + + private func traverseThroughPath(from startCloudPath: CloudPath, to endCloudPath: CloudPath, withStartItem startItem: BoxItem) -> Promise { + assert(startCloudPath.pathComponents.count < endCloudPath.pathComponents.count) + let startIndex = startCloudPath.pathComponents.count + let endIndex = endCloudPath.pathComponents.count + var currentPath = startCloudPath + var parentItem = startItem + return Promise(on: .global()) { fulfill, _ in + for i in startIndex ..< endIndex { + let itemName = endCloudPath.pathComponents[i] + currentPath = currentPath.appendingPathComponent(itemName) + parentItem = try awaitPromise(self.getBoxItem(for: itemName, withParentItem: parentItem)) + try self.identifierCache.addOrUpdate(parentItem) + } + fulfill(parentItem) + } + } + + func getBoxItem(for name: String, withParentItem parentItem: BoxItem) -> Promise { + return Promise { fulfill, reject in + CloudAccessDDLogDebug("PCloudCloudProvider: getBoxItem(for: \(name), withParentItem: \(parentItem.identifier)) called") + + let iterator = self.client.folders.listItems(folderId: parentItem.identifier) + iterator.next { result in + switch result { + case let .success(page): + for item in page.entries { + do { + if let mappedItem = try self.mapFolderItemToBoxItem(name: name, parentItem: parentItem, item: item) { + fulfill(mappedItem) + return + } + } catch { + reject(error) + return + } + } + reject(CloudProviderError.itemNotFound) + case let .failure(error): + reject(error) + } + } + } + } + + func mapFolderItemToBoxItem(name: String, parentItem: BoxItem, item: FolderItem) throws -> BoxItem? { + switch item { + case let .file(file) where file.name == name: + return BoxItem(cloudPath: parentItem.cloudPath.appendingPathComponent(name), file: file) + case let .folder(folder) where folder.name == name: + return BoxItem(cloudPath: parentItem.cloudPath.appendingPathComponent(name), folder: folder) + case .webLink: + throw PCloudError.unexpectedContent + default: + return nil + } + } + + // MARK: - Helpers + + private func convertToCloudItemMetadata(_ content: FolderItem, at cloudPath: CloudPath) throws -> CloudItemMetadata { + switch content { + case let .file(fileMetadata): + return try convertToCloudItemMetadata(fileMetadata, at: cloudPath) + case let .folder(folderMetadata): + return try convertToCloudItemMetadata(folderMetadata, at: cloudPath) + default: //Refactor - no default + throw PCloudError.unexpectedContent + } + } + + private func convertToCloudItemMetadata(_ metadata: File, at cloudPath: CloudPath) throws -> CloudItemMetadata { + let name = metadata.name ?? "" + let itemType = CloudItemType.file + let lastModifiedDate = metadata.modifiedAt + let size = metadata.size + return CloudItemMetadata(name: name, cloudPath: cloudPath, itemType: itemType, lastModifiedDate: lastModifiedDate, size: size) + } + + private func convertToCloudItemMetadata(_ metadata: Folder, at cloudPath: CloudPath) throws -> CloudItemMetadata { + let name = metadata.name ?? "" + let itemType = CloudItemType.folder + let lastModifiedDate = metadata.modifiedAt + return CloudItemMetadata(name: name, cloudPath: cloudPath, itemType: itemType, lastModifiedDate: lastModifiedDate, size: nil) + } + + private func convertToCloudItemList(_ contents: [FolderItem], at cloudPath: CloudPath) throws -> CloudItemList { + var items = [CloudItemMetadata]() + for content in contents { + switch content { + case let .file(fileMetadata): + let itemCloudPath = cloudPath.appendingPathComponent(fileMetadata.name ?? "") + let itemMetadata = try convertToCloudItemMetadata(fileMetadata, at: itemCloudPath) + items.append(itemMetadata) + case let .folder(folderMetadata): + let itemCloudPath = cloudPath.appendingPathComponent(folderMetadata.name ?? "") + let itemMetadata = try convertToCloudItemMetadata(folderMetadata, at: itemCloudPath) + items.append(itemMetadata) + default: + throw PCloudError.unexpectedContent + } + } + return CloudItemList(items: items, nextPageToken: nil) + } +} diff --git a/Sources/CryptomatorCloudAccess/Box/BoxCredential.swift b/Sources/CryptomatorCloudAccess/Box/BoxCredential.swift new file mode 100644 index 0000000..d87bcea --- /dev/null +++ b/Sources/CryptomatorCloudAccess/Box/BoxCredential.swift @@ -0,0 +1,18 @@ +// +// BoxCredential.swift +// +// +// Created by Majid Achhoud on 19.03.24. +// + +import Foundation +import BoxSDK + +public class BoxCredential { + + public var client: BoxClient + + public init() { + self.client = BoxSDK.getClient(token: "m9DKTNlQovcvIgxRIPMaxLdjwQVDxq1g") + } +} diff --git a/Sources/CryptomatorCloudAccess/Box/BoxIdentifierCache.swift b/Sources/CryptomatorCloudAccess/Box/BoxIdentifierCache.swift new file mode 100644 index 0000000..805ba7c --- /dev/null +++ b/Sources/CryptomatorCloudAccess/Box/BoxIdentifierCache.swift @@ -0,0 +1,45 @@ +// +// BoxIdentifierCache.swift +// CryptomatorCloudAccess +// +// Created by Majid Achhoud on 19.03.24. +// Copyright © 2024 Skymatic GmbH. All rights reserved. +// + +import Foundation +import GRDB +import BoxSDK + +class BoxIdentifierCache { + private let inMemoryDB: DatabaseQueue + + init() throws { + self.inMemoryDB = DatabaseQueue() + try inMemoryDB.write { db in + try db.create(table: BoxItem.databaseTableName) { table in + table.column(BoxItem.cloudPathKey, .text).notNull().primaryKey() + table.column(BoxItem.identifierKey, .text).notNull() + table.column(BoxItem.itemTypeKey, .text).notNull() + } + try BoxItem(cloudPath: CloudPath("/"), identifier: "0", itemType: .folder).save(db) + } + } + + func get(_ cloudPath: CloudPath) -> BoxItem? { + try? inMemoryDB.read { db in + return try BoxItem.fetchOne(db, key: cloudPath) + } + } + + func addOrUpdate(_ item: BoxItem) throws { + try inMemoryDB.write { db in + try item.save(db) + } + } + + func invalidate(_ item: BoxItem) throws { + try inMemoryDB.write { db in + try db.execute(sql: "DELETE FROM \(BoxItem.databaseTableName) WHERE \(BoxItem.cloudPathKey) LIKE ?", arguments: ["\(item.cloudPath.path)%"]) + } + } +} diff --git a/Sources/CryptomatorCloudAccess/Box/BoxItem.swift b/Sources/CryptomatorCloudAccess/Box/BoxItem.swift new file mode 100644 index 0000000..f1cbe93 --- /dev/null +++ b/Sources/CryptomatorCloudAccess/Box/BoxItem.swift @@ -0,0 +1,55 @@ +// +// BoxItem.swift +// CryptomatorCloudAccess +// +// Created by Majid Achhoud on 19.03.24. +// Copyright © 2024 Skymatic GmbH. All rights reserved. +// + +import BoxSDK +import Foundation +import GRDB + +struct BoxItem: Decodable, FetchableRecord, TableRecord, Equatable { + static let databaseTableName = "CachedEntries" + static let cloudPathKey = "cloudPath" + static let identifierKey = "identifier" + static let itemTypeKey = "itemType" + + let cloudPath: CloudPath + let identifier: String + let itemType: CloudItemType +} + +extension BoxItem { + init(cloudPath: CloudPath, folderItem: FolderItem) throws { + switch folderItem { + case let .file(file): + self.init(cloudPath: cloudPath, file: file) + case let .folder(folder): + self.init(cloudPath: cloudPath, folder: folder) + case let .webLink(webLink): + throw PCloudError.unexpectedContent + } + } + + init(cloudPath: CloudPath, file: File) { + self.cloudPath = cloudPath + self.identifier = file.id + self.itemType = .file + } + + init(cloudPath: CloudPath, folder: Folder) { + self.cloudPath = cloudPath + self.identifier = folder.id + self.itemType = .folder + } +} + +extension BoxItem: PersistableRecord { + func encode(to container: inout PersistenceContainer) { + container[BoxItem.cloudPathKey] = cloudPath + container[BoxItem.identifierKey] = identifier + container[BoxItem.itemTypeKey] = itemType + } +} diff --git a/Sources/CryptomatorCloudAccess/Box/BoxSetup.swift b/Sources/CryptomatorCloudAccess/Box/BoxSetup.swift new file mode 100644 index 0000000..c35640c --- /dev/null +++ b/Sources/CryptomatorCloudAccess/Box/BoxSetup.swift @@ -0,0 +1,24 @@ +// +// BoxSetup.swift +// +// +// Created by Majid Achhoud on 18.03.24. +// + +import Foundation + +public class BoxSetup { + public static var constants: BoxSetup! + + public let clientId: String + public let clientSecret: String + public let redirectURL: URL + public let sharedContainerIdentifier: String? + + public init(clientId: String, clientSecret: String, redirectURL: URL, sharedContainerIdentifier: String?) { + self.clientId = clientId + self.clientSecret = clientSecret + self.redirectURL = redirectURL + self.sharedContainerIdentifier = sharedContainerIdentifier + } +} diff --git a/Tests/CryptomatorCloudAccessIntegrationTests/Box/BoxCloudProviderIntegrationTests.swift b/Tests/CryptomatorCloudAccessIntegrationTests/Box/BoxCloudProviderIntegrationTests.swift new file mode 100644 index 0000000..ccc6db5 --- /dev/null +++ b/Tests/CryptomatorCloudAccessIntegrationTests/Box/BoxCloudProviderIntegrationTests.swift @@ -0,0 +1,44 @@ +// +// BoxCloudProviderIntegrationTests.swift +// CryptomatorCloudAccess +// +// Created by Majid Achhoud on 19.03.24. +// Copyright © 2024 Skymatic GmbH. All rights reserved. +// + +#if canImport(CryptomatorCloudAccessCore) +import CryptomatorCloudAccessCore +#else +import CryptomatorCloudAccess +#endif +import Promises +import XCTest + +class BoxCloudProviderIntegrationTests: CloudAccessIntegrationTestWithAuthentication { + override class var defaultTestSuite: XCTestSuite { + return XCTestSuite(forTestCaseClass: BoxCloudProviderIntegrationTests.self) + } + + private let credential = BoxCredential() + + override class func setUp() { + integrationTestParentCloudPath = CloudPath("/iOS-IntegrationTests-Plain") + let credential = BoxCredential() + // swiftlint:disable:next force_try + setUpProvider = try! BoxCloudProvider(credential: credential) + super.setUp() + } + + override func setUpWithError() throws { + try super.setUpWithError() + provider = try BoxCloudProvider(credential: credential) + } + + override func deauthenticate() -> Promise { + return Promise(()) + } + + override func createLimitedCloudProvider() throws -> CloudProvider { + return try BoxCloudProvider(credential: credential) + } +} From e52202ee7855845b9179f7df826f037a132777c2 Mon Sep 17 00:00:00 2001 From: Majid Achhoud Date: Tue, 26 Mar 2024 09:22:20 +0100 Subject: [PATCH 02/21] Add placeholder for developer token --- Sources/CryptomatorCloudAccess/Box/BoxCredential.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CryptomatorCloudAccess/Box/BoxCredential.swift b/Sources/CryptomatorCloudAccess/Box/BoxCredential.swift index d87bcea..a31f1b6 100644 --- a/Sources/CryptomatorCloudAccess/Box/BoxCredential.swift +++ b/Sources/CryptomatorCloudAccess/Box/BoxCredential.swift @@ -13,6 +13,6 @@ public class BoxCredential { public var client: BoxClient public init() { - self.client = BoxSDK.getClient(token: "m9DKTNlQovcvIgxRIPMaxLdjwQVDxq1g") + self.client = BoxSDK.getClient(token: "") } } From afcdff926128da65ce725e3062a49c44b4af9b89 Mon Sep 17 00:00:00 2001 From: Majid Achhoud Date: Mon, 15 Apr 2024 20:51:08 +0200 Subject: [PATCH 03/21] Added BoxCloudProvider, authentication logic, tests, and credentials for Box integration --- .../project.pbxproj | 12 +- .../Box/BoxAuthenticator.swift | 45 +-- .../Box/BoxCloudProvider.swift | 298 +++++++++++------- .../Box/BoxCredential.swift | 69 +++- .../CryptomatorCloudAccess/Box/BoxError.swift | 15 + .../CryptomatorCloudAccess/Box/BoxSetup.swift | 20 +- .../PCloud/PCloudCredential.swift | 3 +- .../BoxCloudProviderIntegrationTests.swift | 42 +-- .../Box/BoxCredentialMock.swift | 32 ++ ...leDriveCloudProviderIntegrationTests.swift | 2 +- create-integration-test-secrets-file.sh | 2 + 11 files changed, 361 insertions(+), 179 deletions(-) create mode 100644 Sources/CryptomatorCloudAccess/Box/BoxError.swift create mode 100644 Tests/CryptomatorCloudAccessIntegrationTests/Box/BoxCredentialMock.swift diff --git a/CryptomatorCloudAccess.xcodeproj/project.pbxproj b/CryptomatorCloudAccess.xcodeproj/project.pbxproj index 9242474..37eb103 100644 --- a/CryptomatorCloudAccess.xcodeproj/project.pbxproj +++ b/CryptomatorCloudAccess.xcodeproj/project.pbxproj @@ -183,6 +183,8 @@ 9ED0E624246198F600FDB438 /* VaultFormat7CloudProviderMockTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ED0E623246198F600FDB438 /* VaultFormat7CloudProviderMockTests.swift */; }; 9EE62A0D247D54760089DAF7 /* CloudProvider+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EE62A0C247D54760089DAF7 /* CloudProvider+Convenience.swift */; }; 9EE62A10247D54E90089DAF7 /* CloudProvider+ConvenienceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EE62A0F247D54E90089DAF7 /* CloudProvider+ConvenienceTests.swift */; }; + B3408AC82BCD32CA005271D2 /* BoxCredentialMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3408AC72BCD32CA005271D2 /* BoxCredentialMock.swift */; }; + B3408ACA2BCDAA09005271D2 /* BoxError.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3408AC92BCDAA09005271D2 /* BoxError.swift */; }; B3D513912BA9A32200DE0D36 /* BoxAuthenticator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3D5138D2BA9A32200DE0D36 /* BoxAuthenticator.swift */; }; B3D513922BA9A32200DE0D36 /* BoxCredential.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3D5138E2BA9A32200DE0D36 /* BoxCredential.swift */; }; B3D513932BA9A32200DE0D36 /* BoxSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3D5138F2BA9A32200DE0D36 /* BoxSetup.swift */; }; @@ -376,6 +378,8 @@ 9ED0E623246198F600FDB438 /* VaultFormat7CloudProviderMockTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultFormat7CloudProviderMockTests.swift; sourceTree = ""; }; 9EE62A0C247D54760089DAF7 /* CloudProvider+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CloudProvider+Convenience.swift"; sourceTree = ""; }; 9EE62A0F247D54E90089DAF7 /* CloudProvider+ConvenienceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CloudProvider+ConvenienceTests.swift"; sourceTree = ""; }; + B3408AC72BCD32CA005271D2 /* BoxCredentialMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxCredentialMock.swift; sourceTree = ""; }; + B3408AC92BCDAA09005271D2 /* BoxError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxError.swift; sourceTree = ""; }; B3D5138D2BA9A32200DE0D36 /* BoxAuthenticator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BoxAuthenticator.swift; sourceTree = ""; }; B3D5138E2BA9A32200DE0D36 /* BoxCredential.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BoxCredential.swift; sourceTree = ""; }; B3D5138F2BA9A32200DE0D36 /* BoxSetup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BoxSetup.swift; sourceTree = ""; }; @@ -982,11 +986,12 @@ isa = PBXGroup; children = ( B3D5138D2BA9A32200DE0D36 /* BoxAuthenticator.swift */, - B3D5138E2BA9A32200DE0D36 /* BoxCredential.swift */, - B3D5138F2BA9A32200DE0D36 /* BoxSetup.swift */, B3FC94A52BA9AA4400D1ECFD /* BoxCloudProvider.swift */, + B3D5138E2BA9A32200DE0D36 /* BoxCredential.swift */, + B3408AC92BCDAA09005271D2 /* BoxError.swift */, B3FC94A72BA9AEEC00D1ECFD /* BoxIdentifierCache.swift */, B3FC94A92BA9AEFC00D1ECFD /* BoxItem.swift */, + B3D5138F2BA9A32200DE0D36 /* BoxSetup.swift */, ); name = Box; path = Sources/CryptomatorCloudAccess/Box; @@ -996,6 +1001,7 @@ isa = PBXGroup; children = ( B3D513952BA9A3BB00DE0D36 /* BoxCloudProviderIntegrationTests.swift */, + B3408AC72BCD32CA005271D2 /* BoxCredentialMock.swift */, ); path = Box; sourceTree = ""; @@ -1337,6 +1343,7 @@ 748A42C024AB424500DEB6D0 /* WebDAVClient.swift in Sources */, B3D513932BA9A32200DE0D36 /* BoxSetup.swift in Sources */, 4A567B082615C6AF002C4D82 /* DropboxCloudProvider.swift in Sources */, + B3408ACA2BCDAA09005271D2 /* BoxError.swift in Sources */, 74C596E824F022AF00FFD17E /* CloudPath.swift in Sources */, 4A05900C2451A107008831F9 /* CloudProvider.swift in Sources */, 4A567B142615C8B8002C4D82 /* GoogleDriveAuthenticator.swift in Sources */, @@ -1415,6 +1422,7 @@ 4AC75F9C2861A6DE002731FE /* VaultFormat6S3IntegrationTests.swift in Sources */, 4ACA63A02615FE2C00D19304 /* CloudAccessIntegrationTest.swift in Sources */, B3D513972BA9A44000DE0D36 /* BoxCloudProviderIntegrationTests.swift in Sources */, + B3408AC82BCD32CA005271D2 /* BoxCredentialMock.swift in Sources */, 4ACA64262616054F00D19304 /* IntegrationTestSecrets.swift in Sources */, 4A41D2432641938A00B5D787 /* VaultFormat6OneDriveIntegrationTests.swift in Sources */, 7470C54B26569A7E00E361B8 /* MSAuthenticationProviderMock.swift in Sources */, diff --git a/Sources/CryptomatorCloudAccess/Box/BoxAuthenticator.swift b/Sources/CryptomatorCloudAccess/Box/BoxAuthenticator.swift index a3258a1..73f5253 100644 --- a/Sources/CryptomatorCloudAccess/Box/BoxAuthenticator.swift +++ b/Sources/CryptomatorCloudAccess/Box/BoxAuthenticator.swift @@ -5,29 +5,38 @@ // Created by Majid Achhoud on 18.03.24. // -import Foundation +#if canImport(CryptomatorCloudAccessCore) +import CryptomatorCloudAccessCore +#endif +import AuthenticationServices import BoxSDK import Promises import UIKit public enum BoxAuthenticatorError: Error { - case authenticationFailed + case authenticationFailed } -public struct BoxAuthenticator { - - private static let sdk = BoxSDK(clientId: BoxSetup.constants.clientId, clientSecret: BoxSetup.constants.clientSecret) - - public static func authenticate(from viewController: UIViewController) -> Promise { - return Promise { fulfill, reject in - sdk.getOAuth2Client() { result in - switch result { - case let .success(client): - fulfill(client) - case .failure(_): - reject(BoxAuthenticatorError.authenticationFailed) - } - } - } - } +public enum BoxAuthenticator { + public static let sdk = BoxSDK(clientId: BoxSetup.constants.clientId, clientSecret: BoxSetup.constants.clientSecret) + + public static func authenticate(from viewController: UIViewController, tokenStore: TokenStore) -> Promise<(BoxClient, String)> { + return Promise { fulfill, reject in + sdk.getOAuth2Client(tokenStore: tokenStore, context: viewController as! ASWebAuthenticationPresentationContextProviding) { result in + switch result { + case let .success(client): + client.users.getCurrent(fields: ["id"]) { userResult in + switch userResult { + case let .success(user): + fulfill((client, user.id)) + case .failure: + reject(BoxAuthenticatorError.authenticationFailed) + } + } + case .failure: + reject(BoxAuthenticatorError.authenticationFailed) + } + } + } + } } diff --git a/Sources/CryptomatorCloudAccess/Box/BoxCloudProvider.swift b/Sources/CryptomatorCloudAccess/Box/BoxCloudProvider.swift index e91a9b6..4794265 100644 --- a/Sources/CryptomatorCloudAccess/Box/BoxCloudProvider.swift +++ b/Sources/CryptomatorCloudAccess/Box/BoxCloudProvider.swift @@ -11,12 +11,14 @@ import Foundation import Promises public class BoxCloudProvider: CloudProvider { - private let client: BoxClient + private let credential: BoxCredential private let identifierCache: BoxIdentifierCache + private let maxPageSize: Int - public init(credential: BoxCredential) throws { - self.client = credential.client + public init(credential: BoxCredential, maxPageSize: Int = .max) throws { + self.credential = credential self.identifierCache = try BoxIdentifierCache() + self.maxPageSize = max(1, min(maxPageSize, 1000)) } public func fetchItemMetadata(at cloudPath: CloudPath) -> Promise { @@ -26,9 +28,6 @@ public class BoxCloudProvider: CloudProvider { } public func fetchItemList(forFolderAt cloudPath: CloudPath, withPageToken pageToken: String?) -> Promise { - guard pageToken == nil else { - return Promise(CloudProviderError.pageTokenInvalid) - } return resolvePath(forItemAt: cloudPath).then { item in self.fetchItemList(for: item, pageToken: pageToken) } @@ -58,6 +57,7 @@ public class BoxCloudProvider: CloudProvider { if !replaceExisting || (replaceExisting && metadata.itemType == .folder) { throw CloudProviderError.itemAlreadyExists } + }.recover { error -> Void in guard case CloudProviderError.itemNotFound = error else { throw error @@ -99,9 +99,9 @@ public class BoxCloudProvider: CloudProvider { throw CloudProviderError.itemAlreadyExists } }.then { - return all(self.resolvePath(forItemAt: sourceCloudPath), self.resolveParentPath(forItemAt: targetCloudPath)) + return all(self.resolvePath(forItemAt: sourceCloudPath), self.resolveParentPath(forItemAt: targetCloudPath)) }.then { item, targetParentItem in - return self.moveFile(from: item, toParent: targetParentItem, targetCloudPath: targetCloudPath) + return self.moveFile(from: item, toParent: targetParentItem, targetCloudPath: targetCloudPath) } } @@ -111,9 +111,9 @@ public class BoxCloudProvider: CloudProvider { throw CloudProviderError.itemAlreadyExists } }.then { - return all(self.resolvePath(forItemAt: sourceCloudPath), self.resolveParentPath(forItemAt: targetCloudPath)) + return all(self.resolvePath(forItemAt: sourceCloudPath), self.resolveParentPath(forItemAt: targetCloudPath)) }.then { item, targetParentItem in - return self.moveFolder(from: item, toParent: targetParentItem, targetCloudPath: targetCloudPath) + return self.moveFolder(from: item, toParent: targetParentItem, targetCloudPath: targetCloudPath) } } @@ -126,7 +126,7 @@ public class BoxCloudProvider: CloudProvider { return fetchFolderMetadata(for: item) } else { let error = CloudProviderError.itemTypeMismatch - CloudAccessDDLogDebug("PCloudCloudProvider: fetchItemMetadata(for: \(item.identifier)) failed with error: \(error)") + CloudAccessDDLogDebug("BoxCloudCloudProvider: fetchItemMetadata(for: \(item.identifier)) failed with error: \(error)") return Promise(error) } } @@ -134,12 +134,15 @@ public class BoxCloudProvider: CloudProvider { private func fetchFileMetadata(for item: BoxItem) -> Promise { assert(item.itemType == .file) CloudAccessDDLogDebug("BoxCloudProvider: fetchFileMetadata(for: \(item.identifier)) called") + guard let client = credential.client else { + return Promise(CloudProviderError.unauthorized) + } return Promise { fulfill, reject in - self.client.files.get(fileId: item.identifier, fields: ["name", "size", "modified_at"]) { result in + client.files.get(fileId: item.identifier, fields: ["name", "size", "modified_at"]) { result in switch result { case let .success(file): do { - let metadata = try self.convertToCloudItemMetadata(file, at: item.cloudPath) + let metadata = self.convertToCloudItemMetadata(file, at: item.cloudPath) try self.identifierCache.addOrUpdate(item) CloudAccessDDLogDebug("BoxCloudProvider: fetchFileMetadata(for: \(item.identifier)) successful") fulfill(metadata) @@ -149,7 +152,11 @@ public class BoxCloudProvider: CloudProvider { } case let .failure(error): CloudAccessDDLogDebug("BoxCloudProvider: fetchFileMetadata(for: \(item.identifier)) failed with error: \(error)") - reject(error) + if error.message == .unauthorizedAccess { + reject(CloudProviderError.unauthorized) + } else { + reject(error) + } } } } @@ -158,12 +165,15 @@ public class BoxCloudProvider: CloudProvider { private func fetchFolderMetadata(for item: BoxItem) -> Promise { assert(item.itemType == .folder) CloudAccessDDLogDebug("BoxCloudProvider: fetchFolderMetadata(for: \(item.identifier)) called") + guard let client = credential.client else { + return Promise(CloudProviderError.unauthorized) + } return Promise { fulfill, reject in - self.client.folders.get(folderId: item.identifier, fields: ["name", "modified_at"]) { result in + client.folders.get(folderId: item.identifier, fields: ["name", "modified_at"]) { result in switch result { case let .success(folder): do { - let metadata = try self.convertToCloudItemMetadata(folder, at: item.cloudPath) + let metadata = self.convertToCloudItemMetadata(folder, at: item.cloudPath) try self.identifierCache.addOrUpdate(item) CloudAccessDDLogDebug("BoxCloudProvider: fetchFolderMetadata(for: \(item.identifier)) successful") fulfill(metadata) @@ -179,51 +189,37 @@ public class BoxCloudProvider: CloudProvider { } } - private func fetchItemList(for item: BoxItem, pageToken: String?) -> Promise { - CloudAccessDDLogDebug("BoxCloudProvider: fetchItemList(forFolderAt: \(item.identifier)) called") - guard item.itemType == .folder else { - let error = CloudProviderError.itemTypeMismatch - CloudAccessDDLogDebug("BoxCloudProvider: fetchItemList(forFolderAt: \(item.identifier)) failed with error: \(error)") - return Promise(error) + private func fetchItemList(for folderItem: BoxItem, pageToken: String?) -> Promise { + guard folderItem.itemType == .folder else { + return Promise(CloudProviderError.itemTypeMismatch) } - + + guard let client = credential.client else { + return Promise(CloudProviderError.unauthorized) + } + return Promise { fulfill, reject in - let iterator = self.client.folders.listItems(folderId: item.identifier, usemarker: true, marker: pageToken) - var allItems: [CloudItemMetadata] = [] - + let iterator = client.folders.listItems(folderId: folderItem.identifier, usemarker: true, marker: pageToken, limit: self.maxPageSize, fields: ["name", "size", "modified_at"]) + iterator.next { result in switch result { case let .success(page): - for folderItem in page.entries { - do { - let childCloudPath: CloudPath - let childItemMetadata: CloudItemMetadata - - switch folderItem { - case let .file(file): - childCloudPath = item.cloudPath.appendingPathComponent(file.name ?? "") - childItemMetadata = try self.convertToCloudItemMetadata(file, at: childCloudPath) - case let .folder(folder): - childCloudPath = item.cloudPath.appendingPathComponent(folder.name ?? "") - childItemMetadata = try self.convertToCloudItemMetadata(folder, at: childCloudPath) - case .webLink: - continue - } - - allItems.append(childItemMetadata) - - let newItem = try BoxItem(cloudPath: childCloudPath, folderItem: folderItem) - try self.identifierCache.addOrUpdate(newItem) - } catch { - reject(error) - return + let allItems = page.entries.compactMap { entry -> CloudItemMetadata? in + switch entry { + case let .file(file): + return self.convertToCloudItemMetadata(file, at: folderItem.cloudPath.appendingPathComponent(file.name ?? "")) + case let .folder(folder): + return self.convertToCloudItemMetadata(folder, at: folderItem.cloudPath.appendingPathComponent(folder.name ?? "")) + case .webLink: + // Handling of web links as required + return nil } } fulfill(CloudItemList(items: allItems, nextPageToken: page.nextMarker)) - + case let .failure(error): - reject(error) + reject(CloudProviderError.pageTokenInvalid) } } } @@ -231,75 +227,101 @@ public class BoxCloudProvider: CloudProvider { private func downloadFile(for item: BoxItem, to localURL: URL) -> Promise { CloudAccessDDLogDebug("BoxCloudProvider: downloadFile(for: \(item.identifier), to: \(localURL)) called") + guard item.itemType == .file else { + return Promise(CloudProviderError.itemTypeMismatch) + } + + guard let client = credential.client else { + return Promise(CloudProviderError.unauthorized) + } + return Promise { fulfill, reject in - let task = self.client.files.download(fileId: item.identifier, destinationURL: localURL) { result in + client.files.download(fileId: item.identifier, destinationURL: localURL) { result in switch result { case .success: CloudAccessDDLogDebug("BoxCloudProvider: downloadFile(for: \(item.identifier), to: \(localURL)) finished downloading") fulfill(()) case let .failure(error): CloudAccessDDLogDebug("BoxCloudProvider: downloadFile(for: \(item.identifier), to: \(localURL)) failed with error: \(error)") - reject(error) + if error.message == .unauthorizedAccess { + reject(CloudProviderError.unauthorized) + } else { + reject(error) + } } } } } private func uploadFile(for parentItem: BoxItem, from localURL: URL, to cloudPath: CloudPath) -> Promise { - CloudAccessDDLogDebug("BoxCloudProvider: uploadFile(for: \(parentItem.identifier), from: \(localURL), to: \(cloudPath.path)) called") + guard let client = credential.client else { + return Promise(CloudProviderError.unauthorized) + } + return Promise { fulfill, reject in - do { - let fileData = try Data(contentsOf: localURL) //Refactor - let progress = Progress(totalUnitCount: Int64(fileData.count)) - self.client.files.upload(data: fileData, name: cloudPath.lastPathComponent, parentId: parentItem.identifier, progress: { progressUpdate in - print("Upload progress: \(progressUpdate.fractionCompleted)") - }) { (result: Result) in + let targetFileName = cloudPath.lastPathComponent + + guard let data = try? Data(contentsOf: localURL) else { + reject(CloudProviderError.itemNotFound) + return + } + + self.resolvePath(forItemAt: cloudPath).then { existingItem -> Void in + client.files.uploadVersion(forFile: existingItem.identifier, data: data, completion: { result in switch result { - case let .success(file): - CloudAccessDDLogDebug("BoxCloudProvider: uploadFile successful with file ID: \(file.id)") - let metadata = CloudItemMetadata(name: file.name ?? "", cloudPath: cloudPath, itemType: .file, lastModifiedDate: file.modifiedAt, size: file.size) - do { - let boxItem = BoxItem(cloudPath: cloudPath, identifier: file.id, itemType: .file) - try self.identifierCache.addOrUpdate(boxItem) - fulfill(metadata) - } catch { - reject(error) - } + case let .success(updatedFile): + let metadata = self.convertToCloudItemMetadata(updatedFile, at: cloudPath) + fulfill(metadata) case let .failure(error): - CloudAccessDDLogDebug("BoxCloudProvider: uploadFile failed with error: \(error.localizedDescription)") reject(error) } + }) + }.recover { error -> Void in + guard case CloudProviderError.itemNotFound = error else { + throw error } - } catch { - reject(error) + client.files.upload(data: data, name: targetFileName, parentId: parentItem.identifier, completion: { result in + switch result { + case let .success(newFile): + let metadata = self.convertToCloudItemMetadata(newFile, at: cloudPath) + fulfill(metadata) + case let .failure(error): + if error.message == .unauthorizedAccess { + reject(CloudProviderError.unauthorized) + } else { + reject(error) + } + } + }) } } } private func createFolder(for parentItem: BoxItem, with name: String) -> Promise { CloudAccessDDLogDebug("BoxCloudProvider: createFolder(for: \(parentItem.identifier), with: \(name)) called") + guard let client = credential.client else { + return Promise(CloudProviderError.unauthorized) + } return Promise { fulfill, reject in - let cloudPath = parentItem.cloudPath.appendingPathComponent(name) - self.resolveParentPath(forItemAt: cloudPath.deletingLastPathComponent()).then { parentItem -> Void in - self.client.folders.create(name: name, parentId: parentItem.identifier) { result in - switch result { - case let .success(folder): - CloudAccessDDLogDebug("BoxCloudProvider: createFolder successful with folder ID: \(folder.id)") - let newItemMetadata = CloudItemMetadata(name: folder.name ?? "", cloudPath: cloudPath.appendingPathComponent(name), itemType: .folder, lastModifiedDate: folder.modifiedAt, size: nil) - do { - let newItem = BoxItem(cloudPath: cloudPath.appendingPathComponent(name), identifier: folder.id, itemType: .folder) - try self.identifierCache.addOrUpdate(newItem) - fulfill(()) - } catch { - reject(error) - } - case let .failure(error): - CloudAccessDDLogDebug("BoxCloudProvider: createFolder failed with error: \(error.localizedDescription)") + client.folders.create(name: name, parentId: parentItem.identifier) { result in + switch result { + case let .success(folder): + CloudAccessDDLogDebug("BoxCloudProvider: createFolder successful with folder ID: \(folder.id)") + do { + let newItem = BoxItem(cloudPath: parentItem.cloudPath.appendingPathComponent(name), identifier: folder.id, itemType: .folder) + try self.identifierCache.addOrUpdate(newItem) + fulfill(()) + } catch { + reject(error) + } + case let .failure(error): + CloudAccessDDLogDebug("BoxCloudProvider: createFolder failed with error: \(error.localizedDescription)") + if error.message == .unauthorizedAccess { + reject(CloudProviderError.unauthorized) + } else { reject(error) } } - }.catch { error in - reject(error) } } } @@ -309,8 +331,13 @@ public class BoxCloudProvider: CloudProvider { guard item.itemType == .file else { return Promise(CloudProviderError.itemTypeMismatch) } + + guard let client = credential.client else { + return Promise(CloudProviderError.unauthorized) + } + return Promise { fulfill, reject in - self.client.files.delete(fileId: item.identifier) { result in + client.files.delete(fileId: item.identifier) { result in switch result { case .success: CloudAccessDDLogDebug("BoxCloudProvider: deleteFile(for: \(item.identifier)) succeeded") @@ -322,11 +349,15 @@ public class BoxCloudProvider: CloudProvider { reject(error) } case let .failure(error): - CloudAccessDDLogDebug("BoxCloudProvider: deleteFolder(for: \(item.identifier)) failed with error: \(error)") + CloudAccessDDLogDebug("BoxCloudProvider: deleteFile(for: \(item.identifier)) failed with error: \(error)") if case BoxSDKErrorEnum.notFound = error.message { reject(CloudProviderError.itemNotFound) } else { - reject(error) + if error.message == .unauthorizedAccess { + reject(CloudProviderError.unauthorized) + } else { + reject(error) + } } } } @@ -334,15 +365,20 @@ public class BoxCloudProvider: CloudProvider { } private func deleteFolder(for item: BoxItem) -> Promise { - CloudAccessDDLogDebug("BoxCloudProvider: deleteFile(for: \(item.identifier)) called") + CloudAccessDDLogDebug("BoxCloudProvider: deleteFolder(for: \(item.identifier)) called") guard item.itemType == .folder else { return Promise(CloudProviderError.itemTypeMismatch) } + + guard let client = credential.client else { + return Promise(CloudProviderError.unauthorized) + } + return Promise { fulfill, reject in - self.client.folders.delete(folderId: item.identifier) { result in + client.folders.delete(folderId: item.identifier, recursive: true) { result in switch result { case .success: - CloudAccessDDLogDebug("BoxCloudProvider: deleteFile(for: \(item.identifier)) succeeded") + CloudAccessDDLogDebug("BoxCloudProvider: deleteFolder(for: \(item.identifier)) succeeded") do { try self.identifierCache.invalidate(item) fulfill(()) @@ -355,7 +391,11 @@ public class BoxCloudProvider: CloudProvider { if case BoxSDKErrorEnum.notFound = error.message { reject(CloudProviderError.itemNotFound) } else { - reject(error) + if error.message == .unauthorizedAccess { + reject(CloudProviderError.unauthorized) + } else { + reject(error) + } } } } @@ -364,10 +404,13 @@ public class BoxCloudProvider: CloudProvider { private func moveFile(from sourceItem: BoxItem, toParent targetParentItem: BoxItem, targetCloudPath: CloudPath) -> Promise { CloudAccessDDLogDebug("BoxCloudProvider: moveFile(from: \(sourceItem.identifier), toParent: \(targetParentItem.identifier), targetCloudPath: \(targetCloudPath.path)) called") - + guard let client = credential.client else { + return Promise(CloudProviderError.unauthorized) + } + return Promise { fulfill, reject in let newName = targetCloudPath.lastPathComponent - self.client.files.update(fileId: sourceItem.identifier, name: newName, parentId: targetParentItem.identifier) { result in + client.files.update(fileId: sourceItem.identifier, name: newName, parentId: targetParentItem.identifier) { result in switch result { case .success: CloudAccessDDLogDebug("BoxCloudProvider: moveFile succeeded for \(sourceItem.identifier) to \(targetCloudPath.path)") @@ -381,7 +424,11 @@ public class BoxCloudProvider: CloudProvider { } case let .failure(error): CloudAccessDDLogDebug("BoxCloudProvider: moveFile failed for \(sourceItem.identifier) with error: \(error)") - reject(error) + if error.message == .unauthorizedAccess { + reject(CloudProviderError.unauthorized) + } else { + reject(error) + } } } } @@ -389,10 +436,13 @@ public class BoxCloudProvider: CloudProvider { private func moveFolder(from sourceItem: BoxItem, toParent targetParentItem: BoxItem, targetCloudPath: CloudPath) -> Promise { CloudAccessDDLogDebug("BoxCloudProvider: moveFolder(from: \(sourceItem.identifier), toParent: \(targetParentItem.identifier), targetCloudPath: \(targetCloudPath.path)) called") - + guard let client = credential.client else { + return Promise(CloudProviderError.unauthorized) + } + return Promise { fulfill, reject in let newName = targetCloudPath.lastPathComponent - self.client.folders.update(folderId: sourceItem.identifier, name: newName, parentId: targetParentItem.identifier) { result in + client.folders.update(folderId: sourceItem.identifier, name: newName, parentId: targetParentItem.identifier) { result in switch result { case .success: CloudAccessDDLogDebug("BoxCloudProvider: moveFolder succeeded for \(sourceItem.identifier) to \(targetCloudPath.path)") @@ -406,7 +456,11 @@ public class BoxCloudProvider: CloudProvider { } case let .failure(error): CloudAccessDDLogDebug("BoxCloudProvider: moveFolder failed for \(sourceItem.identifier) with error: \(error)") - reject(error) + if error.message == .unauthorizedAccess { + reject(CloudProviderError.unauthorized) + } else { + reject(error) + } } } } @@ -422,7 +476,7 @@ public class BoxCloudProvider: CloudProvider { cachedItem = identifierCache.get(pathToCheckForCache) } guard let item = cachedItem else { - return Promise(PCloudError.inconsistentCache) + return Promise(BoxError.inconsistentCache) } if pathToCheckForCache != cloudPath { return traverseThroughPath(from: pathToCheckForCache, to: cloudPath, withStartItem: item) @@ -459,10 +513,14 @@ public class BoxCloudProvider: CloudProvider { } func getBoxItem(for name: String, withParentItem parentItem: BoxItem) -> Promise { + guard let client = credential.client else { + return Promise(CloudProviderError.unauthorized) + } + return Promise { fulfill, reject in - CloudAccessDDLogDebug("PCloudCloudProvider: getBoxItem(for: \(name), withParentItem: \(parentItem.identifier)) called") + CloudAccessDDLogDebug("BoxCloudCloudProvider: getBoxItem(for: \(name), withParentItem: \(parentItem.identifier)) called") - let iterator = self.client.folders.listItems(folderId: parentItem.identifier) + let iterator = client.folders.listItems(folderId: parentItem.identifier) iterator.next { result in switch result { case let .success(page): @@ -479,7 +537,11 @@ public class BoxCloudProvider: CloudProvider { } reject(CloudProviderError.itemNotFound) case let .failure(error): - reject(error) + if error.message == .unauthorizedAccess { + reject(CloudProviderError.unauthorized) + } else { + reject(error) + } } } } @@ -492,7 +554,7 @@ public class BoxCloudProvider: CloudProvider { case let .folder(folder) where folder.name == name: return BoxItem(cloudPath: parentItem.cloudPath.appendingPathComponent(name), folder: folder) case .webLink: - throw PCloudError.unexpectedContent + throw BoxError.unexpectedContent default: return nil } @@ -503,15 +565,15 @@ public class BoxCloudProvider: CloudProvider { private func convertToCloudItemMetadata(_ content: FolderItem, at cloudPath: CloudPath) throws -> CloudItemMetadata { switch content { case let .file(fileMetadata): - return try convertToCloudItemMetadata(fileMetadata, at: cloudPath) + return convertToCloudItemMetadata(fileMetadata, at: cloudPath) case let .folder(folderMetadata): - return try convertToCloudItemMetadata(folderMetadata, at: cloudPath) - default: //Refactor - no default - throw PCloudError.unexpectedContent + return convertToCloudItemMetadata(folderMetadata, at: cloudPath) + default: // Refactor - no default + throw BoxError.unexpectedContent } } - private func convertToCloudItemMetadata(_ metadata: File, at cloudPath: CloudPath) throws -> CloudItemMetadata { + private func convertToCloudItemMetadata(_ metadata: File, at cloudPath: CloudPath) -> CloudItemMetadata { let name = metadata.name ?? "" let itemType = CloudItemType.file let lastModifiedDate = metadata.modifiedAt @@ -519,7 +581,7 @@ public class BoxCloudProvider: CloudProvider { return CloudItemMetadata(name: name, cloudPath: cloudPath, itemType: itemType, lastModifiedDate: lastModifiedDate, size: size) } - private func convertToCloudItemMetadata(_ metadata: Folder, at cloudPath: CloudPath) throws -> CloudItemMetadata { + private func convertToCloudItemMetadata(_ metadata: Folder, at cloudPath: CloudPath) -> CloudItemMetadata { let name = metadata.name ?? "" let itemType = CloudItemType.folder let lastModifiedDate = metadata.modifiedAt @@ -532,14 +594,14 @@ public class BoxCloudProvider: CloudProvider { switch content { case let .file(fileMetadata): let itemCloudPath = cloudPath.appendingPathComponent(fileMetadata.name ?? "") - let itemMetadata = try convertToCloudItemMetadata(fileMetadata, at: itemCloudPath) + let itemMetadata = convertToCloudItemMetadata(fileMetadata, at: itemCloudPath) items.append(itemMetadata) case let .folder(folderMetadata): let itemCloudPath = cloudPath.appendingPathComponent(folderMetadata.name ?? "") - let itemMetadata = try convertToCloudItemMetadata(folderMetadata, at: itemCloudPath) + let itemMetadata = convertToCloudItemMetadata(folderMetadata, at: itemCloudPath) items.append(itemMetadata) default: - throw PCloudError.unexpectedContent + throw BoxError.unexpectedContent } } return CloudItemList(items: items, nextPageToken: nil) diff --git a/Sources/CryptomatorCloudAccess/Box/BoxCredential.swift b/Sources/CryptomatorCloudAccess/Box/BoxCredential.swift index a31f1b6..03b75e3 100644 --- a/Sources/CryptomatorCloudAccess/Box/BoxCredential.swift +++ b/Sources/CryptomatorCloudAccess/Box/BoxCredential.swift @@ -5,14 +5,69 @@ // Created by Majid Achhoud on 19.03.24. // -import Foundation +import AuthenticationServices import BoxSDK +import Foundation +import Promises + +public enum BoxCredentialErrors: Error { + case noUsername +} public class BoxCredential { - - public var client: BoxClient - - public init() { - self.client = BoxSDK.getClient(token: "") - } + public internal(set) var client: BoxClient? + private(set) var userId: String? + + public init(tokenStore: TokenStore) { + let sdk = BoxSDK(clientId: BoxSetup.constants.clientId, clientSecret: BoxSetup.constants.clientSecret) + sdk.getOAuth2Client(tokenStore: tokenStore) { result in + switch result { + case let .success(client): + self.client = client + self.retrieveAndStoreUserId() + case let .failure(error): + break + } + } + } + + public func deauthenticate() -> Promise { + return Promise { fulfill, reject in + self.client?.destroy { result in + switch result { + case .success: + fulfill(()) + case let .failure(error): + reject(error) + } + } + } + } + + public func getUsername() -> Promise { + return Promise { fulfill, reject in + self.client?.users.getCurrent(fields: ["name"]) { result in + switch result { + case let .success(user): + if let name = user.name { + fulfill(name) + } else { + reject(BoxCredentialErrors.noUsername) + } + case let .failure(error): + reject(error) + } + } + } + } + + private func retrieveAndStoreUserId() { + client?.users.getCurrent(fields: ["id"]) { [weak self] result in + switch result { + case let .success(user): + self?.userId = user.id + case .failure: break // TODO: Break ersetzen + } + } + } } diff --git a/Sources/CryptomatorCloudAccess/Box/BoxError.swift b/Sources/CryptomatorCloudAccess/Box/BoxError.swift new file mode 100644 index 0000000..b78b95e --- /dev/null +++ b/Sources/CryptomatorCloudAccess/Box/BoxError.swift @@ -0,0 +1,15 @@ +// +// BoxError.swift +// CryptomatorCloudAccess +// +// Created by Majid Achhoud on 15.04.24. +// Copyright © 2024 Skymatic GmbH. All rights reserved. +// + +import Foundation + +public enum BoxError: Error { + case unexpectedContent + case inconsistentCache + case fileLinkNotFound +} diff --git a/Sources/CryptomatorCloudAccess/Box/BoxSetup.swift b/Sources/CryptomatorCloudAccess/Box/BoxSetup.swift index c35640c..f4214be 100644 --- a/Sources/CryptomatorCloudAccess/Box/BoxSetup.swift +++ b/Sources/CryptomatorCloudAccess/Box/BoxSetup.swift @@ -8,17 +8,15 @@ import Foundation public class BoxSetup { - public static var constants: BoxSetup! + public static var constants: BoxSetup! - public let clientId: String - public let clientSecret: String - public let redirectURL: URL - public let sharedContainerIdentifier: String? + public let clientId: String + public let clientSecret: String + public let sharedContainerIdentifier: String? - public init(clientId: String, clientSecret: String, redirectURL: URL, sharedContainerIdentifier: String?) { - self.clientId = clientId - self.clientSecret = clientSecret - self.redirectURL = redirectURL - self.sharedContainerIdentifier = sharedContainerIdentifier - } + public init(clientId: String, clientSecret: String, sharedContainerIdentifier: String?) { + self.clientId = clientId + self.clientSecret = clientSecret + self.sharedContainerIdentifier = sharedContainerIdentifier + } } diff --git a/Sources/CryptomatorCloudAccess/PCloud/PCloudCredential.swift b/Sources/CryptomatorCloudAccess/PCloud/PCloudCredential.swift index e68d974..cfefa5c 100644 --- a/Sources/CryptomatorCloudAccess/PCloud/PCloudCredential.swift +++ b/Sources/CryptomatorCloudAccess/PCloud/PCloudCredential.swift @@ -19,7 +19,8 @@ public class PCloudCredential { private let client: PCloudClient public init(user: OAuth.User) { - self.user = user + + self.user = user self.client = PCloud.createClient(with: user) } diff --git a/Tests/CryptomatorCloudAccessIntegrationTests/Box/BoxCloudProviderIntegrationTests.swift b/Tests/CryptomatorCloudAccessIntegrationTests/Box/BoxCloudProviderIntegrationTests.swift index ccc6db5..25015b3 100644 --- a/Tests/CryptomatorCloudAccessIntegrationTests/Box/BoxCloudProviderIntegrationTests.swift +++ b/Tests/CryptomatorCloudAccessIntegrationTests/Box/BoxCloudProviderIntegrationTests.swift @@ -15,30 +15,30 @@ import Promises import XCTest class BoxCloudProviderIntegrationTests: CloudAccessIntegrationTestWithAuthentication { - override class var defaultTestSuite: XCTestSuite { - return XCTestSuite(forTestCaseClass: BoxCloudProviderIntegrationTests.self) - } + override class var defaultTestSuite: XCTestSuite { + return XCTestSuite(forTestCaseClass: BoxCloudProviderIntegrationTests.self) + } - private let credential = BoxCredential() + private let credential = BoxCredentialMock() - override class func setUp() { - integrationTestParentCloudPath = CloudPath("/iOS-IntegrationTests-Plain") - let credential = BoxCredential() - // swiftlint:disable:next force_try - setUpProvider = try! BoxCloudProvider(credential: credential) - super.setUp() - } + override class func setUp() { + integrationTestParentCloudPath = CloudPath("/iOS-IntegrationTests-Plain") + let credential = BoxCredentialMock() + // swiftlint:disable:next force_try + setUpProvider = try! BoxCloudProvider(credential: credential) + super.setUp() + } - override func setUpWithError() throws { - try super.setUpWithError() - provider = try BoxCloudProvider(credential: credential) - } + override func setUpWithError() throws { + try super.setUpWithError() + provider = try BoxCloudProvider(credential: credential) + } - override func deauthenticate() -> Promise { - return Promise(()) - } + override func deauthenticate() -> Promise { + return credential.deauthenticate() + } - override func createLimitedCloudProvider() throws -> CloudProvider { - return try BoxCloudProvider(credential: credential) - } + override func createLimitedCloudProvider() throws -> CloudProvider { + return try BoxCloudProvider(credential: credential, maxPageSize: maxPageSizeForLimitedCloudProvider) + } } diff --git a/Tests/CryptomatorCloudAccessIntegrationTests/Box/BoxCredentialMock.swift b/Tests/CryptomatorCloudAccessIntegrationTests/Box/BoxCredentialMock.swift new file mode 100644 index 0000000..9e1b26e --- /dev/null +++ b/Tests/CryptomatorCloudAccessIntegrationTests/Box/BoxCredentialMock.swift @@ -0,0 +1,32 @@ +// +// BoxCredentialMock.swift +// CryptomatorCloudAccessIntegrationTests +// +// Created by Majid Achhoud on 15.04.24. +// Copyright © 2024 Skymatic GmbH. All rights reserved. +// + +import Foundation +import Promises +@testable import BoxSDK +#if canImport(CryptomatorCloudAccessCore) +@testable import CryptomatorCloudAccessCore +#else +@testable import CryptomatorCloudAccess +#endif + +class BoxCredentialMock: BoxCredential { + let tokenStore: MemoryTokenStore + + init() { + BoxSetup.constants = BoxSetup(clientId: "", clientSecret: "", sharedContainerIdentifier: "") + self.tokenStore = MemoryTokenStore() + tokenStore.tokenInfo = TokenInfo(accessToken: IntegrationTestSecrets.boxAccessToken, refreshToken: IntegrationTestSecrets.boxRefreshToken, expiresIn: 3600, tokenType: "bearer") + super.init(tokenStore: tokenStore) + } + + override func deauthenticate() -> Promise { + tokenStore.tokenInfo = TokenInfo(accessToken: "invalid", expiresIn: 0) + return Promise(()) + } +} diff --git a/Tests/CryptomatorCloudAccessIntegrationTests/Google Drive/GoogleDriveCloudProviderIntegrationTests.swift b/Tests/CryptomatorCloudAccessIntegrationTests/Google Drive/GoogleDriveCloudProviderIntegrationTests.swift index 4f5d142..ed11bec 100644 --- a/Tests/CryptomatorCloudAccessIntegrationTests/Google Drive/GoogleDriveCloudProviderIntegrationTests.swift +++ b/Tests/CryptomatorCloudAccessIntegrationTests/Google Drive/GoogleDriveCloudProviderIntegrationTests.swift @@ -24,7 +24,7 @@ class GoogleDriveCloudProviderIntegrationTests: CloudAccessIntegrationTestWithAu override class func setUp() { integrationTestParentCloudPath = CloudPath("/iOS-IntegrationTests-Plain") let credential = GoogleDriveAuthenticatorMock.generateAuthorizedCredential(withRefreshToken: IntegrationTestSecrets.googleDriveRefreshToken, tokenUID: "IntegrationTest") - // swiftlint:disable:next force_try + setUpProvider = try! GoogleDriveCloudProvider(credential: credential, useBackgroundSession: false) super.setUp() } diff --git a/create-integration-test-secrets-file.sh b/create-integration-test-secrets-file.sh index 137ec19..7ef5fbf 100755 --- a/create-integration-test-secrets-file.sh +++ b/create-integration-test-secrets-file.sh @@ -17,6 +17,8 @@ import CryptomatorCloudAccess import Foundation struct IntegrationTestSecrets { + static let boxAccessToken = "${BOX_ACCESS_TOKEN}" + static let boxRefreshToken = "${BOX_REFRESH_TOKEN}" static let dropboxAccessToken = "${DROPBOX_ACCESS_TOKEN}" static let googleDriveClientId = "${GOOGLE_DRIVE_CLIENT_ID}" static let googleDriveRefreshToken = "${GOOGLE_DRIVE_REFRESH_TOKEN}" From 888bbb60e43ad1fe300bf796e0e8e24cbb3d1653 Mon Sep 17 00:00:00 2001 From: Majid Achhoud Date: Tue, 16 Apr 2024 10:50:15 +0200 Subject: [PATCH 04/21] Added safe cast in BoxAuthenticator, remove userID from BoxCredential, and adjust formatting --- Package.swift | 4 +- .../Box/BoxAuthenticator.swift | 9 +- .../Box/BoxCloudProvider.swift | 1124 ++++++++--------- .../Box/BoxCredential.swift | 12 - .../Box/BoxIdentifierCache.swift | 56 +- .../CryptomatorCloudAccess/Box/BoxItem.swift | 64 +- .../PCloud/PCloudCredential.swift | 3 +- ...leDriveCloudProviderIntegrationTests.swift | 2 +- 8 files changed, 634 insertions(+), 640 deletions(-) diff --git a/Package.swift b/Package.swift index ed021c3..f739c00 100644 --- a/Package.swift +++ b/Package.swift @@ -42,7 +42,7 @@ let package = Package( .package(url: "https://github.com/phil1995/dropbox-sdk-obj-c-spm.git", .upToNextMinor(from: "7.2.0")), .package(url: "https://github.com/phil1995/msgraph-sdk-objc-spm.git", .upToNextMinor(from: "1.0.0")), .package(url: "https://github.com/phil1995/msgraph-sdk-objc-models-spm.git", .upToNextMinor(from: "1.3.0")), - .package(url: "https://github.com/box/box-ios-sdk.git", .upToNextMinor(from: "5.5.0")), + .package(url: "https://github.com/box/box-ios-sdk.git", .upToNextMinor(from: "5.5.0")) ], targets: [ .target( @@ -63,7 +63,7 @@ let package = Package( .product(name: "ObjectiveDropboxOfficial", package: "dropbox-sdk-obj-c-spm"), .product(name: "PCloudSDKSwift", package: "pcloud-sdk-swift"), .product(name: "Promises", package: "promises"), - .product(name: "BoxSDK", package: "box-ios-sdk") + .product(name: "BoxSDK", package: "box-ios-sdk") ], path: "Sources/CryptomatorCloudAccess", exclude: appExtensionUnsafeSources diff --git a/Sources/CryptomatorCloudAccess/Box/BoxAuthenticator.swift b/Sources/CryptomatorCloudAccess/Box/BoxAuthenticator.swift index 73f5253..7101fbd 100644 --- a/Sources/CryptomatorCloudAccess/Box/BoxAuthenticator.swift +++ b/Sources/CryptomatorCloudAccess/Box/BoxAuthenticator.swift @@ -15,6 +15,7 @@ import UIKit public enum BoxAuthenticatorError: Error { case authenticationFailed + case invalidContext } public enum BoxAuthenticator { @@ -22,7 +23,13 @@ public enum BoxAuthenticator { public static func authenticate(from viewController: UIViewController, tokenStore: TokenStore) -> Promise<(BoxClient, String)> { return Promise { fulfill, reject in - sdk.getOAuth2Client(tokenStore: tokenStore, context: viewController as! ASWebAuthenticationPresentationContextProviding) { result in + + guard let context = viewController as? ASWebAuthenticationPresentationContextProviding else { + reject(BoxAuthenticatorError.invalidContext) + return + } + + sdk.getOAuth2Client(tokenStore: tokenStore, context: context) { result in switch result { case let .success(client): client.users.getCurrent(fields: ["id"]) { userResult in diff --git a/Sources/CryptomatorCloudAccess/Box/BoxCloudProvider.swift b/Sources/CryptomatorCloudAccess/Box/BoxCloudProvider.swift index 4794265..654ad92 100644 --- a/Sources/CryptomatorCloudAccess/Box/BoxCloudProvider.swift +++ b/Sources/CryptomatorCloudAccess/Box/BoxCloudProvider.swift @@ -11,599 +11,599 @@ import Foundation import Promises public class BoxCloudProvider: CloudProvider { - private let credential: BoxCredential - private let identifierCache: BoxIdentifierCache - private let maxPageSize: Int - - public init(credential: BoxCredential, maxPageSize: Int = .max) throws { - self.credential = credential - self.identifierCache = try BoxIdentifierCache() - self.maxPageSize = max(1, min(maxPageSize, 1000)) - } - - public func fetchItemMetadata(at cloudPath: CloudPath) -> Promise { - return resolvePath(forItemAt: cloudPath).then { item in - self.fetchItemMetadata(for: item) - } - } - - public func fetchItemList(forFolderAt cloudPath: CloudPath, withPageToken pageToken: String?) -> Promise { - return resolvePath(forItemAt: cloudPath).then { item in - self.fetchItemList(for: item, pageToken: pageToken) - } - } - - public func downloadFile(from cloudPath: CloudPath, to localURL: URL, onTaskCreation: ((URLSessionDownloadTask?) -> Void)?) -> Promise { - precondition(localURL.isFileURL) - if FileManager.default.fileExists(atPath: localURL.path) { - return Promise(CloudProviderError.itemAlreadyExists) - } - return resolvePath(forItemAt: cloudPath).then { item in - self.downloadFile(for: item, to: localURL) - } - } - - public func uploadFile(from localURL: URL, to cloudPath: CloudPath, replaceExisting: Bool, onTaskCreation: ((URLSessionUploadTask?) -> Void)?) -> Promise { - precondition(localURL.isFileURL) - var isDirectory: ObjCBool = false - let fileExists = FileManager.default.fileExists(atPath: localURL.path, isDirectory: &isDirectory) - if !fileExists { - return Promise(CloudProviderError.itemNotFound) - } - if isDirectory.boolValue { - return Promise(CloudProviderError.itemTypeMismatch) - } - return fetchItemMetadata(at: cloudPath).then { metadata -> Void in - if !replaceExisting || (replaceExisting && metadata.itemType == .folder) { - throw CloudProviderError.itemAlreadyExists - } - - }.recover { error -> Void in - guard case CloudProviderError.itemNotFound = error else { - throw error - } - }.then { _ -> Promise in - return self.resolveParentPath(forItemAt: cloudPath) - }.then { parentItem in - return self.uploadFile(for: parentItem, from: localURL, to: cloudPath) - } - } - - public func createFolder(at cloudPath: CloudPath) -> Promise { - return checkForItemExistence(at: cloudPath).then { itemExists in - if itemExists { - throw CloudProviderError.itemAlreadyExists - } - }.then { _ -> Promise in - return self.resolveParentPath(forItemAt: cloudPath) - }.then { parentItem in - return self.createFolder(for: parentItem, with: cloudPath.lastPathComponent) - } - } - - public func deleteFile(at cloudPath: CloudPath) -> Promise { - return resolvePath(forItemAt: cloudPath).then { item in - self.deleteFile(for: item) - } - } - - public func deleteFolder(at cloudPath: CloudPath) -> Promise { - return resolvePath(forItemAt: cloudPath).then { item in - self.deleteFolder(for: item) - } - } - - public func moveFile(from sourceCloudPath: CloudPath, to targetCloudPath: CloudPath) -> Promise { - return checkForItemExistence(at: targetCloudPath).then { itemExists -> Void in - if itemExists { - throw CloudProviderError.itemAlreadyExists - } - }.then { - return all(self.resolvePath(forItemAt: sourceCloudPath), self.resolveParentPath(forItemAt: targetCloudPath)) - }.then { item, targetParentItem in - return self.moveFile(from: item, toParent: targetParentItem, targetCloudPath: targetCloudPath) - } - } - - public func moveFolder(from sourceCloudPath: CloudPath, to targetCloudPath: CloudPath) -> Promise { - return checkForItemExistence(at: targetCloudPath).then { itemExists -> Void in - if itemExists { - throw CloudProviderError.itemAlreadyExists - } - }.then { - return all(self.resolvePath(forItemAt: sourceCloudPath), self.resolveParentPath(forItemAt: targetCloudPath)) - }.then { item, targetParentItem in - return self.moveFolder(from: item, toParent: targetParentItem, targetCloudPath: targetCloudPath) - } - } - - // MARK: - Operations - - private func fetchItemMetadata(for item: BoxItem) -> Promise { - if item.itemType == .file { - return fetchFileMetadata(for: item) - } else if item.itemType == .folder { - return fetchFolderMetadata(for: item) - } else { - let error = CloudProviderError.itemTypeMismatch - CloudAccessDDLogDebug("BoxCloudCloudProvider: fetchItemMetadata(for: \(item.identifier)) failed with error: \(error)") - return Promise(error) - } - } - - private func fetchFileMetadata(for item: BoxItem) -> Promise { - assert(item.itemType == .file) - CloudAccessDDLogDebug("BoxCloudProvider: fetchFileMetadata(for: \(item.identifier)) called") + private let credential: BoxCredential + private let identifierCache: BoxIdentifierCache + private let maxPageSize: Int + + public init(credential: BoxCredential, maxPageSize: Int = .max) throws { + self.credential = credential + self.identifierCache = try BoxIdentifierCache() + self.maxPageSize = max(1, min(maxPageSize, 1000)) + } + + public func fetchItemMetadata(at cloudPath: CloudPath) -> Promise { + return resolvePath(forItemAt: cloudPath).then { item in + self.fetchItemMetadata(for: item) + } + } + + public func fetchItemList(forFolderAt cloudPath: CloudPath, withPageToken pageToken: String?) -> Promise { + return resolvePath(forItemAt: cloudPath).then { item in + self.fetchItemList(for: item, pageToken: pageToken) + } + } + + public func downloadFile(from cloudPath: CloudPath, to localURL: URL, onTaskCreation: ((URLSessionDownloadTask?) -> Void)?) -> Promise { + precondition(localURL.isFileURL) + if FileManager.default.fileExists(atPath: localURL.path) { + return Promise(CloudProviderError.itemAlreadyExists) + } + return resolvePath(forItemAt: cloudPath).then { item in + self.downloadFile(for: item, to: localURL) + } + } + + public func uploadFile(from localURL: URL, to cloudPath: CloudPath, replaceExisting: Bool, onTaskCreation: ((URLSessionUploadTask?) -> Void)?) -> Promise { + precondition(localURL.isFileURL) + var isDirectory: ObjCBool = false + let fileExists = FileManager.default.fileExists(atPath: localURL.path, isDirectory: &isDirectory) + if !fileExists { + return Promise(CloudProviderError.itemNotFound) + } + if isDirectory.boolValue { + return Promise(CloudProviderError.itemTypeMismatch) + } + return fetchItemMetadata(at: cloudPath).then { metadata -> Void in + if !replaceExisting || (replaceExisting && metadata.itemType == .folder) { + throw CloudProviderError.itemAlreadyExists + } + + }.recover { error -> Void in + guard case CloudProviderError.itemNotFound = error else { + throw error + } + }.then { _ -> Promise in + return self.resolveParentPath(forItemAt: cloudPath) + }.then { parentItem in + return self.uploadFile(for: parentItem, from: localURL, to: cloudPath) + } + } + + public func createFolder(at cloudPath: CloudPath) -> Promise { + return checkForItemExistence(at: cloudPath).then { itemExists in + if itemExists { + throw CloudProviderError.itemAlreadyExists + } + }.then { _ -> Promise in + return self.resolveParentPath(forItemAt: cloudPath) + }.then { parentItem in + return self.createFolder(for: parentItem, with: cloudPath.lastPathComponent) + } + } + + public func deleteFile(at cloudPath: CloudPath) -> Promise { + return resolvePath(forItemAt: cloudPath).then { item in + self.deleteFile(for: item) + } + } + + public func deleteFolder(at cloudPath: CloudPath) -> Promise { + return resolvePath(forItemAt: cloudPath).then { item in + self.deleteFolder(for: item) + } + } + + public func moveFile(from sourceCloudPath: CloudPath, to targetCloudPath: CloudPath) -> Promise { + return checkForItemExistence(at: targetCloudPath).then { itemExists -> Void in + if itemExists { + throw CloudProviderError.itemAlreadyExists + } + }.then { + return all(self.resolvePath(forItemAt: sourceCloudPath), self.resolveParentPath(forItemAt: targetCloudPath)) + }.then { item, targetParentItem in + return self.moveFile(from: item, toParent: targetParentItem, targetCloudPath: targetCloudPath) + } + } + + public func moveFolder(from sourceCloudPath: CloudPath, to targetCloudPath: CloudPath) -> Promise { + return checkForItemExistence(at: targetCloudPath).then { itemExists -> Void in + if itemExists { + throw CloudProviderError.itemAlreadyExists + } + }.then { + return all(self.resolvePath(forItemAt: sourceCloudPath), self.resolveParentPath(forItemAt: targetCloudPath)) + }.then { item, targetParentItem in + return self.moveFolder(from: item, toParent: targetParentItem, targetCloudPath: targetCloudPath) + } + } + + // MARK: - Operations + + private func fetchItemMetadata(for item: BoxItem) -> Promise { + if item.itemType == .file { + return fetchFileMetadata(for: item) + } else if item.itemType == .folder { + return fetchFolderMetadata(for: item) + } else { + let error = CloudProviderError.itemTypeMismatch + CloudAccessDDLogDebug("BoxCloudCloudProvider: fetchItemMetadata(for: \(item.identifier)) failed with error: \(error)") + return Promise(error) + } + } + + private func fetchFileMetadata(for item: BoxItem) -> Promise { + assert(item.itemType == .file) + CloudAccessDDLogDebug("BoxCloudProvider: fetchFileMetadata(for: \(item.identifier)) called") guard let client = credential.client else { return Promise(CloudProviderError.unauthorized) } - return Promise { fulfill, reject in - client.files.get(fileId: item.identifier, fields: ["name", "size", "modified_at"]) { result in - switch result { - case let .success(file): - do { - let metadata = self.convertToCloudItemMetadata(file, at: item.cloudPath) - try self.identifierCache.addOrUpdate(item) - CloudAccessDDLogDebug("BoxCloudProvider: fetchFileMetadata(for: \(item.identifier)) successful") - fulfill(metadata) - } catch { - CloudAccessDDLogDebug("BoxCloudProvider: fetchFileMetadata(for: \(item.identifier)) error: \(error)") - reject(error) - } - case let .failure(error): - CloudAccessDDLogDebug("BoxCloudProvider: fetchFileMetadata(for: \(item.identifier)) failed with error: \(error)") - if error.message == .unauthorizedAccess { - reject(CloudProviderError.unauthorized) - } else { - reject(error) - } - } - } - } - } - - private func fetchFolderMetadata(for item: BoxItem) -> Promise { - assert(item.itemType == .folder) - CloudAccessDDLogDebug("BoxCloudProvider: fetchFolderMetadata(for: \(item.identifier)) called") + return Promise { fulfill, reject in + client.files.get(fileId: item.identifier, fields: ["name", "size", "modified_at"]) { result in + switch result { + case let .success(file): + do { + let metadata = self.convertToCloudItemMetadata(file, at: item.cloudPath) + try self.identifierCache.addOrUpdate(item) + CloudAccessDDLogDebug("BoxCloudProvider: fetchFileMetadata(for: \(item.identifier)) successful") + fulfill(metadata) + } catch { + CloudAccessDDLogDebug("BoxCloudProvider: fetchFileMetadata(for: \(item.identifier)) error: \(error)") + reject(error) + } + case let .failure(error): + CloudAccessDDLogDebug("BoxCloudProvider: fetchFileMetadata(for: \(item.identifier)) failed with error: \(error)") + if error.message == .unauthorizedAccess { + reject(CloudProviderError.unauthorized) + } else { + reject(error) + } + } + } + } + } + + private func fetchFolderMetadata(for item: BoxItem) -> Promise { + assert(item.itemType == .folder) + CloudAccessDDLogDebug("BoxCloudProvider: fetchFolderMetadata(for: \(item.identifier)) called") guard let client = credential.client else { return Promise(CloudProviderError.unauthorized) } - return Promise { fulfill, reject in - client.folders.get(folderId: item.identifier, fields: ["name", "modified_at"]) { result in - switch result { - case let .success(folder): - do { - let metadata = self.convertToCloudItemMetadata(folder, at: item.cloudPath) - try self.identifierCache.addOrUpdate(item) - CloudAccessDDLogDebug("BoxCloudProvider: fetchFolderMetadata(for: \(item.identifier)) successful") - fulfill(metadata) - } catch { - CloudAccessDDLogDebug("BoxCloudProvider: fetchFolderMetadata(for: \(item.identifier)) error: \(error)") - reject(error) - } - case let .failure(error): - CloudAccessDDLogDebug("BoxCloudProvider: fetchFolderMetadata(for: \(item.identifier)) failed with error: \(error)") - reject(error) - } - } - } - } - - private func fetchItemList(for folderItem: BoxItem, pageToken: String?) -> Promise { - guard folderItem.itemType == .folder else { - return Promise(CloudProviderError.itemTypeMismatch) - } - + return Promise { fulfill, reject in + client.folders.get(folderId: item.identifier, fields: ["name", "modified_at"]) { result in + switch result { + case let .success(folder): + do { + let metadata = self.convertToCloudItemMetadata(folder, at: item.cloudPath) + try self.identifierCache.addOrUpdate(item) + CloudAccessDDLogDebug("BoxCloudProvider: fetchFolderMetadata(for: \(item.identifier)) successful") + fulfill(metadata) + } catch { + CloudAccessDDLogDebug("BoxCloudProvider: fetchFolderMetadata(for: \(item.identifier)) error: \(error)") + reject(error) + } + case let .failure(error): + CloudAccessDDLogDebug("BoxCloudProvider: fetchFolderMetadata(for: \(item.identifier)) failed with error: \(error)") + reject(error) + } + } + } + } + + private func fetchItemList(for folderItem: BoxItem, pageToken: String?) -> Promise { + guard folderItem.itemType == .folder else { + return Promise(CloudProviderError.itemTypeMismatch) + } + guard let client = credential.client else { return Promise(CloudProviderError.unauthorized) } - - return Promise { fulfill, reject in - let iterator = client.folders.listItems(folderId: folderItem.identifier, usemarker: true, marker: pageToken, limit: self.maxPageSize, fields: ["name", "size", "modified_at"]) - - iterator.next { result in - switch result { - case let .success(page): - let allItems = page.entries.compactMap { entry -> CloudItemMetadata? in - switch entry { - case let .file(file): - return self.convertToCloudItemMetadata(file, at: folderItem.cloudPath.appendingPathComponent(file.name ?? "")) - case let .folder(folder): - return self.convertToCloudItemMetadata(folder, at: folderItem.cloudPath.appendingPathComponent(folder.name ?? "")) - case .webLink: - // Handling of web links as required - return nil - } - } - - fulfill(CloudItemList(items: allItems, nextPageToken: page.nextMarker)) - - case let .failure(error): - reject(CloudProviderError.pageTokenInvalid) - } - } - } - } - - private func downloadFile(for item: BoxItem, to localURL: URL) -> Promise { - CloudAccessDDLogDebug("BoxCloudProvider: downloadFile(for: \(item.identifier), to: \(localURL)) called") - guard item.itemType == .file else { - return Promise(CloudProviderError.itemTypeMismatch) - } - + + return Promise { fulfill, reject in + let iterator = client.folders.listItems(folderId: folderItem.identifier, usemarker: true, marker: pageToken, limit: self.maxPageSize, fields: ["name", "size", "modified_at"]) + + iterator.next { result in + switch result { + case let .success(page): + let allItems = page.entries.compactMap { entry -> CloudItemMetadata? in + switch entry { + case let .file(file): + return self.convertToCloudItemMetadata(file, at: folderItem.cloudPath.appendingPathComponent(file.name ?? "")) + case let .folder(folder): + return self.convertToCloudItemMetadata(folder, at: folderItem.cloudPath.appendingPathComponent(folder.name ?? "")) + case .webLink: + // Handling of web links as required + return nil + } + } + + fulfill(CloudItemList(items: allItems, nextPageToken: page.nextMarker)) + + case let .failure(error): + reject(CloudProviderError.pageTokenInvalid) + } + } + } + } + + private func downloadFile(for item: BoxItem, to localURL: URL) -> Promise { + CloudAccessDDLogDebug("BoxCloudProvider: downloadFile(for: \(item.identifier), to: \(localURL)) called") + guard item.itemType == .file else { + return Promise(CloudProviderError.itemTypeMismatch) + } + guard let client = credential.client else { return Promise(CloudProviderError.unauthorized) } - - return Promise { fulfill, reject in - client.files.download(fileId: item.identifier, destinationURL: localURL) { result in - switch result { - case .success: - CloudAccessDDLogDebug("BoxCloudProvider: downloadFile(for: \(item.identifier), to: \(localURL)) finished downloading") - fulfill(()) - case let .failure(error): - CloudAccessDDLogDebug("BoxCloudProvider: downloadFile(for: \(item.identifier), to: \(localURL)) failed with error: \(error)") - if error.message == .unauthorizedAccess { - reject(CloudProviderError.unauthorized) - } else { - reject(error) - } - } - } - } - } - - private func uploadFile(for parentItem: BoxItem, from localURL: URL, to cloudPath: CloudPath) -> Promise { + + return Promise { fulfill, reject in + client.files.download(fileId: item.identifier, destinationURL: localURL) { result in + switch result { + case .success: + CloudAccessDDLogDebug("BoxCloudProvider: downloadFile(for: \(item.identifier), to: \(localURL)) finished downloading") + fulfill(()) + case let .failure(error): + CloudAccessDDLogDebug("BoxCloudProvider: downloadFile(for: \(item.identifier), to: \(localURL)) failed with error: \(error)") + if error.message == .unauthorizedAccess { + reject(CloudProviderError.unauthorized) + } else { + reject(error) + } + } + } + } + } + + private func uploadFile(for parentItem: BoxItem, from localURL: URL, to cloudPath: CloudPath) -> Promise { guard let client = credential.client else { return Promise(CloudProviderError.unauthorized) } - - return Promise { fulfill, reject in - let targetFileName = cloudPath.lastPathComponent - - guard let data = try? Data(contentsOf: localURL) else { - reject(CloudProviderError.itemNotFound) - return - } - - self.resolvePath(forItemAt: cloudPath).then { existingItem -> Void in - client.files.uploadVersion(forFile: existingItem.identifier, data: data, completion: { result in - switch result { - case let .success(updatedFile): - let metadata = self.convertToCloudItemMetadata(updatedFile, at: cloudPath) - fulfill(metadata) - case let .failure(error): - reject(error) - } - }) - }.recover { error -> Void in - guard case CloudProviderError.itemNotFound = error else { - throw error - } - client.files.upload(data: data, name: targetFileName, parentId: parentItem.identifier, completion: { result in - switch result { - case let .success(newFile): - let metadata = self.convertToCloudItemMetadata(newFile, at: cloudPath) - fulfill(metadata) - case let .failure(error): - if error.message == .unauthorizedAccess { - reject(CloudProviderError.unauthorized) - } else { - reject(error) - } - } - }) - } - } - } - - private func createFolder(for parentItem: BoxItem, with name: String) -> Promise { - CloudAccessDDLogDebug("BoxCloudProvider: createFolder(for: \(parentItem.identifier), with: \(name)) called") + + return Promise { fulfill, reject in + let targetFileName = cloudPath.lastPathComponent + + guard let data = try? Data(contentsOf: localURL) else { + reject(CloudProviderError.itemNotFound) + return + } + + self.resolvePath(forItemAt: cloudPath).then { existingItem -> Void in + client.files.uploadVersion(forFile: existingItem.identifier, data: data, completion: { result in + switch result { + case let .success(updatedFile): + let metadata = self.convertToCloudItemMetadata(updatedFile, at: cloudPath) + fulfill(metadata) + case let .failure(error): + reject(error) + } + }) + }.recover { error -> Void in + guard case CloudProviderError.itemNotFound = error else { + throw error + } + client.files.upload(data: data, name: targetFileName, parentId: parentItem.identifier, completion: { result in + switch result { + case let .success(newFile): + let metadata = self.convertToCloudItemMetadata(newFile, at: cloudPath) + fulfill(metadata) + case let .failure(error): + if error.message == .unauthorizedAccess { + reject(CloudProviderError.unauthorized) + } else { + reject(error) + } + } + }) + } + } + } + + private func createFolder(for parentItem: BoxItem, with name: String) -> Promise { + CloudAccessDDLogDebug("BoxCloudProvider: createFolder(for: \(parentItem.identifier), with: \(name)) called") guard let client = credential.client else { return Promise(CloudProviderError.unauthorized) } - return Promise { fulfill, reject in - client.folders.create(name: name, parentId: parentItem.identifier) { result in - switch result { - case let .success(folder): - CloudAccessDDLogDebug("BoxCloudProvider: createFolder successful with folder ID: \(folder.id)") - do { - let newItem = BoxItem(cloudPath: parentItem.cloudPath.appendingPathComponent(name), identifier: folder.id, itemType: .folder) - try self.identifierCache.addOrUpdate(newItem) - fulfill(()) - } catch { - reject(error) - } - case let .failure(error): - CloudAccessDDLogDebug("BoxCloudProvider: createFolder failed with error: \(error.localizedDescription)") - if error.message == .unauthorizedAccess { - reject(CloudProviderError.unauthorized) - } else { - reject(error) - } - } - } - } - } - - private func deleteFile(for item: BoxItem) -> Promise { - CloudAccessDDLogDebug("BoxCloudProvider: deleteFile(for: \(item.identifier)) called") - guard item.itemType == .file else { - return Promise(CloudProviderError.itemTypeMismatch) - } - + return Promise { fulfill, reject in + client.folders.create(name: name, parentId: parentItem.identifier) { result in + switch result { + case let .success(folder): + CloudAccessDDLogDebug("BoxCloudProvider: createFolder successful with folder ID: \(folder.id)") + do { + let newItem = BoxItem(cloudPath: parentItem.cloudPath.appendingPathComponent(name), identifier: folder.id, itemType: .folder) + try self.identifierCache.addOrUpdate(newItem) + fulfill(()) + } catch { + reject(error) + } + case let .failure(error): + CloudAccessDDLogDebug("BoxCloudProvider: createFolder failed with error: \(error.localizedDescription)") + if error.message == .unauthorizedAccess { + reject(CloudProviderError.unauthorized) + } else { + reject(error) + } + } + } + } + } + + private func deleteFile(for item: BoxItem) -> Promise { + CloudAccessDDLogDebug("BoxCloudProvider: deleteFile(for: \(item.identifier)) called") + guard item.itemType == .file else { + return Promise(CloudProviderError.itemTypeMismatch) + } + guard let client = credential.client else { return Promise(CloudProviderError.unauthorized) } - - return Promise { fulfill, reject in - client.files.delete(fileId: item.identifier) { result in - switch result { - case .success: - CloudAccessDDLogDebug("BoxCloudProvider: deleteFile(for: \(item.identifier)) succeeded") - do { - try self.identifierCache.invalidate(item) - fulfill(()) - } catch { - CloudAccessDDLogDebug("BoxCloudProvider: Cache update failed with error: \(error)") - reject(error) - } - case let .failure(error): - CloudAccessDDLogDebug("BoxCloudProvider: deleteFile(for: \(item.identifier)) failed with error: \(error)") - if case BoxSDKErrorEnum.notFound = error.message { - reject(CloudProviderError.itemNotFound) - } else { - if error.message == .unauthorizedAccess { - reject(CloudProviderError.unauthorized) - } else { - reject(error) - } - } - } - } - } - } - - private func deleteFolder(for item: BoxItem) -> Promise { - CloudAccessDDLogDebug("BoxCloudProvider: deleteFolder(for: \(item.identifier)) called") - guard item.itemType == .folder else { - return Promise(CloudProviderError.itemTypeMismatch) - } - + + return Promise { fulfill, reject in + client.files.delete(fileId: item.identifier) { result in + switch result { + case .success: + CloudAccessDDLogDebug("BoxCloudProvider: deleteFile(for: \(item.identifier)) succeeded") + do { + try self.identifierCache.invalidate(item) + fulfill(()) + } catch { + CloudAccessDDLogDebug("BoxCloudProvider: Cache update failed with error: \(error)") + reject(error) + } + case let .failure(error): + CloudAccessDDLogDebug("BoxCloudProvider: deleteFile(for: \(item.identifier)) failed with error: \(error)") + if case BoxSDKErrorEnum.notFound = error.message { + reject(CloudProviderError.itemNotFound) + } else { + if error.message == .unauthorizedAccess { + reject(CloudProviderError.unauthorized) + } else { + reject(error) + } + } + } + } + } + } + + private func deleteFolder(for item: BoxItem) -> Promise { + CloudAccessDDLogDebug("BoxCloudProvider: deleteFolder(for: \(item.identifier)) called") + guard item.itemType == .folder else { + return Promise(CloudProviderError.itemTypeMismatch) + } + guard let client = credential.client else { return Promise(CloudProviderError.unauthorized) } - - return Promise { fulfill, reject in - client.folders.delete(folderId: item.identifier, recursive: true) { result in - switch result { - case .success: - CloudAccessDDLogDebug("BoxCloudProvider: deleteFolder(for: \(item.identifier)) succeeded") - do { - try self.identifierCache.invalidate(item) - fulfill(()) - } catch { - CloudAccessDDLogDebug("BoxCloudProvider: Cache update failed with error: \(error)") - reject(error) - } - case let .failure(error): - CloudAccessDDLogDebug("BoxCloudProvider: deleteFolder(for: \(item.identifier)) failed with error: \(error)") - if case BoxSDKErrorEnum.notFound = error.message { - reject(CloudProviderError.itemNotFound) - } else { - if error.message == .unauthorizedAccess { - reject(CloudProviderError.unauthorized) - } else { - reject(error) - } - } - } - } - } - } - - private func moveFile(from sourceItem: BoxItem, toParent targetParentItem: BoxItem, targetCloudPath: CloudPath) -> Promise { - CloudAccessDDLogDebug("BoxCloudProvider: moveFile(from: \(sourceItem.identifier), toParent: \(targetParentItem.identifier), targetCloudPath: \(targetCloudPath.path)) called") + + return Promise { fulfill, reject in + client.folders.delete(folderId: item.identifier, recursive: true) { result in + switch result { + case .success: + CloudAccessDDLogDebug("BoxCloudProvider: deleteFolder(for: \(item.identifier)) succeeded") + do { + try self.identifierCache.invalidate(item) + fulfill(()) + } catch { + CloudAccessDDLogDebug("BoxCloudProvider: Cache update failed with error: \(error)") + reject(error) + } + case let .failure(error): + CloudAccessDDLogDebug("BoxCloudProvider: deleteFolder(for: \(item.identifier)) failed with error: \(error)") + if case BoxSDKErrorEnum.notFound = error.message { + reject(CloudProviderError.itemNotFound) + } else { + if error.message == .unauthorizedAccess { + reject(CloudProviderError.unauthorized) + } else { + reject(error) + } + } + } + } + } + } + + private func moveFile(from sourceItem: BoxItem, toParent targetParentItem: BoxItem, targetCloudPath: CloudPath) -> Promise { + CloudAccessDDLogDebug("BoxCloudProvider: moveFile(from: \(sourceItem.identifier), toParent: \(targetParentItem.identifier), targetCloudPath: \(targetCloudPath.path)) called") guard let client = credential.client else { return Promise(CloudProviderError.unauthorized) } - - return Promise { fulfill, reject in - let newName = targetCloudPath.lastPathComponent - client.files.update(fileId: sourceItem.identifier, name: newName, parentId: targetParentItem.identifier) { result in - switch result { - case .success: - CloudAccessDDLogDebug("BoxCloudProvider: moveFile succeeded for \(sourceItem.identifier) to \(targetCloudPath.path)") - do { - try self.identifierCache.invalidate(sourceItem) - let newItem = BoxItem(cloudPath: targetCloudPath, identifier: sourceItem.identifier, itemType: sourceItem.itemType) - try self.identifierCache.addOrUpdate(newItem) - fulfill(()) - } catch { - reject(error) - } - case let .failure(error): - CloudAccessDDLogDebug("BoxCloudProvider: moveFile failed for \(sourceItem.identifier) with error: \(error)") - if error.message == .unauthorizedAccess { - reject(CloudProviderError.unauthorized) - } else { - reject(error) - } - } - } - } - } - - private func moveFolder(from sourceItem: BoxItem, toParent targetParentItem: BoxItem, targetCloudPath: CloudPath) -> Promise { - CloudAccessDDLogDebug("BoxCloudProvider: moveFolder(from: \(sourceItem.identifier), toParent: \(targetParentItem.identifier), targetCloudPath: \(targetCloudPath.path)) called") + + return Promise { fulfill, reject in + let newName = targetCloudPath.lastPathComponent + client.files.update(fileId: sourceItem.identifier, name: newName, parentId: targetParentItem.identifier) { result in + switch result { + case .success: + CloudAccessDDLogDebug("BoxCloudProvider: moveFile succeeded for \(sourceItem.identifier) to \(targetCloudPath.path)") + do { + try self.identifierCache.invalidate(sourceItem) + let newItem = BoxItem(cloudPath: targetCloudPath, identifier: sourceItem.identifier, itemType: sourceItem.itemType) + try self.identifierCache.addOrUpdate(newItem) + fulfill(()) + } catch { + reject(error) + } + case let .failure(error): + CloudAccessDDLogDebug("BoxCloudProvider: moveFile failed for \(sourceItem.identifier) with error: \(error)") + if error.message == .unauthorizedAccess { + reject(CloudProviderError.unauthorized) + } else { + reject(error) + } + } + } + } + } + + private func moveFolder(from sourceItem: BoxItem, toParent targetParentItem: BoxItem, targetCloudPath: CloudPath) -> Promise { + CloudAccessDDLogDebug("BoxCloudProvider: moveFolder(from: \(sourceItem.identifier), toParent: \(targetParentItem.identifier), targetCloudPath: \(targetCloudPath.path)) called") guard let client = credential.client else { return Promise(CloudProviderError.unauthorized) } - - return Promise { fulfill, reject in - let newName = targetCloudPath.lastPathComponent - client.folders.update(folderId: sourceItem.identifier, name: newName, parentId: targetParentItem.identifier) { result in - switch result { - case .success: - CloudAccessDDLogDebug("BoxCloudProvider: moveFolder succeeded for \(sourceItem.identifier) to \(targetCloudPath.path)") - do { - try self.identifierCache.invalidate(sourceItem) - let newItem = BoxItem(cloudPath: targetCloudPath, identifier: sourceItem.identifier, itemType: sourceItem.itemType) - try self.identifierCache.addOrUpdate(newItem) - fulfill(()) - } catch { - reject(error) - } - case let .failure(error): - CloudAccessDDLogDebug("BoxCloudProvider: moveFolder failed for \(sourceItem.identifier) with error: \(error)") - if error.message == .unauthorizedAccess { - reject(CloudProviderError.unauthorized) - } else { - reject(error) - } - } - } - } - } - - // MARK: - Resolve Path - - private func resolvePath(forItemAt cloudPath: CloudPath) -> Promise { - var pathToCheckForCache = cloudPath - var cachedItem = identifierCache.get(pathToCheckForCache) - while cachedItem == nil, !pathToCheckForCache.pathComponents.isEmpty { - pathToCheckForCache = pathToCheckForCache.deletingLastPathComponent() - cachedItem = identifierCache.get(pathToCheckForCache) - } - guard let item = cachedItem else { - return Promise(BoxError.inconsistentCache) - } - if pathToCheckForCache != cloudPath { - return traverseThroughPath(from: pathToCheckForCache, to: cloudPath, withStartItem: item) - } - return Promise(item) - } - - private func resolveParentPath(forItemAt cloudPath: CloudPath) -> Promise { - let parentCloudPath = cloudPath.deletingLastPathComponent() - return resolvePath(forItemAt: parentCloudPath).recover { error -> BoxItem in - if case CloudProviderError.itemNotFound = error { - throw CloudProviderError.parentFolderDoesNotExist - } else { - throw error - } - } - } - - private func traverseThroughPath(from startCloudPath: CloudPath, to endCloudPath: CloudPath, withStartItem startItem: BoxItem) -> Promise { - assert(startCloudPath.pathComponents.count < endCloudPath.pathComponents.count) - let startIndex = startCloudPath.pathComponents.count - let endIndex = endCloudPath.pathComponents.count - var currentPath = startCloudPath - var parentItem = startItem - return Promise(on: .global()) { fulfill, _ in - for i in startIndex ..< endIndex { - let itemName = endCloudPath.pathComponents[i] - currentPath = currentPath.appendingPathComponent(itemName) - parentItem = try awaitPromise(self.getBoxItem(for: itemName, withParentItem: parentItem)) - try self.identifierCache.addOrUpdate(parentItem) - } - fulfill(parentItem) - } - } - - func getBoxItem(for name: String, withParentItem parentItem: BoxItem) -> Promise { + + return Promise { fulfill, reject in + let newName = targetCloudPath.lastPathComponent + client.folders.update(folderId: sourceItem.identifier, name: newName, parentId: targetParentItem.identifier) { result in + switch result { + case .success: + CloudAccessDDLogDebug("BoxCloudProvider: moveFolder succeeded for \(sourceItem.identifier) to \(targetCloudPath.path)") + do { + try self.identifierCache.invalidate(sourceItem) + let newItem = BoxItem(cloudPath: targetCloudPath, identifier: sourceItem.identifier, itemType: sourceItem.itemType) + try self.identifierCache.addOrUpdate(newItem) + fulfill(()) + } catch { + reject(error) + } + case let .failure(error): + CloudAccessDDLogDebug("BoxCloudProvider: moveFolder failed for \(sourceItem.identifier) with error: \(error)") + if error.message == .unauthorizedAccess { + reject(CloudProviderError.unauthorized) + } else { + reject(error) + } + } + } + } + } + + // MARK: - Resolve Path + + private func resolvePath(forItemAt cloudPath: CloudPath) -> Promise { + var pathToCheckForCache = cloudPath + var cachedItem = identifierCache.get(pathToCheckForCache) + while cachedItem == nil, !pathToCheckForCache.pathComponents.isEmpty { + pathToCheckForCache = pathToCheckForCache.deletingLastPathComponent() + cachedItem = identifierCache.get(pathToCheckForCache) + } + guard let item = cachedItem else { + return Promise(BoxError.inconsistentCache) + } + if pathToCheckForCache != cloudPath { + return traverseThroughPath(from: pathToCheckForCache, to: cloudPath, withStartItem: item) + } + return Promise(item) + } + + private func resolveParentPath(forItemAt cloudPath: CloudPath) -> Promise { + let parentCloudPath = cloudPath.deletingLastPathComponent() + return resolvePath(forItemAt: parentCloudPath).recover { error -> BoxItem in + if case CloudProviderError.itemNotFound = error { + throw CloudProviderError.parentFolderDoesNotExist + } else { + throw error + } + } + } + + private func traverseThroughPath(from startCloudPath: CloudPath, to endCloudPath: CloudPath, withStartItem startItem: BoxItem) -> Promise { + assert(startCloudPath.pathComponents.count < endCloudPath.pathComponents.count) + let startIndex = startCloudPath.pathComponents.count + let endIndex = endCloudPath.pathComponents.count + var currentPath = startCloudPath + var parentItem = startItem + return Promise(on: .global()) { fulfill, _ in + for i in startIndex ..< endIndex { + let itemName = endCloudPath.pathComponents[i] + currentPath = currentPath.appendingPathComponent(itemName) + parentItem = try awaitPromise(self.getBoxItem(for: itemName, withParentItem: parentItem)) + try self.identifierCache.addOrUpdate(parentItem) + } + fulfill(parentItem) + } + } + + func getBoxItem(for name: String, withParentItem parentItem: BoxItem) -> Promise { guard let client = credential.client else { return Promise(CloudProviderError.unauthorized) } - - return Promise { fulfill, reject in - CloudAccessDDLogDebug("BoxCloudCloudProvider: getBoxItem(for: \(name), withParentItem: \(parentItem.identifier)) called") - - let iterator = client.folders.listItems(folderId: parentItem.identifier) - iterator.next { result in - switch result { - case let .success(page): - for item in page.entries { - do { - if let mappedItem = try self.mapFolderItemToBoxItem(name: name, parentItem: parentItem, item: item) { - fulfill(mappedItem) - return - } - } catch { - reject(error) - return - } - } - reject(CloudProviderError.itemNotFound) - case let .failure(error): - if error.message == .unauthorizedAccess { - reject(CloudProviderError.unauthorized) - } else { - reject(error) - } - } - } - } - } - - func mapFolderItemToBoxItem(name: String, parentItem: BoxItem, item: FolderItem) throws -> BoxItem? { - switch item { - case let .file(file) where file.name == name: - return BoxItem(cloudPath: parentItem.cloudPath.appendingPathComponent(name), file: file) - case let .folder(folder) where folder.name == name: - return BoxItem(cloudPath: parentItem.cloudPath.appendingPathComponent(name), folder: folder) - case .webLink: - throw BoxError.unexpectedContent - default: - return nil - } - } - - // MARK: - Helpers - - private func convertToCloudItemMetadata(_ content: FolderItem, at cloudPath: CloudPath) throws -> CloudItemMetadata { - switch content { - case let .file(fileMetadata): - return convertToCloudItemMetadata(fileMetadata, at: cloudPath) - case let .folder(folderMetadata): - return convertToCloudItemMetadata(folderMetadata, at: cloudPath) - default: // Refactor - no default - throw BoxError.unexpectedContent - } - } - - private func convertToCloudItemMetadata(_ metadata: File, at cloudPath: CloudPath) -> CloudItemMetadata { - let name = metadata.name ?? "" - let itemType = CloudItemType.file - let lastModifiedDate = metadata.modifiedAt - let size = metadata.size - return CloudItemMetadata(name: name, cloudPath: cloudPath, itemType: itemType, lastModifiedDate: lastModifiedDate, size: size) - } - - private func convertToCloudItemMetadata(_ metadata: Folder, at cloudPath: CloudPath) -> CloudItemMetadata { - let name = metadata.name ?? "" - let itemType = CloudItemType.folder - let lastModifiedDate = metadata.modifiedAt - return CloudItemMetadata(name: name, cloudPath: cloudPath, itemType: itemType, lastModifiedDate: lastModifiedDate, size: nil) - } - - private func convertToCloudItemList(_ contents: [FolderItem], at cloudPath: CloudPath) throws -> CloudItemList { - var items = [CloudItemMetadata]() - for content in contents { - switch content { - case let .file(fileMetadata): - let itemCloudPath = cloudPath.appendingPathComponent(fileMetadata.name ?? "") - let itemMetadata = convertToCloudItemMetadata(fileMetadata, at: itemCloudPath) - items.append(itemMetadata) - case let .folder(folderMetadata): - let itemCloudPath = cloudPath.appendingPathComponent(folderMetadata.name ?? "") - let itemMetadata = convertToCloudItemMetadata(folderMetadata, at: itemCloudPath) - items.append(itemMetadata) - default: - throw BoxError.unexpectedContent - } - } - return CloudItemList(items: items, nextPageToken: nil) - } + + return Promise { fulfill, reject in + CloudAccessDDLogDebug("BoxCloudCloudProvider: getBoxItem(for: \(name), withParentItem: \(parentItem.identifier)) called") + + let iterator = client.folders.listItems(folderId: parentItem.identifier) + iterator.next { result in + switch result { + case let .success(page): + for item in page.entries { + do { + if let mappedItem = try self.mapFolderItemToBoxItem(name: name, parentItem: parentItem, item: item) { + fulfill(mappedItem) + return + } + } catch { + reject(error) + return + } + } + reject(CloudProviderError.itemNotFound) + case let .failure(error): + if error.message == .unauthorizedAccess { + reject(CloudProviderError.unauthorized) + } else { + reject(error) + } + } + } + } + } + + func mapFolderItemToBoxItem(name: String, parentItem: BoxItem, item: FolderItem) throws -> BoxItem? { + switch item { + case let .file(file) where file.name == name: + return BoxItem(cloudPath: parentItem.cloudPath.appendingPathComponent(name), file: file) + case let .folder(folder) where folder.name == name: + return BoxItem(cloudPath: parentItem.cloudPath.appendingPathComponent(name), folder: folder) + case .webLink: + throw BoxError.unexpectedContent + default: + return nil + } + } + + // MARK: - Helpers + + private func convertToCloudItemMetadata(_ content: FolderItem, at cloudPath: CloudPath) throws -> CloudItemMetadata { + switch content { + case let .file(fileMetadata): + return convertToCloudItemMetadata(fileMetadata, at: cloudPath) + case let .folder(folderMetadata): + return convertToCloudItemMetadata(folderMetadata, at: cloudPath) + default: + throw BoxError.unexpectedContent + } + } + + private func convertToCloudItemMetadata(_ metadata: File, at cloudPath: CloudPath) -> CloudItemMetadata { + let name = metadata.name ?? "" + let itemType = CloudItemType.file + let lastModifiedDate = metadata.modifiedAt + let size = metadata.size + return CloudItemMetadata(name: name, cloudPath: cloudPath, itemType: itemType, lastModifiedDate: lastModifiedDate, size: size) + } + + private func convertToCloudItemMetadata(_ metadata: Folder, at cloudPath: CloudPath) -> CloudItemMetadata { + let name = metadata.name ?? "" + let itemType = CloudItemType.folder + let lastModifiedDate = metadata.modifiedAt + return CloudItemMetadata(name: name, cloudPath: cloudPath, itemType: itemType, lastModifiedDate: lastModifiedDate, size: nil) + } + + private func convertToCloudItemList(_ contents: [FolderItem], at cloudPath: CloudPath) throws -> CloudItemList { + var items = [CloudItemMetadata]() + for content in contents { + switch content { + case let .file(fileMetadata): + let itemCloudPath = cloudPath.appendingPathComponent(fileMetadata.name ?? "") + let itemMetadata = convertToCloudItemMetadata(fileMetadata, at: itemCloudPath) + items.append(itemMetadata) + case let .folder(folderMetadata): + let itemCloudPath = cloudPath.appendingPathComponent(folderMetadata.name ?? "") + let itemMetadata = convertToCloudItemMetadata(folderMetadata, at: itemCloudPath) + items.append(itemMetadata) + default: + throw BoxError.unexpectedContent + } + } + return CloudItemList(items: items, nextPageToken: nil) + } } diff --git a/Sources/CryptomatorCloudAccess/Box/BoxCredential.swift b/Sources/CryptomatorCloudAccess/Box/BoxCredential.swift index 03b75e3..6dfb26a 100644 --- a/Sources/CryptomatorCloudAccess/Box/BoxCredential.swift +++ b/Sources/CryptomatorCloudAccess/Box/BoxCredential.swift @@ -16,7 +16,6 @@ public enum BoxCredentialErrors: Error { public class BoxCredential { public internal(set) var client: BoxClient? - private(set) var userId: String? public init(tokenStore: TokenStore) { let sdk = BoxSDK(clientId: BoxSetup.constants.clientId, clientSecret: BoxSetup.constants.clientSecret) @@ -24,7 +23,6 @@ public class BoxCredential { switch result { case let .success(client): self.client = client - self.retrieveAndStoreUserId() case let .failure(error): break } @@ -60,14 +58,4 @@ public class BoxCredential { } } } - - private func retrieveAndStoreUserId() { - client?.users.getCurrent(fields: ["id"]) { [weak self] result in - switch result { - case let .success(user): - self?.userId = user.id - case .failure: break // TODO: Break ersetzen - } - } - } } diff --git a/Sources/CryptomatorCloudAccess/Box/BoxIdentifierCache.swift b/Sources/CryptomatorCloudAccess/Box/BoxIdentifierCache.swift index 805ba7c..daa078c 100644 --- a/Sources/CryptomatorCloudAccess/Box/BoxIdentifierCache.swift +++ b/Sources/CryptomatorCloudAccess/Box/BoxIdentifierCache.swift @@ -6,40 +6,40 @@ // Copyright © 2024 Skymatic GmbH. All rights reserved. // +import BoxSDK import Foundation import GRDB -import BoxSDK class BoxIdentifierCache { - private let inMemoryDB: DatabaseQueue + private let inMemoryDB: DatabaseQueue - init() throws { - self.inMemoryDB = DatabaseQueue() - try inMemoryDB.write { db in - try db.create(table: BoxItem.databaseTableName) { table in - table.column(BoxItem.cloudPathKey, .text).notNull().primaryKey() - table.column(BoxItem.identifierKey, .text).notNull() - table.column(BoxItem.itemTypeKey, .text).notNull() - } - try BoxItem(cloudPath: CloudPath("/"), identifier: "0", itemType: .folder).save(db) - } - } + init() throws { + self.inMemoryDB = DatabaseQueue() + try inMemoryDB.write { db in + try db.create(table: BoxItem.databaseTableName) { table in + table.column(BoxItem.cloudPathKey, .text).notNull().primaryKey() + table.column(BoxItem.identifierKey, .text).notNull() + table.column(BoxItem.itemTypeKey, .text).notNull() + } + try BoxItem(cloudPath: CloudPath("/"), identifier: "0", itemType: .folder).save(db) + } + } - func get(_ cloudPath: CloudPath) -> BoxItem? { - try? inMemoryDB.read { db in - return try BoxItem.fetchOne(db, key: cloudPath) - } - } + func get(_ cloudPath: CloudPath) -> BoxItem? { + try? inMemoryDB.read { db in + return try BoxItem.fetchOne(db, key: cloudPath) + } + } - func addOrUpdate(_ item: BoxItem) throws { - try inMemoryDB.write { db in - try item.save(db) - } - } + func addOrUpdate(_ item: BoxItem) throws { + try inMemoryDB.write { db in + try item.save(db) + } + } - func invalidate(_ item: BoxItem) throws { - try inMemoryDB.write { db in - try db.execute(sql: "DELETE FROM \(BoxItem.databaseTableName) WHERE \(BoxItem.cloudPathKey) LIKE ?", arguments: ["\(item.cloudPath.path)%"]) - } - } + func invalidate(_ item: BoxItem) throws { + try inMemoryDB.write { db in + try db.execute(sql: "DELETE FROM \(BoxItem.databaseTableName) WHERE \(BoxItem.cloudPathKey) LIKE ?", arguments: ["\(item.cloudPath.path)%"]) + } + } } diff --git a/Sources/CryptomatorCloudAccess/Box/BoxItem.swift b/Sources/CryptomatorCloudAccess/Box/BoxItem.swift index f1cbe93..752d123 100644 --- a/Sources/CryptomatorCloudAccess/Box/BoxItem.swift +++ b/Sources/CryptomatorCloudAccess/Box/BoxItem.swift @@ -11,45 +11,45 @@ import Foundation import GRDB struct BoxItem: Decodable, FetchableRecord, TableRecord, Equatable { - static let databaseTableName = "CachedEntries" - static let cloudPathKey = "cloudPath" - static let identifierKey = "identifier" - static let itemTypeKey = "itemType" + static let databaseTableName = "CachedEntries" + static let cloudPathKey = "cloudPath" + static let identifierKey = "identifier" + static let itemTypeKey = "itemType" - let cloudPath: CloudPath - let identifier: String - let itemType: CloudItemType + let cloudPath: CloudPath + let identifier: String + let itemType: CloudItemType } extension BoxItem { - init(cloudPath: CloudPath, folderItem: FolderItem) throws { - switch folderItem { - case let .file(file): - self.init(cloudPath: cloudPath, file: file) - case let .folder(folder): - self.init(cloudPath: cloudPath, folder: folder) - case let .webLink(webLink): - throw PCloudError.unexpectedContent - } - } + init(cloudPath: CloudPath, folderItem: FolderItem) throws { + switch folderItem { + case let .file(file): + self.init(cloudPath: cloudPath, file: file) + case let .folder(folder): + self.init(cloudPath: cloudPath, folder: folder) + case let .webLink(webLink): + throw PCloudError.unexpectedContent + } + } - init(cloudPath: CloudPath, file: File) { - self.cloudPath = cloudPath - self.identifier = file.id - self.itemType = .file - } + init(cloudPath: CloudPath, file: File) { + self.cloudPath = cloudPath + self.identifier = file.id + self.itemType = .file + } - init(cloudPath: CloudPath, folder: Folder) { - self.cloudPath = cloudPath - self.identifier = folder.id - self.itemType = .folder - } + init(cloudPath: CloudPath, folder: Folder) { + self.cloudPath = cloudPath + self.identifier = folder.id + self.itemType = .folder + } } extension BoxItem: PersistableRecord { - func encode(to container: inout PersistenceContainer) { - container[BoxItem.cloudPathKey] = cloudPath - container[BoxItem.identifierKey] = identifier - container[BoxItem.itemTypeKey] = itemType - } + func encode(to container: inout PersistenceContainer) { + container[BoxItem.cloudPathKey] = cloudPath + container[BoxItem.identifierKey] = identifier + container[BoxItem.itemTypeKey] = itemType + } } diff --git a/Sources/CryptomatorCloudAccess/PCloud/PCloudCredential.swift b/Sources/CryptomatorCloudAccess/PCloud/PCloudCredential.swift index cfefa5c..e68d974 100644 --- a/Sources/CryptomatorCloudAccess/PCloud/PCloudCredential.swift +++ b/Sources/CryptomatorCloudAccess/PCloud/PCloudCredential.swift @@ -19,8 +19,7 @@ public class PCloudCredential { private let client: PCloudClient public init(user: OAuth.User) { - - self.user = user + self.user = user self.client = PCloud.createClient(with: user) } diff --git a/Tests/CryptomatorCloudAccessIntegrationTests/Google Drive/GoogleDriveCloudProviderIntegrationTests.swift b/Tests/CryptomatorCloudAccessIntegrationTests/Google Drive/GoogleDriveCloudProviderIntegrationTests.swift index ed11bec..9ea9476 100644 --- a/Tests/CryptomatorCloudAccessIntegrationTests/Google Drive/GoogleDriveCloudProviderIntegrationTests.swift +++ b/Tests/CryptomatorCloudAccessIntegrationTests/Google Drive/GoogleDriveCloudProviderIntegrationTests.swift @@ -24,7 +24,7 @@ class GoogleDriveCloudProviderIntegrationTests: CloudAccessIntegrationTestWithAu override class func setUp() { integrationTestParentCloudPath = CloudPath("/iOS-IntegrationTests-Plain") let credential = GoogleDriveAuthenticatorMock.generateAuthorizedCredential(withRefreshToken: IntegrationTestSecrets.googleDriveRefreshToken, tokenUID: "IntegrationTest") - + setUpProvider = try! GoogleDriveCloudProvider(credential: credential, useBackgroundSession: false) super.setUp() } From ce58f0aa4bfb4af059d60a2b4134bd56feb70203 Mon Sep 17 00:00:00 2001 From: Majid Achhoud Date: Tue, 16 Apr 2024 11:16:16 +0200 Subject: [PATCH 05/21] Disable force_try in GoogleDriveIntegrationTests --- .../Google Drive/GoogleDriveCloudProviderIntegrationTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/CryptomatorCloudAccessIntegrationTests/Google Drive/GoogleDriveCloudProviderIntegrationTests.swift b/Tests/CryptomatorCloudAccessIntegrationTests/Google Drive/GoogleDriveCloudProviderIntegrationTests.swift index 9ea9476..4f5d142 100644 --- a/Tests/CryptomatorCloudAccessIntegrationTests/Google Drive/GoogleDriveCloudProviderIntegrationTests.swift +++ b/Tests/CryptomatorCloudAccessIntegrationTests/Google Drive/GoogleDriveCloudProviderIntegrationTests.swift @@ -24,7 +24,7 @@ class GoogleDriveCloudProviderIntegrationTests: CloudAccessIntegrationTestWithAu override class func setUp() { integrationTestParentCloudPath = CloudPath("/iOS-IntegrationTests-Plain") let credential = GoogleDriveAuthenticatorMock.generateAuthorizedCredential(withRefreshToken: IntegrationTestSecrets.googleDriveRefreshToken, tokenUID: "IntegrationTest") - + // swiftlint:disable:next force_try setUpProvider = try! GoogleDriveCloudProvider(credential: credential, useBackgroundSession: false) super.setUp() } From db2e16396062cd9fd93bbf13ee475049b7942701 Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Tue, 16 Apr 2024 13:09:44 +0200 Subject: [PATCH 06/21] Downgraded Package.resolved to version 2 --- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CryptomatorCloudAccess.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/CryptomatorCloudAccess.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 24acdb3..ee60dfd 100644 --- a/CryptomatorCloudAccess.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/CryptomatorCloudAccess.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,4 @@ { - "originHash" : "e5917b23d4b3d376d037268c2d81d7b09bc24bae3352b91337ce008605fe3761", "pins" : [ { "identity" : "appauth-ios", @@ -164,5 +163,5 @@ } } ], - "version" : 3 + "version" : 2 } From 4b7ec5ba78eb45e499d171035c539ae0db14afd4 Mon Sep 17 00:00:00 2001 From: Majid Achhoud Date: Thu, 18 Apr 2024 11:32:08 +0200 Subject: [PATCH 07/21] Fixed 'getUsername' --- Sources/CryptomatorCloudAccess/Box/BoxCredential.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/CryptomatorCloudAccess/Box/BoxCredential.swift b/Sources/CryptomatorCloudAccess/Box/BoxCredential.swift index 6dfb26a..af7f718 100644 --- a/Sources/CryptomatorCloudAccess/Box/BoxCredential.swift +++ b/Sources/CryptomatorCloudAccess/Box/BoxCredential.swift @@ -23,7 +23,7 @@ public class BoxCredential { switch result { case let .success(client): self.client = client - case let .failure(error): + case let .failure: break } } @@ -43,7 +43,7 @@ public class BoxCredential { } public func getUsername() -> Promise { - return Promise { fulfill, reject in + return Promise(on: .global()) { fulfill, reject in self.client?.users.getCurrent(fields: ["name"]) { result in switch result { case let .success(user): From 79bb7cdad6ff73482f5a0a66e20a718a67daf8a5 Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Tue, 23 Apr 2024 18:33:50 +0200 Subject: [PATCH 08/21] Removed unnecessary diff --- CryptomatorCloudAccess.xcodeproj/project.pbxproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/CryptomatorCloudAccess.xcodeproj/project.pbxproj b/CryptomatorCloudAccess.xcodeproj/project.pbxproj index 100192d..bcf3799 100644 --- a/CryptomatorCloudAccess.xcodeproj/project.pbxproj +++ b/CryptomatorCloudAccess.xcodeproj/project.pbxproj @@ -1603,7 +1603,6 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Sources/CryptomatorCloudAccess/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1630,7 +1629,6 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Sources/CryptomatorCloudAccess/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", From ee39812926a0b8f7ea9ebf4edba287475c17b66e Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Tue, 23 Apr 2024 18:34:12 +0200 Subject: [PATCH 09/21] Reordered entries in Package.swift --- Package.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Package.swift b/Package.swift index 4edc3fb..07a7cd2 100644 --- a/Package.swift +++ b/Package.swift @@ -27,9 +27,9 @@ let package = Package( .library(name: "CryptomatorCloudAccessCore", targets: ["CryptomatorCloudAccessCore"]) ], dependencies: [ - .package(url: "https://github.com/tobihagemann/JOSESwift.git", exact: "2.4.1-cryptomator"), .package(url: "https://github.com/AzureAD/microsoft-authentication-library-for-objc.git", .upToNextMinor(from: "1.3.0")), .package(url: "https://github.com/aws-amplify/aws-sdk-ios-spm.git", .upToNextMinor(from: "2.34.0")), + .package(url: "https://github.com/box/box-ios-sdk.git", .upToNextMinor(from: "5.5.0")), .package(url: "https://github.com/cryptomator/cryptolib-swift.git", .upToNextMinor(from: "1.1.0")), .package(url: "https://github.com/CocoaLumberjack/CocoaLumberjack.git", .upToNextMinor(from: "3.8.0")), .package(url: "https://github.com/google/google-api-objectivec-client-for-rest.git", .upToNextMinor(from: "3.4.0")), @@ -42,7 +42,7 @@ let package = Package( .package(url: "https://github.com/phil1995/dropbox-sdk-obj-c-spm.git", .upToNextMinor(from: "7.2.0")), .package(url: "https://github.com/phil1995/msgraph-sdk-objc-spm.git", .upToNextMinor(from: "1.0.0")), .package(url: "https://github.com/phil1995/msgraph-sdk-objc-models-spm.git", .upToNextMinor(from: "1.3.0")), - .package(url: "https://github.com/box/box-ios-sdk.git", .upToNextMinor(from: "5.5.0")) + .package(url: "https://github.com/tobihagemann/JOSESwift.git", exact: "2.4.1-cryptomator") ], targets: [ .target( @@ -50,6 +50,7 @@ let package = Package( dependencies: [ .product(name: "AWSCore", package: "aws-sdk-ios-spm"), .product(name: "AWSS3", package: "aws-sdk-ios-spm"), + .product(name: "BoxSDK", package: "box-ios-sdk"), .product(name: "CocoaLumberjackSwift", package: "CocoaLumberjack"), .product(name: "CryptomatorCryptoLib", package: "cryptolib-swift"), .product(name: "GoogleAPIClientForREST_Drive", package: "google-api-objectivec-client-for-rest"), @@ -62,8 +63,7 @@ let package = Package( .product(name: "MSGraphClientSDK", package: "msgraph-sdk-objc-models-spm"), .product(name: "ObjectiveDropboxOfficial", package: "dropbox-sdk-obj-c-spm"), .product(name: "PCloudSDKSwift", package: "pcloud-sdk-swift"), - .product(name: "Promises", package: "promises"), - .product(name: "BoxSDK", package: "box-ios-sdk") + .product(name: "Promises", package: "promises") ], path: "Sources/CryptomatorCloudAccess", exclude: appExtensionUnsafeSources From 0462aa1ff4036110f548f5bb1894b6f9eee53ef9 Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Tue, 23 Apr 2024 18:35:12 +0200 Subject: [PATCH 10/21] Updated file header comments --- Sources/CryptomatorCloudAccess/Box/BoxAuthenticator.swift | 3 ++- Sources/CryptomatorCloudAccess/Box/BoxCredential.swift | 3 ++- Sources/CryptomatorCloudAccess/Box/BoxSetup.swift | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Sources/CryptomatorCloudAccess/Box/BoxAuthenticator.swift b/Sources/CryptomatorCloudAccess/Box/BoxAuthenticator.swift index 7101fbd..40aa768 100644 --- a/Sources/CryptomatorCloudAccess/Box/BoxAuthenticator.swift +++ b/Sources/CryptomatorCloudAccess/Box/BoxAuthenticator.swift @@ -1,8 +1,9 @@ // // BoxAuthenticator.swift -// +// CryptomatorCloudAccess // // Created by Majid Achhoud on 18.03.24. +// Copyright © 2024 Skymatic GmbH. All rights reserved. // #if canImport(CryptomatorCloudAccessCore) diff --git a/Sources/CryptomatorCloudAccess/Box/BoxCredential.swift b/Sources/CryptomatorCloudAccess/Box/BoxCredential.swift index af7f718..5337dcc 100644 --- a/Sources/CryptomatorCloudAccess/Box/BoxCredential.swift +++ b/Sources/CryptomatorCloudAccess/Box/BoxCredential.swift @@ -1,8 +1,9 @@ // // BoxCredential.swift -// +// CryptomatorCloudAccess // // Created by Majid Achhoud on 19.03.24. +// Copyright © 2024 Skymatic GmbH. All rights reserved. // import AuthenticationServices diff --git a/Sources/CryptomatorCloudAccess/Box/BoxSetup.swift b/Sources/CryptomatorCloudAccess/Box/BoxSetup.swift index f4214be..222039f 100644 --- a/Sources/CryptomatorCloudAccess/Box/BoxSetup.swift +++ b/Sources/CryptomatorCloudAccess/Box/BoxSetup.swift @@ -1,8 +1,9 @@ // // BoxSetup.swift -// +// CryptomatorCloudAccess // // Created by Majid Achhoud on 18.03.24. +// Copyright © 2024 Skymatic GmbH. All rights reserved. // import Foundation From c86a6c70e7931b73f8943e6af39dc1ec4914aa9e Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Tue, 23 Apr 2024 18:39:12 +0200 Subject: [PATCH 11/21] Fixed typo in debug logs --- Sources/CryptomatorCloudAccess/Box/BoxCloudProvider.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/CryptomatorCloudAccess/Box/BoxCloudProvider.swift b/Sources/CryptomatorCloudAccess/Box/BoxCloudProvider.swift index 654ad92..85c9088 100644 --- a/Sources/CryptomatorCloudAccess/Box/BoxCloudProvider.swift +++ b/Sources/CryptomatorCloudAccess/Box/BoxCloudProvider.swift @@ -126,7 +126,7 @@ public class BoxCloudProvider: CloudProvider { return fetchFolderMetadata(for: item) } else { let error = CloudProviderError.itemTypeMismatch - CloudAccessDDLogDebug("BoxCloudCloudProvider: fetchItemMetadata(for: \(item.identifier)) failed with error: \(error)") + CloudAccessDDLogDebug("BoxCloudProvider: fetchItemMetadata(for: \(item.identifier)) failed with error: \(error)") return Promise(error) } } @@ -518,7 +518,7 @@ public class BoxCloudProvider: CloudProvider { } return Promise { fulfill, reject in - CloudAccessDDLogDebug("BoxCloudCloudProvider: getBoxItem(for: \(name), withParentItem: \(parentItem.identifier)) called") + CloudAccessDDLogDebug("BoxCloudProvider: getBoxItem(for: \(name), withParentItem: \(parentItem.identifier)) called") let iterator = client.folders.listItems(folderId: parentItem.identifier) iterator.next { result in From f995ff9c7ee3d3ff7b3d6d258788c715d33ec0cc Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Tue, 23 Apr 2024 18:40:00 +0200 Subject: [PATCH 12/21] Removed unused Box error --- Sources/CryptomatorCloudAccess/Box/BoxError.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/CryptomatorCloudAccess/Box/BoxError.swift b/Sources/CryptomatorCloudAccess/Box/BoxError.swift index b78b95e..b657aee 100644 --- a/Sources/CryptomatorCloudAccess/Box/BoxError.swift +++ b/Sources/CryptomatorCloudAccess/Box/BoxError.swift @@ -11,5 +11,4 @@ import Foundation public enum BoxError: Error { case unexpectedContent case inconsistentCache - case fileLinkNotFound } From eb9a34db5e84d36e02c11b2132fc4ee9453de0fa Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Tue, 23 Apr 2024 18:40:17 +0200 Subject: [PATCH 13/21] Fixed wrong usage of Box error --- Sources/CryptomatorCloudAccess/Box/BoxItem.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CryptomatorCloudAccess/Box/BoxItem.swift b/Sources/CryptomatorCloudAccess/Box/BoxItem.swift index 752d123..92ee242 100644 --- a/Sources/CryptomatorCloudAccess/Box/BoxItem.swift +++ b/Sources/CryptomatorCloudAccess/Box/BoxItem.swift @@ -29,7 +29,7 @@ extension BoxItem { case let .folder(folder): self.init(cloudPath: cloudPath, folder: folder) case let .webLink(webLink): - throw PCloudError.unexpectedContent + throw BoxError.unexpectedContent } } From dab7df98306a044e21cfc17b806ae215b18e620e Mon Sep 17 00:00:00 2001 From: Majid Achhoud Date: Thu, 25 Apr 2024 13:55:51 +0200 Subject: [PATCH 14/21] Updated READMEs --- README.md | 29 +++++++++++++++++++ .../README.md | 17 +++++++++++ 2 files changed, 46 insertions(+) diff --git a/README.md b/README.md index ad2e66a..bdf0b5f 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,35 @@ let cryptoDecorator = try VaultProviderFactory.createLegacyVaultProvider(from: m :warning: This library supports vault version 6 and higher. +### Box + +Modify your app delegate as described in [Box iOS SDK](https://github.com/box/box-ios-sdk). In addition, the following constants must be set once, e.g. in your app delegate: + +```swift +let clientId = ... // your Box client identifier +let clientSecret = ... // your Box client secret +let sharedContainerIdentifier = ... // optional: only needed if you want to create a `BoxCloudProvider` with a background `URLSession` in an app extension +BoxSetup.constants = BoxSetup(clientId: clientId, clientSecret: clientSecret, sharedContainerIdentifier: sharedContainerIdentifier) +``` + +Begin the authentication flow: + +```swift +let tokenStore = BoxTokenStore() +let viewController = ... // the presenting `UIViewController` +BoxAuthenticator.authenticate(credential: credential, from: viewController).then { + // authentication successful +}.catch { error in + // error handling +} +``` + +You can then use the credential to create a Box provider: + +```swift +let provider = BoxCloudProvider(credential: credential) +``` + ### Dropbox Set up the `Info.plist` as described in the [official Dropbox Objective-C SDK](https://github.com/dropbox/dropbox-sdk-obj-c). In addition, the following constants must be set once, e.g. in your app delegate: diff --git a/Tests/CryptomatorCloudAccessIntegrationTests/README.md b/Tests/CryptomatorCloudAccessIntegrationTests/README.md index 4588fac..62645d8 100644 --- a/Tests/CryptomatorCloudAccessIntegrationTests/README.md +++ b/Tests/CryptomatorCloudAccessIntegrationTests/README.md @@ -8,6 +8,8 @@ If you would like to run integration tests that require authentication, you have ```sh #!/bin/sh +export BOX_ACCESS_TOKEN=... +export BOX_REFRESH_TOKEN=... export DROPBOX_ACCESS_TOKEN=... export GOOGLE_DRIVE_CLIENT_ID=... export GOOGLE_DRIVE_REFRESH_TOKEN=... @@ -33,6 +35,21 @@ If you are building via a CI system, set these secret environment variables acco ### How to Get the Secrets +#### Box + +To get the access token for Box, generate a developer token in the Box Developer Portal. For more detailed instructions, check out the [OAuth 2.0 Documentation from Box](https://developer.box.com/guides/authentication/oauth2/). + +To obtain the refresh token from Box, it is recommended to extract it from `authenticate` after a successful login. The easiest way to do this is to set a breakpoint inside the `BoxAuthenticator`: + +```swift +public static func authenticate(from viewController: UIViewController, tokenStore: TokenStore) -> Promise<(BoxClient, String)> { + return Promise { + // ... + fulfill((client, user.id)) // set breakpoint here + // ... +} +``` + #### Dropbox To get the access token for Dropbox, generate a token in the Dropbox Developer Portal. For more detailed instructions, check out the [OAuth Guide from Dropbox](https://developers.dropbox.com/oauth-guide). From 995defccd09647bdf2ecbe642733ed5f09fca271 Mon Sep 17 00:00:00 2001 From: Majid Achhoud Date: Mon, 29 Apr 2024 10:21:30 +0200 Subject: [PATCH 15/21] Added VaultFormatIntegrationsTests for box, improved README's and integrate developer token. --- .../project.pbxproj | 8 ++ .../xcschemes/BoxIntegrationTests.xcscheme | 129 ++++++++++++++++++ README.md | 1 + .../Box/BoxAuthenticatorMock.swift | 9 ++ .../Box/BoxCredentialMock.swift | 9 +- .../VaultFormat6BoxIntegrationTests.swift | 74 ++++++++++ .../VaultFormat7BoxIntegrationTests.swift | 74 ++++++++++ .../README.md | 5 +- create-integration-test-secrets-file.sh | 3 +- 9 files changed, 301 insertions(+), 11 deletions(-) create mode 100644 CryptomatorCloudAccess.xcodeproj/xcshareddata/xcschemes/BoxIntegrationTests.xcscheme create mode 100644 Tests/CryptomatorCloudAccessIntegrationTests/Box/BoxAuthenticatorMock.swift create mode 100644 Tests/CryptomatorCloudAccessIntegrationTests/CryptoDecorator/VaultFormat6/VaultFormat6BoxIntegrationTests.swift create mode 100644 Tests/CryptomatorCloudAccessIntegrationTests/CryptoDecorator/VaultFormat7/VaultFormat7BoxIntegrationTests.swift diff --git a/CryptomatorCloudAccess.xcodeproj/project.pbxproj b/CryptomatorCloudAccess.xcodeproj/project.pbxproj index bcf3799..8d2a623 100644 --- a/CryptomatorCloudAccess.xcodeproj/project.pbxproj +++ b/CryptomatorCloudAccess.xcodeproj/project.pbxproj @@ -186,6 +186,8 @@ 9ED0E624246198F600FDB438 /* VaultFormat7CloudProviderMockTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ED0E623246198F600FDB438 /* VaultFormat7CloudProviderMockTests.swift */; }; 9EE62A0D247D54760089DAF7 /* CloudProvider+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EE62A0C247D54760089DAF7 /* CloudProvider+Convenience.swift */; }; 9EE62A10247D54E90089DAF7 /* CloudProvider+ConvenienceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EE62A0F247D54E90089DAF7 /* CloudProvider+ConvenienceTests.swift */; }; + B322A2BB2BDF7F9F00306F01 /* VaultFormat7BoxIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B322A2BA2BDF7F9F00306F01 /* VaultFormat7BoxIntegrationTests.swift */; }; + B322A2BD2BDF7FBE00306F01 /* VaultFormat6BoxIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B322A2BC2BDF7FBE00306F01 /* VaultFormat6BoxIntegrationTests.swift */; }; B3408AC82BCD32CA005271D2 /* BoxCredentialMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3408AC72BCD32CA005271D2 /* BoxCredentialMock.swift */; }; B3408ACA2BCDAA09005271D2 /* BoxError.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3408AC92BCDAA09005271D2 /* BoxError.swift */; }; B3D513912BA9A32200DE0D36 /* BoxAuthenticator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3D5138D2BA9A32200DE0D36 /* BoxAuthenticator.swift */; }; @@ -384,6 +386,8 @@ 9ED0E623246198F600FDB438 /* VaultFormat7CloudProviderMockTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultFormat7CloudProviderMockTests.swift; sourceTree = ""; }; 9EE62A0C247D54760089DAF7 /* CloudProvider+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CloudProvider+Convenience.swift"; sourceTree = ""; }; 9EE62A0F247D54E90089DAF7 /* CloudProvider+ConvenienceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CloudProvider+ConvenienceTests.swift"; sourceTree = ""; }; + B322A2BA2BDF7F9F00306F01 /* VaultFormat7BoxIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultFormat7BoxIntegrationTests.swift; sourceTree = ""; }; + B322A2BC2BDF7FBE00306F01 /* VaultFormat6BoxIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultFormat6BoxIntegrationTests.swift; sourceTree = ""; }; B3408AC72BCD32CA005271D2 /* BoxCredentialMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxCredentialMock.swift; sourceTree = ""; }; B3408AC92BCDAA09005271D2 /* BoxError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxError.swift; sourceTree = ""; }; B3D5138D2BA9A32200DE0D36 /* BoxAuthenticator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BoxAuthenticator.swift; sourceTree = ""; }; @@ -653,6 +657,7 @@ 4ACA63DA2615FF3B00D19304 /* VaultFormat6 */ = { isa = PBXGroup; children = ( + B322A2BC2BDF7FBE00306F01 /* VaultFormat6BoxIntegrationTests.swift */, 4ACA63CB2615FF0000D19304 /* VaultFormat6DropboxIntegrationTests.swift */, 4ACA63D02615FF1600D19304 /* VaultFormat6GoogleDriveIntegrationTests.swift */, 4ACA63C62615FED700D19304 /* VaultFormat6LocalFileSystemIntegrationTests.swift */, @@ -667,6 +672,7 @@ 4ACA63E12615FF6400D19304 /* VaultFormat7 */ = { isa = PBXGroup; children = ( + B322A2BA2BDF7F9F00306F01 /* VaultFormat7BoxIntegrationTests.swift */, 4ACA63E42615FF6400D19304 /* VaultFormat7DropboxIntegrationTests.swift */, 4ACA63E22615FF6400D19304 /* VaultFormat7GoogleDriveIntegrationTests.swift */, 4ACA63E52615FF6400D19304 /* VaultFormat7LocalFileSystemIntegrationTests.swift */, @@ -1433,6 +1439,7 @@ 7467A0D627DF9A8000BCFDF8 /* VaultFormat6PCloudIntegrationTests.swift in Sources */, 4AC75F9C2861A6DE002731FE /* VaultFormat6S3IntegrationTests.swift in Sources */, 4ACA63A02615FE2C00D19304 /* CloudAccessIntegrationTest.swift in Sources */, + B322A2BB2BDF7F9F00306F01 /* VaultFormat7BoxIntegrationTests.swift in Sources */, B3D513972BA9A44000DE0D36 /* BoxCloudProviderIntegrationTests.swift in Sources */, B3408AC82BCD32CA005271D2 /* BoxCredentialMock.swift in Sources */, 4ACA64262616054F00D19304 /* IntegrationTestSecrets.swift in Sources */, @@ -1444,6 +1451,7 @@ 4ACA63BB2615FEA600D19304 /* DecoratorFactory.swift in Sources */, 4ACA64042615FF9800D19304 /* GoogleDriveAuthenticatorMock.swift in Sources */, 4ACA64082615FF9800D19304 /* CloudProvider+CreateIntermediateFolderTests.swift in Sources */, + B322A2BD2BDF7FBE00306F01 /* VaultFormat6BoxIntegrationTests.swift in Sources */, 4ACA64022615FF9800D19304 /* DropboxCloudProviderIntegrationTests.swift in Sources */, 4ACA63D62615FF2E00D19304 /* VaultFormat6WebDAVIntegrationTests.swift in Sources */, 4AFC5F67263190BB00744715 /* OneDriveCloudProviderIntegrationTests.swift in Sources */, diff --git a/CryptomatorCloudAccess.xcodeproj/xcshareddata/xcschemes/BoxIntegrationTests.xcscheme b/CryptomatorCloudAccess.xcodeproj/xcshareddata/xcschemes/BoxIntegrationTests.xcscheme new file mode 100644 index 0000000..fa130ba --- /dev/null +++ b/CryptomatorCloudAccess.xcodeproj/xcshareddata/xcschemes/BoxIntegrationTests.xcscheme @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md index bdf0b5f..0d7a129 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,7 @@ Begin the authentication flow: ```swift let tokenStore = BoxTokenStore() +let credential = BoxCredential(tokenStore: tokenStore) let viewController = ... // the presenting `UIViewController` BoxAuthenticator.authenticate(credential: credential, from: viewController).then { // authentication successful diff --git a/Tests/CryptomatorCloudAccessIntegrationTests/Box/BoxAuthenticatorMock.swift b/Tests/CryptomatorCloudAccessIntegrationTests/Box/BoxAuthenticatorMock.swift new file mode 100644 index 0000000..d12d536 --- /dev/null +++ b/Tests/CryptomatorCloudAccessIntegrationTests/Box/BoxAuthenticatorMock.swift @@ -0,0 +1,9 @@ +// +// BoxAuthenticatorMock.swift +// CryptomatorCloudAccessIntegrationTests +// +// Created by Majid Achhoud on 25.04.24. +// Copyright © 2024 Skymatic GmbH. All rights reserved. +// + +import Foundation diff --git a/Tests/CryptomatorCloudAccessIntegrationTests/Box/BoxCredentialMock.swift b/Tests/CryptomatorCloudAccessIntegrationTests/Box/BoxCredentialMock.swift index 9e1b26e..3ba8053 100644 --- a/Tests/CryptomatorCloudAccessIntegrationTests/Box/BoxCredentialMock.swift +++ b/Tests/CryptomatorCloudAccessIntegrationTests/Box/BoxCredentialMock.swift @@ -16,17 +16,14 @@ import Promises #endif class BoxCredentialMock: BoxCredential { - let tokenStore: MemoryTokenStore - init() { BoxSetup.constants = BoxSetup(clientId: "", clientSecret: "", sharedContainerIdentifier: "") - self.tokenStore = MemoryTokenStore() - tokenStore.tokenInfo = TokenInfo(accessToken: IntegrationTestSecrets.boxAccessToken, refreshToken: IntegrationTestSecrets.boxRefreshToken, expiresIn: 3600, tokenType: "bearer") - super.init(tokenStore: tokenStore) + super.init(tokenStore: MemoryTokenStore()) + client = BoxSDK.getClient(token: IntegrationTestSecrets.boxDeveloperToken) } override func deauthenticate() -> Promise { - tokenStore.tokenInfo = TokenInfo(accessToken: "invalid", expiresIn: 0) + client = BoxSDK.getClient(token: "invalid") return Promise(()) } } diff --git a/Tests/CryptomatorCloudAccessIntegrationTests/CryptoDecorator/VaultFormat6/VaultFormat6BoxIntegrationTests.swift b/Tests/CryptomatorCloudAccessIntegrationTests/CryptoDecorator/VaultFormat6/VaultFormat6BoxIntegrationTests.swift new file mode 100644 index 0000000..4dd5ca0 --- /dev/null +++ b/Tests/CryptomatorCloudAccessIntegrationTests/CryptoDecorator/VaultFormat6/VaultFormat6BoxIntegrationTests.swift @@ -0,0 +1,74 @@ +// +// VaultFormat6BoxIntegrationTests.swift +// CryptomatorCloudAccessIntegrationTests +// +// Created by Majid Achhoud on 29.04.24. +// Copyright © 2024 Skymatic GmbH. All rights reserved. +// + +import XCTest +#if canImport(CryptomatorCloudAccessCore) +@testable import CryptomatorCloudAccessCore +#else +@testable import CryptomatorCloudAccess +#endif +@testable import Promises + +class VaultFormat6BoxIntegrationTests: CloudAccessIntegrationTest { + override class var defaultTestSuite: XCTestSuite { + return XCTestSuite(forTestCaseClass: VaultFormat6BoxIntegrationTests.self) + } + + private static let credential = BoxCredentialMock() + // swiftlint:disable:next force_try + private static let cloudProvider = try! BoxCloudProvider(credential: credential) + private static let vaultPath = CloudPath("/iOS-IntegrationTests-VaultFormat6") + + override class func setUp() { + integrationTestParentCloudPath = CloudPath("/") + let setUpPromise = cloudProvider.deleteFolderIfExisting(at: vaultPath).then { + DecoratorFactory.createNewVaultFormat6(delegate: cloudProvider, vaultPath: vaultPath, password: "IntegrationTest") + }.then { decorator in + setUpProvider = decorator + } + guard waitForPromises(timeout: 60.0) else { + classSetUpError = IntegrationTestError.oneTimeSetUpTimeout + return + } + if let error = setUpPromise.error { + classSetUpError = error + return + } + super.setUp() + } + + override func setUpWithError() throws { + try super.setUpWithError() + let credential = BoxCredentialMock() + let cloudProvider = try BoxCloudProvider(credential: credential) + let setUpPromise = DecoratorFactory.createFromExistingVaultFormat6(delegate: cloudProvider, vaultPath: VaultFormat6BoxIntegrationTests.vaultPath, password: "IntegrationTest").then { decorator in + self.provider = decorator + } + guard waitForPromises(timeout: 60.0) else { + if let error = setUpPromise.error { + throw error + } + throw IntegrationTestError.setUpTimeout + } + } + + override func createLimitedCloudProvider() throws -> CloudProvider { + let credential = BoxCredentialMock() + let limitedDelegate = try BoxCloudProvider(credential: credential, maxPageSize: maxPageSizeForLimitedCloudProvider) + let setUpPromise = DecoratorFactory.createFromExistingVaultFormat6(delegate: limitedDelegate, vaultPath: VaultFormat6BoxIntegrationTests.vaultPath, password: "IntegrationTest").then { decorator in + self.provider = decorator + } + guard waitForPromises(timeout: 60.0) else { + if let error = setUpPromise.error { + throw error + } + throw IntegrationTestError.setUpTimeout + } + return try XCTUnwrap(setUpPromise.value) + } +} diff --git a/Tests/CryptomatorCloudAccessIntegrationTests/CryptoDecorator/VaultFormat7/VaultFormat7BoxIntegrationTests.swift b/Tests/CryptomatorCloudAccessIntegrationTests/CryptoDecorator/VaultFormat7/VaultFormat7BoxIntegrationTests.swift new file mode 100644 index 0000000..ea817df --- /dev/null +++ b/Tests/CryptomatorCloudAccessIntegrationTests/CryptoDecorator/VaultFormat7/VaultFormat7BoxIntegrationTests.swift @@ -0,0 +1,74 @@ +// +// VaultFormat7BoxIntegrationTests.swift +// CryptomatorCloudAccessIntegrationTests +// +// Created by Majid Achhoud on 29.04.24. +// Copyright © 2024 Skymatic GmbH. All rights reserved. +// + +import XCTest +#if canImport(CryptomatorCloudAccessCore) +@testable import CryptomatorCloudAccessCore +#else +@testable import CryptomatorCloudAccess +#endif +@testable import Promises + +class VaultFormat7BoxIntegrationTests: CloudAccessIntegrationTest { + override class var defaultTestSuite: XCTestSuite { + return XCTestSuite(forTestCaseClass: VaultFormat7BoxIntegrationTests.self) + } + + private static let credential = BoxCredentialMock() + // swiftlint:disable:next force_try + private static let cloudProvider = try! BoxCloudProvider(credential: credential) + private static let vaultPath = CloudPath("/iOS-IntegrationTests-VaultFormat7") + + override class func setUp() { + integrationTestParentCloudPath = CloudPath("/") + let setUpPromise = cloudProvider.deleteFolderIfExisting(at: vaultPath).then { + DecoratorFactory.createNewVaultFormat7(delegate: cloudProvider, vaultPath: vaultPath, password: "IntegrationTest") + }.then { decorator in + setUpProvider = decorator + } + guard waitForPromises(timeout: 60.0) else { + classSetUpError = IntegrationTestError.oneTimeSetUpTimeout + return + } + if let error = setUpPromise.error { + classSetUpError = error + return + } + super.setUp() + } + + override func setUpWithError() throws { + try super.setUpWithError() + let credential = BoxCredentialMock() + let cloudProvider = try BoxCloudProvider(credential: credential) + let setUpPromise = DecoratorFactory.createFromExistingVaultFormat7(delegate: cloudProvider, vaultPath: VaultFormat7BoxIntegrationTests.vaultPath, password: "IntegrationTest").then { decorator in + self.provider = decorator + } + guard waitForPromises(timeout: 60.0) else { + if let error = setUpPromise.error { + throw error + } + throw IntegrationTestError.setUpTimeout + } + } + + override func createLimitedCloudProvider() throws -> CloudProvider { + let credential = BoxCredentialMock() + let limitedDelegate = try BoxCloudProvider(credential: credential, maxPageSize: maxPageSizeForLimitedCloudProvider) + let setUpPromise = DecoratorFactory.createFromExistingVaultFormat7(delegate: limitedDelegate, vaultPath: VaultFormat7BoxIntegrationTests.vaultPath, password: "IntegrationTest").then { decorator in + self.provider = decorator + } + guard waitForPromises(timeout: 60.0) else { + if let error = setUpPromise.error { + throw error + } + throw IntegrationTestError.setUpTimeout + } + return try XCTUnwrap(setUpPromise.value) + } +} diff --git a/Tests/CryptomatorCloudAccessIntegrationTests/README.md b/Tests/CryptomatorCloudAccessIntegrationTests/README.md index 62645d8..7a33107 100644 --- a/Tests/CryptomatorCloudAccessIntegrationTests/README.md +++ b/Tests/CryptomatorCloudAccessIntegrationTests/README.md @@ -8,8 +8,7 @@ If you would like to run integration tests that require authentication, you have ```sh #!/bin/sh -export BOX_ACCESS_TOKEN=... -export BOX_REFRESH_TOKEN=... +export BOX_DEVELOPER_TOKEN=... export DROPBOX_ACCESS_TOKEN=... export GOOGLE_DRIVE_CLIENT_ID=... export GOOGLE_DRIVE_REFRESH_TOKEN=... @@ -37,7 +36,7 @@ If you are building via a CI system, set these secret environment variables acco #### Box -To get the access token for Box, generate a developer token in the Box Developer Portal. For more detailed instructions, check out the [OAuth 2.0 Documentation from Box](https://developer.box.com/guides/authentication/oauth2/). +To get a developer token for Box, generate it in the Box Developer Portal, keeping in mind that it expires after 60 minutes. For more detailed instructions, check out the [OAuth 2.0 Documentation from Box](https://developer.box.com/guides/authentication/oauth2/). To obtain the refresh token from Box, it is recommended to extract it from `authenticate` after a successful login. The easiest way to do this is to set a breakpoint inside the `BoxAuthenticator`: diff --git a/create-integration-test-secrets-file.sh b/create-integration-test-secrets-file.sh index d19ff5c..8d3b7a1 100755 --- a/create-integration-test-secrets-file.sh +++ b/create-integration-test-secrets-file.sh @@ -17,8 +17,7 @@ import CryptomatorCloudAccess import Foundation enum IntegrationTestSecrets { - static let boxAccessToken = "${BOX_ACCESS_TOKEN}" - static let boxRefreshToken = "${BOX_REFRESH_TOKEN}" + static let boxDeveloperToken = "${BOX_DEVELOPER_TOKEN}" static let dropboxAccessToken = "${DROPBOX_ACCESS_TOKEN}" static let googleDriveClientId = "${GOOGLE_DRIVE_CLIENT_ID}" static let googleDriveRefreshToken = "${GOOGLE_DRIVE_REFRESH_TOKEN}" From b63b18dbb8964ba4d1d564e2fdc7094b92e93bf5 Mon Sep 17 00:00:00 2001 From: Majid Achhoud Date: Mon, 29 Apr 2024 17:33:20 +0200 Subject: [PATCH 16/21] Remove BoxAuthenticatorMock and fix README --- .../Box/BoxAuthenticatorMock.swift | 9 --------- .../CryptomatorCloudAccessIntegrationTests/README.md | 11 ----------- 2 files changed, 20 deletions(-) delete mode 100644 Tests/CryptomatorCloudAccessIntegrationTests/Box/BoxAuthenticatorMock.swift diff --git a/Tests/CryptomatorCloudAccessIntegrationTests/Box/BoxAuthenticatorMock.swift b/Tests/CryptomatorCloudAccessIntegrationTests/Box/BoxAuthenticatorMock.swift deleted file mode 100644 index d12d536..0000000 --- a/Tests/CryptomatorCloudAccessIntegrationTests/Box/BoxAuthenticatorMock.swift +++ /dev/null @@ -1,9 +0,0 @@ -// -// BoxAuthenticatorMock.swift -// CryptomatorCloudAccessIntegrationTests -// -// Created by Majid Achhoud on 25.04.24. -// Copyright © 2024 Skymatic GmbH. All rights reserved. -// - -import Foundation diff --git a/Tests/CryptomatorCloudAccessIntegrationTests/README.md b/Tests/CryptomatorCloudAccessIntegrationTests/README.md index 7a33107..5bc22ea 100644 --- a/Tests/CryptomatorCloudAccessIntegrationTests/README.md +++ b/Tests/CryptomatorCloudAccessIntegrationTests/README.md @@ -38,17 +38,6 @@ If you are building via a CI system, set these secret environment variables acco To get a developer token for Box, generate it in the Box Developer Portal, keeping in mind that it expires after 60 minutes. For more detailed instructions, check out the [OAuth 2.0 Documentation from Box](https://developer.box.com/guides/authentication/oauth2/). -To obtain the refresh token from Box, it is recommended to extract it from `authenticate` after a successful login. The easiest way to do this is to set a breakpoint inside the `BoxAuthenticator`: - -```swift -public static func authenticate(from viewController: UIViewController, tokenStore: TokenStore) -> Promise<(BoxClient, String)> { - return Promise { - // ... - fulfill((client, user.id)) // set breakpoint here - // ... -} -``` - #### Dropbox To get the access token for Dropbox, generate a token in the Dropbox Developer Portal. For more detailed instructions, check out the [OAuth Guide from Dropbox](https://developers.dropbox.com/oauth-guide). From 05837ec9aeece4830cab0a73545b07b8cf235c16 Mon Sep 17 00:00:00 2001 From: Majid Achhoud Date: Thu, 23 May 2024 17:40:17 +0200 Subject: [PATCH 17/21] "Replaced old Box SDK with new Box SDK GENERATED" --- .../project.pbxproj | 22 +- .../xcshareddata/swiftpm/Package.resolved | 29 +- .../xcschemes/BoxIntegrationTests.xcscheme | 9 +- Package.resolved | 8 +- Package.swift | 7 +- .../Box/BoxAuthenticator.swift | 41 +- .../Box/BoxCloudProvider.swift | 611 ++++++++++-------- .../Box/BoxCredential.swift | 64 +- .../Box/BoxIdentifierCache.swift | 1 - .../CryptomatorCloudAccess/Box/BoxItem.swift | 13 +- .../Box/BoxCredentialMock.swift | 34 +- 11 files changed, 468 insertions(+), 371 deletions(-) diff --git a/CryptomatorCloudAccess.xcodeproj/project.pbxproj b/CryptomatorCloudAccess.xcodeproj/project.pbxproj index 8d2a623..cf61495 100644 --- a/CryptomatorCloudAccess.xcodeproj/project.pbxproj +++ b/CryptomatorCloudAccess.xcodeproj/project.pbxproj @@ -194,7 +194,7 @@ B3D513922BA9A32200DE0D36 /* BoxCredential.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3D5138E2BA9A32200DE0D36 /* BoxCredential.swift */; }; B3D513932BA9A32200DE0D36 /* BoxSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3D5138F2BA9A32200DE0D36 /* BoxSetup.swift */; }; B3D513972BA9A44000DE0D36 /* BoxCloudProviderIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3D513952BA9A3BB00DE0D36 /* BoxCloudProviderIntegrationTests.swift */; }; - B3FC94A42BA9A98200D1ECFD /* BoxSDK in Frameworks */ = {isa = PBXBuildFile; productRef = B3FC94A32BA9A98200D1ECFD /* BoxSDK */; }; + B3D620D22BFC7B2E007301C1 /* BoxSdkGen in Frameworks */ = {isa = PBXBuildFile; productRef = B3D620D12BFC7B2E007301C1 /* BoxSdkGen */; }; B3FC94A62BA9AA4400D1ECFD /* BoxCloudProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FC94A52BA9AA4400D1ECFD /* BoxCloudProvider.swift */; }; B3FC94A82BA9AEEC00D1ECFD /* BoxIdentifierCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FC94A72BA9AEEC00D1ECFD /* BoxIdentifierCache.swift */; }; B3FC94AA2BA9AEFC00D1ECFD /* BoxItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FC94A92BA9AEFC00D1ECFD /* BoxItem.swift */; }; @@ -405,7 +405,6 @@ buildActionMask = 2147483647; files = ( 4A75E1C428806F5800952FE6 /* MSGraphClientModels in Frameworks */, - B3FC94A42BA9A98200D1ECFD /* BoxSDK in Frameworks */, 4A75E1C728806FA100952FE6 /* MSGraphClientSDK in Frameworks */, 4AF0AA7C2844DDD200C20B75 /* AWSS3 in Frameworks */, 74F9355D251F67A3001F4ADA /* CryptomatorCryptoLib in Frameworks */, @@ -419,6 +418,7 @@ 746F090E27BC0932003FCD9F /* PCloudSDKSwift in Frameworks */, 4A567B372615CAAC002C4D82 /* GTMSessionFetcher in Frameworks */, 4A567B1A2615C917002C4D82 /* GTMAppAuth in Frameworks */, + B3D620D22BFC7B2E007301C1 /* BoxSdkGen in Frameworks */, 4A8B872F287D7E77002D676E /* CocoaLumberjackSwift in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1074,7 +1074,7 @@ 4A75E1C628806FA100952FE6 /* MSGraphClientSDK */, 4A75E1C928806FF000952FE6 /* ObjectiveDropboxOfficial */, 4A8B872E287D7E77002D676E /* CocoaLumberjackSwift */, - B3FC94A32BA9A98200D1ECFD /* BoxSDK */, + B3D620D12BFC7B2E007301C1 /* BoxSdkGen */, ); productName = CloudAccess; productReference = 4A058FF124519FFC008831F9 /* CryptomatorCloudAccess.framework */; @@ -1165,7 +1165,7 @@ 4A75E1C528806FA100952FE6 /* XCRemoteSwiftPackageReference "msgraph-sdk-objc-spm" */, 4A75E1C828806FF000952FE6 /* XCRemoteSwiftPackageReference "dropbox-sdk-obj-c-spm" */, 4A8B872D287D7E77002D676E /* XCRemoteSwiftPackageReference "CocoaLumberjack" */, - B3FC94A12BA9A8E600D1ECFD /* XCRemoteSwiftPackageReference "box-ios-sdk" */, + B3C72B752BFC798A006F8218 /* XCRemoteSwiftPackageReference "box-swift-sdk-gen" */, ); productRefGroup = 4A058FF224519FFC008831F9 /* Products */; projectDirPath = ""; @@ -1888,12 +1888,12 @@ minimumVersion = 2.3.0; }; }; - B3FC94A12BA9A8E600D1ECFD /* XCRemoteSwiftPackageReference "box-ios-sdk" */ = { + B3C72B752BFC798A006F8218 /* XCRemoteSwiftPackageReference "box-swift-sdk-gen" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/box/box-ios-sdk.git"; + repositoryURL = "https://github.com/box/box-swift-sdk-gen.git"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 5.5.0; + kind = upToNextMinorVersion; + minimumVersion = 0.2.0; }; }; /* End XCRemoteSwiftPackageReference section */ @@ -1974,10 +1974,10 @@ package = 74F93565251F6863001F4ADA /* XCRemoteSwiftPackageReference "promises" */; productName = Promises; }; - B3FC94A32BA9A98200D1ECFD /* BoxSDK */ = { + B3D620D12BFC7B2E007301C1 /* BoxSdkGen */ = { isa = XCSwiftPackageProductDependency; - package = B3FC94A12BA9A8E600D1ECFD /* XCRemoteSwiftPackageReference "box-ios-sdk" */; - productName = BoxSDK; + package = B3C72B752BFC798A006F8218 /* XCRemoteSwiftPackageReference "box-swift-sdk-gen" */; + productName = BoxSdkGen; }; /* End XCSwiftPackageProductDependency section */ }; diff --git a/CryptomatorCloudAccess.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/CryptomatorCloudAccess.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 9a34456..87e5597 100644 --- a/CryptomatorCloudAccess.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/CryptomatorCloudAccess.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,4 +1,5 @@ { + "originHash" : "9cadc7a70613b038fe8d97d191b9a6f48d09309fa0636f36dfa1ef6376591ebc", "pins" : [ { "identity" : "appauth-ios", @@ -14,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/aws-amplify/aws-sdk-ios-spm.git", "state" : { - "revision" : "cfcf97f6994b6ffd9a3244dc638458f5822aba56", - "version" : "2.34.0" + "revision" : "8ff8bebfe24271f7b16c5abaeb78daf82bee3a80", + "version" : "2.34.2" } }, { @@ -28,12 +29,12 @@ } }, { - "identity" : "box-ios-sdk", + "identity" : "box-swift-sdk-gen", "kind" : "remoteSourceControl", - "location" : "https://github.com/box/box-ios-sdk.git", + "location" : "https://github.com/box/box-swift-sdk-gen.git", "state" : { - "revision" : "daffd86b861a5f5882655bf7a01b891f6b808c1f", - "version" : "5.5.0" + "revision" : "e5069af728c4b4f048078dfb5aed683c926da857", + "version" : "0.2.0" } }, { @@ -41,8 +42,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/CocoaLumberjack/CocoaLumberjack.git", "state" : { - "revision" : "67ec5818a757aba4d7c534e21a905d878d128dbf", - "version" : "3.8.1" + "revision" : "4b8714a7fb84d42393314ce897127b3939885ec3", + "version" : "3.8.5" } }, { @@ -113,8 +114,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/AzureAD/microsoft-authentication-library-for-objc.git", "state" : { - "revision" : "f3f84a63de86f7f121544ec917e0365e624d6a97", - "version" : "1.3.0" + "revision" : "d2f81ded070ac6452b2a6acb5bc45eb566427fe7", + "version" : "1.3.3" } }, { @@ -156,12 +157,12 @@ { "identity" : "swift-log", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-log.git", + "location" : "https://github.com/apple/swift-log", "state" : { - "revision" : "532d8b529501fb73a2455b179e0bbb6d49b652ed", - "version" : "1.5.3" + "revision" : "e97a6fcb1ab07462881ac165fdbb37f067e205d5", + "version" : "1.5.4" } } ], - "version" : 2 + "version" : 3 } diff --git a/CryptomatorCloudAccess.xcodeproj/xcshareddata/xcschemes/BoxIntegrationTests.xcscheme b/CryptomatorCloudAccess.xcodeproj/xcshareddata/xcschemes/BoxIntegrationTests.xcscheme index fa130ba..627de32 100644 --- a/CryptomatorCloudAccess.xcodeproj/xcshareddata/xcschemes/BoxIntegrationTests.xcscheme +++ b/CryptomatorCloudAccess.xcodeproj/xcshareddata/xcschemes/BoxIntegrationTests.xcscheme @@ -10,7 +10,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "NO"> @@ -111,6 +111,13 @@ debugDocumentVersioning = "YES" debugServiceExtension = "internal" allowLocationSimulation = "YES"> + + + + Promise { + let pendingPromise = Promise.pending() - public static func authenticate(from viewController: UIViewController, tokenStore: TokenStore) -> Promise<(BoxClient, String)> { - return Promise { fulfill, reject in + _Concurrency.Task { + do { + guard let context = viewController as? ASWebAuthenticationPresentationContextProviding else { + throw BoxAuthenticatorError.invalidContext + } - guard let context = viewController as? ASWebAuthenticationPresentationContextProviding else { - reject(BoxAuthenticatorError.invalidContext) - return - } + let config = OAuthConfig(clientId: BoxSetup.constants.clientId, clientSecret: BoxSetup.constants.clientSecret) + let oauth = BoxOAuth(config: config) - sdk.getOAuth2Client(tokenStore: tokenStore, context: context) { result in - switch result { - case let .success(client): - client.users.getCurrent(fields: ["id"]) { userResult in - switch userResult { - case let .success(user): - fulfill((client, user.id)) - case .failure: - reject(BoxAuthenticatorError.authenticationFailed) - } - } - case .failure: - reject(BoxAuthenticatorError.authenticationFailed) - } + // Run the login flow and store the access token using tokenStorage + try await oauth.runLoginFlow(options: .init(), context: context) + // TODO: Catch error when login failed + + pendingPromise.fulfill(BoxCredential(tokenStore: tokenStorage)) + } catch { + pendingPromise.reject(BoxAuthenticatorError.authenticationFailed) } } + + return pendingPromise } } diff --git a/Sources/CryptomatorCloudAccess/Box/BoxCloudProvider.swift b/Sources/CryptomatorCloudAccess/Box/BoxCloudProvider.swift index 85c9088..cccceaa 100644 --- a/Sources/CryptomatorCloudAccess/Box/BoxCloudProvider.swift +++ b/Sources/CryptomatorCloudAccess/Box/BoxCloudProvider.swift @@ -6,7 +6,7 @@ // Copyright © 2024 Skymatic GmbH. All rights reserved. // -import BoxSDK +import BoxSdkGen import Foundation import Promises @@ -133,60 +133,54 @@ public class BoxCloudProvider: CloudProvider { private func fetchFileMetadata(for item: BoxItem) -> Promise { assert(item.itemType == .file) - CloudAccessDDLogDebug("BoxCloudProvider: fetchFileMetadata(for: \(item.identifier)) called") - guard let client = credential.client else { - return Promise(CloudProviderError.unauthorized) - } - return Promise { fulfill, reject in - client.files.get(fileId: item.identifier, fields: ["name", "size", "modified_at"]) { result in - switch result { - case let .success(file): - do { - let metadata = self.convertToCloudItemMetadata(file, at: item.cloudPath) - try self.identifierCache.addOrUpdate(item) - CloudAccessDDLogDebug("BoxCloudProvider: fetchFileMetadata(for: \(item.identifier)) successful") - fulfill(metadata) - } catch { - CloudAccessDDLogDebug("BoxCloudProvider: fetchFileMetadata(for: \(item.identifier)) error: \(error)") - reject(error) - } - case let .failure(error): - CloudAccessDDLogDebug("BoxCloudProvider: fetchFileMetadata(for: \(item.identifier)) failed with error: \(error)") - if error.message == .unauthorizedAccess { - reject(CloudProviderError.unauthorized) - } else { - reject(error) - } - } + CloudAccessDDLogDebug("BoxCloudProvider: fetchFileMetadata(for: \(item)) called") + + let client = credential.client + + let pendingPromise = Promise.pending() + + _Concurrency.Task { + do { + let fileMetadata = try await client.files.getFileById(fileId: item.identifier) + let cloudMetadata = convertToCloudItemMetadata(fileMetadata, at: item.cloudPath) + CloudAccessDDLogDebug("BoxCloudProvider: fetchFileMetadata(for: \(item.identifier)) successful") + pendingPromise.fulfill(cloudMetadata) + } catch let error as BoxSDKError where error.message.contains("Developer token has expired") { + CloudAccessDDLogDebug("BoxCloudProvider: fetchFileMetadata(for: \(item.identifier)) error: unauthorized access") + pendingPromise.reject(CloudProviderError.unauthorized) + } catch { + CloudAccessDDLogDebug("BoxCloudProvider: fetchFileMetadata(for: \(item.identifier)) error: \(error.localizedDescription)") + pendingPromise.reject(error) } } + + return pendingPromise } private func fetchFolderMetadata(for item: BoxItem) -> Promise { assert(item.itemType == .folder) CloudAccessDDLogDebug("BoxCloudProvider: fetchFolderMetadata(for: \(item.identifier)) called") - guard let client = credential.client else { - return Promise(CloudProviderError.unauthorized) - } - return Promise { fulfill, reject in - client.folders.get(folderId: item.identifier, fields: ["name", "modified_at"]) { result in - switch result { - case let .success(folder): - do { - let metadata = self.convertToCloudItemMetadata(folder, at: item.cloudPath) - try self.identifierCache.addOrUpdate(item) - CloudAccessDDLogDebug("BoxCloudProvider: fetchFolderMetadata(for: \(item.identifier)) successful") - fulfill(metadata) - } catch { - CloudAccessDDLogDebug("BoxCloudProvider: fetchFolderMetadata(for: \(item.identifier)) error: \(error)") - reject(error) - } - case let .failure(error): - CloudAccessDDLogDebug("BoxCloudProvider: fetchFolderMetadata(for: \(item.identifier)) failed with error: \(error)") - reject(error) - } + + let client = credential.client + + let pendingPromise = Promise.pending() + + _Concurrency.Task { + do { + let fileMetadata = try await client.folders.getFolderById(folderId: item.identifier) + let cloudMetadata = convertToCloudItemMetadata(fileMetadata, at: item.cloudPath) + CloudAccessDDLogDebug("BoxCloudProvider: fetchFolderMetadata(for: \(item.identifier)) successful") + pendingPromise.fulfill(cloudMetadata) + } catch let error as BoxSDKError where error.message.contains("Developer token has expired") { + CloudAccessDDLogDebug("BoxCloudProvider: fetchFolderMetadata(for: \(item.identifier)) error: unauthorized access") + pendingPromise.reject(CloudProviderError.unauthorized) + } catch { + CloudAccessDDLogDebug("BoxCloudProvider: fetchFolderMetadata(for: \(item.identifier)) error: \(error.localizedDescription)") + pendingPromise.reject(error) } } + + return pendingPromise } private func fetchItemList(for folderItem: BoxItem, pageToken: String?) -> Promise { @@ -194,35 +188,36 @@ public class BoxCloudProvider: CloudProvider { return Promise(CloudProviderError.itemTypeMismatch) } - guard let client = credential.client else { - return Promise(CloudProviderError.unauthorized) - } + let client = credential.client - return Promise { fulfill, reject in - let iterator = client.folders.listItems(folderId: folderItem.identifier, usemarker: true, marker: pageToken, limit: self.maxPageSize, fields: ["name", "size", "modified_at"]) + let pendingPromise = Promise.pending() - iterator.next { result in - switch result { - case let .success(page): - let allItems = page.entries.compactMap { entry -> CloudItemMetadata? in + _Concurrency.Task { + do { + let queryParams = GetFolderItemsQueryParams(fields: ["name", "size", "modified_at"], usemarker: true, marker: pageToken, limit: Int64(self.maxPageSize)) + let page = try await client.folders.getFolderItems(folderId: folderItem.identifier, queryParams: queryParams) + if let entries = page.entries { + let allItems = entries.compactMap { entry -> CloudItemMetadata? in switch entry { - case let .file(file): + case let .fileFull(file): return self.convertToCloudItemMetadata(file, at: folderItem.cloudPath.appendingPathComponent(file.name ?? "")) - case let .folder(folder): + case let .folderMini(folder): return self.convertToCloudItemMetadata(folder, at: folderItem.cloudPath.appendingPathComponent(folder.name ?? "")) case .webLink: // Handling of web links as required return nil } } - - fulfill(CloudItemList(items: allItems, nextPageToken: page.nextMarker)) - - case let .failure(error): - reject(CloudProviderError.pageTokenInvalid) + pendingPromise.fulfill(CloudItemList(items: allItems, nextPageToken: nil)) + } else { + pendingPromise.reject(BoxError.unexpectedContent) } + } catch { + pendingPromise.reject(error) } } + + return pendingPromise } private func downloadFile(for item: BoxItem, to localURL: URL) -> Promise { @@ -231,99 +226,114 @@ public class BoxCloudProvider: CloudProvider { return Promise(CloudProviderError.itemTypeMismatch) } - guard let client = credential.client else { - return Promise(CloudProviderError.unauthorized) - } + let client = credential.client - return Promise { fulfill, reject in - client.files.download(fileId: item.identifier, destinationURL: localURL) { result in - switch result { - case .success: - CloudAccessDDLogDebug("BoxCloudProvider: downloadFile(for: \(item.identifier), to: \(localURL)) finished downloading") - fulfill(()) - case let .failure(error): - CloudAccessDDLogDebug("BoxCloudProvider: downloadFile(for: \(item.identifier), to: \(localURL)) failed with error: \(error)") - if error.message == .unauthorizedAccess { - reject(CloudProviderError.unauthorized) - } else { - reject(error) - } - } + let pendingPromise = Promise.pending() + + _Concurrency.Task { + do { + _ = try await client.downloads.downloadFile(fileId: item.identifier, downloadDestinationURL: localURL) + CloudAccessDDLogDebug("BoxCloudProvider: downloadFile(for: \(item.identifier), to: \(localURL)) finished downloading") + pendingPromise.fulfill(()) + } catch let error as BoxSDKError where error.message.contains("Developer token has expired") { + CloudAccessDDLogDebug("BoxCloudProvider: downloadFile(for: \(item.identifier)) error: unauthorized access") + pendingPromise.reject(CloudProviderError.unauthorized) + } catch { + CloudAccessDDLogDebug("BoxCloudProvider: downloadFile(for: \(item.identifier)) error: \(error.localizedDescription)") + pendingPromise.reject(error) } } + + return pendingPromise } private func uploadFile(for parentItem: BoxItem, from localURL: URL, to cloudPath: CloudPath) -> Promise { - guard let client = credential.client else { - return Promise(CloudProviderError.unauthorized) - } + let client = credential.client - return Promise { fulfill, reject in - let targetFileName = cloudPath.lastPathComponent + let pendingPromise = Promise.pending() - guard let data = try? Data(contentsOf: localURL) else { - reject(CloudProviderError.itemNotFound) - return - } + _Concurrency.Task { + do { + guard let fileStream = InputStream(url: localURL) else { + return pendingPromise.reject(CloudProviderError.itemNotFound) + } - self.resolvePath(forItemAt: cloudPath).then { existingItem -> Void in - client.files.uploadVersion(forFile: existingItem.identifier, data: data, completion: { result in - switch result { - case let .success(updatedFile): - let metadata = self.convertToCloudItemMetadata(updatedFile, at: cloudPath) - fulfill(metadata) - case let .failure(error): - reject(error) - } - }) - }.recover { error -> Void in - guard case CloudProviderError.itemNotFound = error else { - throw error + let targetFileName = cloudPath.lastPathComponent + + let requestBody = UploadFileVersionRequestBody( + attributes: UploadFileVersionRequestBodyAttributesField( + name: targetFileName + ), + file: fileStream + ) + + let existingItem = try await resolvePath(forItemAt: cloudPath).async() + let updatedFile = try await client.uploads.uploadFileVersion(fileId: existingItem.identifier, requestBody: requestBody) + let list = try self.convertToCloudItemList(updatedFile, at: cloudPath.deletingLastPathComponent()) + guard let metadata = list.items.first else { + throw CloudProviderError.itemNotFound } - client.files.upload(data: data, name: targetFileName, parentId: parentItem.identifier, completion: { result in - switch result { - case let .success(newFile): - let metadata = self.convertToCloudItemMetadata(newFile, at: cloudPath) - fulfill(metadata) - case let .failure(error): - if error.message == .unauthorizedAccess { - reject(CloudProviderError.unauthorized) - } else { - reject(error) - } + + pendingPromise.fulfill(metadata) + + } catch CloudProviderError.itemNotFound { + do { + guard let fileStream = InputStream(url: localURL) else { + return pendingPromise.reject(CloudProviderError.itemNotFound) + } + + let targetFileName = cloudPath.lastPathComponent + + let requestBody = UploadFileRequestBody( + attributes: UploadFileRequestBodyAttributesField( + name: targetFileName, + parent: UploadFileRequestBodyAttributesParentField(id: parentItem.identifier) + ), + file: fileStream + ) + let newFile = try await client.uploads.uploadFile(requestBody: requestBody) + let list = try self.convertToCloudItemList(newFile, at: cloudPath.deletingLastPathComponent()) + guard let metadata = list.items.first else { + throw CloudProviderError.itemNotFound } - }) + pendingPromise.fulfill(metadata) + } catch let error as BoxSDKError where error.message.contains("Developer token has expired") { + pendingPromise.reject(CloudProviderError.unauthorized) + } catch { + // Handling other upload errors + pendingPromise.reject(error) + } + } catch { + // General error handling if something goes wrong when determining the path + pendingPromise.reject(error) } } + + return pendingPromise } private func createFolder(for parentItem: BoxItem, with name: String) -> Promise { - CloudAccessDDLogDebug("BoxCloudProvider: createFolder(for: \(parentItem.identifier), with: \(name)) called") - guard let client = credential.client else { - return Promise(CloudProviderError.unauthorized) - } - return Promise { fulfill, reject in - client.folders.create(name: name, parentId: parentItem.identifier) { result in - switch result { - case let .success(folder): - CloudAccessDDLogDebug("BoxCloudProvider: createFolder successful with folder ID: \(folder.id)") - do { - let newItem = BoxItem(cloudPath: parentItem.cloudPath.appendingPathComponent(name), identifier: folder.id, itemType: .folder) - try self.identifierCache.addOrUpdate(newItem) - fulfill(()) - } catch { - reject(error) - } - case let .failure(error): - CloudAccessDDLogDebug("BoxCloudProvider: createFolder failed with error: \(error.localizedDescription)") - if error.message == .unauthorizedAccess { - reject(CloudProviderError.unauthorized) - } else { - reject(error) - } - } + let client = credential.client + + let pendingPromise = Promise.pending() + + _Concurrency.Task { + do { + let folder = try await client.folders.createFolder(requestBody: CreateFolderRequestBody(name: name, parent: CreateFolderRequestBodyParentField(id: parentItem.identifier))) + CloudAccessDDLogDebug("BoxCloudProvider: createFolder successful with folder ID: \(folder.id)") + let newItem = BoxItem(cloudPath: parentItem.cloudPath.appendingPathComponent(name), identifier: folder.id, itemType: .folder) + try self.identifierCache.addOrUpdate(newItem) + pendingPromise.fulfill(()) + } catch let error as BoxSDKError where error.message.contains("Developer token has expired") { + CloudAccessDDLogDebug("BoxCloudProvider: createFolder failed with error: unauthorized access") + pendingPromise.reject(CloudProviderError.unauthorized) + } catch { + CloudAccessDDLogDebug("BoxCloudProvider: createFolder failed with error: \(error.localizedDescription)") + pendingPromise.reject(error) } } + + return pendingPromise } private func deleteFile(for item: BoxItem) -> Promise { @@ -332,36 +342,34 @@ public class BoxCloudProvider: CloudProvider { return Promise(CloudProviderError.itemTypeMismatch) } - guard let client = credential.client else { - return Promise(CloudProviderError.unauthorized) - } + let client = credential.client - return Promise { fulfill, reject in - client.files.delete(fileId: item.identifier) { result in - switch result { - case .success: - CloudAccessDDLogDebug("BoxCloudProvider: deleteFile(for: \(item.identifier)) succeeded") - do { - try self.identifierCache.invalidate(item) - fulfill(()) - } catch { - CloudAccessDDLogDebug("BoxCloudProvider: Cache update failed with error: \(error)") - reject(error) - } - case let .failure(error): - CloudAccessDDLogDebug("BoxCloudProvider: deleteFile(for: \(item.identifier)) failed with error: \(error)") - if case BoxSDKErrorEnum.notFound = error.message { - reject(CloudProviderError.itemNotFound) - } else { - if error.message == .unauthorizedAccess { - reject(CloudProviderError.unauthorized) - } else { - reject(error) - } - } + let pendingPromise = Promise.pending() + + _Concurrency.Task { + do { + try await client.files.deleteFileById(fileId: item.identifier) + CloudAccessDDLogDebug("BoxCloudProvider: deleteFile(for: \(item.identifier)) succeeded") + do { + try self.identifierCache.invalidate(item) + pendingPromise.fulfill(()) + } catch { + CloudAccessDDLogDebug("BoxCloudProvider: Cache update failed with error: \(error)") + pendingPromise.reject(error) } + } catch let error as BoxSDKError where error.message.contains("Developer token has expired") { + CloudAccessDDLogDebug("BoxCloudProvider: deleteFile(for: \(item.identifier)) failed with error: unauthorized access") + pendingPromise.reject(CloudProviderError.unauthorized) + } catch let error as BoxSDKError where error.message.contains("notFound") { + CloudAccessDDLogDebug("BoxCloudProvider: deleteFile(for: \(item.identifier)) failed with error: not found") + pendingPromise.reject(CloudProviderError.itemNotFound) + } catch { + CloudAccessDDLogDebug("BoxCloudProvider: deleteFile(for: \(item.identifier)) failed with error: \(error.localizedDescription)") + pendingPromise.reject(error) } } + + return pendingPromise } private func deleteFolder(for item: BoxItem) -> Promise { @@ -370,100 +378,105 @@ public class BoxCloudProvider: CloudProvider { return Promise(CloudProviderError.itemTypeMismatch) } - guard let client = credential.client else { - return Promise(CloudProviderError.unauthorized) - } + let client = credential.client - return Promise { fulfill, reject in - client.folders.delete(folderId: item.identifier, recursive: true) { result in - switch result { - case .success: - CloudAccessDDLogDebug("BoxCloudProvider: deleteFolder(for: \(item.identifier)) succeeded") - do { - try self.identifierCache.invalidate(item) - fulfill(()) - } catch { - CloudAccessDDLogDebug("BoxCloudProvider: Cache update failed with error: \(error)") - reject(error) - } - case let .failure(error): - CloudAccessDDLogDebug("BoxCloudProvider: deleteFolder(for: \(item.identifier)) failed with error: \(error)") - if case BoxSDKErrorEnum.notFound = error.message { - reject(CloudProviderError.itemNotFound) - } else { - if error.message == .unauthorizedAccess { - reject(CloudProviderError.unauthorized) - } else { - reject(error) - } - } + let pendingPromise = Promise.pending() + + _Concurrency.Task { + do { + let queryParams = DeleteFolderByIdQueryParams(recursive: true) + try await client.folders.deleteFolderById(folderId: item.identifier, queryParams: queryParams) + CloudAccessDDLogDebug("BoxCloudProvider: deleteFolder(for: \(item.identifier)) succeeded") + do { + try self.identifierCache.invalidate(item) + pendingPromise.fulfill(()) + } catch { + CloudAccessDDLogDebug("BoxCloudProvider: Cache update failed with error: \(error)") + pendingPromise.reject(error) } + } catch let error as BoxSDKError where error.message.contains("Developer token has expired") { + CloudAccessDDLogDebug("BoxCloudProvider: deleteFolder(for: \(item.identifier)) failed with error: unauthorized access") + pendingPromise.reject(CloudProviderError.unauthorized) + } catch let error as BoxSDKError where error.message.contains("notFound") { + CloudAccessDDLogDebug("BoxCloudProvider: deleteFolder(for: \(item.identifier)) failed with error: not found") + pendingPromise.reject(CloudProviderError.itemNotFound) + } catch { + CloudAccessDDLogDebug("BoxCloudProvider: deleteFolder(for: \(item.identifier)) failed with error: \(error.localizedDescription)") + pendingPromise.reject(error) } } + + return pendingPromise } private func moveFile(from sourceItem: BoxItem, toParent targetParentItem: BoxItem, targetCloudPath: CloudPath) -> Promise { CloudAccessDDLogDebug("BoxCloudProvider: moveFile(from: \(sourceItem.identifier), toParent: \(targetParentItem.identifier), targetCloudPath: \(targetCloudPath.path)) called") - guard let client = credential.client else { - return Promise(CloudProviderError.unauthorized) - } - - return Promise { fulfill, reject in - let newName = targetCloudPath.lastPathComponent - client.files.update(fileId: sourceItem.identifier, name: newName, parentId: targetParentItem.identifier) { result in - switch result { - case .success: - CloudAccessDDLogDebug("BoxCloudProvider: moveFile succeeded for \(sourceItem.identifier) to \(targetCloudPath.path)") - do { - try self.identifierCache.invalidate(sourceItem) - let newItem = BoxItem(cloudPath: targetCloudPath, identifier: sourceItem.identifier, itemType: sourceItem.itemType) - try self.identifierCache.addOrUpdate(newItem) - fulfill(()) - } catch { - reject(error) - } - case let .failure(error): - CloudAccessDDLogDebug("BoxCloudProvider: moveFile failed for \(sourceItem.identifier) with error: \(error)") - if error.message == .unauthorizedAccess { - reject(CloudProviderError.unauthorized) - } else { - reject(error) - } + + let client = credential.client + + let pendingPromise = Promise.pending() + + _Concurrency.Task { + do { + let newName = targetCloudPath.lastPathComponent + let parentId = UpdateFileByIdRequestBodyParentField(id: targetParentItem.identifier) + let requestBody = UpdateFileByIdRequestBody(name: newName, parent: parentId) + _ = try await client.files.updateFileById(fileId: sourceItem.identifier, requestBody: requestBody) + CloudAccessDDLogDebug("BoxCloudProvider: moveFile succeeded for \(sourceItem.identifier) to \(targetCloudPath.path)") + do { + try self.identifierCache.invalidate(sourceItem) + let newItem = BoxItem(cloudPath: targetCloudPath, identifier: sourceItem.identifier, itemType: sourceItem.itemType) + try self.identifierCache.addOrUpdate(newItem) + pendingPromise.fulfill(()) + } catch { + CloudAccessDDLogDebug("BoxCloudProvider: Cache update failed with error: \(error)") + pendingPromise.reject(error) } + } catch let error as BoxSDKError where error.message.contains("Developer token has expired") { + CloudAccessDDLogDebug("BoxCloudProvider: moveFile failed for \(sourceItem.identifier) with error: unauthorized access") + pendingPromise.reject(CloudProviderError.unauthorized) + } catch { + CloudAccessDDLogDebug("BoxCloudProvider: moveFile failed for \(sourceItem.identifier) with error: \(error.localizedDescription)") + pendingPromise.reject(error) } } + + return pendingPromise } private func moveFolder(from sourceItem: BoxItem, toParent targetParentItem: BoxItem, targetCloudPath: CloudPath) -> Promise { CloudAccessDDLogDebug("BoxCloudProvider: moveFolder(from: \(sourceItem.identifier), toParent: \(targetParentItem.identifier), targetCloudPath: \(targetCloudPath.path)) called") - guard let client = credential.client else { - return Promise(CloudProviderError.unauthorized) - } - - return Promise { fulfill, reject in - let newName = targetCloudPath.lastPathComponent - client.folders.update(folderId: sourceItem.identifier, name: newName, parentId: targetParentItem.identifier) { result in - switch result { - case .success: - CloudAccessDDLogDebug("BoxCloudProvider: moveFolder succeeded for \(sourceItem.identifier) to \(targetCloudPath.path)") - do { - try self.identifierCache.invalidate(sourceItem) - let newItem = BoxItem(cloudPath: targetCloudPath, identifier: sourceItem.identifier, itemType: sourceItem.itemType) - try self.identifierCache.addOrUpdate(newItem) - fulfill(()) - } catch { - reject(error) - } - case let .failure(error): - CloudAccessDDLogDebug("BoxCloudProvider: moveFolder failed for \(sourceItem.identifier) with error: \(error)") - if error.message == .unauthorizedAccess { - reject(CloudProviderError.unauthorized) - } else { - reject(error) - } + + let client = credential.client + + let pendingPromise = Promise.pending() + + _Concurrency.Task { + do { + let newName = targetCloudPath.lastPathComponent + let parentId = UpdateFolderByIdRequestBodyParentField(id: targetParentItem.identifier) + let requestBody = UpdateFolderByIdRequestBody(name: newName, parent: parentId) + _ = try await client.folders.updateFolderById(folderId: sourceItem.identifier, requestBody: requestBody) + CloudAccessDDLogDebug("BoxCloudProvider: moveFolder succeeded for \(sourceItem.identifier) to \(targetCloudPath.path)") + do { + try self.identifierCache.invalidate(sourceItem) + let newItem = BoxItem(cloudPath: targetCloudPath, identifier: sourceItem.identifier, itemType: sourceItem.itemType) + try self.identifierCache.addOrUpdate(newItem) + pendingPromise.fulfill(()) + } catch { + CloudAccessDDLogDebug("BoxCloudProvider: Cache update failed with error: \(error)") + pendingPromise.reject(error) } + } catch let error as BoxSDKError where error.message.contains("Developer token has expired") { + CloudAccessDDLogDebug("BoxCloudProvider: moveFolder failed for \(sourceItem.identifier) with error: unauthorized access") + pendingPromise.reject(CloudProviderError.unauthorized) + } catch { + CloudAccessDDLogDebug("BoxCloudProvider: moveFolder failed for \(sourceItem.identifier) with error: \(error.localizedDescription)") + pendingPromise.reject(error) } } + + return pendingPromise } // MARK: - Resolve Path @@ -513,45 +526,54 @@ public class BoxCloudProvider: CloudProvider { } func getBoxItem(for name: String, withParentItem parentItem: BoxItem) -> Promise { - guard let client = credential.client else { - return Promise(CloudProviderError.unauthorized) - } - - return Promise { fulfill, reject in - CloudAccessDDLogDebug("BoxCloudProvider: getBoxItem(for: \(name), withParentItem: \(parentItem.identifier)) called") - - let iterator = client.folders.listItems(folderId: parentItem.identifier) - iterator.next { result in - switch result { - case let .success(page): - for item in page.entries { - do { - if let mappedItem = try self.mapFolderItemToBoxItem(name: name, parentItem: parentItem, item: item) { - fulfill(mappedItem) - return + let client = credential.client + + let pendingPromise = Promise.pending() + + _Concurrency.Task { + do { + var foundItem: BoxItem? + var keepFetching = true + var nextMarker: String? + + while keepFetching { + let queryParams = GetFolderItemsQueryParams(fields: ["name", "size", "modified_at"], usemarker: true, marker: nextMarker, limit: Int64(self.maxPageSize)) + let page = try await client.folders.getFolderItems(folderId: parentItem.identifier, queryParams: queryParams) + + if let entries = page.entries { + for entry in entries { + if let mappedItem = try self.mapEntryToBoxItem(name: name, parentItem: parentItem, entry: entry) { + foundItem = mappedItem } - } catch { - reject(error) - return } } - reject(CloudProviderError.itemNotFound) - case let .failure(error): - if error.message == .unauthorizedAccess { - reject(CloudProviderError.unauthorized) - } else { - reject(error) - } + keepFetching = false // TODO: fix when nextMarker is available } + + if let item = foundItem { + CloudAccessDDLogDebug("BoxCloudProvider: Found item \(name) in folder \(parentItem.identifier)") + pendingPromise.fulfill(item) + } else { + CloudAccessDDLogDebug("BoxCloudProvider: Item \(name) not found in folder \(parentItem.identifier)") + pendingPromise.reject(CloudProviderError.itemNotFound) + } + } catch let error as BoxSDKError where error.message.contains("Developer token has expired") { + CloudAccessDDLogDebug("BoxCloudProvider: Unauthorized access error while searching for item \(name) in folder \(parentItem.identifier)") + pendingPromise.reject(CloudProviderError.unauthorized) + } catch { + CloudAccessDDLogDebug("BoxCloudProvider: Error searching for item \(name) in folder \(parentItem.identifier): \(error.localizedDescription)") + pendingPromise.reject(error) } } + + return pendingPromise } - func mapFolderItemToBoxItem(name: String, parentItem: BoxItem, item: FolderItem) throws -> BoxItem? { - switch item { - case let .file(file) where file.name == name: + func mapEntryToBoxItem(name: String, parentItem: BoxItem, entry: FileFullOrFolderMiniOrWebLink) throws -> BoxItem? { + switch entry { + case let .fileFull(file) where file.name == name: return BoxItem(cloudPath: parentItem.cloudPath.appendingPathComponent(name), file: file) - case let .folder(folder) where folder.name == name: + case let .folderMini(folder) where folder.name == name: return BoxItem(cloudPath: parentItem.cloudPath.appendingPathComponent(name), folder: folder) case .webLink: throw BoxError.unexpectedContent @@ -562,7 +584,7 @@ public class BoxCloudProvider: CloudProvider { // MARK: - Helpers - private func convertToCloudItemMetadata(_ content: FolderItem, at cloudPath: CloudPath) throws -> CloudItemMetadata { + private func convertToCloudItemMetadata(_ content: FileOrFolderOrWebLink, at cloudPath: CloudPath) throws -> CloudItemMetadata { switch content { case let .file(fileMetadata): return convertToCloudItemMetadata(fileMetadata, at: cloudPath) @@ -576,19 +598,47 @@ public class BoxCloudProvider: CloudProvider { private func convertToCloudItemMetadata(_ metadata: File, at cloudPath: CloudPath) -> CloudItemMetadata { let name = metadata.name ?? "" let itemType = CloudItemType.file - let lastModifiedDate = metadata.modifiedAt - let size = metadata.size + let size = metadata.size.map { Int($0) } + let dateString = metadata.modifiedAt + + let dateFormatter = ISO8601DateFormatter() + + let lastModifiedDate = dateString != nil ? dateFormatter.date(from: dateString!) : nil return CloudItemMetadata(name: name, cloudPath: cloudPath, itemType: itemType, lastModifiedDate: lastModifiedDate, size: size) } private func convertToCloudItemMetadata(_ metadata: Folder, at cloudPath: CloudPath) -> CloudItemMetadata { let name = metadata.name ?? "" let itemType = CloudItemType.folder - let lastModifiedDate = metadata.modifiedAt + let dateString = metadata.modifiedAt + + let dateFormatter = ISO8601DateFormatter() + + let lastModifiedDate = dateString != nil ? dateFormatter.date(from: dateString!) : nil + return CloudItemMetadata(name: name, cloudPath: cloudPath, itemType: itemType, lastModifiedDate: lastModifiedDate, size: nil) } - private func convertToCloudItemList(_ contents: [FolderItem], at cloudPath: CloudPath) throws -> CloudItemList { + private func convertToCloudItemMetadata(_ metadata: FileFull, at cloudPath: CloudPath) -> CloudItemMetadata { + let name = metadata.name ?? "" + let itemType = CloudItemType.file + let size = metadata.size.map { Int($0) } + let dateString = metadata.modifiedAt + + let dateFormatter = ISO8601DateFormatter() + + let lastModifiedDate = dateString != nil ? dateFormatter.date(from: dateString!) : nil + return CloudItemMetadata(name: name, cloudPath: cloudPath, itemType: itemType, lastModifiedDate: lastModifiedDate, size: size) + } + + private func convertToCloudItemMetadata(_ metadata: FolderMini, at cloudPath: CloudPath) -> CloudItemMetadata { + let name = metadata.name ?? "" + let itemType = CloudItemType.folder + + return CloudItemMetadata(name: name, cloudPath: cloudPath, itemType: itemType, lastModifiedDate: nil, size: nil) + } + + private func convertToCloudItemList(_ contents: [FileOrFolderOrWebLink], at cloudPath: CloudPath) throws -> CloudItemList { var items = [CloudItemMetadata]() for content in contents { switch content { @@ -606,4 +656,17 @@ public class BoxCloudProvider: CloudProvider { } return CloudItemList(items: items, nextPageToken: nil) } + + private func convertToCloudItemList(_ contents: Files, at cloudPath: CloudPath) throws -> CloudItemList { + var items = [CloudItemMetadata]() + guard let entries = contents.entries else { + return CloudItemList(items: []) + } + for content in entries { + let itemCloudPath = cloudPath.appendingPathComponent(content.name ?? "") + let itemMetadata = convertToCloudItemMetadata(content, at: itemCloudPath) + items.append(itemMetadata) + } + return CloudItemList(items: items, nextPageToken: nil) + } } diff --git a/Sources/CryptomatorCloudAccess/Box/BoxCredential.swift b/Sources/CryptomatorCloudAccess/Box/BoxCredential.swift index 5337dcc..746c1ab 100644 --- a/Sources/CryptomatorCloudAccess/Box/BoxCredential.swift +++ b/Sources/CryptomatorCloudAccess/Box/BoxCredential.swift @@ -7,56 +7,56 @@ // import AuthenticationServices -import BoxSDK +import BoxSdkGen import Foundation import Promises public enum BoxCredentialErrors: Error { case noUsername + case authenticationFailed } public class BoxCredential { - public internal(set) var client: BoxClient? - - public init(tokenStore: TokenStore) { - let sdk = BoxSDK(clientId: BoxSetup.constants.clientId, clientSecret: BoxSetup.constants.clientSecret) - sdk.getOAuth2Client(tokenStore: tokenStore) { result in - switch result { - case let .success(client): - self.client = client - case let .failure: - break - } - } + public var client: BoxClient + + public init(tokenStore: TokenStorage) { + let config = OAuthConfig(clientId: BoxSetup.constants.clientId, clientSecret: BoxSetup.constants.clientSecret, tokenStorage: tokenStore) + let oauth = BoxOAuth(config: config) + self.client = BoxClient(auth: oauth) } public func deauthenticate() -> Promise { - return Promise { fulfill, reject in - self.client?.destroy { result in - switch result { - case .success: - fulfill(()) - case let .failure(error): - reject(error) - } + let pendingPromise = Promise.pending() + + _Concurrency.Task { + do { + let networkSession = NetworkSession() + try await self.client.auth.revokeToken(networkSession: networkSession) + pendingPromise.fulfill(()) + } catch { + pendingPromise.reject(error) } } + + return pendingPromise } public func getUsername() -> Promise { - return Promise(on: .global()) { fulfill, reject in - self.client?.users.getCurrent(fields: ["name"]) { result in - switch result { - case let .success(user): - if let name = user.name { - fulfill(name) - } else { - reject(BoxCredentialErrors.noUsername) - } - case let .failure(error): - reject(error) + let pendingPromise = Promise.pending() + + _Concurrency.Task { + do { + let user = try await client.users.getUserMe() + if let name = user.name { + pendingPromise.fulfill(name) + } else { + pendingPromise.reject(BoxCredentialErrors.noUsername) } + } catch { + pendingPromise.reject(error) } } + + return pendingPromise } } diff --git a/Sources/CryptomatorCloudAccess/Box/BoxIdentifierCache.swift b/Sources/CryptomatorCloudAccess/Box/BoxIdentifierCache.swift index daa078c..05e9b10 100644 --- a/Sources/CryptomatorCloudAccess/Box/BoxIdentifierCache.swift +++ b/Sources/CryptomatorCloudAccess/Box/BoxIdentifierCache.swift @@ -6,7 +6,6 @@ // Copyright © 2024 Skymatic GmbH. All rights reserved. // -import BoxSDK import Foundation import GRDB diff --git a/Sources/CryptomatorCloudAccess/Box/BoxItem.swift b/Sources/CryptomatorCloudAccess/Box/BoxItem.swift index 92ee242..546d842 100644 --- a/Sources/CryptomatorCloudAccess/Box/BoxItem.swift +++ b/Sources/CryptomatorCloudAccess/Box/BoxItem.swift @@ -6,7 +6,7 @@ // Copyright © 2024 Skymatic GmbH. All rights reserved. // -import BoxSDK +import BoxSdkGen import Foundation import GRDB @@ -22,24 +22,27 @@ struct BoxItem: Decodable, FetchableRecord, TableRecord, Equatable { } extension BoxItem { - init(cloudPath: CloudPath, folderItem: FolderItem) throws { + // TODO: Must be checked whether this init is needed at all + + init(cloudPath: CloudPath, folderItem: FileOrFolderOrWebLink) throws { switch folderItem { case let .file(file): self.init(cloudPath: cloudPath, file: file) case let .folder(folder): self.init(cloudPath: cloudPath, folder: folder) - case let .webLink(webLink): + // Weblinks are currently not supported, if required they can be added later. + case .webLink: throw BoxError.unexpectedContent } } - init(cloudPath: CloudPath, file: File) { + init(cloudPath: CloudPath, file: FileBase) { self.cloudPath = cloudPath self.identifier = file.id self.itemType = .file } - init(cloudPath: CloudPath, folder: Folder) { + init(cloudPath: CloudPath, folder: FolderBase) { self.cloudPath = cloudPath self.identifier = folder.id self.itemType = .folder diff --git a/Tests/CryptomatorCloudAccessIntegrationTests/Box/BoxCredentialMock.swift b/Tests/CryptomatorCloudAccessIntegrationTests/Box/BoxCredentialMock.swift index 3ba8053..a530519 100644 --- a/Tests/CryptomatorCloudAccessIntegrationTests/Box/BoxCredentialMock.swift +++ b/Tests/CryptomatorCloudAccessIntegrationTests/Box/BoxCredentialMock.swift @@ -8,22 +8,48 @@ import Foundation import Promises -@testable import BoxSDK +@testable import BoxSdkGen #if canImport(CryptomatorCloudAccessCore) @testable import CryptomatorCloudAccessCore #else @testable import CryptomatorCloudAccess #endif +// Custom in-memory token storage +class InMemoryTokenStore: TokenStorage { + private var tokenInfo: AccessToken? + + func store(token: AccessToken) async throws { + tokenInfo = token + } + + func get() async throws -> AccessToken? { + return tokenInfo + } + + func clear() async throws { + tokenInfo = nil + } +} + class BoxCredentialMock: BoxCredential { init() { + // Set up Box constants for testing purposes BoxSetup.constants = BoxSetup(clientId: "", clientSecret: "", sharedContainerIdentifier: "") - super.init(tokenStore: MemoryTokenStore()) - client = BoxSDK.getClient(token: IntegrationTestSecrets.boxDeveloperToken) + + // Initialize the BoxCredential with InMemoryTokenStore + let tokenStore = InMemoryTokenStore() + super.init(tokenStore: tokenStore) + + // Override the client with a test token using BoxDeveloperTokenAuth + let devTokenAuth = BoxDeveloperTokenAuth(token: IntegrationTestSecrets.boxDeveloperToken) + client = BoxClient(auth: devTokenAuth) } override func deauthenticate() -> Promise { - client = BoxSDK.getClient(token: "invalid") + // Set the client to an invalid token for deauthentication in tests + let invalidTokenAuth = BoxDeveloperTokenAuth(token: "invalid") + client = BoxClient(auth: invalidTokenAuth) return Promise(()) } } From 17de27ab7824b6d8af344354e0ba6beba9de12ff Mon Sep 17 00:00:00 2001 From: Majid Achhoud Date: Mon, 27 May 2024 17:01:21 +0200 Subject: [PATCH 18/21] Add token storage in BoxAuthenticator and getUserId method in BoxCredential --- .../Box/BoxAuthenticator.swift | 2 +- .../Box/BoxCredential.swift | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/Sources/CryptomatorCloudAccess/Box/BoxAuthenticator.swift b/Sources/CryptomatorCloudAccess/Box/BoxAuthenticator.swift index 950c9b2..0aaf577 100644 --- a/Sources/CryptomatorCloudAccess/Box/BoxAuthenticator.swift +++ b/Sources/CryptomatorCloudAccess/Box/BoxAuthenticator.swift @@ -29,7 +29,7 @@ public enum BoxAuthenticator { throw BoxAuthenticatorError.invalidContext } - let config = OAuthConfig(clientId: BoxSetup.constants.clientId, clientSecret: BoxSetup.constants.clientSecret) + let config = OAuthConfig(clientId: BoxSetup.constants.clientId, clientSecret: BoxSetup.constants.clientSecret, tokenStorage: tokenStorage) let oauth = BoxOAuth(config: config) // Run the login flow and store the access token using tokenStorage diff --git a/Sources/CryptomatorCloudAccess/Box/BoxCredential.swift b/Sources/CryptomatorCloudAccess/Box/BoxCredential.swift index 746c1ab..d9ba7b2 100644 --- a/Sources/CryptomatorCloudAccess/Box/BoxCredential.swift +++ b/Sources/CryptomatorCloudAccess/Box/BoxCredential.swift @@ -59,4 +59,19 @@ public class BoxCredential { return pendingPromise } + + public func getUserId() -> Promise { + let pendingPromise = Promise.pending() + + _Concurrency.Task { + do { + let user = try await client.users.getUserMe() + pendingPromise.fulfill(user.id) + } catch { + pendingPromise.reject(error) + } + } + + return pendingPromise + } } From 0af03cd1b63663c69aca55f7fbc1fc821c93e335 Mon Sep 17 00:00:00 2001 From: Majid Achhoud Date: Fri, 21 Jun 2024 15:00:28 +0200 Subject: [PATCH 19/21] Revert specific changes in Package.resolved and BoxIntegrationTests.xcscheme --- .../xcshareddata/swiftpm/Package.resolved | 17 ++++++++--------- .../xcschemes/BoxIntegrationTests.xcscheme | 9 +-------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/CryptomatorCloudAccess.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/CryptomatorCloudAccess.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 87e5597..a7086da 100644 --- a/CryptomatorCloudAccess.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/CryptomatorCloudAccess.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,4 @@ { - "originHash" : "9cadc7a70613b038fe8d97d191b9a6f48d09309fa0636f36dfa1ef6376591ebc", "pins" : [ { "identity" : "appauth-ios", @@ -15,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/aws-amplify/aws-sdk-ios-spm.git", "state" : { - "revision" : "8ff8bebfe24271f7b16c5abaeb78daf82bee3a80", - "version" : "2.34.2" + "revision" : "cfcf97f6994b6ffd9a3244dc638458f5822aba56", + "version" : "2.34.0" } }, { @@ -42,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/CocoaLumberjack/CocoaLumberjack.git", "state" : { - "revision" : "4b8714a7fb84d42393314ce897127b3939885ec3", - "version" : "3.8.5" + "revision" : "67ec5818a757aba4d7c534e21a905d878d128dbf", + "version" : "3.8.1" } }, { @@ -114,8 +113,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/AzureAD/microsoft-authentication-library-for-objc.git", "state" : { - "revision" : "d2f81ded070ac6452b2a6acb5bc45eb566427fe7", - "version" : "1.3.3" + "revision" : "f3f84a63de86f7f121544ec917e0365e624d6a97", + "version" : "1.3.0" } }, { @@ -157,12 +156,12 @@ { "identity" : "swift-log", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-log", + "location" : "https://github.com/apple/swift-log.git", "state" : { "revision" : "e97a6fcb1ab07462881ac165fdbb37f067e205d5", "version" : "1.5.4" } } ], - "version" : 3 + "version" : 2 } diff --git a/CryptomatorCloudAccess.xcodeproj/xcshareddata/xcschemes/BoxIntegrationTests.xcscheme b/CryptomatorCloudAccess.xcodeproj/xcshareddata/xcschemes/BoxIntegrationTests.xcscheme index 627de32..fa130ba 100644 --- a/CryptomatorCloudAccess.xcodeproj/xcshareddata/xcschemes/BoxIntegrationTests.xcscheme +++ b/CryptomatorCloudAccess.xcodeproj/xcshareddata/xcschemes/BoxIntegrationTests.xcscheme @@ -10,7 +10,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "NO"> + shouldUseLaunchSchemeArgsEnv = "YES"> @@ -111,13 +111,6 @@ debugDocumentVersioning = "YES" debugServiceExtension = "internal" allowLocationSimulation = "YES"> - - - - Date: Fri, 21 Jun 2024 15:28:07 +0200 Subject: [PATCH 20/21] Implement chunked upload, adjust authentication, and some improvements --- Package.resolved | 167 ------------- .../Box/BoxAuthenticator.swift | 16 +- .../Box/BoxCloudProvider.swift | 233 ++++++++++++++---- .../Box/BoxCredential.swift | 4 +- .../Box/BoxCredentialMock.swift | 21 +- 5 files changed, 206 insertions(+), 235 deletions(-) delete mode 100644 Package.resolved diff --git a/Package.resolved b/Package.resolved deleted file mode 100644 index 5cfc438..0000000 --- a/Package.resolved +++ /dev/null @@ -1,167 +0,0 @@ -{ - "pins" : [ - { - "identity" : "appauth-ios", - "kind" : "remoteSourceControl", - "location" : "https://github.com/openid/AppAuth-iOS.git", - "state" : { - "revision" : "71cde449f13d453227e687458144bde372d30fc7", - "version" : "1.6.2" - } - }, - { - "identity" : "aws-sdk-ios-spm", - "kind" : "remoteSourceControl", - "location" : "https://github.com/aws-amplify/aws-sdk-ios-spm.git", - "state" : { - "revision" : "cfcf97f6994b6ffd9a3244dc638458f5822aba56", - "version" : "2.34.0" - } - }, - { - "identity" : "base32", - "kind" : "remoteSourceControl", - "location" : "https://github.com/norio-nomura/Base32.git", - "state" : { - "revision" : "c4bc0a49689999ae2c7c778f3830a6a6e694efb8", - "version" : "0.9.0" - } - }, - { - "identity" : "box-swift-sdk-gen", - "kind" : "remoteSourceControl", - "location" : "https://github.com/box/box-swift-sdk-gen.git", - "state" : { - "revision" : "e5069af728c4b4f048078dfb5aed683c926da857", - "version" : "0.2.0" - } - }, - { - "identity" : "cocoalumberjack", - "kind" : "remoteSourceControl", - "location" : "https://github.com/CocoaLumberjack/CocoaLumberjack.git", - "state" : { - "revision" : "4b8714a7fb84d42393314ce897127b3939885ec3", - "version" : "3.8.5" - } - }, - { - "identity" : "cryptolib-swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/cryptomator/cryptolib-swift.git", - "state" : { - "revision" : "6e5dbea6e05742ad82a074bf7ee8c3305d92fbae", - "version" : "1.1.0" - } - }, - { - "identity" : "dropbox-sdk-obj-c-spm", - "kind" : "remoteSourceControl", - "location" : "https://github.com/phil1995/dropbox-sdk-obj-c-spm.git", - "state" : { - "revision" : "87c1fcf96622ab90a956bdf89331ddb4164f4855", - "version" : "7.2.0" - } - }, - { - "identity" : "google-api-objectivec-client-for-rest", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/google-api-objectivec-client-for-rest.git", - "state" : { - "revision" : "bcb0439b37d16d39da6f62139d4009d09e7aef14", - "version" : "3.4.0" - } - }, - { - "identity" : "grdb.swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/groue/GRDB.swift.git", - "state" : { - "revision" : "dd7e7f39e8e4d7a22d258d9809a882f914690b01", - "version" : "5.26.1" - } - }, - { - "identity" : "gtm-session-fetcher", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/gtm-session-fetcher.git", - "state" : { - "revision" : "d415594121c9e8a4f9d79cecee0965cf35e74dbd", - "version" : "3.1.1" - } - }, - { - "identity" : "gtmappauth", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/GTMAppAuth.git", - "state" : { - "revision" : "41aba100f28395ebe842cd66e5d371cdd46c6792", - "version" : "4.0.0" - } - }, - { - "identity" : "joseswift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tobihagemann/JOSESwift.git", - "state" : { - "revision" : "3544f8117908ef12ea13b1c0927e0e3c0d30ee01", - "version" : "2.4.1-cryptomator" - } - }, - { - "identity" : "microsoft-authentication-library-for-objc", - "kind" : "remoteSourceControl", - "location" : "https://github.com/AzureAD/microsoft-authentication-library-for-objc.git", - "state" : { - "revision" : "f3f84a63de86f7f121544ec917e0365e624d6a97", - "version" : "1.3.0" - } - }, - { - "identity" : "msgraph-sdk-objc-models-spm", - "kind" : "remoteSourceControl", - "location" : "https://github.com/phil1995/msgraph-sdk-objc-models-spm.git", - "state" : { - "revision" : "172b07fe8a7da6072149e2fd92051a510b25035e", - "version" : "1.3.0" - } - }, - { - "identity" : "msgraph-sdk-objc-spm", - "kind" : "remoteSourceControl", - "location" : "https://github.com/phil1995/msgraph-sdk-objc-spm.git", - "state" : { - "revision" : "0320c6a99207b53288970382afcf5054852f9724", - "version" : "1.0.0" - } - }, - { - "identity" : "pcloud-sdk-swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pCloud/pcloud-sdk-swift.git", - "state" : { - "revision" : "6da4ca6bb4e7068145d9325988e29862d26300ba", - "version" : "3.2.0" - } - }, - { - "identity" : "promises", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/promises.git", - "state" : { - "revision" : "e70e889c0196c76d22759eb50d6a0270ca9f1d9e", - "version" : "2.3.1" - } - }, - { - "identity" : "swift-log", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-log", - "state" : { - "revision" : "e97a6fcb1ab07462881ac165fdbb37f067e205d5", - "version" : "1.5.4" - } - } - ], - "version" : 2 -} diff --git a/Sources/CryptomatorCloudAccess/Box/BoxAuthenticator.swift b/Sources/CryptomatorCloudAccess/Box/BoxAuthenticator.swift index 0aaf577..cd8ce74 100644 --- a/Sources/CryptomatorCloudAccess/Box/BoxAuthenticator.swift +++ b/Sources/CryptomatorCloudAccess/Box/BoxAuthenticator.swift @@ -17,6 +17,7 @@ import UIKit public enum BoxAuthenticatorError: Error { case authenticationFailed case invalidContext + case loginCancelled } public enum BoxAuthenticator { @@ -34,11 +35,18 @@ public enum BoxAuthenticator { // Run the login flow and store the access token using tokenStorage try await oauth.runLoginFlow(options: .init(), context: context) - // TODO: Catch error when login failed - pendingPromise.fulfill(BoxCredential(tokenStore: tokenStorage)) - } catch { - pendingPromise.reject(BoxAuthenticatorError.authenticationFailed) + pendingPromise.fulfill(BoxCredential(tokenStorage: tokenStorage)) + } catch let error as ASWebAuthenticationSessionError { + if error.code == .canceledLogin { + // Handle the login cancellation + CloudAccessDDLogDebug("BoxAuthenticator: Login flow cancelled by the user.") + pendingPromise.reject(BoxAuthenticatorError.loginCancelled) + } else { + // Handle other authentication errors + CloudAccessDDLogDebug("BoxAuthenticator: Authentication failed with error: \(error.localizedDescription).") + pendingPromise.reject(BoxAuthenticatorError.authenticationFailed) + } } } diff --git a/Sources/CryptomatorCloudAccess/Box/BoxCloudProvider.swift b/Sources/CryptomatorCloudAccess/Box/BoxCloudProvider.swift index cccceaa..108dead 100644 --- a/Sources/CryptomatorCloudAccess/Box/BoxCloudProvider.swift +++ b/Sources/CryptomatorCloudAccess/Box/BoxCloudProvider.swift @@ -7,6 +7,7 @@ // import BoxSdkGen +import CryptoKit import Foundation import Promises @@ -14,11 +15,23 @@ public class BoxCloudProvider: CloudProvider { private let credential: BoxCredential private let identifierCache: BoxIdentifierCache private let maxPageSize: Int + private let networkSession: NetworkSession - public init(credential: BoxCredential, maxPageSize: Int = .max) throws { + public init(credential: BoxCredential, maxPageSize: Int = .max, urlSessionConfiguration: URLSessionConfiguration) throws { self.credential = credential self.identifierCache = try BoxIdentifierCache() self.maxPageSize = max(1, min(maxPageSize, 1000)) + self.networkSession = NetworkSession(configuration: urlSessionConfiguration) + } + + public convenience init(credential: BoxCredential, maxPageSize: Int = .max) throws { + try self.init(credential: credential, maxPageSize: maxPageSize, urlSessionConfiguration: .default) + } + + public static func withBackgroundSession(credential: BoxCredential, maxPageSize: Int = .max, sessionIdentifier: String) throws -> BoxCloudProvider { + let configuration = URLSessionConfiguration.background(withIdentifier: sessionIdentifier) + configuration.sharedContainerIdentifier = BoxSetup.constants.sharedContainerIdentifier + return try BoxCloudProvider(credential: credential, maxPageSize: maxPageSize, urlSessionConfiguration: configuration) } public func fetchItemMetadata(at cloudPath: CloudPath) -> Promise { @@ -39,7 +52,7 @@ public class BoxCloudProvider: CloudProvider { return Promise(CloudProviderError.itemAlreadyExists) } return resolvePath(forItemAt: cloudPath).then { item in - self.downloadFile(for: item, to: localURL) + self.downloadFile(for: item, to: localURL, onTaskCreation: onTaskCreation) } } @@ -65,7 +78,7 @@ public class BoxCloudProvider: CloudProvider { }.then { _ -> Promise in return self.resolveParentPath(forItemAt: cloudPath) }.then { parentItem in - return self.uploadFile(for: parentItem, from: localURL, to: cloudPath) + return self.uploadFile(for: parentItem, from: localURL, to: cloudPath, onTaskCreation: onTaskCreation) } } @@ -220,7 +233,7 @@ public class BoxCloudProvider: CloudProvider { return pendingPromise } - private func downloadFile(for item: BoxItem, to localURL: URL) -> Promise { + private func downloadFile(for item: BoxItem, to localURL: URL, onTaskCreation: ((URLSessionDownloadTask?) -> Void)?) -> Promise { CloudAccessDDLogDebug("BoxCloudProvider: downloadFile(for: \(item.identifier), to: \(localURL)) called") guard item.itemType == .file else { return Promise(CloudProviderError.itemTypeMismatch) @@ -232,14 +245,27 @@ public class BoxCloudProvider: CloudProvider { _Concurrency.Task { do { - _ = try await client.downloads.downloadFile(fileId: item.identifier, downloadDestinationURL: localURL) - CloudAccessDDLogDebug("BoxCloudProvider: downloadFile(for: \(item.identifier), to: \(localURL)) finished downloading") - pendingPromise.fulfill(()) - } catch let error as BoxSDKError where error.message.contains("Developer token has expired") { - CloudAccessDDLogDebug("BoxCloudProvider: downloadFile(for: \(item.identifier)) error: unauthorized access") - pendingPromise.reject(CloudProviderError.unauthorized) + let request = try await client.downloads.downloadFile(fileId: item.identifier, downloadDestinationURL: localURL) + let task = networkSession.session.downloadTask(with: request) { url, _, error in + if let error = error { + CloudAccessDDLogDebug("BoxCloudProvider: downloadFile(for: \(item.identifier), to: \(localURL)) failed with error: \(error.localizedDescription)") + pendingPromise.reject(error) + } else if let url = url { + do { + try FileManager.default.moveItem(at: url, to: localURL) + CloudAccessDDLogDebug("BoxCloudProvider: downloadFile(for: \(item.identifier), to: \(localURL)) succeeded") + pendingPromise.fulfill(()) + } catch { + CloudAccessDDLogDebug("BoxCloudProvider: downloadFile(for: \(item.identifier), to: \(localURL)) failed with error: \(error.localizedDescription)") + pendingPromise.reject(error) + } + } + } + + onTaskCreation?(task) + task.resume() } catch { - CloudAccessDDLogDebug("BoxCloudProvider: downloadFile(for: \(item.identifier)) error: \(error.localizedDescription)") + CloudAccessDDLogDebug("BoxCloudProvider: downloadFile(for: \(item.identifier), to: \(localURL)) failed with error: \(error.localizedDescription)") pendingPromise.reject(error) } } @@ -247,64 +273,185 @@ public class BoxCloudProvider: CloudProvider { return pendingPromise } - private func uploadFile(for parentItem: BoxItem, from localURL: URL, to cloudPath: CloudPath) -> Promise { + private func uploadFile(for parentItem: BoxItem, from localURL: URL, to cloudPath: CloudPath, onTaskCreation: ((URLSessionUploadTask?) -> Void)?) -> Promise { let client = credential.client - let pendingPromise = Promise.pending() _Concurrency.Task { do { - guard let fileStream = InputStream(url: localURL) else { - return pendingPromise.reject(CloudProviderError.itemNotFound) + // TODO: Change Error Type + let attributes = try FileManager.default.attributesOfItem(atPath: localURL.path) + guard let fileSize = attributes[.size] as? Int64 else { + throw CloudProviderError.unauthorized } - let targetFileName = cloudPath.lastPathComponent - - let requestBody = UploadFileVersionRequestBody( - attributes: UploadFileVersionRequestBodyAttributesField( - name: targetFileName - ), - file: fileStream - ) - - let existingItem = try await resolvePath(forItemAt: cloudPath).async() - let updatedFile = try await client.uploads.uploadFileVersion(fileId: existingItem.identifier, requestBody: requestBody) - let list = try self.convertToCloudItemList(updatedFile, at: cloudPath.deletingLastPathComponent()) - guard let metadata = list.items.first else { + guard FileManager.default.fileExists(atPath: localURL.path) else { throw CloudProviderError.itemNotFound } - pendingPromise.fulfill(metadata) + let targetFileName = cloudPath.lastPathComponent - } catch CloudProviderError.itemNotFound { - do { - guard let fileStream = InputStream(url: localURL) else { - return pendingPromise.reject(CloudProviderError.itemNotFound) + if fileSize > 20 * 1024 * 1024 { + // Use Chunked Upload API for files larger than 20MB + CloudAccessDDLogDebug("BoxCloudProvider: Starting chunked upload for file: \(targetFileName)") + chunkedFileUpload(client: client, from: localURL, fileSize: fileSize, targetFileName: targetFileName, parentItem: parentItem, cloudPath: cloudPath, onTaskCreation: onTaskCreation).then { metadata in + pendingPromise.fulfill(metadata) + }.catch { error in + pendingPromise.reject(error) + } + } else { + // Use normal upload for smaller files + CloudAccessDDLogDebug("BoxCloudProvider: Starting normal upload for file: \(targetFileName)") + normalFileUpload(for: parentItem, from: localURL, to: cloudPath, onTaskCreation: onTaskCreation).then { metadata in + pendingPromise.fulfill(metadata) + }.catch { error in + pendingPromise.reject(error) } + } + } catch { + CloudAccessDDLogDebug("BoxCloudProvider: uploadFile(for: \(parentItem.identifier), from: \(localURL), to: \(cloudPath)) failed with error: \(error.localizedDescription)") + pendingPromise.reject(error) + } + } + + return pendingPromise + } + + private func normalFileUpload(for parentItem: BoxItem, from localURL: URL, to cloudPath: CloudPath, onTaskCreation: ((URLSessionUploadTask?) -> Void)?) -> Promise { + let client = credential.client + let pendingPromise = Promise.pending() + + _Concurrency.Task { + do { + guard let fileStream = InputStream(url: localURL) else { + CloudAccessDDLogDebug("BoxCloudProvider: normalFileUpload(for: \(parentItem.identifier), to: \(cloudPath)) - file not found") + return pendingPromise.reject(CloudProviderError.itemNotFound) + } - let targetFileName = cloudPath.lastPathComponent + let targetFileName = cloudPath.lastPathComponent + do { + let existingItem = try await resolvePath(forItemAt: cloudPath).async() + let requestBody = UploadFileVersionRequestBody( + attributes: UploadFileVersionRequestBodyAttributesField(name: targetFileName), + file: fileStream, + fileFileName: targetFileName + ) + // Use InputStream directly for uploading + let files = try await client.uploads.uploadFileVersion(fileId: existingItem.identifier, requestBody: requestBody) + + if let updatedFile = files.entries?.first { + let cloudMetadata = convertToCloudItemMetadata(updatedFile, at: cloudPath) + CloudAccessDDLogDebug("BoxCloudProvider: normalFileUpload(for: \(parentItem.identifier), to: \(cloudPath)) succeeded") + pendingPromise.fulfill(cloudMetadata) + } else { + throw CloudProviderError.itemNotFound + } + } catch CloudProviderError.itemNotFound { let requestBody = UploadFileRequestBody( attributes: UploadFileRequestBodyAttributesField( name: targetFileName, parent: UploadFileRequestBodyAttributesParentField(id: parentItem.identifier) ), - file: fileStream + file: fileStream, + fileFileName: targetFileName ) - let newFile = try await client.uploads.uploadFile(requestBody: requestBody) - let list = try self.convertToCloudItemList(newFile, at: cloudPath.deletingLastPathComponent()) - guard let metadata = list.items.first else { + // Use InputStream directly for uploading + let files = try await client.uploads.uploadFile(requestBody: requestBody) + + if let newFile = files.entries?.first { + let cloudMetadata = convertToCloudItemMetadata(newFile, at: cloudPath) + CloudAccessDDLogDebug("BoxCloudProvider: normalFileUpload(for: \(parentItem.identifier), to: \(cloudPath)) new file created successfully") + pendingPromise.fulfill(cloudMetadata) + } else { throw CloudProviderError.itemNotFound } - pendingPromise.fulfill(metadata) - } catch let error as BoxSDKError where error.message.contains("Developer token has expired") { - pendingPromise.reject(CloudProviderError.unauthorized) } catch { - // Handling other upload errors + CloudAccessDDLogDebug("BoxCloudProvider: normalFileUpload(for: \(parentItem.identifier), to: \(cloudPath)) failed with error: \(error.localizedDescription)") pendingPromise.reject(error) } } catch { - // General error handling if something goes wrong when determining the path + CloudAccessDDLogDebug("BoxCloudProvider: normalFileUpload(for: \(parentItem.identifier), to: \(cloudPath)) failed with error: \(error.localizedDescription)") + pendingPromise.reject(error) + } + } + + return pendingPromise + } + + private func chunkedFileUpload(client: BoxClient, from localURL: URL, fileSize: Int64, targetFileName: String, parentItem: BoxItem, cloudPath: CloudPath, onTaskCreation: ((URLSessionUploadTask?) -> Void)?) -> Promise { + let pendingPromise = Promise.pending() + + _Concurrency.Task { + do { + let requestBody = CreateFileUploadSessionRequestBody(folderId: parentItem.identifier, fileSize: fileSize, fileName: targetFileName) + let uploadSession = try await client.chunkedUploads.createFileUploadSession(requestBody: requestBody) + + guard let uploadSessionId = uploadSession.id else { + throw BoxSDKError(message: "Failed to retrieve upload session ID") + } + CloudAccessDDLogDebug("BoxCloudProvider: Upload session created with ID: \(uploadSessionId)") + + let chunkSize = uploadSession.partSize ?? 8 * 1024 * 1024 // Default to 8MB per chunk + var bytesUploaded: Int64 = 0 + var partArray: [UploadPart] = [] + + guard let fileStream = InputStream(url: localURL) else { + throw BoxSDKError(message: "Unable to create input stream from file URL") + } + + fileStream.open() + var totalSHA1 = Insecure.SHA1() + + while bytesUploaded < fileSize { + var buffer = [UInt8](repeating: 0, count: Int(chunkSize)) + let bytesRead = fileStream.read(&buffer, maxLength: buffer.count) + if bytesRead < 0 { + throw fileStream.streamError ?? BoxSDKError(message: "Unknown file stream error") + } + + let chunkData = Data(buffer[0 ..< bytesRead]) + let range = bytesUploaded ..< bytesUploaded + Int64(bytesRead) + let contentRange = "bytes \(range.lowerBound)-\(range.upperBound - 1)/\(fileSize)" + + let sha1 = Insecure.SHA1.hash(data: chunkData) + let sha1Base64 = Data(sha1).base64EncodedString() + let digestHeader = "sha=\(sha1Base64)" + totalSHA1.update(data: chunkData) + + let headers = UploadFilePartHeaders(digest: digestHeader, contentRange: contentRange) + CloudAccessDDLogDebug("BoxCloudProvider: Uploading chunk with content range: \(contentRange)") + + let uploadedPart = try await client.chunkedUploads.uploadFilePart(uploadSessionId: uploadSessionId, requestBody: InputStream(data: chunkData), headers: headers) + + if let part = uploadedPart.part { + partArray.append(part) + } else { + throw BoxSDKError(message: "Failed to retrieve upload part") + } + + bytesUploaded += Int64(bytesRead) + } + + fileStream.close() + + let finalSha1Base64 = Data(totalSHA1.finalize()).base64EncodedString() + let digestHeaderFinal = "sha=\(finalSha1Base64)" + let commitRequestBody = CreateFileUploadSessionCommitRequestBody(parts: partArray) + let commitHeaders = CreateFileUploadSessionCommitHeaders(digest: digestHeaderFinal) + CloudAccessDDLogDebug("BoxCloudProvider: Committing upload session with ID: \(uploadSessionId)") + + let commitResponse = try await client.chunkedUploads.createFileUploadSessionCommit(uploadSessionId: uploadSession.id ?? "", requestBody: commitRequestBody, headers: commitHeaders) + + guard let file = commitResponse.entries?.first else { + throw CloudProviderError.itemNotFound + } + + let cloudMetadata = convertToCloudItemMetadata(file, at: cloudPath) + CloudAccessDDLogDebug("BoxCloudProvider: chunkedFileUpload(for: \(parentItem.identifier), to: \(cloudPath)) succeeded") + pendingPromise.fulfill(cloudMetadata) + } catch { + CloudAccessDDLogDebug("BoxCloudProvider: chunkedFileUpload(for: \(parentItem.identifier), to: \(cloudPath)) failed with error: \(error.localizedDescription)") pendingPromise.reject(error) } } diff --git a/Sources/CryptomatorCloudAccess/Box/BoxCredential.swift b/Sources/CryptomatorCloudAccess/Box/BoxCredential.swift index d9ba7b2..18d534f 100644 --- a/Sources/CryptomatorCloudAccess/Box/BoxCredential.swift +++ b/Sources/CryptomatorCloudAccess/Box/BoxCredential.swift @@ -19,8 +19,8 @@ public enum BoxCredentialErrors: Error { public class BoxCredential { public var client: BoxClient - public init(tokenStore: TokenStorage) { - let config = OAuthConfig(clientId: BoxSetup.constants.clientId, clientSecret: BoxSetup.constants.clientSecret, tokenStorage: tokenStore) + public init(tokenStorage: TokenStorage) { + let config = OAuthConfig(clientId: BoxSetup.constants.clientId, clientSecret: BoxSetup.constants.clientSecret, tokenStorage: tokenStorage) let oauth = BoxOAuth(config: config) self.client = BoxClient(auth: oauth) } diff --git a/Tests/CryptomatorCloudAccessIntegrationTests/Box/BoxCredentialMock.swift b/Tests/CryptomatorCloudAccessIntegrationTests/Box/BoxCredentialMock.swift index a530519..d3729d7 100644 --- a/Tests/CryptomatorCloudAccessIntegrationTests/Box/BoxCredentialMock.swift +++ b/Tests/CryptomatorCloudAccessIntegrationTests/Box/BoxCredentialMock.swift @@ -15,31 +15,14 @@ import Promises @testable import CryptomatorCloudAccess #endif -// Custom in-memory token storage -class InMemoryTokenStore: TokenStorage { - private var tokenInfo: AccessToken? - - func store(token: AccessToken) async throws { - tokenInfo = token - } - - func get() async throws -> AccessToken? { - return tokenInfo - } - - func clear() async throws { - tokenInfo = nil - } -} - class BoxCredentialMock: BoxCredential { init() { // Set up Box constants for testing purposes BoxSetup.constants = BoxSetup(clientId: "", clientSecret: "", sharedContainerIdentifier: "") // Initialize the BoxCredential with InMemoryTokenStore - let tokenStore = InMemoryTokenStore() - super.init(tokenStore: tokenStore) + let tokenStore = InMemoryTokenStorage() + super.init(tokenStorage: tokenStore) // Override the client with a test token using BoxDeveloperTokenAuth let devTokenAuth = BoxDeveloperTokenAuth(token: IntegrationTestSecrets.boxDeveloperToken) From ee2f7a6c5d6fa855fb2515be43e21cf24f3e1ca9 Mon Sep 17 00:00:00 2001 From: Majid Achhoud Date: Wed, 26 Jun 2024 11:56:42 +0200 Subject: [PATCH 21/21] Update README with rationale for using Developer Tokens over OAuth Tokens --- Tests/CryptomatorCloudAccessIntegrationTests/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/CryptomatorCloudAccessIntegrationTests/README.md b/Tests/CryptomatorCloudAccessIntegrationTests/README.md index 5bc22ea..4653f4d 100644 --- a/Tests/CryptomatorCloudAccessIntegrationTests/README.md +++ b/Tests/CryptomatorCloudAccessIntegrationTests/README.md @@ -38,6 +38,8 @@ If you are building via a CI system, set these secret environment variables acco To get a developer token for Box, generate it in the Box Developer Portal, keeping in mind that it expires after 60 minutes. For more detailed instructions, check out the [OAuth 2.0 Documentation from Box](https://developer.box.com/guides/authentication/oauth2/). +We use Developer Tokens instead of OAuth 2.0 tokens for our integration tests because they are simpler to manage. OAuth tokens require a mechanism to refresh tokens and update secrets, which introduces complexity. Developer Tokens, although they expire every 60 minutes, are easier to generate and replace manually, making them more practical for our current setup. + #### Dropbox To get the access token for Dropbox, generate a token in the Dropbox Developer Portal. For more detailed instructions, check out the [OAuth Guide from Dropbox](https://developers.dropbox.com/oauth-guide).