diff --git a/CryptomatorCloudAccess.xcodeproj/project.pbxproj b/CryptomatorCloudAccess.xcodeproj/project.pbxproj index 298dc6b..cf61495 100644 --- a/CryptomatorCloudAccess.xcodeproj/project.pbxproj +++ b/CryptomatorCloudAccess.xcodeproj/project.pbxproj @@ -186,6 +186,18 @@ 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 */; }; + 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 */; }; + 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 */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -374,6 +386,17 @@ 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 = ""; }; + 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 */ @@ -395,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; @@ -424,6 +448,7 @@ 74F9354B251F66EE001F4ADA /* Sources */, 74F9354C251F66F8001F4ADA /* Tests */, 4A058FF224519FFC008831F9 /* Products */, + B3FC94A22BA9A98200D1ECFD /* Frameworks */, ); sourceTree = ""; }; @@ -443,6 +468,7 @@ 4A058FF424519FFC008831F9 /* CryptomatorCloudAccess.h */, 4A058FF524519FFC008831F9 /* Info.plist */, 4A0590162451A1BB008831F9 /* API */, + B3D513902BA9A32200DE0D36 /* Box */, 4A741FF0287C696F00489C23 /* Common */, 7416F22424F658160074DA8E /* Crypto */, 4A567AF02615C2DE002C4D82 /* Dropbox */, @@ -604,6 +630,7 @@ 4ACA63B02615FE8000D19304 /* CloudAccessIntegrationTestWithAuthentication.swift */, 4ACA63A42615FE5700D19304 /* IntegrationTestError.swift */, 4ACA64252616054F00D19304 /* IntegrationTestSecrets.swift */, + B3D513942BA9A37A00DE0D36 /* Box */, 4ACA63BF2615FEB200D19304 /* CryptoDecorator */, 4ACA63F02615FF9700D19304 /* Dropbox */, 4ACA63F92615FF9700D19304 /* Extensions */, @@ -630,6 +657,7 @@ 4ACA63DA2615FF3B00D19304 /* VaultFormat6 */ = { isa = PBXGroup; children = ( + B322A2BC2BDF7FBE00306F01 /* VaultFormat6BoxIntegrationTests.swift */, 4ACA63CB2615FF0000D19304 /* VaultFormat6DropboxIntegrationTests.swift */, 4ACA63D02615FF1600D19304 /* VaultFormat6GoogleDriveIntegrationTests.swift */, 4ACA63C62615FED700D19304 /* VaultFormat6LocalFileSystemIntegrationTests.swift */, @@ -644,6 +672,7 @@ 4ACA63E12615FF6400D19304 /* VaultFormat7 */ = { isa = PBXGroup; children = ( + B322A2BA2BDF7F9F00306F01 /* VaultFormat7BoxIntegrationTests.swift */, 4ACA63E42615FF6400D19304 /* VaultFormat7DropboxIntegrationTests.swift */, 4ACA63E22615FF6400D19304 /* VaultFormat7GoogleDriveIntegrationTests.swift */, 4ACA63E52615FF6400D19304 /* VaultFormat7LocalFileSystemIntegrationTests.swift */, @@ -968,6 +997,37 @@ path = API; sourceTree = ""; }; + B3D513902BA9A32200DE0D36 /* Box */ = { + isa = PBXGroup; + children = ( + B3D5138D2BA9A32200DE0D36 /* BoxAuthenticator.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; + sourceTree = SOURCE_ROOT; + }; + B3D513942BA9A37A00DE0D36 /* Box */ = { + isa = PBXGroup; + children = ( + B3D513952BA9A3BB00DE0D36 /* BoxCloudProviderIntegrationTests.swift */, + B3408AC72BCD32CA005271D2 /* BoxCredentialMock.swift */, + ); + path = Box; + sourceTree = ""; + }; + B3FC94A22BA9A98200D1ECFD /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -1014,6 +1074,7 @@ 4A75E1C628806FA100952FE6 /* MSGraphClientSDK */, 4A75E1C928806FF000952FE6 /* ObjectiveDropboxOfficial */, 4A8B872E287D7E77002D676E /* CocoaLumberjackSwift */, + B3D620D12BFC7B2E007301C1 /* BoxSdkGen */, ); productName = CloudAccess; productReference = 4A058FF124519FFC008831F9 /* CryptomatorCloudAccess.framework */; @@ -1104,6 +1165,7 @@ 4A75E1C528806FA100952FE6 /* XCRemoteSwiftPackageReference "msgraph-sdk-objc-spm" */, 4A75E1C828806FF000952FE6 /* XCRemoteSwiftPackageReference "dropbox-sdk-obj-c-spm" */, 4A8B872D287D7E77002D676E /* XCRemoteSwiftPackageReference "CocoaLumberjack" */, + B3C72B752BFC798A006F8218 /* XCRemoteSwiftPackageReference "box-swift-sdk-gen" */, ); productRefGroup = 4A058FF224519FFC008831F9 /* Products */; projectDirPath = ""; @@ -1224,9 +1286,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 */, @@ -1243,6 +1308,7 @@ 7471BDAE24865B6F000D05FC /* LocalFileSystemProvider.swift in Sources */, 7484608729795421009933D8 /* VaultConfigHelper.swift in Sources */, 4ABE9AA32BCAC8FE00675D74 /* Promise+Async.swift in Sources */, + B3FC94A62BA9AA4400D1ECFD /* BoxCloudProvider.swift in Sources */, 74073D1927C9406000A86C9A /* Task+Promises.swift in Sources */, 4A1A1194262EC46E00DAF62F /* OneDriveIdentifierCache.swift in Sources */, 746F091327BC0DA2003FCD9F /* PCloudCredential.swift in Sources */, @@ -1284,6 +1350,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 */, @@ -1291,7 +1358,9 @@ 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 */, + B3408ACA2BCDAA09005271D2 /* BoxError.swift in Sources */, 74C596E824F022AF00FFD17E /* CloudPath.swift in Sources */, 4A05900C2451A107008831F9 /* CloudProvider.swift in Sources */, 4A567B142615C8B8002C4D82 /* GoogleDriveAuthenticator.swift in Sources */, @@ -1370,6 +1439,9 @@ 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 */, 4A41D2432641938A00B5D787 /* VaultFormat6OneDriveIntegrationTests.swift in Sources */, 7470C54B26569A7E00E361B8 /* MSAuthenticationProviderMock.swift in Sources */, @@ -1379,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 */, @@ -1815,6 +1888,14 @@ minimumVersion = 2.3.0; }; }; + B3C72B752BFC798A006F8218 /* XCRemoteSwiftPackageReference "box-swift-sdk-gen" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/box/box-swift-sdk-gen.git"; + requirement = { + kind = upToNextMinorVersion; + minimumVersion = 0.2.0; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -1893,6 +1974,11 @@ package = 74F93565251F6863001F4ADA /* XCRemoteSwiftPackageReference "promises" */; productName = Promises; }; + B3D620D12BFC7B2E007301C1 /* BoxSdkGen */ = { + isa = XCSwiftPackageProductDependency; + package = B3C72B752BFC798A006F8218 /* XCRemoteSwiftPackageReference "box-swift-sdk-gen" */; + productName = BoxSdkGen; + }; /* 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 b2f7e31..a7086da 100644 --- a/CryptomatorCloudAccess.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/CryptomatorCloudAccess.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -27,6 +27,15 @@ "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", @@ -149,8 +158,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-log.git", "state" : { - "revision" : "532d8b529501fb73a2455b179e0bbb6d49b652ed", - "version" : "1.5.3" + "revision" : "e97a6fcb1ab07462881ac165fdbb37f067e205d5", + "version" : "1.5.4" } } ], 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/Package.resolved b/Package.resolved deleted file mode 100644 index 9ad3fb7..0000000 --- a/Package.resolved +++ /dev/null @@ -1,158 +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" : "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/Package.swift b/Package.swift index beacb7c..b411e98 100644 --- a/Package.swift +++ b/Package.swift @@ -14,7 +14,8 @@ let appExtensionUnsafeSources = [ "Dropbox/DropboxAuthenticator.swift", "GoogleDrive/GoogleDriveAuthenticator.swift", "OneDrive/OneDriveAuthenticator.swift", - "PCloud/PCloudAuthenticator.swift" + "PCloud/PCloudAuthenticator.swift", + "Box/BoxAuthenticator.swift" ] let package = Package( @@ -27,9 +28,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-swift-sdk-gen.git", .upToNextMinor(from: "0.2.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")), @@ -41,7 +42,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/tobihagemann/JOSESwift.git", exact: "2.4.1-cryptomator") ], targets: [ .target( @@ -49,6 +51,7 @@ let package = Package( dependencies: [ .product(name: "AWSCore", package: "aws-sdk-ios-spm"), .product(name: "AWSS3", package: "aws-sdk-ios-spm"), + .product(name: "BoxSdkGen", package: "box-swift-sdk-gen"), .product(name: "CocoaLumberjackSwift", package: "CocoaLumberjack"), .product(name: "CryptomatorCryptoLib", package: "cryptolib-swift"), .product(name: "GoogleAPIClientForREST_Drive", package: "google-api-objectivec-client-for-rest"), diff --git a/README.md b/README.md index ad2e66a..0d7a129 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,36 @@ 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 credential = BoxCredential(tokenStore: tokenStore) +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/Sources/CryptomatorCloudAccess/Box/BoxAuthenticator.swift b/Sources/CryptomatorCloudAccess/Box/BoxAuthenticator.swift new file mode 100644 index 0000000..cd8ce74 --- /dev/null +++ b/Sources/CryptomatorCloudAccess/Box/BoxAuthenticator.swift @@ -0,0 +1,55 @@ +// +// BoxAuthenticator.swift +// CryptomatorCloudAccess +// +// Created by Majid Achhoud on 18.03.24. +// Copyright © 2024 Skymatic GmbH. All rights reserved. +// + +#if canImport(CryptomatorCloudAccessCore) +import CryptomatorCloudAccessCore +#endif +import AuthenticationServices +import BoxSdkGen +import Promises +import UIKit + +public enum BoxAuthenticatorError: Error { + case authenticationFailed + case invalidContext + case loginCancelled +} + +public enum BoxAuthenticator { + public static func authenticate(from viewController: UIViewController, tokenStorage: TokenStorage) -> Promise { + let pendingPromise = Promise.pending() + + _Concurrency.Task { + do { + guard let context = viewController as? ASWebAuthenticationPresentationContextProviding else { + throw BoxAuthenticatorError.invalidContext + } + + 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 + try await oauth.runLoginFlow(options: .init(), context: context) + + 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) + } + } + } + + return pendingPromise + } +} diff --git a/Sources/CryptomatorCloudAccess/Box/BoxCloudProvider.swift b/Sources/CryptomatorCloudAccess/Box/BoxCloudProvider.swift new file mode 100644 index 0000000..108dead --- /dev/null +++ b/Sources/CryptomatorCloudAccess/Box/BoxCloudProvider.swift @@ -0,0 +1,819 @@ +// +// BoxCloudProvider.swift +// CryptomatorCloudAccess +// +// Created by Majid Achhoud on 19.03.24. +// Copyright © 2024 Skymatic GmbH. All rights reserved. +// + +import BoxSdkGen +import CryptoKit +import Foundation +import Promises + +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, 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 { + 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, onTaskCreation: onTaskCreation) + } + } + + 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, onTaskCreation: onTaskCreation) + } + } + + 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("BoxCloudProvider: 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)) 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") + + 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 { + guard folderItem.itemType == .folder else { + return Promise(CloudProviderError.itemTypeMismatch) + } + + let client = credential.client + + let pendingPromise = Promise.pending() + + _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 .fileFull(file): + return self.convertToCloudItemMetadata(file, at: folderItem.cloudPath.appendingPathComponent(file.name ?? "")) + case let .folderMini(folder): + return self.convertToCloudItemMetadata(folder, at: folderItem.cloudPath.appendingPathComponent(folder.name ?? "")) + case .webLink: + // Handling of web links as required + return nil + } + } + 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, onTaskCreation: ((URLSessionDownloadTask?) -> Void)?) -> Promise { + CloudAccessDDLogDebug("BoxCloudProvider: downloadFile(for: \(item.identifier), to: \(localURL)) called") + guard item.itemType == .file else { + return Promise(CloudProviderError.itemTypeMismatch) + } + + let client = credential.client + + let pendingPromise = Promise.pending() + + _Concurrency.Task { + do { + 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), to: \(localURL)) failed with error: \(error.localizedDescription)") + pendingPromise.reject(error) + } + } + + return pendingPromise + } + + 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 { + // TODO: Change Error Type + let attributes = try FileManager.default.attributesOfItem(atPath: localURL.path) + guard let fileSize = attributes[.size] as? Int64 else { + throw CloudProviderError.unauthorized + } + + guard FileManager.default.fileExists(atPath: localURL.path) else { + throw CloudProviderError.itemNotFound + } + + let targetFileName = cloudPath.lastPathComponent + + 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 + + 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, + fileFileName: targetFileName + ) + // 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 + } + } catch { + CloudAccessDDLogDebug("BoxCloudProvider: normalFileUpload(for: \(parentItem.identifier), to: \(cloudPath)) failed with error: \(error.localizedDescription)") + pendingPromise.reject(error) + } + } catch { + 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) + } + } + + return pendingPromise + } + + private func createFolder(for parentItem: BoxItem, with name: String) -> Promise { + 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 { + CloudAccessDDLogDebug("BoxCloudProvider: deleteFile(for: \(item.identifier)) called") + guard item.itemType == .file else { + return Promise(CloudProviderError.itemTypeMismatch) + } + + let client = credential.client + + 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 { + CloudAccessDDLogDebug("BoxCloudProvider: deleteFolder(for: \(item.identifier)) called") + guard item.itemType == .folder else { + return Promise(CloudProviderError.itemTypeMismatch) + } + + let client = credential.client + + 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") + + 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") + + 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 + + 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 { + 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 + } + } + } + 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 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 .folderMini(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: FileOrFolderOrWebLink, 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 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 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 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 { + 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) + } + + 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 new file mode 100644 index 0000000..18d534f --- /dev/null +++ b/Sources/CryptomatorCloudAccess/Box/BoxCredential.swift @@ -0,0 +1,77 @@ +// +// BoxCredential.swift +// CryptomatorCloudAccess +// +// Created by Majid Achhoud on 19.03.24. +// Copyright © 2024 Skymatic GmbH. All rights reserved. +// + +import AuthenticationServices +import BoxSdkGen +import Foundation +import Promises + +public enum BoxCredentialErrors: Error { + case noUsername + case authenticationFailed +} + +public class BoxCredential { + public var client: BoxClient + + 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) + } + + public func deauthenticate() -> Promise { + 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 { + 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 + } + + 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 + } +} diff --git a/Sources/CryptomatorCloudAccess/Box/BoxError.swift b/Sources/CryptomatorCloudAccess/Box/BoxError.swift new file mode 100644 index 0000000..b657aee --- /dev/null +++ b/Sources/CryptomatorCloudAccess/Box/BoxError.swift @@ -0,0 +1,14 @@ +// +// 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 +} diff --git a/Sources/CryptomatorCloudAccess/Box/BoxIdentifierCache.swift b/Sources/CryptomatorCloudAccess/Box/BoxIdentifierCache.swift new file mode 100644 index 0000000..05e9b10 --- /dev/null +++ b/Sources/CryptomatorCloudAccess/Box/BoxIdentifierCache.swift @@ -0,0 +1,44 @@ +// +// BoxIdentifierCache.swift +// CryptomatorCloudAccess +// +// Created by Majid Achhoud on 19.03.24. +// Copyright © 2024 Skymatic GmbH. All rights reserved. +// + +import Foundation +import GRDB + +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..546d842 --- /dev/null +++ b/Sources/CryptomatorCloudAccess/Box/BoxItem.swift @@ -0,0 +1,58 @@ +// +// BoxItem.swift +// CryptomatorCloudAccess +// +// Created by Majid Achhoud on 19.03.24. +// Copyright © 2024 Skymatic GmbH. All rights reserved. +// + +import BoxSdkGen +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 { + // 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) + // Weblinks are currently not supported, if required they can be added later. + case .webLink: + throw BoxError.unexpectedContent + } + } + + init(cloudPath: CloudPath, file: FileBase) { + self.cloudPath = cloudPath + self.identifier = file.id + self.itemType = .file + } + + init(cloudPath: CloudPath, folder: FolderBase) { + 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..222039f --- /dev/null +++ b/Sources/CryptomatorCloudAccess/Box/BoxSetup.swift @@ -0,0 +1,23 @@ +// +// BoxSetup.swift +// CryptomatorCloudAccess +// +// Created by Majid Achhoud on 18.03.24. +// Copyright © 2024 Skymatic GmbH. All rights reserved. +// + +import Foundation + +public class BoxSetup { + public static var constants: BoxSetup! + + public let clientId: String + public let clientSecret: String + public let sharedContainerIdentifier: String? + + public init(clientId: String, clientSecret: String, sharedContainerIdentifier: String?) { + self.clientId = clientId + self.clientSecret = clientSecret + self.sharedContainerIdentifier = sharedContainerIdentifier + } +} diff --git a/Tests/CryptomatorCloudAccessIntegrationTests/Box/BoxCloudProviderIntegrationTests.swift b/Tests/CryptomatorCloudAccessIntegrationTests/Box/BoxCloudProviderIntegrationTests.swift new file mode 100644 index 0000000..25015b3 --- /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 = BoxCredentialMock() + + 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 deauthenticate() -> Promise { + return credential.deauthenticate() + } + + 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..d3729d7 --- /dev/null +++ b/Tests/CryptomatorCloudAccessIntegrationTests/Box/BoxCredentialMock.swift @@ -0,0 +1,38 @@ +// +// BoxCredentialMock.swift +// CryptomatorCloudAccessIntegrationTests +// +// Created by Majid Achhoud on 15.04.24. +// Copyright © 2024 Skymatic GmbH. All rights reserved. +// + +import Foundation +import Promises +@testable import BoxSdkGen +#if canImport(CryptomatorCloudAccessCore) +@testable import CryptomatorCloudAccessCore +#else +@testable import CryptomatorCloudAccess +#endif + +class BoxCredentialMock: BoxCredential { + init() { + // Set up Box constants for testing purposes + BoxSetup.constants = BoxSetup(clientId: "", clientSecret: "", sharedContainerIdentifier: "") + + // Initialize the BoxCredential with InMemoryTokenStore + let tokenStore = InMemoryTokenStorage() + super.init(tokenStorage: tokenStore) + + // Override the client with a test token using BoxDeveloperTokenAuth + let devTokenAuth = BoxDeveloperTokenAuth(token: IntegrationTestSecrets.boxDeveloperToken) + client = BoxClient(auth: devTokenAuth) + } + + override func deauthenticate() -> Promise { + // Set the client to an invalid token for deauthentication in tests + let invalidTokenAuth = BoxDeveloperTokenAuth(token: "invalid") + client = BoxClient(auth: invalidTokenAuth) + 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 4588fac..4653f4d 100644 --- a/Tests/CryptomatorCloudAccessIntegrationTests/README.md +++ b/Tests/CryptomatorCloudAccessIntegrationTests/README.md @@ -8,6 +8,7 @@ If you would like to run integration tests that require authentication, you have ```sh #!/bin/sh +export BOX_DEVELOPER_TOKEN=... export DROPBOX_ACCESS_TOKEN=... export GOOGLE_DRIVE_CLIENT_ID=... export GOOGLE_DRIVE_REFRESH_TOKEN=... @@ -33,6 +34,12 @@ If you are building via a CI system, set these secret environment variables acco ### How to Get the Secrets +#### Box + +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). diff --git a/create-integration-test-secrets-file.sh b/create-integration-test-secrets-file.sh index 4a8b3bb..8d3b7a1 100755 --- a/create-integration-test-secrets-file.sh +++ b/create-integration-test-secrets-file.sh @@ -17,6 +17,7 @@ import CryptomatorCloudAccess import Foundation enum IntegrationTestSecrets { + 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}"