├── .gitignore ├── .jazzy.yaml ├── DataCollectionPractices.md ├── Example_iOS ├── Example_iOS.xcodeproj │ ├── project.pbxproj │ └── xcshareddata │ │ └── xcschemes │ │ └── Example_iOS.xcscheme └── Example_iOS │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ ├── content_file.imageset │ │ ├── Contents.json │ │ ├── u64.png │ │ └── u96.png │ └── content_folder.imageset │ │ ├── Contents.json │ │ ├── f64.png │ │ └── f96.png │ ├── AuthViewController.swift │ ├── Base.lproj │ └── LaunchScreen.storyboard │ ├── ContentTableViewCell.swift │ ├── FolderTableViewController.swift │ ├── Info.plist │ └── LoadingTableViewCell.swift ├── LICENSE ├── PCloudSDKSwift.podspec ├── PCloudSDKSwift ├── PCloudSDKSwift.xcodeproj │ ├── project.pbxproj │ └── xcshareddata │ │ └── xcschemes │ │ ├── SDK iOS.xcscheme │ │ └── SDK macOS.xcscheme ├── Source │ ├── Common │ │ ├── APIServerRegion.swift │ │ ├── APITaskBuilding.swift │ │ ├── AnyCancellationToken.swift │ │ ├── Atomic.swift │ │ ├── Authenticator.swift │ │ ├── CallError.swift │ │ ├── CallTask.swift │ │ ├── Cancellable.swift │ │ ├── Content.swift │ │ ├── DownloadTask.swift │ │ ├── FileLink.swift │ │ ├── HostProvider.swift │ │ ├── Keychain.swift │ │ ├── Lock.swift │ │ ├── NetworkAPI.swift │ │ ├── OAuth.swift │ │ ├── OAuthAccessTokenBasedAuthenticator.swift │ │ ├── PCloud.swift │ │ ├── PCloudAPI.swift │ │ ├── PCloudClient.swift │ │ ├── PCloudSDKSwift.h │ │ ├── Parser.swift │ │ ├── ResultUtilities.swift │ │ ├── UInt64+Clamping.swift │ │ ├── URLSessionBasedCallOperation.swift │ │ ├── URLSessionBasedDownloadOperation.swift │ │ ├── URLSessionBasedNetworkOperation.swift │ │ ├── URLSessionBasedNetworkOperationUtilities.swift │ │ ├── URLSessionBasedUploadOperation.swift │ │ ├── URLSessionEventHub.swift │ │ ├── URLSessionTaskBuilder.swift │ │ ├── UploadInfo.swift │ │ ├── UploadTask.swift │ │ ├── User.swift │ │ └── VoidCancellationToken.swift │ ├── iOS │ │ ├── Info.plist │ │ ├── OAuthWebViewMobile.swift │ │ └── PCloud+OAuthMobile.swift │ └── macOS │ │ ├── Info.plist │ │ ├── OAuthWebViewDesktop.swift │ │ ├── PCloud+OAuthDesktop.swift │ │ └── WebViewControllerDesktop.xib └── Tests │ ├── Common │ ├── NetworkOperationMocks.swift │ └── Utilities.swift │ ├── Info.plist │ └── Tests │ ├── APITaskBuildingTests.swift │ ├── OAuthTests.swift │ ├── PCloudAPITests.swift │ ├── TaskTests.swift │ ├── URLSessionBasedOperationsTests.swift │ ├── URLSessionEventHubTests.swift │ └── URLSessionTaskBuilderTests.swift ├── Package.swift ├── README.md └── docs ├── Classes.html ├── Classes ├── AnyCancellationToken.html ├── CallTask.html ├── DownloadTask.html ├── PCloudClient.html ├── URLSessionBasedCallOperation.html ├── URLSessionBasedDownloadOperation.html ├── URLSessionBasedNetworkOperation.html ├── URLSessionBasedUploadOperation.html ├── URLSessionEventHub.html ├── UploadTask.html ├── WebViewControllerMobile.html └── WebViewControllerPresenterMobile.html ├── Enums.html ├── Enums ├── APIServerRegion.html ├── CallError.html ├── Content.html ├── Content │ └── Metadata.html ├── NetworkOperationError.html ├── NetworkOperationState.html ├── PCloud.html ├── URLScheme.html └── URLSessionTaskBuilder.html ├── Extensions.html ├── Extensions ├── Dictionary.html └── String.html ├── Functions.html ├── Protocols.html ├── Protocols ├── Authenticator.html ├── CallOperation.html ├── Cancellable.html ├── DownloadOperation.html ├── HostProvider.html ├── NetworkOperation.html ├── OAuthAuthorizationFlowView.html ├── PCloudApiMethod.html ├── Parser.html ├── URLSessionObserver.html └── UploadOperation.html ├── Structs.html ├── Structs ├── AudioMetadataParser.html ├── Call.html ├── Call │ ├── Command.html │ ├── Command │ │ └── Parameter.html │ └── Request.html ├── ContentListParser.html ├── Download.html ├── Download │ └── Request.html ├── File.html ├── File │ ├── Media.html │ ├── Media │ │ ├── AudioMetadata.html │ │ ├── Icon.html │ │ ├── ImageMetadata.html │ │ └── VideoMetadata.html │ └── Metadata.html ├── FileIconParser.html ├── FileLink.html ├── FileLink │ └── Metadata.html ├── FileLinkMetadataParser.html ├── FileMetadataParser.html ├── Folder.html ├── Folder │ ├── Metadata.html │ ├── Ownership.html │ └── Permissions.html ├── FolderMetadataParser.html ├── ImageMetadataParser.html ├── NullError.html ├── OAuth.html ├── OAuth │ ├── Error.html │ ├── Result.html │ └── User.html ├── OAuthAccessTokenBasedAuthenticator.html ├── PCloudAPI │ ├── AuthError.html │ ├── CreateUpload.html │ ├── GetUploadInfo.html │ ├── GetUploadInfo │ │ └── Error.html │ ├── PermissionError.html │ ├── SaveUpload.html │ ├── SaveUpload │ │ ├── ConflictResolutionPolicy.html │ │ └── Error.html │ ├── WriteToUpload.html │ └── WriteToUpload │ │ └── Error.html ├── PCloudAPICallTaskBuilder.html ├── PCloudAPIDownloadTaskBuilder.html ├── PCloudAPIUploadTaskBuilder.html ├── PCloudApi.html ├── PCloudApi │ ├── CopyFile.html │ ├── CopyFile │ │ └── Error.html │ ├── CopyFolder.html │ ├── CopyFolder │ │ ├── Error.html │ │ └── NameConflictPolicy.html │ ├── CreateFolder.html │ ├── CreateFolder │ │ └── Error.html │ ├── DeleteFile.html │ ├── DeleteFile │ │ └── Error.html │ ├── DeleteFolderRecursive.html │ ├── DeleteFolderRecursive │ │ └── Error.html │ ├── Error.html │ ├── GetFileLink.html │ ├── GetFileLink │ │ └── Error.html │ ├── GetThumbnailLink.html │ ├── GetThumbnailLink │ │ └── Error.html │ ├── GetThumbnailsLinks.html │ ├── ListFolder.html │ ├── ListFolder │ │ └── Error.html │ ├── MoveFile.html │ ├── MoveFile │ │ └── Error.html │ ├── MoveFolder.html │ ├── MoveFolder │ │ └── Error.html │ ├── RenameFile.html │ ├── RenameFile │ │ └── Error.html │ ├── RenameFolder.html │ ├── RenameFolder │ │ └── Error.html │ ├── UploadFile.html │ ├── UploadFile │ │ └── Error.html │ └── UserInfo.html ├── URLSessionBasedNetworkOperationUtilities.html ├── Upload.html ├── Upload │ ├── Request.html │ └── Request │ │ └── Body.html ├── UploadInfo.html ├── UploadInfoParser.html ├── User.html ├── User │ └── Metadata.html ├── UserMetadataParser.html ├── VideoMetadataParser.html └── VoidCancellationToken.html ├── badge.svg ├── css ├── highlight.css └── jazzy.css ├── docsets ├── PCloudSDKSwift.docset │ └── Contents │ │ ├── Info.plist │ │ └── Resources │ │ ├── Documents │ │ ├── Classes.html │ │ ├── Classes │ │ │ ├── AnyCancellationToken.html │ │ │ ├── CallTask.html │ │ │ ├── DownloadTask.html │ │ │ ├── PCloudClient.html │ │ │ ├── URLSessionBasedCallOperation.html │ │ │ ├── URLSessionBasedDownloadOperation.html │ │ │ ├── URLSessionBasedNetworkOperation.html │ │ │ ├── URLSessionBasedUploadOperation.html │ │ │ ├── URLSessionEventHub.html │ │ │ ├── UploadTask.html │ │ │ ├── WebViewControllerMobile.html │ │ │ └── WebViewControllerPresenterMobile.html │ │ ├── Enums.html │ │ ├── Enums │ │ │ ├── APIServerRegion.html │ │ │ ├── CallError.html │ │ │ ├── Content.html │ │ │ ├── Content │ │ │ │ └── Metadata.html │ │ │ ├── NetworkOperationError.html │ │ │ ├── NetworkOperationState.html │ │ │ ├── PCloud.html │ │ │ ├── URLScheme.html │ │ │ └── URLSessionTaskBuilder.html │ │ ├── Extensions.html │ │ ├── Extensions │ │ │ ├── Dictionary.html │ │ │ └── String.html │ │ ├── Functions.html │ │ ├── Protocols.html │ │ ├── Protocols │ │ │ ├── Authenticator.html │ │ │ ├── CallOperation.html │ │ │ ├── Cancellable.html │ │ │ ├── DownloadOperation.html │ │ │ ├── HostProvider.html │ │ │ ├── NetworkOperation.html │ │ │ ├── OAuthAuthorizationFlowView.html │ │ │ ├── PCloudApiMethod.html │ │ │ ├── Parser.html │ │ │ ├── URLSessionObserver.html │ │ │ └── UploadOperation.html │ │ ├── Structs.html │ │ ├── Structs │ │ │ ├── AudioMetadataParser.html │ │ │ ├── Call.html │ │ │ ├── Call │ │ │ │ ├── Command.html │ │ │ │ ├── Command │ │ │ │ │ └── Parameter.html │ │ │ │ └── Request.html │ │ │ ├── ContentListParser.html │ │ │ ├── Download.html │ │ │ ├── Download │ │ │ │ └── Request.html │ │ │ ├── File.html │ │ │ ├── File │ │ │ │ ├── Media.html │ │ │ │ ├── Media │ │ │ │ │ ├── AudioMetadata.html │ │ │ │ │ ├── Icon.html │ │ │ │ │ ├── ImageMetadata.html │ │ │ │ │ └── VideoMetadata.html │ │ │ │ └── Metadata.html │ │ │ ├── FileIconParser.html │ │ │ ├── FileLink.html │ │ │ ├── FileLink │ │ │ │ └── Metadata.html │ │ │ ├── FileLinkMetadataParser.html │ │ │ ├── FileMetadataParser.html │ │ │ ├── Folder.html │ │ │ ├── Folder │ │ │ │ ├── Metadata.html │ │ │ │ ├── Ownership.html │ │ │ │ └── Permissions.html │ │ │ ├── FolderMetadataParser.html │ │ │ ├── ImageMetadataParser.html │ │ │ ├── NullError.html │ │ │ ├── OAuth.html │ │ │ ├── OAuth │ │ │ │ ├── Error.html │ │ │ │ ├── Result.html │ │ │ │ └── User.html │ │ │ ├── OAuthAccessTokenBasedAuthenticator.html │ │ │ ├── PCloudAPI │ │ │ │ ├── AuthError.html │ │ │ │ ├── CreateUpload.html │ │ │ │ ├── GetUploadInfo.html │ │ │ │ ├── GetUploadInfo │ │ │ │ │ └── Error.html │ │ │ │ ├── PermissionError.html │ │ │ │ ├── SaveUpload.html │ │ │ │ ├── SaveUpload │ │ │ │ │ ├── ConflictResolutionPolicy.html │ │ │ │ │ └── Error.html │ │ │ │ ├── WriteToUpload.html │ │ │ │ └── WriteToUpload │ │ │ │ │ └── Error.html │ │ │ ├── PCloudAPICallTaskBuilder.html │ │ │ ├── PCloudAPIDownloadTaskBuilder.html │ │ │ ├── PCloudAPIUploadTaskBuilder.html │ │ │ ├── PCloudApi.html │ │ │ ├── PCloudApi │ │ │ │ ├── CopyFile.html │ │ │ │ ├── CopyFile │ │ │ │ │ └── Error.html │ │ │ │ ├── CopyFolder.html │ │ │ │ ├── CopyFolder │ │ │ │ │ ├── Error.html │ │ │ │ │ └── NameConflictPolicy.html │ │ │ │ ├── CreateFolder.html │ │ │ │ ├── CreateFolder │ │ │ │ │ └── Error.html │ │ │ │ ├── DeleteFile.html │ │ │ │ ├── DeleteFile │ │ │ │ │ └── Error.html │ │ │ │ ├── DeleteFolderRecursive.html │ │ │ │ ├── DeleteFolderRecursive │ │ │ │ │ └── Error.html │ │ │ │ ├── Error.html │ │ │ │ ├── GetFileLink.html │ │ │ │ ├── GetFileLink │ │ │ │ │ └── Error.html │ │ │ │ ├── GetThumbnailLink.html │ │ │ │ ├── GetThumbnailLink │ │ │ │ │ └── Error.html │ │ │ │ ├── GetThumbnailsLinks.html │ │ │ │ ├── ListFolder.html │ │ │ │ ├── ListFolder │ │ │ │ │ └── Error.html │ │ │ │ ├── MoveFile.html │ │ │ │ ├── MoveFile │ │ │ │ │ └── Error.html │ │ │ │ ├── MoveFolder.html │ │ │ │ ├── MoveFolder │ │ │ │ │ └── Error.html │ │ │ │ ├── RenameFile.html │ │ │ │ ├── RenameFile │ │ │ │ │ └── Error.html │ │ │ │ ├── RenameFolder.html │ │ │ │ ├── RenameFolder │ │ │ │ │ └── Error.html │ │ │ │ ├── UploadFile.html │ │ │ │ ├── UploadFile │ │ │ │ │ └── Error.html │ │ │ │ └── UserInfo.html │ │ │ ├── URLSessionBasedNetworkOperationUtilities.html │ │ │ ├── Upload.html │ │ │ ├── Upload │ │ │ │ ├── Request.html │ │ │ │ └── Request │ │ │ │ │ └── Body.html │ │ │ ├── UploadInfo.html │ │ │ ├── UploadInfoParser.html │ │ │ ├── User.html │ │ │ ├── User │ │ │ │ └── Metadata.html │ │ │ ├── UserMetadataParser.html │ │ │ ├── VideoMetadataParser.html │ │ │ └── VoidCancellationToken.html │ │ ├── css │ │ │ ├── highlight.css │ │ │ └── jazzy.css │ │ ├── img │ │ │ ├── carat.png │ │ │ ├── dash.png │ │ │ └── gh.png │ │ ├── index.html │ │ ├── js │ │ │ ├── jazzy.js │ │ │ └── jquery.min.js │ │ └── search.json │ │ └── docSet.dsidx └── PCloudSDKSwift.tgz ├── img ├── carat.png ├── dash.png └── gh.png ├── index.html ├── js ├── jazzy.js └── jquery.min.js ├── search.json └── undocumented.json /.gitignore: -------------------------------------------------------------------------------- 1 | *DS_Store 2 | project.xcworkspace 3 | *xcuserdata 4 | *build/ 5 | Pods 6 | /Packages 7 | -------------------------------------------------------------------------------- /.jazzy.yaml: -------------------------------------------------------------------------------- 1 | module: 'PCloudSDKSwift' 2 | module_version: 3.0.1 3 | 4 | author: 'pCloud' 5 | author_url: 'http://pcloud.com' 6 | theme: 'apple' 7 | 8 | readme: 'README.md' 9 | github_url: 'https://github.com/pcloud/pcloud-sdk-swift' 10 | 11 | source_directory: 'PCloudSDKSwift/Source' 12 | exclude: '**/*.h' 13 | # swift_version: 3.0 14 | clean: true 15 | -------------------------------------------------------------------------------- /DataCollectionPractices.md: -------------------------------------------------------------------------------- 1 | ### Data collection practices for pCloud's Swift SDK 2 | 3 | Type of data | Collected? | Used for tracking? | Context 4 | --- | --- | --- | --- 5 | **Contact info** | 6 | Name | No | No 7 | Email address | Yes | No | The SDK itself does not use the user's email address. However, the user may need to enter their email address during the OAuth flow. The pCloud service stores it and uses it for authentication and communication with the end user. 8 | Phone number | No | No 9 | Physical address | No | No 10 | Other user contact info | No | No 11 | **Health and fitness** | 12 | Health | No | No 13 | Fitness | No | No 14 | **Financial info** | 15 | Payment info | No | No 16 | Credit info | No | No 17 | Other financial info | No | No 18 | **Location** | 19 | Precise location | No | No 20 | Coarse location | No | No 21 | **Sensitive info** | 22 | Sensitive info | No | No 23 | **Contacts** | 24 | Contacts | No | No 25 | **User content** | 26 | Emails or text messages | No | No 27 | Photos or videos | No | No 28 | Audio data | No | No 29 | Gameplay content | No | No 30 | Customer support | No | No 31 | Other user content | No | No 32 | **Browsing history** | 33 | Browsing history | No | No 34 | **Search history** | 35 | Search history | No | No 36 | **Identifiers** | 37 | User ID | No | Yes | An identifier assigned to a user by the pCloud service upon account creation. Used by the SDK to keep track of the currently authenticated user. It is provided by the pCloud API and stored in the device's keychain. The pCloud API will also use it to track user behaviour in order to improve the performance and functionality of the service. 38 | Device ID | No | No 39 | **Purchases** | 40 | Purchase history | No | No 41 | **Usage data** | 42 | Product interaction | No | No 43 | Advertising data | No | No 44 | Other usage data | No | No 45 | **Diagnostics** | 46 | Crash data | No | No 47 | Performance data | No | No 48 | Other diagnostic data | No | No 49 | **Other data** | 50 | Other data types | No | No 51 | 52 | 53 | -------------------------------------------------------------------------------- /Example_iOS/Example_iOS.xcodeproj/xcshareddata/xcschemes/Example_iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 56 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 75 | 77 | 83 | 84 | 85 | 86 | 88 | 89 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /Example_iOS/Example_iOS/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Example_iOS 4 | // 5 | // Created by Genislav Hristov on 12/29/16. 6 | // Copyright © 2016 pCloud. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import PCloudSDKSwift 11 | 12 | @UIApplicationMain 13 | class AppDelegate: UIResponder, UIApplicationDelegate { 14 | 15 | var window: UIWindow? 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | window = UIWindow(frame: UIScreen.main.bounds) 20 | 21 | // Enter your app key here. 22 | let appKey = "wU1vJ4PFk0J" 23 | PCloud.setUp(withAppKey: appKey) 24 | 25 | if let _ = PCloud.sharedClient { 26 | let folderViewController = FolderTableViewController() 27 | window!.rootViewController = UINavigationController(rootViewController: folderViewController) 28 | } else { 29 | // Need to authorize 30 | let controller = AuthViewController(window: window!) 31 | window!.rootViewController = controller 32 | } 33 | 34 | window!.makeKeyAndVisible() 35 | 36 | return true 37 | } 38 | 39 | func applicationWillResignActive(_ application: UIApplication) { 40 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 41 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 42 | } 43 | 44 | func applicationDidEnterBackground(_ application: UIApplication) { 45 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 46 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 47 | } 48 | 49 | func applicationWillEnterForeground(_ application: UIApplication) { 50 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 51 | } 52 | 53 | func applicationDidBecomeActive(_ application: UIApplication) { 54 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 55 | } 56 | 57 | func applicationWillTerminate(_ application: UIApplication) { 58 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 59 | } 60 | 61 | 62 | } 63 | 64 | -------------------------------------------------------------------------------- /Example_iOS/Example_iOS/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | } 43 | ], 44 | "info" : { 45 | "version" : 1, 46 | "author" : "xcode" 47 | } 48 | } -------------------------------------------------------------------------------- /Example_iOS/Example_iOS/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example_iOS/Example_iOS/Assets.xcassets/content_file.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "u64.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "u96.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Example_iOS/Example_iOS/Assets.xcassets/content_file.imageset/u64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pCloud/pcloud-sdk-swift/cc81e0250a9f378019470c78ce9a8bb501dcaeda/Example_iOS/Example_iOS/Assets.xcassets/content_file.imageset/u64.png -------------------------------------------------------------------------------- /Example_iOS/Example_iOS/Assets.xcassets/content_file.imageset/u96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pCloud/pcloud-sdk-swift/cc81e0250a9f378019470c78ce9a8bb501dcaeda/Example_iOS/Example_iOS/Assets.xcassets/content_file.imageset/u96.png -------------------------------------------------------------------------------- /Example_iOS/Example_iOS/Assets.xcassets/content_folder.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "f64.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "f96.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Example_iOS/Example_iOS/Assets.xcassets/content_folder.imageset/f64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pCloud/pcloud-sdk-swift/cc81e0250a9f378019470c78ce9a8bb501dcaeda/Example_iOS/Example_iOS/Assets.xcassets/content_folder.imageset/f64.png -------------------------------------------------------------------------------- /Example_iOS/Example_iOS/Assets.xcassets/content_folder.imageset/f96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pCloud/pcloud-sdk-swift/cc81e0250a9f378019470c78ce9a8bb501dcaeda/Example_iOS/Example_iOS/Assets.xcassets/content_folder.imageset/f96.png -------------------------------------------------------------------------------- /Example_iOS/Example_iOS/AuthViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthViewController.swift 3 | // Example_iOS 4 | // 5 | // Created by Genislav Hristov on 12/29/16. 6 | // Copyright © 2016 pCloud. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import PCloudSDKSwift 11 | 12 | class AuthViewController: UIViewController { 13 | fileprivate let window: UIWindow 14 | fileprivate var authButton: UIButton! 15 | 16 | init(window: UIWindow) { 17 | self.window = window 18 | super.init(nibName: nil, bundle: nil) 19 | } 20 | 21 | required init?(coder aDecoder: NSCoder) { 22 | fatalError("init(coder:) has not been implemented") 23 | } 24 | 25 | override func viewDidLoad() { 26 | super.viewDidLoad() 27 | 28 | authButton = UIButton(type: .system) 29 | authButton.setTitle("Authorize", for: .normal) 30 | authButton.titleLabel!.font = UIFont.systemFont(ofSize: 24) 31 | authButton.addTarget(self, action: #selector(didTapAuthorizeButton), for: .touchUpInside) 32 | view.addSubview(authButton) 33 | 34 | view.backgroundColor = .white 35 | } 36 | 37 | override func viewDidLayoutSubviews() { 38 | super.viewDidLayoutSubviews() 39 | 40 | authButton.sizeToFit() 41 | authButton.center = view.center 42 | } 43 | 44 | @objc func didTapAuthorizeButton(_: UIButton) { 45 | PCloud.authorize(with: self) { result in 46 | if case .success(_) = result { 47 | self.switchToAccountContentInterface() 48 | } 49 | } 50 | } 51 | 52 | fileprivate func switchToAccountContentInterface() { 53 | let folderViewController = FolderTableViewController() 54 | window.rootViewController = UINavigationController(rootViewController: folderViewController) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Example_iOS/Example_iOS/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Example_iOS/Example_iOS/ContentTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentTableViewCell.swift 3 | // Example_iOS 4 | // 5 | // Created by Genislav Hristov on 12/30/16. 6 | // Copyright © 2016 pCloud. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ContentTableViewCell: UITableViewCell { 12 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 13 | super.init(style: .subtitle, reuseIdentifier: reuseIdentifier) 14 | } 15 | 16 | required init?(coder aDecoder: NSCoder) { 17 | fatalError("init(coder:) has not been implemented") 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Example_iOS/Example_iOS/FolderTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FolderTableViewController.swift 3 | // Example_iOS 4 | // 5 | // Created by Genislav Hristov on 12/29/16. 6 | // Copyright © 2016 pCloud. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import PCloudSDKSwift 11 | 12 | private let RootFolderId: UInt64 = 0 13 | private let CellIdentifier = "FolderTableViewCell" 14 | 15 | class FolderTableViewController: UITableViewController { 16 | fileprivate let folderId: UInt64 17 | fileprivate var folderMetadata: Folder.Metadata? 18 | fileprivate var listFolderTask: CallTask? 19 | fileprivate var isPreparingContent: Bool = false 20 | fileprivate var error: Error? 21 | 22 | fileprivate lazy var loadingPlaceholderCell: UITableViewCell = { 23 | let cell = LoadingTableViewCell() 24 | cell.selectionStyle = .none 25 | cell.loadingIndicator.startAnimating() 26 | return cell 27 | }() 28 | 29 | fileprivate lazy var errorPlaceholderCell: UITableViewCell = { 30 | let cell = UITableViewCell() 31 | cell.selectionStyle = .none 32 | cell.textLabel!.textColor = .red 33 | cell.textLabel!.textAlignment = .center 34 | cell.textLabel!.numberOfLines = 2 35 | return cell 36 | }() 37 | 38 | init(folderId: UInt64 = RootFolderId) { 39 | self.folderId = folderId 40 | super.init(nibName: nil, bundle: nil) 41 | } 42 | 43 | required init?(coder aDecoder: NSCoder) { 44 | fatalError("init(coder:) has not been implemented") 45 | } 46 | 47 | override func viewDidLoad() { 48 | super.viewDidLoad() 49 | 50 | updateTitle() 51 | 52 | listFolderTask = PCloud.sharedClient!.listFolder(folderId, recursively: false) 53 | listFolderTask!.addCompletionBlock { result in 54 | self.isPreparingContent = false 55 | 56 | switch result { 57 | case .success(let folderMetadata): 58 | self.folderMetadata = folderMetadata 59 | self.updateTitle() 60 | case .failure(let error): 61 | self.error = error 62 | } 63 | 64 | self.tableView.reloadData() 65 | } 66 | 67 | isPreparingContent = true 68 | listFolderTask!.start() 69 | 70 | tableView.register(ContentTableViewCell.self, forCellReuseIdentifier: CellIdentifier) 71 | } 72 | 73 | override func viewWillDisappear(_ animated: Bool) { 74 | super.viewWillDisappear(animated) 75 | 76 | guard let task = listFolderTask else { 77 | return 78 | } 79 | 80 | if task.isCancelled { 81 | return 82 | } 83 | 84 | task.cancel() 85 | } 86 | 87 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 88 | if isPreparingContent || error != nil { 89 | return 1 90 | } 91 | 92 | guard let contents = folderMetadata?.contents else { 93 | return 0 94 | } 95 | 96 | return contents.count 97 | } 98 | 99 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 100 | if let error = error { 101 | return errorPlaceholderCell(error) 102 | } 103 | 104 | if isPreparingContent { 105 | return loadingPlaceholderCell 106 | } 107 | 108 | let cell = tableView.dequeueReusableCell(withIdentifier: CellIdentifier, for: indexPath) as! ContentTableViewCell 109 | configureContentCell(cell, atIndexPath: indexPath) 110 | 111 | return cell 112 | } 113 | 114 | private func configureContentCell(_ cell: ContentTableViewCell, atIndexPath indexPath: IndexPath) { 115 | var name: String 116 | var detailText = "" 117 | var image: UIImage 118 | var selectionStyle: UITableViewCell.SelectionStyle = .default 119 | 120 | let content = folderMetadata!.contents[indexPath.row] 121 | 122 | if content.isFolder { 123 | name = content.folderMetadata!.name 124 | image = UIImage(named: "content_folder")! 125 | } else { 126 | let fileSize = content.fileMetadata!.size 127 | name = content.fileMetadata!.name 128 | detailText = ByteCountFormatter.string(fromByteCount: Int64(fileSize), countStyle: .binary) 129 | image = UIImage(named: "content_file")! 130 | selectionStyle = .none 131 | } 132 | 133 | cell.textLabel!.text = name 134 | cell.detailTextLabel!.text = detailText 135 | cell.imageView!.image = image 136 | cell.selectionStyle = selectionStyle 137 | } 138 | 139 | fileprivate func errorPlaceholderCell(_ error: Error) -> UITableViewCell { 140 | let cell = errorPlaceholderCell 141 | cell.textLabel!.text = "Error: \(error.localizedDescription)" 142 | return cell 143 | } 144 | 145 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 146 | guard let content = folderMetadata?.contents[indexPath.row] else { 147 | return 148 | } 149 | 150 | if content.isFolder { 151 | let controller = FolderTableViewController(folderId: content.folderMetadata!.id) 152 | navigationController?.pushViewController(controller, animated: true) 153 | } 154 | } 155 | 156 | fileprivate func updateTitle() { 157 | if RootFolderId == folderId { 158 | title = "PCloudSDK Example" 159 | return 160 | } 161 | 162 | title = folderMetadata?.name 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /Example_iOS/Example_iOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /Example_iOS/Example_iOS/LoadingTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingTableViewCell.swift 3 | // Example_iOS 4 | // 5 | // Created by Genislav Hristov on 12/30/16. 6 | // Copyright © 2016 pCloud. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class LoadingTableViewCell: UITableViewCell { 12 | let loadingIndicator = UIActivityIndicatorView(style: .gray) 13 | 14 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 15 | super.init(style: style, reuseIdentifier: reuseIdentifier) 16 | initialize() 17 | } 18 | 19 | required init?(coder aDecoder: NSCoder) { 20 | super.init(coder: aDecoder) 21 | initialize() 22 | } 23 | 24 | override func awakeFromNib() { 25 | super.awakeFromNib() 26 | initialize() 27 | } 28 | 29 | fileprivate func initialize() { 30 | loadingIndicator.color = .black 31 | contentView.addSubview(loadingIndicator) 32 | } 33 | 34 | override func layoutSubviews() { 35 | super.layoutSubviews() 36 | loadingIndicator.center = CGPoint(x: contentView.bounds.width / 2, y: contentView.bounds.height / 2) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 pCloud LTD 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /PCloudSDKSwift.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'PCloudSDKSwift' 3 | s.version = '3.2.3' 4 | s.summary = 'Swift SDK for the pCloud API' 5 | s.homepage = 'https://github.com/pcloud/pcloud-sdk-swift' 6 | s.license = 'MIT' 7 | s.author = 'pCloud' 8 | 9 | s.source = { :git => "https://github.com/pcloud/pcloud-sdk-swift.git", :tag => '3.2.3' } 10 | s.swift_version = '5' 11 | 12 | s.osx.deployment_target = '10.13' 13 | s.ios.deployment_target = '12.0' 14 | 15 | s.osx.frameworks = 'AppKit', 'Webkit', 'SystemConfiguration', 'Foundation' 16 | s.ios.frameworks = 'UIKit', 'Webkit', 'SystemConfiguration', 'Foundation' 17 | 18 | s.ios.public_header_files = 'PCloudSDKSwift/Source/**/*.h' 19 | s.osx.public_header_files = 'PCloudSDKSwift/Source/**/*.h' 20 | 21 | s.osx.source_files = "PCloudSDKSwift/Source/Common/**/*.{swift,h}", "PCloudSDKSwift/Source/macOS/**/*.swift" 22 | s.ios.source_files = "PCloudSDKSwift/Source/Common/**/*.{swift,h}", "PCloudSDKSwift/Source/iOS/**/*.swift" 23 | 24 | s.osx.resource_bundle = { 'PCloudSDKSwiftResources' => "PCloudSDKSwift/Source/macOS/*.xib" } 25 | 26 | s.requires_arc = true 27 | end 28 | -------------------------------------------------------------------------------- /PCloudSDKSwift/PCloudSDKSwift.xcodeproj/xcshareddata/xcschemes/SDK iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 42 | 48 | 49 | 50 | 51 | 52 | 62 | 63 | 69 | 70 | 71 | 72 | 78 | 79 | 85 | 86 | 87 | 88 | 90 | 91 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /PCloudSDKSwift/PCloudSDKSwift.xcodeproj/xcshareddata/xcschemes/SDK macOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 53 | 59 | 60 | 61 | 62 | 68 | 69 | 75 | 76 | 77 | 78 | 80 | 81 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Source/Common/APIServerRegion.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIServerRegion.swift 3 | // PCloudSDKSwift 4 | // 5 | // Created by Todor Pitekov on 21.04.20. 6 | // Copyright © 2020 pCloud LTD. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum APIServerRegion: UInt { 12 | case unitedStates = 1 13 | case europe = 2 14 | } 15 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Source/Common/APITaskBuilding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APITaskBuilding.swift 3 | // SDK iOS 4 | // 5 | // Created by Todor Pitekov on 11/28/17. 6 | // Copyright © 2017 pCloud LTD. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Utility struct containing logic for building `CallTask` instances for `PCloudAPIMethod`s. 12 | public struct PCloudAPICallTaskBuilder { 13 | private let hostProvider: HostProvider 14 | private let authenticator: Authenticator 15 | private let operationBuilder: (Call.Request) -> CallOperation 16 | private let defaultTimeoutInterval: TimeInterval? 17 | 18 | public init(hostProvider: HostProvider, 19 | authenticator: Authenticator, 20 | defaultTimeoutInterval: TimeInterval? = nil, 21 | operationBuilder: @escaping (Call.Request) -> CallOperation) { 22 | self.hostProvider = hostProvider 23 | self.authenticator = authenticator 24 | self.operationBuilder = operationBuilder 25 | self.defaultTimeoutInterval = defaultTimeoutInterval 26 | } 27 | 28 | /// Creates a `CallTask` instance executing a specific API method. 29 | /// 30 | /// - parameter method: A method for the task to execute. 31 | /// - parameter hostNameOverride: If non-`nil`, this will override the default host name from the host provider. 32 | /// - parameter timeoutInterval: The timeout interval for this call task. If `nil`, the default timeout interval will be used. 33 | /// - returns: An instance of `CallTask` in suspended state. 34 | public func createTask(for method: T, 35 | hostNameOverride: String? = nil, 36 | timeoutInterval: TimeInterval? = nil) -> CallTask { 37 | var command = method.createCommand() 38 | 39 | if method.requiresAuthentication { 40 | command.parameters.append(contentsOf: authenticator.authenticationParameters) 41 | } 42 | 43 | let request = Call.Request(command: command, 44 | hostName: hostNameOverride ?? hostProvider.defaultHostName, 45 | timeoutInterval: timeoutInterval ?? defaultTimeoutInterval) 46 | 47 | let operation = operationBuilder(request) 48 | let responseParser = method.createResponseParser() 49 | 50 | return CallTask(operation: operation, responseParser: responseParser) 51 | } 52 | } 53 | 54 | 55 | /// Utility struct containing logic for building `UploadTask` instances for `PCloudAPIMethod`s. 56 | public struct PCloudAPIUploadTaskBuilder { 57 | private let hostProvider: HostProvider 58 | private let authenticator: Authenticator 59 | private let operationBuilder: (Upload.Request) -> UploadOperation 60 | private let defaultTimeoutInterval: TimeInterval? 61 | 62 | public init(hostProvider: HostProvider, 63 | authenticator: Authenticator, 64 | defaultTimeoutInterval: TimeInterval? = nil, 65 | operationBuilder: @escaping (Upload.Request) -> UploadOperation) { 66 | self.hostProvider = hostProvider 67 | self.authenticator = authenticator 68 | self.operationBuilder = operationBuilder 69 | self.defaultTimeoutInterval = defaultTimeoutInterval 70 | } 71 | 72 | /// Creates an `UploadTask` instance executing a specific API method. 73 | /// 74 | /// - parameter method: A method for the task to execute. 75 | /// - parameter body: The data to upload. 76 | /// - parameter hostNameOverride: If non-`nil`, this will override the default host name from the host provider. 77 | /// - parameter timeoutInterval: The timeout interval for this call task. If `nil`, the default timeout interval will be used. 78 | /// - returns: An instance of `UploadTask` in suspended state. 79 | public func createTask(for method: T, 80 | with body: Upload.Request.Body, 81 | hostNameOverride: String? = nil, 82 | timeoutInterval: TimeInterval? = nil) -> UploadTask { 83 | var command = method.createCommand() 84 | 85 | if method.requiresAuthentication { 86 | command.parameters.append(contentsOf: authenticator.authenticationParameters) 87 | } 88 | 89 | let request = Upload.Request(command: command, 90 | body: body, 91 | hostName: hostNameOverride ?? hostProvider.defaultHostName, 92 | timeoutInterval: timeoutInterval ?? defaultTimeoutInterval) 93 | 94 | let operation = operationBuilder(request) 95 | let responseParser = method.createResponseParser() 96 | 97 | return UploadTask(operation: operation, responseParser: responseParser) 98 | } 99 | } 100 | 101 | /// Utility struct containing logic for building `DownloadTask` instances. 102 | public struct PCloudAPIDownloadTaskBuilder { 103 | private let hostProvider: HostProvider 104 | private let authenticator: Authenticator 105 | private let defaultTimeoutInterval: TimeInterval? 106 | private let operationBuilder: (Download.Request) -> DownloadOperation 107 | 108 | public init(hostProvider: HostProvider, 109 | authenticator: Authenticator, 110 | defaultTimeoutInterval: TimeInterval? = nil, 111 | operationBuilder: @escaping (Download.Request) -> DownloadOperation) { 112 | self.hostProvider = hostProvider 113 | self.authenticator = authenticator 114 | self.operationBuilder = operationBuilder 115 | self.defaultTimeoutInterval = defaultTimeoutInterval 116 | } 117 | 118 | /// Creates a `DownloadTask` instance for downloading a file from a remote address. 119 | /// 120 | /// - parameter resourceAddress: The location of the file to download. 121 | /// - parameter downloadTag: To be passed alongside `FileLink.Metadata` resource addresses. Authenticates this client to the storage servers. 122 | /// - parameter timeoutInterval: The timeout interval for this call task. If `nil`, the default timeout interval will be used. 123 | /// - parameter destination: A block called with the temporary location of the file on disk. The block must either move 124 | /// the file or open it for reading, otherwise the file gets deleted after the block returns. 125 | /// The block should return the new path of the file. 126 | /// - returns: An instance of `DownloadTask` in suspended state. 127 | public func createTask(with resourceAddress: URL, 128 | downloadTag: String? = nil, 129 | timeoutInterval: TimeInterval? = nil, 130 | destination: @escaping (URL) throws -> URL) -> DownloadTask { 131 | let cookies: [String: String] = { 132 | if let downloadTag = downloadTag { 133 | return ["dwltag": downloadTag] 134 | } 135 | 136 | return [:] 137 | }() 138 | 139 | let request = Download.Request(resourceAddress: resourceAddress, 140 | cookies: cookies, 141 | timeoutInterval: timeoutInterval ?? defaultTimeoutInterval, 142 | destination: destination) 143 | 144 | let operation = operationBuilder(request) 145 | return DownloadTask(operation: operation) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Source/Common/AnyCancellationToken.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnyCancellationToken.swift 3 | // PCloudSDKSwift 4 | // 5 | // Created by Todor Pitekov on 03/01/2017 6 | // Copyright © 2017 pCloud LTD. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import libkern 11 | 12 | /// Concrete type-erased implementation of `Cancellable`. It forwards its `cancel()` invocation to a block. 13 | public final class AnyCancellationToken { 14 | // The block to call when cancel() is invoked. 15 | private var body: (() -> Void)? 16 | 17 | // Used for an atomic swap in cancel(). 0 indicates cancel() has not been called and 1 indicates otherwise. 18 | private var cancellationPredicate: Int32 = 0 19 | 20 | /// Creates a new token. 21 | /// 22 | /// - parameter body: A block to forward the `cancel()` invocation to. The block is released when `cancel()` is invoked. 23 | public init(body: (() -> Void)? = nil) { 24 | self.body = body 25 | } 26 | } 27 | 28 | extension AnyCancellationToken: Cancellable { 29 | public var isCancelled: Bool { 30 | return cancellationPredicate != 0 31 | } 32 | 33 | public func cancel() { 34 | if !OSAtomicCompareAndSwap32Barrier(0, 1, &cancellationPredicate) { 35 | return 36 | } 37 | 38 | if let body = body { 39 | self.body = nil 40 | body() 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Source/Common/Atomic.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Atomic.swift 3 | // PCloudSDKSwift 4 | // 5 | // Created by Todor Pitekov on 03/01/2017 6 | // Copyright © 2017 pCloud LTD. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// An object providing atomic access to an underlying value. 12 | final class Atomic { 13 | // The lock controlling access to this instance's value. 14 | private let lock = Lock() 15 | // The underlying value of this instance. 16 | private var resource: T 17 | 18 | /// The underlying value of this instance. Getting and setting the value is done atomically. 19 | var value: T { 20 | get { 21 | return lock.inCriticalScope { self.resource } 22 | } 23 | set { 24 | lock.inCriticalScope { self.resource = newValue } 25 | } 26 | } 27 | 28 | /// Initializes a new instance with a value. 29 | /// 30 | /// - parameter value: The initial underlying value of this instance. 31 | init(_ value: T) { 32 | resource = value 33 | } 34 | 35 | /// Atomically modifies this instance's value and returns the value returned from the block. 36 | /// 37 | /// - parameter block: A block that takes the current value as the input argument. 38 | /// - returns: The return value, if any, of the `block` parameter. 39 | /// - throws: The error thrown by the block (if any). 40 | @discardableResult func withValue(_ block: (inout T) throws -> (R)) rethrows -> R { 41 | return try lock.inCriticalScope { 42 | return try block(&resource) 43 | } 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Source/Common/Authenticator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Authenticator.swift 3 | // PCloudSDKSwift 4 | // 5 | // Created by Todor Pitekov on 03/01/2017 6 | // Copyright © 2017 pCloud LTD. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Authenticates a `Call.Command`. 12 | public protocol Authenticator { 13 | /// Parameters containing authentication information for a `Call.Command`. 14 | var authenticationParameters: [Call.Command.Parameter] { get } 15 | } 16 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Source/Common/CallError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CallError.swift 3 | // SDK iOS 4 | // 5 | // Created by Todor Pitekov on 11/28/17. 6 | // Copyright © 2017 pCloud LTD. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// An error combining API and network layer errors. 12 | public enum CallError: Error { 13 | case authError(PCloudAPI.AuthError) 14 | case permissionError(PCloudAPI.PermissionError) 15 | case badInputError(Int, String?) 16 | case rateLimitError 17 | case methodError(MethodError) 18 | case serverInternalError(Int, String?) 19 | case otherAPIError(Int, String?) 20 | case clientError(Error) 21 | case protocolError(Error) 22 | } 23 | 24 | 25 | extension CallError where MethodError: RawRepresentable, MethodError.RawValue == Int { 26 | /// Initializes an instance of a `CallError` with an API error. 27 | public init(apiError: PCloudAPI.Error) { 28 | switch apiError { 29 | case .authError(let authError): 30 | self = .authError(authError) 31 | 32 | case .permissionError(let permissionError): 33 | self = .permissionError(permissionError) 34 | 35 | case let .badInputError(code, message): 36 | self = .badInputError(code, message) 37 | 38 | case .methodError(let error): 39 | self = .methodError(error) 40 | 41 | case .rateLimitError: 42 | self = .rateLimitError 43 | 44 | case let .serverInternalError(code, message): 45 | self = .serverInternalError(code, message) 46 | 47 | case let .other(code, message): 48 | self = .otherAPIError(code, message) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Source/Common/CallTask.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CallTask.swift 3 | // PCloudSDKSwift 4 | // 5 | // Created by Todor Pitekov on 03/01/2017 6 | // Copyright © 2017 pCloud LTD. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | /// Executes a network call to the pCloud API. 13 | public final class CallTask: Cancellable { 14 | public typealias Parser = Method.Parser 15 | public typealias CompletionBlock = (Result>) -> Void 16 | 17 | // The underlying operation executing the network call. 18 | private let operation: CallOperation 19 | 20 | private let lock = Lock() 21 | private var completionBlocks: [CompletionBlock] = [] 22 | 23 | /// `true` if `cancel()` has been invoked on this instance, `false` otherwise. 24 | public var isCancelled: Bool { 25 | return operation.isCancelled 26 | } 27 | 28 | /// Initializes a non-running task with a network operation and a parser. 29 | /// 30 | /// - parameter operation: An operation in a suspended state that would execute the network call. 31 | /// - parameter responseParser: A block parsing an object from a response dictionary. 32 | public init(operation: CallOperation, responseParser: @escaping Parser) { 33 | self.operation = operation 34 | 35 | // Parse the response on a background queue. 36 | operation.addCompletionBlock(with: .global()) { response in 37 | // Compute the response. 38 | let result: Result> = { 39 | switch response { 40 | case .failure(.clientError(let error)): 41 | return .failure(.clientError(error)) 42 | 43 | case .failure(.protocolError(let error)): 44 | return .failure(.protocolError(error)) 45 | 46 | case .success(let payload): 47 | do { 48 | return try responseParser(payload).mapError { error in 49 | CallError(apiError: error) 50 | } 51 | } catch { 52 | return .failure(.clientError(error)) 53 | } 54 | } 55 | }() 56 | 57 | // Notify observers. 58 | let completionBlocks: [CompletionBlock] = self.lock.inCriticalScope { 59 | defer { self.completionBlocks.removeAll() } 60 | return self.completionBlocks 61 | } 62 | 63 | DispatchQueue.main.async { 64 | for block in completionBlocks { 65 | block(result) 66 | } 67 | } 68 | } 69 | } 70 | 71 | /// Adds a completion block to this instance to be called when the task completes either successfully or with a failure. 72 | /// 73 | /// - parameter block: A block called on the main thread with the result of the task. 74 | /// - returns: This task. 75 | @discardableResult public func addCompletionBlock(_ block: @escaping CompletionBlock) -> CallTask { 76 | lock.inCriticalScope { 77 | completionBlocks.append(block) 78 | } 79 | 80 | return self 81 | } 82 | 83 | /// Starts the task if it is not already running. 84 | /// 85 | /// - returns: This task. 86 | @discardableResult public func start() -> CallTask { 87 | operation.start() 88 | return self 89 | } 90 | 91 | /// Interrupts and invalidates the task. An invalidated task cannot run again. 92 | public func cancel() { 93 | operation.cancel() 94 | 95 | lock.inCriticalScope { 96 | completionBlocks.removeAll() 97 | } 98 | } 99 | } 100 | 101 | extension CallTask: CustomStringConvertible { 102 | public var description: String { 103 | return "\(operation.state), id=\(operation.id), response=\(operation.response as Any)" 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Source/Common/Cancellable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Cancellable.swift 3 | // PCloudSDKSwift 4 | // 5 | // Created by Todor Pitekov on 03/01/2017 6 | // Copyright © 2017 pCloud LTD. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A disposable resource. 12 | public protocol Cancellable { 13 | /// `true` if this resource has been disposed of, `false` otherwise. 14 | var isCancelled: Bool { get } 15 | 16 | /// Disposes of this resource. 17 | func cancel() 18 | } 19 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Source/Common/DownloadTask.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DownloadTask.swift 3 | // PCloudSDKSwift 4 | // 5 | // Created by Todor Pitekov on 03/01/2017 6 | // Copyright © 2017 pCloud LTD. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A wrapper around `DownloadOperation`. Downloads a file to disk. 12 | public final class DownloadTask: Cancellable { 13 | // The underlying download opearation. 14 | private let operation: DownloadOperation 15 | 16 | /// `true` if `cancel()` has been invoked on this task, `false` otherwise. 17 | public var isCancelled: Bool { 18 | return operation.isCancelled 19 | } 20 | 21 | /// Initializes a new download task. 22 | /// 23 | /// - parameter operation: An operation in a suspended state that would execute the download. 24 | public init(operation: DownloadOperation) { 25 | self.operation = operation 26 | } 27 | 28 | /// Adds a progress block to this instance to be called continuously as data is being downloaded. 29 | /// 30 | /// - parameter block: A block called on the main thread with the number of downloaded bytes and the total number of bytes to download as 31 | /// first and second arguments, respectively. Called each time the number of downloaded bytes changes. 32 | /// - returns: This task. 33 | @discardableResult public func addProgressBlock(_ block: @escaping (Int64, Int64) -> Void) -> DownloadTask { 34 | operation.addProgressBlock(with: .main, block) 35 | return self 36 | } 37 | 38 | /// Adds a completion block to this instance to be called when the task completes either successfully or with a failure. 39 | /// 40 | /// - parameter block: A block called on the main thread with the result of the task. 41 | /// - returns: This task. 42 | @discardableResult public func addCompletionBlock(_ block: @escaping (Download.Response) -> Void) -> DownloadTask { 43 | operation.addCompletionBlock(with: .main, block) 44 | return self 45 | } 46 | 47 | /// Starts the task if it is not already running. 48 | /// - returns: This task. 49 | @discardableResult public func start() -> DownloadTask { 50 | operation.start() 51 | return self 52 | } 53 | 54 | /// Interrupts and invalidates this task. An invalidated task cannot run again. 55 | public func cancel() { 56 | operation.cancel() 57 | } 58 | } 59 | 60 | extension DownloadTask: CustomStringConvertible { 61 | public var description: String { 62 | return "\(operation.state), id=\(operation.id), progress=\(operation.numberOfBytesReceived) / \(operation.totalNumberOfBytesToReceive), response=\(operation.response as Any)" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Source/Common/FileLink.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileLink.swift 3 | // PCloudSDKSwift 4 | // 5 | // Created by Todor Pitekov on 03/01/2017 6 | // Copyright © 2017 pCloud LTD. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// FileLink namespace. 12 | public struct FileLink { 13 | /// An address to a remote resource in pCloud. Available for a specific time interval. 14 | open class Metadata { 15 | /// The address of the resource. 16 | public let address: URL 17 | /// When `address` becomes unreachable. 18 | public let expirationDate: Date 19 | /// A token authenticating the client requesting the file link to the storage server. 20 | public let downloadTag: String 21 | 22 | /// Initializes a file link with an address and expiration date. 23 | public init(address: URL, expirationDate: Date, downloadTag: String) { 24 | self.address = address 25 | self.expirationDate = expirationDate 26 | self.downloadTag = downloadTag 27 | } 28 | } 29 | } 30 | 31 | extension FileLink.Metadata: CustomStringConvertible { 32 | public var description: String { 33 | return address.description 34 | } 35 | } 36 | 37 | 38 | /// Parses `Array` from a pCloud API response dictionary. 39 | public struct FileLinkMetadataParser: Parser { 40 | public init() {} 41 | 42 | public func parse(_ input: [String: Any]) throws -> [FileLink.Metadata] { 43 | let hosts = input["hosts"] as! [String] 44 | let path = input.string("path") 45 | let expirationTimestamp = input.uint32("expires") 46 | let downloadTag = input.string("dwltag") 47 | 48 | return hosts.map { host in 49 | var components = URLComponents() 50 | components.scheme = URLScheme.https.rawValue 51 | components.host = host 52 | components.path = path 53 | 54 | return FileLink.Metadata(address: components.url!, expirationDate: Date(timeIntervalSince1970: TimeInterval(expirationTimestamp)), downloadTag: downloadTag) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Source/Common/HostProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HostProvider.swift 3 | // PCloudSDKSwift 4 | // 5 | // Created by Todor Pitekov on 03/01/2017 6 | // Copyright © 2017 pCloud LTD. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Provides pCloud API host names. 12 | public protocol HostProvider { 13 | /// The current default pCloud API host name. 14 | var defaultHostName: String { get } 15 | } 16 | 17 | extension String: HostProvider { 18 | public var defaultHostName: String { 19 | return self 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Source/Common/Keychain.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Keychain.swift 3 | // PCloudSDKSwift 4 | // 5 | // Created by Todor Pitekov on 03/01/2017 6 | // Copyright © 2017 pCloud LTD. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A simple wrapper around the keychain C API. 12 | struct Keychain { 13 | /// Fetches and returns data for a key. 14 | /// 15 | /// - parameter key: The key to check against. 16 | /// - returns: The data found against `key`, or `nil` if no entry was found for `key`. 17 | static func getData(forKey key: String) -> Data? { 18 | let query = self.query(attributes: [ 19 | kSecAttrAccount as String: key, 20 | kSecReturnData as String: kCFBooleanTrue!, 21 | kSecMatchLimit as String: kSecMatchLimitOne 22 | ]) 23 | 24 | var data: AnyObject? 25 | 26 | let result = withUnsafeMutablePointer(to: &data) { 27 | return SecItemCopyMatching(query, $0) 28 | } 29 | 30 | guard result == noErr else { 31 | return nil 32 | } 33 | 34 | return data as! Data? 35 | } 36 | 37 | /// Stores a data buffer. 38 | /// 39 | /// - parameter value: The data to store. 40 | /// - parameter key: The key to store `value` against. 41 | /// - returns: Whether the operation was successful. 42 | @discardableResult static func set(_ value: Data, forKey key: String) -> Bool { 43 | let query = self.query(attributes: [ 44 | kSecAttrAccount as String: key, 45 | kSecValueData as String: value 46 | ]) 47 | 48 | _ = deleteData(forKey: key) 49 | 50 | return SecItemAdd(query, nil) == noErr 51 | } 52 | 53 | /// Deletes an entry. 54 | /// 55 | /// - parameter key: The key identifying the entry to remove. 56 | /// - returns: Whether the operation was successful. 57 | @discardableResult static func deleteData(forKey key: String) -> Bool { 58 | let query = self.query(attributes: [kSecAttrAccount as String: key]) 59 | return SecItemDelete(query) == noErr 60 | } 61 | 62 | /// Fetches and returns all keys stored in the namespace defined by an instance of this keychain. 63 | /// 64 | /// - returns: An array of all keys stored in the namespace defined by an instance of this keychain or an empty array on error. 65 | static func getAllKeys() -> [String] { 66 | let query = self.query(attributes: [ 67 | kSecReturnAttributes as String: kCFBooleanTrue!, 68 | kSecMatchLimit as String: kSecMatchLimitAll 69 | ]) 70 | 71 | var data: AnyObject? 72 | 73 | let result = withUnsafeMutablePointer(to: &data) { 74 | return SecItemCopyMatching(query, $0) 75 | } 76 | 77 | guard result == noErr else { 78 | return [] 79 | } 80 | 81 | let dictionary = data as! [[String: AnyObject]]? ?? [] 82 | return dictionary.map { $0[kSecAttrAccount as String] as! String } 83 | } 84 | 85 | static func query(attributes: [String: Any]) -> CFDictionary { 86 | var copy = attributes 87 | 88 | copy[kSecAttrService as String] = "\(Bundle.main.bundleIdentifier ?? "").pcloud" 89 | copy[kSecClass as String] = kSecClassGenericPassword 90 | 91 | return copy as CFDictionary 92 | } 93 | } 94 | 95 | 96 | // Utility methods for fetching and storing different types of data in the keychain. 97 | extension Keychain { 98 | /// Stores a string by converting it to data using UTF8 encoding. 99 | /// 100 | /// - parameter value: The string to store. 101 | /// - parameter key: The key to store `value` against. 102 | /// - returns: Whether the operation was successful. 103 | @discardableResult static func set(_ value: String, forKey key: String) -> Bool { 104 | if let data = value.data(using: .utf8) { 105 | return set(data, forKey: key) 106 | } 107 | 108 | return false 109 | } 110 | 111 | /// Fetches data, parses it as a UTF8 string and returns the resulting string. 112 | /// 113 | /// - parameter key: The key to check against. 114 | /// - returns: The data against `key` as a UTF8 string. 115 | static func getString(forKey key: String) -> String? { 116 | if let data = getData(forKey: key) { 117 | return String(data: data, encoding: .utf8) 118 | } 119 | 120 | return nil 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Source/Common/Lock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Lock.swift 3 | // PCloudSDKSwift 4 | // 5 | // Created by Todor Pitekov on 03/01/2017 6 | // Copyright © 2017 pCloud LTD. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Darwin 11 | 12 | /// A non-recursive implementation of a mutex. Coordinates the operation of multiple threads of execution. 13 | final class Lock { 14 | // The underlying mutex. 15 | private var mutex = pthread_mutex_t() 16 | 17 | deinit { 18 | let result = pthread_mutex_destroy(&mutex) 19 | assert(result == 0) 20 | } 21 | 22 | /// Initializes a new instance. 23 | init() { 24 | pthread_mutex_init(&mutex, nil) 25 | } 26 | 27 | /// Locks this mutex. 28 | func lock() { 29 | let result = pthread_mutex_lock(&mutex) 30 | assert(result == 0) 31 | } 32 | 33 | /// Unlocks this mutex. 34 | func unlock() { 35 | let result = pthread_mutex_unlock(&mutex) 36 | assert(result == 0) 37 | } 38 | } 39 | 40 | extension Lock { 41 | /// Executes a block inside a lock/unlock transaction. 42 | /// 43 | /// - parameter block: A block to execute. 44 | /// - returns: The value returned by the block (if any). 45 | /// - throws: The error thrown by the block (if any). 46 | func inCriticalScope(_ block: () throws -> T) rethrows -> T { 47 | lock() 48 | defer { unlock() } 49 | return try block() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Source/Common/NetworkAPI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkAPI.swift 3 | // PCloudSDKSwift 4 | // 5 | // Created by Todor Pitekov on 03/01/2017 6 | // Copyright © 2017 pCloud LTD. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// The possible execution states of a network operation. 12 | public enum NetworkOperationState { 13 | /// The operation is not running. 14 | case suspended 15 | 16 | /// The operation is running. 17 | case running 18 | 19 | /// The operation has completed either successfully or due to a failure. 20 | case completed 21 | 22 | /// The operation has been cancelled. 23 | case cancelled 24 | } 25 | 26 | 27 | /// Base interface for a network operation. 28 | public protocol NetworkOperation: Cancellable { 29 | /// Unique identifier of the operation. 30 | var id: Int { get } 31 | 32 | /// Current state of the operation. 33 | var state: NetworkOperationState { get } 34 | 35 | /// If the state of the operation is `NetworkOperationState.suspended`, enqueues it to run as soon as possible. 36 | /// Does nothing otherwise. 37 | func start() 38 | } 39 | 40 | 41 | /// An error from a network operation. 42 | public enum NetworkOperationError: Error { 43 | case clientError(Error) 44 | case protocolError(Error) 45 | } 46 | 47 | 48 | /// Call namespace. 49 | public struct Call { 50 | /// A result from executing a pCloud API call. 51 | public typealias Response = Result<[String: Any], NetworkOperationError> 52 | 53 | /// A pCloud API command. Describes what the API should do. 54 | public struct Command { 55 | public enum Parameter { 56 | case boolean(name: String, value: Bool) 57 | case number(name: String, value: UInt64) 58 | case string(name: String, value: String) 59 | } 60 | 61 | /// The name of this command. 62 | public var name: String 63 | 64 | /// Parameters to this command. 65 | public var parameters: [Parameter] 66 | 67 | public init(name: String, parameters: [Parameter]) { 68 | self.name = name 69 | self.parameters = parameters 70 | } 71 | } 72 | 73 | /// Combines all the necessary input to execute a pCloud API call. 74 | public struct Request { 75 | /// A command. 76 | public var command: Command 77 | 78 | /// An API host name. 79 | public var hostName: String 80 | 81 | /// The maximum amount of time (in seconds) that an operation's load activity can execute 82 | /// while the operation is running. 83 | public var timeoutInterval: TimeInterval? 84 | 85 | public init(command: Command, hostName: String, timeoutInterval: TimeInterval? = nil) { 86 | self.command = command 87 | self.hostName = hostName 88 | self.timeoutInterval = timeoutInterval 89 | } 90 | } 91 | } 92 | 93 | 94 | /// A network operation that knows how to execute a pCloud API call. 95 | public protocol CallOperation: NetworkOperation { 96 | /// The result from executing the API call. Exists only when `state` is `NetworkOperationState.completed`. 97 | var response: Call.Response? { get } 98 | 99 | /// Adds a block to be called on a specific queue when the operation receives its response. 100 | /// 101 | /// - parameter queue: A queue to call `block` on. If `nil`, the queue on which `block` will be called is undefined. 102 | /// - parameter block: Called as soon as the operation receives its response. Referenced strongly by the operation. 103 | @discardableResult func addCompletionBlock(with queue: DispatchQueue?, _ block: @escaping (Call.Response) -> Void) -> Self 104 | } 105 | 106 | 107 | 108 | /// Upload namespace. 109 | public struct Upload { 110 | /// A result from executing an upload to pCloud. 111 | public typealias Response = Result<[String: Any], NetworkOperationError> 112 | 113 | /// Combines all the necessary input to execute an upload to pCloud. 114 | public struct Request { 115 | /// The source of the upload data. 116 | public enum Body { 117 | /// A file identified by a local path. 118 | case file(URL) 119 | 120 | /// An in-memory buffer identified by a `Data` object. 121 | case data(Data) 122 | } 123 | 124 | /// A command. 125 | public var command: Call.Command 126 | 127 | /// The upload data. 128 | public var body: Body 129 | 130 | /// An API host name. 131 | public var hostName: String 132 | 133 | /// The maximum amount of time (in seconds) that an operation's load activity can execute 134 | /// while the operation is running. 135 | public var timeoutInterval: TimeInterval? 136 | 137 | public init(command: Call.Command, body: Body, hostName: String, timeoutInterval: TimeInterval? = nil) { 138 | self.command = command 139 | self.body = body 140 | self.hostName = hostName 141 | self.timeoutInterval = timeoutInterval 142 | } 143 | } 144 | } 145 | 146 | 147 | /// A network operation that knows how to execute an upload to pCloud. 148 | public protocol UploadOperation: NetworkOperation { 149 | /// The result from executing the upload. Exists only when `state` is `NetworkOperationState.completed`. 150 | var response: Upload.Response? { get } 151 | 152 | /// The number of bytes currently uploaded. 153 | var numberOfBytesSent: Int64 { get } 154 | 155 | /// The total number of bytes to upload. 156 | var totalNumberOfBytesToSend: Int64 { get } 157 | 158 | /// Adds a block to be called on a specific queue when `numberOfBytesSent` changes. 159 | /// 160 | /// - parameter queue: A queue to call `block` on. If `nil`, the queue on which `block` will be called is undefined. 161 | /// - parameter block: A block called with the number of bytes currently uploaded and the total number of bytes to upload as first 162 | /// and second input arguments respectivly. Referenced strongly by the operation. 163 | @discardableResult func addProgressBlock(with queue: DispatchQueue?, _ block: @escaping (Int64, Int64) -> Void) -> Self 164 | 165 | /// Adds a block to be called on a specific queue when the operation receives its response. 166 | /// 167 | /// - parameter queue: A queue to call `block` on. If `nil`, the queue on which `block` will be called is undefined. 168 | /// - parameter block: Called as soon as the operation receives its response. Referenced strongly by the operation. 169 | @discardableResult func addCompletionBlock(with queue: DispatchQueue?, _ block: @escaping (Upload.Response) -> Void) -> Self 170 | } 171 | 172 | 173 | /// Download namespace. 174 | public struct Download { 175 | /// A result from executing a download from pCloud. 176 | public typealias Response = Result 177 | 178 | /// Combines all the necessary input to execute an HTTP download. 179 | public struct Request { 180 | /// The remote address of the resource to download. 181 | public var resourceAddress: URL 182 | 183 | /// A block called with the temporary location of the file on disk. The block must either move 184 | /// the file or open it for reading, otherwise the file gets deleted after the block returns. 185 | /// The block should return the new path of the file. 186 | public var destination: (URL) throws -> URL 187 | 188 | /// The maximum amount of time (in seconds) that an operation's load activity can execute 189 | /// while the operation is running. 190 | public var timeoutInterval: TimeInterval? 191 | 192 | /// Cookies to send along the HTTP request. 193 | public var cookies: [String: String] 194 | 195 | public init(resourceAddress: URL, 196 | cookies: [String: String] = [:], 197 | timeoutInterval: TimeInterval? = nil, 198 | destination: @escaping (URL) throws -> URL) { 199 | self.resourceAddress = resourceAddress 200 | self.destination = destination 201 | self.cookies = cookies 202 | self.timeoutInterval = timeoutInterval 203 | } 204 | } 205 | } 206 | 207 | 208 | /// A network operation that knows how to execute a download. 209 | public protocol DownloadOperation: NetworkOperation { 210 | /// The result from executing the download. Exists only when `state` is `NetworkOperationState.completed`. 211 | var response: Download.Response? { get } 212 | 213 | /// The number of bytes currently downloaded. 214 | var numberOfBytesReceived: Int64 { get } 215 | 216 | /// The total number of bytes to download. 217 | var totalNumberOfBytesToReceive: Int64 { get } 218 | 219 | /// Adds a block to be called on a specific queue when `numberOfBytesReceived` changes. 220 | /// 221 | /// - parameter queue: A queue to call `block` on. If `nil`, the queue on which `block` will be called is undefined. 222 | /// - parameter block: A block called with the number of bytes currently uploaded and the total number of bytes to upload as first 223 | /// and second input arguments respectivly. Referenced strongly by the operation. 224 | @discardableResult func addProgressBlock(with queue: DispatchQueue?, _ block: @escaping (Int64, Int64) -> Void) -> Self 225 | 226 | /// Adds a block to be called on a specific queue when the operation receives its response. 227 | /// 228 | /// - parameter queue: A queue to call `block` on. If `nil`, the queue on which `block` will be called is undefined. 229 | /// - parameter block: Called as soon as the operation receives its response. Referenced strongly by the operation. 230 | @discardableResult func addCompletionBlock(with queue: DispatchQueue?, _ block: @escaping (Download.Response) -> Void) -> Self 231 | } 232 | 233 | 234 | // MARK: - Utility protocol conformances. 235 | 236 | extension Call.Command.Parameter: CustomStringConvertible { 237 | public var description: String { 238 | switch self { 239 | case let .string(name, value): return "\(name):\(value)" 240 | case let .number(name, value): return "\(name):\(value)" 241 | case let .boolean(name, value): return "\(name):\(value)" 242 | } 243 | } 244 | } 245 | 246 | extension Call.Command.Parameter: Hashable { 247 | } 248 | 249 | 250 | extension Call.Command: CustomStringConvertible { 251 | public var description: String { 252 | return "\(name), \(parameters)" 253 | } 254 | } 255 | 256 | extension Call.Command: Equatable { 257 | } 258 | 259 | public func ==(lhs: Call.Command, rhs: Call.Command) -> Bool { 260 | return lhs.name == rhs.name && Set(lhs.parameters) == Set(rhs.parameters) 261 | } 262 | 263 | extension Call.Request: CustomStringConvertible { 264 | public var description: String { 265 | return "\(hostName), \(command)" 266 | } 267 | } 268 | 269 | extension Upload.Request: CustomStringConvertible { 270 | public var description: String { 271 | var result = "\(hostName), \(command), " 272 | 273 | switch body { 274 | case .file(let url): result += url.description 275 | case .data(let data): result += "\(data.count) bytes" 276 | } 277 | 278 | return result 279 | } 280 | } 281 | 282 | extension Download.Request: CustomStringConvertible { 283 | public var description: String { 284 | return resourceAddress.description 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Source/Common/OAuthAccessTokenBasedAuthenticator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OAuthAccessTokenBasedAuthenticator.swift 3 | // PCloudSDKSwift 4 | // 5 | // Created by Todor Pitekov on 03/01/2017 6 | // Copyright © 2017 pCloud LTD. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Concrete implementation of `Authenticator` using an OAuth2 access token. 12 | public struct OAuthAccessTokenBasedAuthenticator { 13 | private let accessToken: String 14 | 15 | /// Initializes a new authenticator with an access token. 16 | /// 17 | /// - parameter accessToken: An OAuth2 access token. 18 | public init(accessToken: String) { 19 | self.accessToken = accessToken 20 | } 21 | } 22 | 23 | extension OAuthAccessTokenBasedAuthenticator: Authenticator { 24 | public var authenticationParameters: [Call.Command.Parameter] { 25 | return [.string(name: "access_token", value: accessToken)] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Source/Common/PCloud.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PCloud.swift 3 | // PCloudSDKSwift 4 | // 5 | // Created by Todor Pitekov on 03/01/2017 6 | // Copyright © 2017 pCloud LTD. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AuthenticationServices 11 | 12 | /// Convenience namespace for the SDK. Hosts a global `PCloudClient` instance. 13 | public enum PCloud { 14 | /// A global client instance. Automatically initialized either inside `setUp()` or `authorize()`. 15 | /// Access on the main thread only. 16 | private(set) public static var sharedClient: PCloudClient? 17 | 18 | /// The app key provided in `setUp()`. 19 | private(set) public static var appKey: String? 20 | 21 | /// Stores the app key and attempts to initialize a pCloud client instance by checking for an existing user in the keychain. 22 | /// The app key is used for the OAuth authorization flow. 23 | /// Generally you should always call this method sometime during app launch. You don't need to call it if you do not intend to use the OAuth 24 | /// authorization methods provided in this namespace. 25 | /// After this method returns, the `sharedClient` property will be non-nil if there is a user in the keychain. 26 | /// Only call this method once for each instance of your app and only on the main thread. 27 | /// 28 | /// - parameter appKey: The app key to initialize the client with. 29 | public static func setUp(withAppKey appKey: String) { 30 | guard self.appKey == nil else { 31 | assertionFailure("PCloud client has already been set up") 32 | return 33 | } 34 | 35 | self.appKey = appKey 36 | 37 | if let user = OAuth.getAnyUser() { 38 | initializeSharedClient(with: user) 39 | } 40 | } 41 | 42 | /// Creates a client object and sets it to the `sharedClient` property. Only call this method on the main thread. 43 | /// Will do nothing if the shared client is already initialized. 44 | /// 45 | /// - parameter user: A `OAuth.User` value obtained from the keychain or from the OAuth flow. 46 | public static func initializeSharedClient(with user: OAuth.User) { 47 | guard sharedClient == nil else { 48 | assertionFailure("Attempting to initialize the global PCloudClient instance, but there already is a global instance.") 49 | return 50 | } 51 | 52 | sharedClient = createClient(with: user) 53 | } 54 | 55 | /// Releases the `sharedClient`. You may call `initializeSharedClient()` again after calling this method. 56 | /// Note that this will not stop any running / pending tasks you've created using the client. Only call this method on the main thread. 57 | public static func clearSharedClient() { 58 | sharedClient = nil 59 | } 60 | 61 | /// Releases the `sharedClient` and deletes all user data in the keychain. You may call `initializeSharedClient()` again after calling 62 | /// this method. Note that this will not stop any running / pending tasks you've created using the client. 63 | /// Only call this method on the main thread. 64 | public static func unlinkAllUsers() { 65 | clearSharedClient() 66 | OAuth.deleteAllUsers() 67 | } 68 | 69 | /// Creates a pCloud client. Does not update the `sharedClient` property. You are responsible for storing it and keeping it alive. Use if 70 | /// you want a more direct control over the lifetime of the `PCloudClient` object. Multiple clients can exist simultaneously. 71 | /// 72 | /// - parameter user: A `OAuth.User` value obtained from the keychain or the OAuth flow. 73 | /// - returns: An instance of a `PCloudClient` ready to take requests. 74 | public static func createClient(with user: OAuth.User) -> PCloudClient { 75 | let authenticator = OAuthAccessTokenBasedAuthenticator(accessToken: user.token) 76 | return createClient(with: authenticator, hostProvider: user.httpAPIHostName) 77 | } 78 | 79 | /// Creates a pCloud client. Does not update the `sharedClient` property. You are responsible for storing it and keeping it alive. Use if 80 | /// you want a more direct control over the lifetime of the `PCloudClient` object. Multiple clients can exist simultaneously. 81 | /// 82 | /// - parameter authenticator: An `Authenticator` used to authenticate the requests to the API. 83 | /// - parameter hostProvider: A `HostProvider` defining the endpoint to connect to. 84 | /// - returns: An instance of a `PCloudClient` ready to take requests. 85 | public static func createClient(with authenticator: Authenticator, hostProvider: HostProvider) -> PCloudClient { 86 | let eventHub = URLSessionEventHub() 87 | let session = URLSession(configuration: .default, delegate: eventHub, delegateQueue: nil) 88 | 89 | // The event hub is expected to be kept in memory by the operation builder blocks. 90 | 91 | let callOperationBuilder = URLSessionBasedNetworkOperationUtilities.createCallOperationBuilder(with: .https, 92 | session: session, 93 | delegate: eventHub) 94 | 95 | let uploadOperationBuilder = URLSessionBasedNetworkOperationUtilities.createUploadOperationBuilder(with: .https, 96 | session: session, 97 | delegate: eventHub) 98 | 99 | let downloadOperationBuilder = URLSessionBasedNetworkOperationUtilities.createDownloadOperationBuilder(with: session, delegate: eventHub) 100 | 101 | let callTaskBuilder = PCloudAPICallTaskBuilder(hostProvider: hostProvider, 102 | authenticator: authenticator, 103 | operationBuilder: callOperationBuilder) 104 | 105 | let uploadTaskBuilder = PCloudAPIUploadTaskBuilder(hostProvider: hostProvider, 106 | authenticator: authenticator, 107 | operationBuilder: uploadOperationBuilder) 108 | 109 | let downloadTaskBuilder = PCloudAPIDownloadTaskBuilder(hostProvider: hostProvider, 110 | authenticator: authenticator, 111 | operationBuilder: downloadOperationBuilder) 112 | 113 | return PCloudClient(callTaskBuilder: callTaskBuilder, uploadTaskBuilder: uploadTaskBuilder, downloadTaskBuilder: downloadTaskBuilder) 114 | } 115 | 116 | @available(iOS 13, OSX 10.15, *) 117 | static func authorize(with anchor: ASPresentationAnchor, completionBlock: @escaping (OAuth.Result) -> Void) { 118 | guard let appKey = self.appKey else { 119 | assertionFailure("Please set up client by calling PCloud.setUp(withAppKey: ) before attempting to authorize using OAuth") 120 | return 121 | } 122 | 123 | OAuth.performAuthorizationFlow(with: anchor, appKey: appKey) { result in 124 | self.handleAuthorizationFlowResult(result) 125 | completionBlock(result) 126 | } 127 | } 128 | 129 | // Starts an authorization flow and initializes the global client on success. 130 | static func authorize(with view: OAuthAuthorizationFlowView, completionBlock: @escaping (OAuth.Result) -> Void) { 131 | guard let appKey = self.appKey else { 132 | assertionFailure("Please set up client by calling PCloud.setUp(withAppKey: ) before attempting to authorize using OAuth") 133 | return 134 | } 135 | 136 | OAuth.performAuthorizationFlow(with: view, appKey: appKey) { result in 137 | self.handleAuthorizationFlowResult(result) 138 | completionBlock(result) 139 | } 140 | } 141 | 142 | private static func handleAuthorizationFlowResult(_ result: OAuth.Result) { 143 | if case let .success(user) = result { 144 | OAuth.store(user) 145 | self.initializeSharedClient(with: user) 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Source/Common/PCloudSDKSwift.h: -------------------------------------------------------------------------------- 1 | // 2 | // PCloudSDKSwift.h 3 | // PCloudSDKSwift 4 | // 5 | // Created by Todor Pitekov on 03/01/2017 6 | // Copyright © 2017 pCloud LTD. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #if TARGET_OS_IPHONE 12 | #import 13 | #else 14 | #import 15 | #endif 16 | 17 | //! Project version number for PCloudSDKSwift. 18 | FOUNDATION_EXPORT double PCloudSDKSwiftVersionNumber; 19 | 20 | //! Project version string for PCloudSDKSwift. 21 | FOUNDATION_EXPORT const unsigned char PCloudSDKSwiftVersionString[]; 22 | 23 | // In this header, you should import all the public headers of your framework using statements like #import 24 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Source/Common/Parser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Parser.swift 3 | // PCloudSDKSwift 4 | // 5 | // Created by Todor Pitekov on 03/01/2017 6 | // Copyright © 2017 pCloud LTD. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A parser. 12 | public protocol Parser { 13 | /// The type of the input data. 14 | associatedtype Input 15 | /// The type of the output data. 16 | associatedtype Output 17 | 18 | /// Parses input data and returns the output data or throws on error. 19 | /// 20 | /// - parameter input: The data to parse. 21 | /// - returns: The parsed data. 22 | /// - throws: Implementation-specific error. 23 | func parse(_ input: Input) throws -> Output 24 | } 25 | 26 | // Utility extension around the API response dictionary, providing convenient interface for accessing typed values. 27 | extension Dictionary where Key == String, Value == Any { 28 | 29 | /// Accesses the value associated with a given key, forcefully casts it to this function's return value and returns it. 30 | /// 31 | /// - parameter key: A key for the value. 32 | /// - returns: The value cast to T, or `nil` if nothing is mapped against `key`. 33 | public func value(_ key: String) -> T? { self[key] as! T? } 34 | 35 | public func intOrNil(_ key: String) -> Int? { numberOrNil(key)?.intValue } 36 | public func boolOrNil(_ key: String) -> Bool? { numberOrNil(key)?.boolValue } 37 | public func stringOrNil(_ key: String) -> String? { value(key) } 38 | public func floatOrNil(_ key: String) -> Float? { numberOrNil(key)?.floatValue } 39 | public func uintOrNil(_ key: String) -> UInt? { numberOrNil(key)?.uintValue } 40 | public func uint32OrNil(_ key: String) -> UInt32? { numberOrNil(key)?.uint32Value } 41 | public func uint64OrNil(_ key: String) -> UInt64? { numberOrNil(key)?.uint64Value } 42 | public func numberOrNil(_ key: String) -> NSNumber? { value(key) } 43 | public func dictionaryOrNil(_ key: String) -> [String: Any]? { value(key) } 44 | 45 | public func int(_ key: String) -> Int { intOrNil(key)! } 46 | public func bool(_ key: String) -> Bool { boolOrNil(key)! } 47 | public func string(_ key: String) -> String { value(key)! } 48 | public func float(_ key: String) -> Float { floatOrNil(key)! } 49 | public func uint(_ key: String) -> UInt { uintOrNil(key)! } 50 | public func uint32(_ key: String) -> UInt32 { uint32OrNil(key)! } 51 | public func uint64(_ key: String) -> UInt64 { uint64OrNil(key)! } 52 | public func number(_ key: String) -> NSNumber { value(key)! } 53 | public func dictionary(_ key: String) -> [String: Any] { value(key)! } 54 | } 55 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Source/Common/ResultUtilities.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Result.swift 3 | // PCloudSDKSwift 4 | // 5 | // Created by Todor Pitekov on 03/01/2017 6 | // Copyright © 2017 pCloud LTD. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Result { 12 | var isSuccess: Bool { 13 | if case .success = self { 14 | return true 15 | } 16 | 17 | return false 18 | } 19 | 20 | var isFailure: Bool { 21 | return !isSuccess 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Source/Common/UInt64+Clamping.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UInt64+Clamping.swift 3 | // PCloudSDKSwift 4 | // 5 | // Created by Todor Pitekov on 10.09.24. 6 | // Copyright © 2024 pCloud LTD. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension UInt64 { 12 | init(clamping value: Double) { 13 | self.init(clamping: NSNumber(value: value).int64Value) 14 | } 15 | 16 | init(clamping value: Float) { 17 | self.init(clamping: NSNumber(value: value).int64Value) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Source/Common/URLSessionBasedCallOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLSessionBasedCallOperation.swift 3 | // PCloudSDKSwift 4 | // 5 | // Created by Todor Pitekov on 03/01/2017 6 | // Copyright © 2017 pCloud LTD. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Concrete implementation of `CallOperation` backed by a `URLSessionDataTask`. 12 | public final class URLSessionBasedCallOperation: URLSessionBasedNetworkOperation { 13 | /// Initializes an operation with a task. 14 | /// 15 | /// - parameter task: A backing data task in a suspended state. 16 | public init(task: URLSessionDataTask) { 17 | super.init(task: task) 18 | 19 | // Assign callbacks. 20 | 21 | var responseBody = Data() 22 | 23 | didReceiveData = { data in 24 | // Build response data. 25 | responseBody.append(data) 26 | } 27 | 28 | didComplete = { [weak self] error in 29 | guard let me = self, !me.isCancelled else { 30 | return 31 | } 32 | 33 | if let error = error, me.errorIsCancellation(error) { 34 | return 35 | } 36 | 37 | // Compute response. 38 | let response: Call.Response = { 39 | if let error = error { 40 | return .failure(.clientError(error)) 41 | } 42 | 43 | do { 44 | let json = try JSONSerialization.jsonObject(with: responseBody, options: []) as! [String: Any] 45 | return .success(json) 46 | } catch { 47 | return .failure(.clientError(error)) 48 | } 49 | }() 50 | 51 | // Complete. 52 | me.complete(response: response) 53 | } 54 | } 55 | } 56 | 57 | 58 | extension URLSessionBasedCallOperation: CallOperation { 59 | public var response: Call.Response? { 60 | return taskResponse 61 | } 62 | 63 | @discardableResult 64 | public func addCompletionBlock(with queue: DispatchQueue?, _ block: @escaping (Call.Response) -> Void) -> URLSessionBasedCallOperation { 65 | addCompletionHandler((block, queue)) 66 | return self 67 | } 68 | } 69 | 70 | extension URLSessionBasedCallOperation: CustomStringConvertible { 71 | public var description: String { 72 | return "\(state), id=\(id), response=\(response as Any)" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Source/Common/URLSessionBasedDownloadOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLSessionBasedDownloadOperation.swift 3 | // PCloudSDKSwift 4 | // 5 | // Created by Todor Pitekov on 03/01/2017 6 | // Copyright © 2017 pCloud LTD. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Concrete implementation of `DownloadOperation` backed by a `URLSessionDownloadTask`. 12 | public final class URLSessionBasedDownloadOperation: URLSessionBasedNetworkOperation { 13 | /// Initializes an operation with a task and destination. 14 | /// 15 | /// - parameter task: A backing download task in a suspended state. 16 | /// - parameter destination: A block called with the temporary location of the downloaded file on disk. 17 | /// The block must either move or open the file for reading before it returns, otherwise the file gets deleted. 18 | /// The block should return the new location of the file. 19 | public init(task: URLSessionDownloadTask, destination: @escaping (URL) throws -> URL) { 20 | super.init(task: task) 21 | 22 | // Assign callbacks. 23 | // Expecting that didFinishDownloading will be called before didComplete and that both callbacks will be called 24 | // on the same thread. 25 | 26 | didWriteData = { [weak self] written, total in 27 | self?.notifyProgress(units: written, outOf: total) 28 | } 29 | 30 | var moveResult: Result? 31 | 32 | didFinishDownloading = { path in 33 | do { 34 | moveResult = .success(try destination(path)) 35 | } catch { 36 | moveResult = .failure(.clientError(error)) 37 | } 38 | } 39 | 40 | didComplete = { [weak self] error in 41 | guard let me = self, !me.isCancelled else { 42 | return 43 | } 44 | 45 | if let error = error, me.errorIsCancellation(error) { 46 | return 47 | } 48 | 49 | // Compute response. 50 | let response: Download.Response = { 51 | if let error = error { 52 | return .failure(.clientError(error)) 53 | } 54 | 55 | return moveResult! 56 | }() 57 | 58 | // Complete. 59 | me.complete(response: response) 60 | } 61 | } 62 | } 63 | 64 | extension URLSessionBasedDownloadOperation: DownloadOperation { 65 | public var response: Download.Response? { 66 | return taskResponse 67 | } 68 | 69 | public var numberOfBytesReceived: Int64 { 70 | return task.countOfBytesReceived 71 | } 72 | 73 | public var totalNumberOfBytesToReceive: Int64 { 74 | return task.countOfBytesExpectedToReceive 75 | } 76 | 77 | @discardableResult 78 | public func addCompletionBlock(with queue: DispatchQueue?, _ block: @escaping (Download.Response) -> Void) -> URLSessionBasedDownloadOperation { 79 | addCompletionHandler((block, queue)) 80 | return self 81 | } 82 | 83 | @discardableResult 84 | public func addProgressBlock(with queue: DispatchQueue?, _ block: @escaping (Int64, Int64) -> Void) -> URLSessionBasedDownloadOperation { 85 | addProgressHandler((block, queue)) 86 | return self 87 | } 88 | } 89 | 90 | extension URLSessionBasedDownloadOperation: CustomStringConvertible { 91 | public var description: String { 92 | return "\(state), id=\(id), progress=\(numberOfBytesReceived) / \(totalNumberOfBytesToReceive), response=\(response as Any)" 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Source/Common/URLSessionBasedNetworkOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLSessionBasedNetworkOperation.swift 3 | // PCloudSDKSwift 4 | // 5 | // Created by Todor Pitekov on 03/01/2017 6 | // Copyright © 2017 pCloud LTD. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Base class for network operations backed by `URLSessionTask`. Conforms to `NetworkOperation`. Forwards `URLSessionObserver` callbacks to blocks. 12 | public class URLSessionBasedNetworkOperation { 13 | typealias CompletionHandler = (callback: (T) -> Void, queue: DispatchQueue?) 14 | typealias ProgressHandler = (callback: (Int64, Int64) -> Void, queue: DispatchQueue?) 15 | 16 | // The task response. Non-nil when the task completes. 17 | var taskResponse: T? { 18 | return lock.inCriticalScope { _taskResponse } 19 | } 20 | 21 | let task: URLSessionTask // The backing task. 22 | 23 | // Blocks corresponding to URLSessionObserver callbacks. Each one is named after the method it is called inside. 24 | var didSendBodyData: ((Int64, Int64) -> Void)? 25 | var didComplete: ((Error?) -> Void)? 26 | var didReceiveData: ((Data) -> Void)? 27 | var didFinishDownloading: ((URL) -> Void)? 28 | var didWriteData: ((Int64, Int64) -> Void)? 29 | 30 | 31 | private var _taskResponse: T? // The backing storage for taskResponse. Always access via the lock. 32 | 33 | // Blocks to call on a specific queue when notifyCompletion() is called by subclasses. 34 | // Do not access directly and use the addCompletionHandler() and notifyCompletion() methods instead. 35 | private var completionHandlers: [CompletionHandler] = [] 36 | 37 | // Blocks to call on a specific queue when notifyProgress() is called by subclasses. 38 | // Do not access directly and use the addProgressHandler() and notifyProgress() methods instead. 39 | private var progressHandlers: [ProgressHandler] = [] 40 | 41 | private let lock = Lock() // Used to protect completion and progress handlers. 42 | 43 | init(task: URLSessionTask) { 44 | self.task = task 45 | } 46 | 47 | // Assigns the response to the taskResponse property, removes all completion and progress handlers and 48 | // calls all completion handler blocks either on the handler's queue or on the main queue if the handler's queue is nil. 49 | func complete(response: T) { 50 | let completionHandlers: [CompletionHandler] = lock.inCriticalScope { 51 | defer { 52 | self.completionHandlers.removeAll() 53 | progressHandlers.removeAll() 54 | } 55 | 56 | _taskResponse = response 57 | return self.completionHandlers 58 | } 59 | 60 | for handler in completionHandlers { 61 | (handler.queue ?? .main).async { 62 | handler.callback(response) 63 | } 64 | } 65 | } 66 | 67 | // Calls all progress handler blocks with the provided arguments either on the handler's queue 68 | // or on the main queue if the handler's queue is nil. 69 | func notifyProgress(units: Int64, outOf totalUnits: Int64) { 70 | let progressHandlers = lock.inCriticalScope { 71 | self.progressHandlers 72 | } 73 | 74 | for handler in progressHandlers { 75 | (handler.queue ?? .main).async { 76 | handler.callback(units, totalUnits) 77 | } 78 | } 79 | } 80 | 81 | func addCompletionHandler(_ handler: CompletionHandler) { 82 | lock.inCriticalScope { 83 | completionHandlers.append(handler) 84 | } 85 | } 86 | 87 | func addProgressHandler(_ handler: ProgressHandler) { 88 | lock.inCriticalScope { 89 | progressHandlers.append(handler) 90 | } 91 | } 92 | 93 | func errorIsCancellation(_ err: Error) -> Bool { 94 | let error = err as NSError 95 | return error.domain == NSURLErrorDomain && error.code == NSURLErrorCancelled 96 | } 97 | } 98 | 99 | 100 | extension URLSessionBasedNetworkOperation: NetworkOperation { 101 | public var id: Int { 102 | return task.taskIdentifier 103 | } 104 | 105 | public var state: NetworkOperationState { 106 | return NetworkOperationState(state: task.state) 107 | } 108 | 109 | public var isCancelled: Bool { 110 | return state == .cancelled 111 | } 112 | 113 | public func start() { 114 | task.resume() 115 | } 116 | 117 | public func cancel() { 118 | task.cancel() 119 | 120 | lock.inCriticalScope { 121 | completionHandlers.removeAll() 122 | progressHandlers.removeAll() 123 | } 124 | } 125 | } 126 | 127 | extension URLSessionBasedNetworkOperation: URLSessionObserver { 128 | public func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { 129 | didSendBodyData?(totalBytesSent, totalBytesExpectedToSend) 130 | } 131 | 132 | public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { 133 | didComplete?(error) 134 | } 135 | 136 | public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { 137 | didReceiveData?(data) 138 | } 139 | 140 | public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { 141 | didFinishDownloading?(location) 142 | } 143 | 144 | public func urlSession(_ session: URLSession, 145 | downloadTask: URLSessionDownloadTask, 146 | didWriteData bytesWritten: Int64, 147 | totalBytesWritten: Int64, 148 | totalBytesExpectedToWrite: Int64) { 149 | didWriteData?(totalBytesWritten, totalBytesExpectedToWrite) 150 | } 151 | } 152 | 153 | extension NetworkOperationState { 154 | public init(state: URLSessionTask.State) { 155 | switch state { 156 | case .running: self = .running 157 | case .suspended: self = .suspended 158 | case .completed: self = .completed 159 | case .canceling: self = .cancelled 160 | @unknown default: fatalError("unhandled or invalid url session state \(state.rawValue)") 161 | } 162 | } 163 | } 164 | 165 | extension NetworkOperationState: CustomStringConvertible { 166 | public var description: String { 167 | switch self { 168 | case .running: return "RUNNING" 169 | case .suspended: return "SUSPENDED" 170 | case .completed: return "COMPLETED" 171 | case .cancelled: return "CANCELLED" 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Source/Common/URLSessionBasedNetworkOperationUtilities.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLSessionBasedNetworkOperationUtilities.swift 3 | // PCloudSDKSwift 4 | // 5 | // Created by Todor Pitekov on 03/01/2017 6 | // Copyright © 2017 pCloud LTD. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct URLSessionBasedNetworkOperationUtilities { 12 | /// Creates and returns a block that builds a `CallOperation` from a `Call.Request` using a `URLSession`. 13 | /// 14 | /// - parameter scheme: A scheme to use when building the operation. 15 | /// - parameter session: A session used to create data tasks. 16 | /// - parameter delegate: The session delegate. 17 | /// - returns: A block that builds a `URLSessionDataTask`-backed `CallOperation` from a `Call.Request`. 18 | public static func createCallOperationBuilder(with scheme: URLScheme, 19 | session: URLSession, 20 | delegate: URLSessionEventHub) -> (Call.Request) -> CallOperation { 21 | return { request in 22 | let task = URLSessionTaskBuilder.createDataTask(with: request, session: session, scheme: scheme) 23 | let operation = URLSessionBasedCallOperation(task: task) 24 | delegate.setObserver(operation, for: task) 25 | 26 | return operation 27 | } 28 | } 29 | 30 | /// Creates and returns a block that builds an `UploadOperation` from an `Upload.Request` using a `URLSession`. 31 | /// 32 | /// - parameter scheme: A scheme to use when building the operation. 33 | /// - parameter session: A session used to create upload tasks. 34 | /// - parameter delegate: The session delegate. 35 | /// - returns: A block that builds a `URLSessionUploadTask`-backed `UploadOperation` from an `Upload.Request`. 36 | public static func createUploadOperationBuilder(with scheme: URLScheme, 37 | session: URLSession, 38 | delegate: URLSessionEventHub) -> (Upload.Request) -> UploadOperation { 39 | return { request in 40 | let task = URLSessionTaskBuilder.createUploadTask(with: request, session: session, scheme: scheme) 41 | let operation = URLSessionBasedUploadOperation(task: task) 42 | delegate.setObserver(operation, for: task) 43 | 44 | return operation 45 | } 46 | } 47 | 48 | /// Creates and returns a block that builds a `DownloadOperation` from a `Download.Request` using a `URLSession`. 49 | /// 50 | /// - parameter session: A session used to create download tasks. 51 | /// - parameter delegate: The session delegate. 52 | /// - returns: A block that builds a `URLSessionDownloadTask`-backed `DownloadOperation` from a `Download.Request`. 53 | public static func createDownloadOperationBuilder(with session: URLSession, 54 | delegate: URLSessionEventHub) -> (Download.Request) -> DownloadOperation { 55 | return { request in 56 | let task = URLSessionTaskBuilder.createDownloadTask(with: request, session: session) 57 | let operation = URLSessionBasedDownloadOperation(task: task, destination: request.destination) 58 | delegate.setObserver(operation, for: task) 59 | 60 | return operation 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Source/Common/URLSessionBasedUploadOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLSessionBasedUploadOperation.swift 3 | // PCloudSDKSwift 4 | // 5 | // Created by Todor Pitekov on 03/01/2017 6 | // Copyright © 2017 pCloud LTD. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Concrete implementation of `UploadOperation` backed by a `URLSessionUploadTask`. 12 | public final class URLSessionBasedUploadOperation: URLSessionBasedNetworkOperation { 13 | /// Initializes an operation with a task. 14 | /// 15 | /// - parameter task: A backing upload task in a suspended state. 16 | public init(task: URLSessionUploadTask) { 17 | super.init(task: task) 18 | 19 | // Assign callbacks. 20 | 21 | didSendBodyData = { [weak self] sent, total in 22 | self?.notifyProgress(units: sent, outOf: total) 23 | } 24 | 25 | var responseBody = Data() 26 | 27 | didReceiveData = { data in 28 | // Build response data. 29 | responseBody.append(data) 30 | } 31 | 32 | didComplete = { [weak self] error in 33 | guard let me = self, !me.isCancelled else { 34 | return 35 | } 36 | 37 | if let error = error, me.errorIsCancellation(error) { 38 | return 39 | } 40 | 41 | // Compute response. 42 | let response: Upload.Response = { 43 | if let error = error { 44 | return .failure(.clientError(error)) 45 | } 46 | 47 | do { 48 | let json = try JSONSerialization.jsonObject(with: responseBody, options: []) as! [String: Any] 49 | return .success(json) 50 | } catch { 51 | return .failure(.clientError(error)) 52 | } 53 | }() 54 | 55 | // Complete. 56 | me.complete(response: response) 57 | } 58 | } 59 | } 60 | 61 | extension URLSessionBasedUploadOperation: UploadOperation { 62 | public var response: Upload.Response? { 63 | return taskResponse 64 | } 65 | 66 | public var numberOfBytesSent: Int64 { 67 | return task.countOfBytesSent 68 | } 69 | 70 | public var totalNumberOfBytesToSend: Int64 { 71 | return task.countOfBytesExpectedToSend 72 | } 73 | 74 | @discardableResult 75 | public func addCompletionBlock(with queue: DispatchQueue?, _ block: @escaping (Upload.Response) -> Void) -> URLSessionBasedUploadOperation { 76 | addCompletionHandler((block, queue)) 77 | return self 78 | } 79 | 80 | @discardableResult 81 | public func addProgressBlock(with queue: DispatchQueue?, _ block: @escaping (Int64, Int64) -> Void) -> URLSessionBasedUploadOperation { 82 | addProgressHandler((block, queue)) 83 | return self 84 | } 85 | } 86 | 87 | extension URLSessionBasedUploadOperation: CustomStringConvertible { 88 | public var description: String { 89 | return "\(state), id=\(id), progress=\(numberOfBytesSent) / \(totalNumberOfBytesToSend), response=\(response as Any)" 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Source/Common/URLSessionEventHub.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLSessionEventHub.swift 3 | // PCloudSDKSwift 4 | // 5 | // Created by Todor Pitekov on 03/01/2017 6 | // Copyright © 2017 pCloud LTD. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Object handling callbacks from a `URLSession`. 12 | public protocol URLSessionObserver { 13 | func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) 14 | func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) 15 | func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) 16 | func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) 17 | func urlSession(_ session: URLSession, 18 | downloadTask: URLSessionDownloadTask, 19 | didWriteData bytesWritten: Int64, 20 | totalBytesWritten: Int64, 21 | totalBytesExpectedToWrite: Int64) 22 | } 23 | 24 | 25 | /// Forwards callbacks from a `URLSession` to objects implementing `URLSessionObserver`. Assigns a single observer per `URLSessionTask` and that observer 26 | /// receives all callbacks associated with that task. 27 | public final class URLSessionEventHub: NSObject { 28 | // URLSessionTask identifiers mapped to observers. 29 | private var observers = Atomic<[Int: URLSessionObserver]>([:]) 30 | 31 | // Returns an observer for a task. 32 | private subscript(_ task: URLSessionTask) -> URLSessionObserver? { 33 | return observers.value[task.taskIdentifier] 34 | } 35 | 36 | override public init() {} 37 | 38 | /// Assigns an observer for a specific `URLSessionTask`. This unassigns the previous observer to the provided task. 39 | /// The new observer will receive all callbacks for `task` until it is explicitly removed from this instance, or, if 40 | /// `task` completes. A task is considered completed when 41 | /// ``` 42 | /// urlSession(:task:didCompleteWithError:) 43 | /// ``` 44 | /// is invoked in which case the hub will forward this last call to its observer and then remove it. 45 | /// 46 | /// - parameter observer: An object that will receive all callbacks for `task`. 47 | /// - parameter task: The task to associate with `observer`. 48 | public func setObserver(_ observer: URLSessionObserver, for task: URLSessionTask) { 49 | set(observer, for: task) 50 | } 51 | 52 | /// Removes the observer for a specific `URLSessionTask`, if one exists. 53 | /// 54 | /// - parameter task: The task to stop observing. 55 | public func removeObserver(for task: URLSessionTask) { 56 | set(nil, for: task) 57 | } 58 | 59 | // Assigns an observer for a task and returns the old observer, if any. 60 | @discardableResult private func set(_ observer: URLSessionObserver?, for task: URLSessionTask) -> URLSessionObserver? { 61 | let key = task.taskIdentifier 62 | 63 | return observers.withValue { 64 | let oldObserver = $0[key] 65 | $0[key] = observer 66 | return oldObserver 67 | } 68 | } 69 | } 70 | 71 | 72 | // MARK: - Conforming to URLSession delegate protocols. 73 | 74 | extension URLSessionEventHub: URLSessionTaskDelegate { 75 | @objc public func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { 76 | self[task]?.urlSession(session, task: task, didSendBodyData: bytesSent, totalBytesSent: totalBytesSent, totalBytesExpectedToSend: totalBytesExpectedToSend) 77 | } 78 | 79 | @objc public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { 80 | // Remove the observer and forward this callback to it. 81 | set(nil, for: task)?.urlSession(session, task: task, didCompleteWithError: error) 82 | } 83 | } 84 | 85 | extension URLSessionEventHub: URLSessionDataDelegate { 86 | @objc public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { 87 | self[dataTask]?.urlSession(session, dataTask: dataTask, didReceive: data) 88 | } 89 | } 90 | 91 | extension URLSessionEventHub: URLSessionDownloadDelegate { 92 | @objc public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { 93 | self[downloadTask]?.urlSession(session, downloadTask: downloadTask, didFinishDownloadingTo: location) 94 | } 95 | 96 | @objc public func urlSession(_ session: URLSession, 97 | downloadTask: URLSessionDownloadTask, 98 | didWriteData bytesWritten: Int64, 99 | totalBytesWritten: Int64, 100 | totalBytesExpectedToWrite: Int64) { 101 | self[downloadTask]?.urlSession(session, 102 | downloadTask: downloadTask, 103 | didWriteData: bytesWritten, 104 | totalBytesWritten: totalBytesWritten, 105 | totalBytesExpectedToWrite: totalBytesExpectedToWrite) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Source/Common/URLSessionTaskBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLSessionTaskBuilder.swift 3 | // SDK iOS 4 | // 5 | // Created by Todor Pitekov on 11/28/17. 6 | // Copyright © 2017 pCloud LTD. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum URLScheme: String { 12 | case http = "http" 13 | case https = "https" 14 | } 15 | 16 | public enum URLSessionTaskBuilder { 17 | /// Creates a `URLSessionDataTask` using a `URLSession` given a `Call.Request`. 18 | public static func createDataTask(with request: Call.Request, session: URLSession, scheme: URLScheme) -> URLSessionDataTask { 19 | // Build the URL. 20 | let url = buildURL(withScheme: scheme.rawValue, host: request.hostName, commandName: request.command.name, query: nil) 21 | // Build a form data encoded query. 22 | let query = buildQuery(with: request.command.parameters, addingPercentEncoding: true) 23 | 24 | // Build a POST request. 25 | var urlRequest = URLRequest(url: url) 26 | urlRequest.httpMethod = HTTPMethod.post 27 | urlRequest.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") 28 | urlRequest.httpBody = query.data(using: .utf8, allowLossyConversion: false) 29 | 30 | if let timeoutInterval = request.timeoutInterval { 31 | urlRequest.timeoutInterval = timeoutInterval 32 | } 33 | 34 | return session.dataTask(with: urlRequest) 35 | } 36 | 37 | /// Creates a `URLSessionUploadTask` using a `URLSession` given an `Upload.Request`. 38 | public static func createUploadTask(with request: Upload.Request, session: URLSession, scheme: URLScheme) -> URLSessionUploadTask { 39 | // Build a GET query. 40 | let query = buildQuery(with: request.command.parameters, addingPercentEncoding: true) 41 | // Build the URL. 42 | let url = buildURL(withScheme: scheme.rawValue, host: request.hostName, commandName: request.command.name, percentEncodedQuery: query) 43 | 44 | // Build a POST request. 45 | var urlRequest = URLRequest(url: url) 46 | urlRequest.httpMethod = HTTPMethod.post 47 | urlRequest.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type") 48 | 49 | if let timeoutInterval = request.timeoutInterval { 50 | urlRequest.timeoutInterval = timeoutInterval 51 | } 52 | 53 | switch request.body { 54 | case .file(let url): return session.uploadTask(with: urlRequest, fromFile: url) 55 | case .data(let data): return session.uploadTask(with: urlRequest, from: data) 56 | } 57 | } 58 | 59 | /// Creates a `URLSessionDownloadTask` using a `URLSession` given a `Download.Request`. 60 | public static func createDownloadTask(with request: Download.Request, session: URLSession) -> URLSessionDownloadTask { 61 | var urlRequest = URLRequest(url: request.resourceAddress) 62 | 63 | if let timeoutInterval = request.timeoutInterval { 64 | urlRequest.timeoutInterval = timeoutInterval 65 | } 66 | 67 | for (name, value) in buildHTTPHeaderFields(withCookies: request.cookies, resourceAddress: request.resourceAddress) { 68 | urlRequest.addValue(value, forHTTPHeaderField: name) 69 | } 70 | 71 | return session.downloadTask(with: urlRequest) 72 | } 73 | 74 | private static func buildHTTPHeaderFields(withCookies cookies: [String: String], resourceAddress: URL) -> [String: String] { 75 | guard let host = resourceAddress.host else { 76 | return [:] 77 | } 78 | 79 | let path = resourceAddress.path 80 | 81 | let cookies = cookies.compactMap { name, value in 82 | HTTPCookie(properties: [.name: name, .value: value, .domain: host, .path: path]) 83 | } 84 | 85 | return HTTPCookie.requestHeaderFields(with: cookies) 86 | } 87 | 88 | // Functions for encoding command parameter values to percent encoded strings. 89 | 90 | private static func encode(_ value: String) -> String { 91 | return value.addingPercentEncoding(withAllowedCharacters: .urlQueryParameterAllowedCharacterSetRFC3986())! 92 | } 93 | 94 | private static func encode(_ value: Bool) -> String { 95 | return value ? "1" : "0" 96 | } 97 | 98 | private static func encode(_ value: UInt64) -> String { 99 | return "\(value)" 100 | } 101 | 102 | 103 | // Functions for building a query string from command parameters. 104 | 105 | private static func rawQueryComponent(from parameter: Call.Command.Parameter) -> String { 106 | switch parameter { 107 | case let .boolean(name, value): return "\(name)=\(encode(value))" 108 | case let .number(name, value): return "\(name)=\(encode(value))" 109 | case let .string(name, value): return "\(name)=\(value)" 110 | } 111 | } 112 | 113 | private static func percentEncodedQueryComponent(from parameter: Call.Command.Parameter) -> String { 114 | switch parameter { 115 | case let .boolean(name, value): return "\(encode(name))=\(encode(value))" 116 | case let .number(name, value): return "\(encode(name))=\(encode(value))" 117 | case let .string(name, value): return "\(encode(name))=\(encode(value))" 118 | } 119 | } 120 | 121 | private static func buildQuery(with parameters: [Call.Command.Parameter], addingPercentEncoding encode: Bool) -> String { 122 | let components: [String] 123 | 124 | if encode { 125 | components = parameters.map(percentEncodedQueryComponent) 126 | } else { 127 | components = parameters.map(rawQueryComponent) 128 | } 129 | 130 | return components.joined(separator: "&") 131 | } 132 | 133 | private static func buildURL(withScheme scheme: String, host: String, commandName: String, query: String?) -> URL { 134 | var components = URLComponents() 135 | components.scheme = scheme 136 | components.host = host 137 | components.path = "/\(commandName)" 138 | components.query = query 139 | 140 | return components.url! 141 | } 142 | 143 | private static func buildURL(withScheme scheme: String, host: String, commandName: String, percentEncodedQuery: String?) -> URL { 144 | var components = URLComponents() 145 | components.scheme = scheme 146 | components.host = host 147 | components.path = "/\(commandName)" 148 | components.percentEncodedQuery = percentEncodedQuery 149 | 150 | return components.url! 151 | } 152 | } 153 | 154 | private struct HTTPMethod { 155 | static let get = "GET" 156 | static let post = "POST" 157 | static let put = "PUT" 158 | } 159 | 160 | private extension CharacterSet { 161 | // A character with characters allowed in a URL query as per RFC 3986. 162 | static func urlQueryParameterAllowedCharacterSetRFC3986() -> CharacterSet { 163 | return self.init(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~/?") 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Source/Common/UploadInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UploadInfo.swift 3 | // SDK iOS 4 | // 5 | // Created by Todor Pitekov on 11/28/17. 6 | // Copyright © 2017 pCloud LTD. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// The state of a pCloud API upload session. 12 | public struct UploadInfo { 13 | /// The number of bytes written to this upload. 14 | public let dataSize: UInt64 15 | /// The SHA1 digest in hex representation of the uploaded data. 16 | public let dataSHA1: String 17 | /// The MD5 digest in hex representation of the uploaded data. 18 | public let dataMD5: String 19 | 20 | public init(dataSize: UInt64, dataSHA1: String, dataMD5: String) { 21 | self.dataSize = dataSize 22 | self.dataSHA1 = dataSHA1 23 | self.dataMD5 = dataMD5 24 | } 25 | } 26 | 27 | /// Parses `UploadInfo` from a pCloud API response dictionary. 28 | public struct UploadInfoParser: Parser { 29 | public init() {} 30 | 31 | public func parse(_ input: [String: Any]) throws -> UploadInfo { 32 | return UploadInfo(dataSize: input.uint64("size"), dataSHA1: input.string("sha1"), dataMD5: input.string("md5")) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Source/Common/UploadTask.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UploadTask.swift 3 | // PCloudSDKSwift 4 | // 5 | // Created by Todor Pitekov on 03/01/2017 6 | // Copyright © 2017 pCloud LTD. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Executes an upload to the pCloud API. 12 | public final class UploadTask: Cancellable { 13 | public typealias Parser = Method.Parser 14 | public typealias CompletionBlock = (Result>) -> Void 15 | 16 | // The underlying operation executing the upload. 17 | private let operation: UploadOperation 18 | 19 | private let lock = Lock() 20 | private var completionBlocks: [CompletionBlock] = [] 21 | 22 | /// `true` if `cancel()` has been invoked on this instance, `false` otherwise. 23 | public var isCancelled: Bool { 24 | return operation.isCancelled 25 | } 26 | 27 | /// Initializes a non-running task with a network operation and a parser. 28 | /// 29 | /// - parameter operation: An operation in a suspended state that would execute the upload. 30 | /// - parameter responseParser: A block parsing an object from a response dictionary. 31 | public init(operation: UploadOperation, responseParser: @escaping Parser) { 32 | self.operation = operation 33 | 34 | // Parse the response on a background queue. 35 | operation.addCompletionBlock(with: .global()) { response in 36 | // Compute the response. 37 | let result: Result> = { 38 | switch response { 39 | case .failure(.clientError(let error)): 40 | return .failure(.clientError(error)) 41 | 42 | case .failure(.protocolError(let error)): 43 | return .failure(.protocolError(error)) 44 | 45 | case .success(let payload): 46 | do { 47 | return try responseParser(payload).mapError { error in 48 | CallError(apiError: error) 49 | } 50 | } catch { 51 | return .failure(.clientError(error)) 52 | } 53 | } 54 | }() 55 | 56 | // Notify observers. 57 | let completionBlocks: [CompletionBlock] = self.lock.inCriticalScope { 58 | defer { self.completionBlocks.removeAll() } 59 | return self.completionBlocks 60 | } 61 | 62 | DispatchQueue.main.async { 63 | for block in completionBlocks { 64 | block(result) 65 | } 66 | } 67 | } 68 | } 69 | 70 | /// Adds a completion block to this instance to be called when the task completes either successfully or with a failure. 71 | /// 72 | /// - parameter block: A block called on the main thread with the result of the task. 73 | /// - returns: This task. 74 | @discardableResult public func addCompletionBlock(_ block: @escaping CompletionBlock) -> UploadTask { 75 | lock.inCriticalScope { 76 | completionBlocks.append(block) 77 | } 78 | 79 | return self 80 | } 81 | 82 | /// Adds a progress block to this instance to be called continuously as data is being uploaded. 83 | /// 84 | /// - parameter block: A block called on the main thread with the number of uploaded bytes and the total number of bytes to upload as 85 | /// first and second arguments, respectively. Called each time the number of uploaded bytes changes. 86 | /// - returns: This task. 87 | @discardableResult public func addProgressBlock(_ block: @escaping (Int64, Int64) -> Void) -> UploadTask { 88 | operation.addProgressBlock(with: .main, block) 89 | return self 90 | } 91 | 92 | /// Starts the task if it is not already running. 93 | /// 94 | /// - returns: This task. 95 | @discardableResult public func start() -> UploadTask { 96 | operation.start() 97 | return self 98 | } 99 | 100 | /// Interrupts and invalidates the task. An invalidated task cannot run again. 101 | public func cancel() { 102 | operation.cancel() 103 | 104 | lock.inCriticalScope { 105 | completionBlocks.removeAll() 106 | } 107 | } 108 | } 109 | 110 | extension UploadTask: CustomStringConvertible { 111 | public var description: String { 112 | return "\(operation.state), id=\(operation.id), progress=\(operation.numberOfBytesSent) / \(operation.totalNumberOfBytesToSend), response=\(operation.response as Any)" 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Source/Common/User.swift: -------------------------------------------------------------------------------- 1 | // 2 | // User.swift 3 | // PCloudSDKSwift 4 | // 5 | // Created by Todor Pitekov on 03/01/2017 6 | // Copyright © 2017 pCloud LTD. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// User namespace. 12 | public struct User { 13 | /// A pCloud user. 14 | open class Metadata { 15 | /// The unique identifier of this user. 16 | public let id: UInt64 17 | /// The email address this user has registered with. 18 | public let emailAddress: String 19 | /// `true` if the user has verified their email address, `false` otherwise. 20 | public let isEmailVerified: Bool 21 | /// The size of this user's content in bytes. 22 | public let usedQuota: UInt64 23 | /// The available storage space in bytes. 24 | public let availableQuota: UInt64 25 | 26 | /// Initializes a user with all fields of this class. 27 | public init(id: UInt64, emailAddress: String, isEmailVerified: Bool, usedQuota: UInt64, availableQuota: UInt64) { 28 | self.id = id 29 | self.emailAddress = emailAddress 30 | self.isEmailVerified = isEmailVerified 31 | self.usedQuota = usedQuota 32 | self.availableQuota = availableQuota 33 | } 34 | } 35 | } 36 | 37 | extension User.Metadata: CustomStringConvertible { 38 | public var description: String { 39 | let quotaProgress = (Float(usedQuota) / Float(availableQuota)) * 100 40 | return "id=\(id), email=\(emailAddress), verified=\(isEmailVerified), quota=\(usedQuota) / \(availableQuota); \(quotaProgress)%" 41 | } 42 | } 43 | 44 | 45 | /// Parses `User.Metadata` from a pCloud API response dictionary. 46 | public struct UserMetadataParser: Parser { 47 | public init() {} 48 | 49 | public func parse(_ input: [String: Any]) throws -> User.Metadata { 50 | return User.Metadata(id: input.uint64("userid"), 51 | emailAddress: input.string("email"), 52 | isEmailVerified: input.bool("emailverified"), 53 | usedQuota: input.uint64("usedquota"), 54 | availableQuota: input.uint64("quota")) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Source/Common/VoidCancellationToken.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VoidCancellationToken.swift 3 | // PCloudSDKSwift 4 | // 5 | // Created by Todor Pitekov on 03/01/2017 6 | // Copyright © 2017 pCloud LTD. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// An implementation of `Cancellable` that does nothing. 12 | public struct VoidCancellationToken: Cancellable { 13 | public init() {} 14 | 15 | public var isCancelled: Bool { 16 | return false 17 | } 18 | 19 | public func cancel() { 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Source/iOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Source/iOS/OAuthWebViewMobile.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OAuthWebView.swift 3 | // PCloudSDKSwift 4 | // 5 | // Created by Todor Pitekov on 03/01/2017 6 | // Copyright © 2017 pCloud LTD. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | 11 | import UIKit 12 | import WebKit 13 | 14 | /// A concrete implementation of `OAuthAuthorizationFlowView` based on UIKit. 15 | public final class WebViewControllerPresenterMobile: OAuthAuthorizationFlowView { 16 | private let presentingViewController: UIViewController 17 | 18 | /// Initializes a new presenter. 19 | /// 20 | /// - parameter presentingViewController: A controller used to present a web view controller from. 21 | public init(presentingViewController: UIViewController) { 22 | self.presentingViewController = presentingViewController 23 | } 24 | 25 | public func presentWebView(url: URL, interceptNavigation: @escaping (URL) -> Bool, didCancel: @escaping () -> Void) { 26 | let controller = WebViewControllerMobile(address: url, redirectHandler: interceptNavigation, cancelHandler: didCancel) 27 | let navigationWrapper = UINavigationController(rootViewController: controller) 28 | presentingViewController.present(navigationWrapper, animated: true, completion: nil) 29 | } 30 | 31 | public func dismissWebView() { 32 | presentingViewController.dismiss(animated: true, completion: nil) 33 | } 34 | } 35 | 36 | /// A view controller with a web view and a cancel button in the navigation bar. It forwards its navigation actions and cancel button 37 | /// taps to blocks. The controller does not allow the user to type an address. An initial address is instead provided to instances of this 38 | /// class which is loaded when an instance is shown. 39 | public final class WebViewControllerMobile: UIViewController { 40 | private let address: URL 41 | private let redirectHandler: (URL) -> Bool 42 | private let cancelHandler: () -> Void 43 | 44 | private var webView: WKWebView! 45 | private var activityIndicator: UIActivityIndicatorView! // Shown during initial page loading. 46 | private var errorLabel: UILabel! // Shown when a page loading error occurs. 47 | 48 | private var didStartLoading = false 49 | 50 | /// Initializes a new view controller with a web view. 51 | /// 52 | /// - parameter address: The address of the page to open when the view controller is shown. 53 | /// - parameter redirectHandler: A block invoked when a navigation action is about to take place. Called with the 54 | /// destination address. The return value determines whether the controller should allow the redirect or not. 55 | /// - parameter cancelHandler: A block invoked when the user taps on the cancel button. 56 | public init(address: URL, redirectHandler: @escaping (URL) -> Bool, cancelHandler: @escaping () -> Void) { 57 | self.address = address 58 | self.redirectHandler = redirectHandler 59 | self.cancelHandler = cancelHandler 60 | 61 | super.init(nibName: nil, bundle: nil) 62 | } 63 | 64 | public override func viewDidLoad() { 65 | super.viewDidLoad() 66 | 67 | title = "pCloud" 68 | 69 | view.backgroundColor = .white 70 | 71 | webView = WKWebView() 72 | webView.navigationDelegate = self 73 | view.addSubview(webView) 74 | 75 | activityIndicator = UIActivityIndicatorView(style: .gray) 76 | view.addSubview(activityIndicator) 77 | 78 | errorLabel = UILabel() 79 | errorLabel.textAlignment = .center 80 | errorLabel.font = UIFont.systemFont(ofSize: 18) 81 | errorLabel.backgroundColor = .clear 82 | errorLabel.textColor = .black 83 | errorLabel.numberOfLines = 0 84 | view.addSubview(errorLabel) 85 | 86 | navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelButtonTapped(_:))) 87 | } 88 | 89 | public override func viewDidLayoutSubviews() { 90 | super.viewDidLayoutSubviews() 91 | 92 | webView.frame = view.bounds 93 | activityIndicator.center = CGPoint(x: view.bounds.midX, y: view.bounds.midY) 94 | errorLabel.frame = view.bounds.insetBy(dx: 10, dy: 10) 95 | } 96 | 97 | required public init?(coder aDecoder: NSCoder) { 98 | fatalError("init(coder:) has not been implemented") 99 | } 100 | 101 | public override func viewWillAppear(_ animated: Bool) { 102 | super.viewWillAppear(animated) 103 | 104 | if !didStartLoading { 105 | didStartLoading = true 106 | showError(nil) 107 | setActivityIndicatorVisible(true) 108 | load(address) 109 | } 110 | } 111 | 112 | private func load(_ url: URL) { 113 | let request = URLRequest(url: url) 114 | webView.load(request) 115 | } 116 | 117 | private func setActivityIndicatorVisible(_ visible: Bool) { 118 | if visible { 119 | activityIndicator.startAnimating() 120 | } else { 121 | activityIndicator.stopAnimating() 122 | } 123 | 124 | activityIndicator.isHidden = !visible 125 | } 126 | 127 | private func showError(_ message: String?) { 128 | errorLabel.text = message 129 | errorLabel.isHidden = message == nil 130 | } 131 | } 132 | 133 | extension WebViewControllerMobile: WKNavigationDelegate { 134 | public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { 135 | if let navigationAddress = navigationAction.request.url, redirectHandler(navigationAddress) { 136 | decisionHandler(.cancel) 137 | } else { 138 | decisionHandler(.allow) 139 | } 140 | } 141 | 142 | public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { 143 | setActivityIndicatorVisible(false) 144 | } 145 | 146 | public func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { 147 | setActivityIndicatorVisible(false) 148 | showError(error.localizedDescription) 149 | } 150 | } 151 | 152 | extension WebViewControllerMobile { 153 | @objc private func cancelButtonTapped(_ sender: UIBarButtonItem) { 154 | cancelHandler() 155 | } 156 | } 157 | 158 | #endif 159 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Source/iOS/PCloud+OAuthMobile.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PCloud+OAuthMobile.swift 3 | // SDK iOS 4 | // 5 | // Created by Todor Pitekov on 22.04.20. 6 | // Copyright © 2020 pCloud LTD. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | 11 | import UIKit 12 | 13 | extension PCloud { 14 | /// Starts the OAuth authorization flow. Expects `setUp()` to have been called. Call on the main thread. 15 | /// 16 | /// - parameter controller: A view controller to present a web view from. Must be in the view hierarchy. 17 | /// - parameter completionBlock: A block called on the main thread when authorization completes or is cancelled. 18 | /// The global pCloud client will be initialized inside the block if authorization was successful. 19 | public static func authorize(with controller: UIViewController, _ completionBlock: @escaping (OAuth.Result) -> Void) { 20 | if #available(iOS 13, *) { 21 | guard let window = controller.view.window else { 22 | assertionFailure("Cannot present from a view controller that is not part of the view hierarchy.") 23 | return 24 | } 25 | 26 | authorize(with: window, completionBlock: completionBlock) 27 | } else { 28 | authorize(with: WebViewControllerPresenterMobile(presentingViewController: controller), completionBlock: completionBlock) 29 | } 30 | } 31 | } 32 | 33 | #endif 34 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Source/macOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSHumanReadableCopyright 22 | Copyright © 2017 pCloud LTD. All rights reserved. 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Source/macOS/OAuthWebViewDesktop.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OAuthWebViewDesktop.swift 3 | // PCloudSDKSwift 4 | // 5 | // Created by Todor Pitekov on 03/01/2017 6 | // Copyright © 2017 pCloud LTD. All rights reserved. 7 | // 8 | 9 | #if canImport(AppKit) 10 | 11 | import Cocoa 12 | import WebKit 13 | 14 | /// A concrete implementation of `OAuthAuthorizationFlowView` based on AppKit. 15 | public final class WebViewControllerPresenterDesktop: OAuthAuthorizationFlowView { 16 | private let presentingViewController: NSViewController 17 | private var webViewController: WebViewControllerDesktop? // The presented web view controller. Nil if no controller is presented currently. 18 | 19 | /// Initializes a new presenter. 20 | /// 21 | /// - parameter presentingViewController: A controller used to present a web view controller from. 22 | public init(presentingViewController: NSViewController) { 23 | self.presentingViewController = presentingViewController 24 | } 25 | 26 | public func presentWebView(url: URL, interceptNavigation: @escaping (URL) -> Bool, didCancel: @escaping () -> Void) { 27 | let controller = WebViewControllerDesktop(address: url, redirectHandler: interceptNavigation, cancelHandler: didCancel) 28 | webViewController = controller 29 | presentingViewController.presentAsSheet(controller) 30 | } 31 | 32 | public func dismissWebView() { 33 | if let webViewController = webViewController { 34 | self.webViewController = nil 35 | presentingViewController.dismiss(webViewController) 36 | } 37 | } 38 | } 39 | 40 | /// A view controller with a web view and a cancel button. It forwards its navigation actions and cancel button 41 | /// taps to blocks. The controller does not allow the user to type an address. An initial address is instead provided to instances of this 42 | /// class which is loaded when an instance is shown. 43 | public final class WebViewControllerDesktop: NSViewController { 44 | private let address: URL 45 | private let redirectHandler: (URL) -> Bool 46 | private let cancelHandler: () -> Void 47 | 48 | @IBOutlet var webViewContainer: BorderedView! 49 | @IBOutlet var progressIndicator: NSProgressIndicator! // Shown during initial page loading. 50 | @IBOutlet var errorLabel: NSTextField! // Shown when a page loading error occurs. 51 | 52 | private var webView: WKWebView! 53 | 54 | /// Initializes a new view controller with a web view. 55 | /// 56 | /// - parameter address: The address of the page to open when the view controller is shown. 57 | /// - parameter redirectHandler: A block invoked when a navigation action is about to take place. Called with the 58 | /// destination address. The return value determines whether the controller should allow the redirect or not. 59 | /// - parameter cancelHandler: A block invoked when the user taps on the cancel button. 60 | public init(address: URL, redirectHandler: @escaping (URL) -> Bool, cancelHandler: @escaping () -> Void) { 61 | self.address = address 62 | self.redirectHandler = redirectHandler 63 | self.cancelHandler = cancelHandler 64 | 65 | // Try to find our .nib file in the following places: 66 | // - The PCloudSDKSwiftResources.bundle (will exist if we're managed by CocoaPods). 67 | // - Our parent bundle. 68 | // - The main bundle. 69 | 70 | let resourceBundle: Bundle = { 71 | let parentBundle = Bundle(for: WebViewControllerDesktop.self) 72 | 73 | if let resourceBundlePath = parentBundle.path(forResource: "PCloudSDKSwiftResources", ofType: "bundle") { 74 | return Bundle(path: resourceBundlePath)! 75 | } 76 | 77 | if let bundle = [parentBundle, .main].first(where: { $0.path(forResource: "WebViewControllerDesktop", ofType: "nib") != nil }) { 78 | return bundle 79 | } 80 | 81 | fatalError("Cannot find WebViewControllerDesktop.nib") 82 | }() 83 | 84 | super.init(nibName: "WebViewControllerDesktop", bundle: resourceBundle) 85 | } 86 | 87 | required public init?(coder: NSCoder) { 88 | fatalError("init(coder:) has not been implemented") 89 | } 90 | 91 | public override func viewDidLoad() { 92 | super.viewDidLoad() 93 | 94 | preferredContentSize = view.bounds.size 95 | 96 | webViewContainer.borderWidth = 1 97 | webViewContainer.borderColor = .lightGray 98 | 99 | webView = WKWebView(frame: webViewContainer.bounds) 100 | webView.navigationDelegate = self 101 | webViewContainer.addSubview(webView, positioned: .below, relativeTo: nil) 102 | 103 | progressIndicator.controlTint = .graphiteControlTint 104 | 105 | setProgressIndicatorVisible(true) 106 | showError("") 107 | load(address) 108 | } 109 | 110 | private func load(_ url: URL) { 111 | let request = URLRequest(url: url) 112 | webView.load(request) 113 | } 114 | 115 | private func setProgressIndicatorVisible(_ visible: Bool) { 116 | if visible { 117 | progressIndicator.startAnimation(self) 118 | } else { 119 | progressIndicator.stopAnimation(self) 120 | } 121 | 122 | progressIndicator.isHidden = !visible 123 | } 124 | 125 | private func showError(_ message: String) { 126 | errorLabel.stringValue = message 127 | errorLabel.isHidden = message.isEmpty 128 | } 129 | } 130 | 131 | extension WebViewControllerDesktop: WKNavigationDelegate { 132 | public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { 133 | if let navigationAddress = navigationAction.request.url, redirectHandler(navigationAddress) { 134 | decisionHandler(.cancel) 135 | } else { 136 | decisionHandler(.allow) 137 | } 138 | } 139 | 140 | public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { 141 | setProgressIndicatorVisible(false) 142 | } 143 | 144 | public func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { 145 | setProgressIndicatorVisible(false) 146 | showError(error.localizedDescription) 147 | } 148 | } 149 | 150 | extension WebViewControllerDesktop { 151 | @IBAction func cancelButtonTapped(_ sender: NSButton) { 152 | cancelHandler() 153 | } 154 | } 155 | 156 | // A simple view with a border. Clients of the class can control border color and width. 157 | public final class BorderedView: NSView { 158 | public var borderWidth: CGFloat = 0 { 159 | didSet { 160 | if borderWidth != oldValue { 161 | needsDisplay = true 162 | } 163 | } 164 | } 165 | 166 | public var borderColor: NSColor? { 167 | didSet { 168 | if borderColor != oldValue { 169 | needsDisplay = true 170 | } 171 | } 172 | } 173 | 174 | required public init?(coder: NSCoder) { 175 | super.init(coder: coder) 176 | wantsLayer = true 177 | } 178 | 179 | public override var wantsUpdateLayer: Bool { 180 | return true 181 | } 182 | 183 | public override func updateLayer() { 184 | layer!.borderWidth = borderWidth 185 | layer!.borderColor = borderColor?.cgColor 186 | } 187 | } 188 | 189 | #endif 190 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Source/macOS/PCloud+OAuthDesktop.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PCloud+OAuthDesktop.swift 3 | // SDK macOS 4 | // 5 | // Created by Todor Pitekov on 22.04.20. 6 | // Copyright © 2020 pCloud LTD. All rights reserved. 7 | // 8 | 9 | #if canImport(AppKit) 10 | 11 | import AppKit 12 | 13 | extension PCloud { 14 | /// Starts the OAuth authorization flow. Expects `setUp()` to have been called. Call on the main thread. 15 | /// 16 | /// - parameter controller: A view controller to present a web view from. Must be in the view hierarchy. 17 | /// - parameter completionBlock: A block called on the main thread when authorization completes or is cancelled. 18 | /// The global pCloud client will be initialized inside the block if authorization was successful. 19 | public static func authorize(with controller: NSViewController, _ completionBlock: @escaping (OAuth.Result) -> Void) { 20 | if #available(OSX 10.15, *) { 21 | guard let window = controller.view.window else { 22 | assertionFailure("Cannot present from a view controller that is not part of the view hierarchy.") 23 | return 24 | } 25 | 26 | authorize(with: window, completionBlock: completionBlock) 27 | } else { 28 | authorize(with: WebViewControllerPresenterDesktop(presentingViewController: controller), completionBlock: completionBlock) 29 | } 30 | } 31 | } 32 | 33 | #endif 34 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Source/macOS/WebViewControllerDesktop.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 40 | 41 | 42 | 43 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Tests/Common/NetworkOperationMocks.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkOperationMocks.swift 3 | // PCloudSDKSwift 4 | // 5 | // Created by Todor Pitekov on 03/01/2017 6 | // Copyright © 2017 pCloud LTD. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | @testable import PCloudSDKSwift 11 | 12 | class NetworkOperationMock { 13 | var startInvoked = false 14 | var cancelInvoked = false 15 | } 16 | 17 | extension NetworkOperationMock: NetworkOperation { 18 | var id: Int { 19 | return 42 20 | } 21 | 22 | var state: NetworkOperationState { 23 | return .suspended 24 | } 25 | 26 | var isCancelled: Bool { 27 | return cancelInvoked 28 | } 29 | 30 | func start() { 31 | startInvoked = true 32 | } 33 | 34 | func cancel() { 35 | cancelInvoked = true 36 | } 37 | } 38 | 39 | 40 | class CallOperationMock: NetworkOperationMock { 41 | private(set) var completionBlocks: [((Call.Response) -> Void)] = [] 42 | 43 | func invokeCompletion(response: Call.Response) { 44 | for block in completionBlocks { 45 | block(response) 46 | } 47 | } 48 | } 49 | 50 | extension CallOperationMock: CallOperation { 51 | var response: Call.Response? { 52 | return nil 53 | } 54 | 55 | @discardableResult func addCompletionBlock(with queue: DispatchQueue?, _ block: @escaping (Call.Response) -> Void) -> Self { 56 | completionBlocks.append(block) 57 | return self 58 | } 59 | } 60 | 61 | 62 | class UploadOperationMock: NetworkOperationMock { 63 | private(set) var completionBlocks: [((Upload.Response) -> Void)] = [] 64 | private(set) var progressBlocks: [((Int64, Int64) -> Void)] = [] 65 | 66 | func invokeCompletion(response: Upload.Response) { 67 | for block in completionBlocks { 68 | block(response) 69 | } 70 | } 71 | 72 | func invokeProgress(sent: Int64, total: Int64) { 73 | for block in progressBlocks { 74 | block(sent, total) 75 | } 76 | } 77 | } 78 | 79 | extension UploadOperationMock: UploadOperation { 80 | var response: Upload.Response? { 81 | return nil 82 | } 83 | 84 | var numberOfBytesSent: Int64 { 85 | return 0 86 | } 87 | 88 | var totalNumberOfBytesToSend: Int64 { 89 | return 0 90 | } 91 | 92 | @discardableResult func addProgressBlock(with queue: DispatchQueue?, _ block: @escaping (Int64, Int64) -> Void) -> Self { 93 | progressBlocks.append(block) 94 | return self 95 | } 96 | 97 | @discardableResult func addCompletionBlock(with queue: DispatchQueue?, _ block: @escaping (Upload.Response) -> Void) -> Self { 98 | completionBlocks.append(block) 99 | return self 100 | } 101 | } 102 | 103 | 104 | class DownloadOperationMock: NetworkOperationMock { 105 | private(set) var completionBlocks: [((Download.Response) -> Void)] = [] 106 | private(set) var progressBlocks: [((Int64, Int64) -> Void)] = [] 107 | 108 | func invokeCompletion(response: Download.Response) { 109 | for block in completionBlocks { 110 | block(response) 111 | } 112 | } 113 | 114 | func invokeProgress(written: Int64, total: Int64) { 115 | for block in progressBlocks { 116 | block(written, total) 117 | } 118 | } 119 | } 120 | 121 | extension DownloadOperationMock: DownloadOperation { 122 | var response: Download.Response? { 123 | return nil 124 | } 125 | 126 | var numberOfBytesReceived: Int64 { 127 | return 0 128 | } 129 | 130 | var totalNumberOfBytesToReceive: Int64 { 131 | return 0 132 | } 133 | 134 | @discardableResult func addProgressBlock(with queue: DispatchQueue?, _ block: @escaping (Int64, Int64) -> Void) -> Self { 135 | progressBlocks.append(block) 136 | return self 137 | } 138 | 139 | @discardableResult func addCompletionBlock(with queue: DispatchQueue?, _ block: @escaping (Download.Response) -> Void) -> Self { 140 | completionBlocks.append(block) 141 | return self 142 | } 143 | } 144 | 145 | 146 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Tests/Common/Utilities.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Utilities.swift 3 | // PCloudSDKSwift 4 | // 5 | // Created by Todor Pitekov on 03/01/2017 6 | // Copyright © 2017 pCloud LTD. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | @testable import PCloudSDKSwift 12 | 13 | extension Result { 14 | func shallowEquals(_ other: Result) -> Bool { 15 | switch (self, other) { 16 | case (.success(_), .success(_)): return true 17 | case (.failure(_), .failure(_)): return true 18 | default: return false 19 | } 20 | } 21 | } 22 | 23 | extension NSError { 24 | static func void() -> Error { 25 | return NSError(domain: "holy mother of jesus", code: 1, userInfo: nil) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Tests/Tests/OAuthTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OAuthTests.swift 3 | // PCloudSDKSwift 4 | // 5 | // Created by Todor Pitekov on 03/01/2017 6 | // Copyright © 2017 pCloud LTD. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import PCloudSDKSwift 11 | 12 | final class OAuthTests: XCTestCase { 13 | func createRedirectURL(withAppKey appKey: String, fragment: String?) -> URL { 14 | var components = URLComponents() 15 | components.scheme = "pclsdk-w-\(appKey)" 16 | components.host = "oauth2redirect" 17 | components.fragment = fragment 18 | 19 | return components.url! 20 | } 21 | 22 | func testCreatesCorrectRedirectionURL() { 23 | // Given 24 | let appKey = "foo" 25 | 26 | // When 27 | let url = OAuth.createRedirectURL(withAppKey: appKey) 28 | 29 | // Expect 30 | let expectedURL = createRedirectURL(withAppKey: appKey, fragment: nil) 31 | XCTAssert(url == expectedURL, "invalid redirect url, expected \(expectedURL), got \(url)") 32 | } 33 | 34 | func testCreatesCorrectAuthorizationURL() { 35 | // Given 36 | let appKey = "foo" 37 | let redirectURL = createRedirectURL(withAppKey: appKey, fragment: nil) 38 | 39 | // When 40 | let url = URLComponents(string: OAuth.createAuthorizationURL(withAppKey: appKey, redirectURL: redirectURL).absoluteString)! 41 | 42 | // Expect 43 | XCTAssert(url.scheme == "https", "invalid scheme") 44 | XCTAssert(url.host == "e.pcloud.com", "invalid host") 45 | XCTAssert(url.path == "/oauth2/authorize", "invalid path") 46 | 47 | let query = (url.queryItems ?? []).dictionary(transform: { ($0.name, $0.value!) }) 48 | 49 | XCTAssert(query["client_id"] == appKey, "invalid client id") 50 | XCTAssert(query["response_type"] == "token", "invalid response type") 51 | XCTAssert(query["redirect_uri"] == redirectURL.absoluteString, "invalid redirect url") 52 | } 53 | 54 | func testHandleRedirectURLReturnsNilWhenProvidedURLIsNotARedirectURL() { 55 | // Given 56 | let url = URL(string: "https://dummy.com")! 57 | 58 | // When 59 | let result = OAuth.handleRedirectURL(url, appKey: "foo") 60 | 61 | // Expect 62 | XCTAssert(result == nil, "unexpected redirect result") 63 | } 64 | 65 | func testHandleRedirectURLReturnsAccessDeniedErrorWhenURLDoesNotContainFragment() { 66 | // Given 67 | let url = createRedirectURL(withAppKey: "foo", fragment: nil) 68 | 69 | // When 70 | let result = OAuth.handleRedirectURL(url, appKey: "foo")! 71 | 72 | // Expect 73 | validate(result, against: .failure(OAuth.Error.accessDenied)) 74 | } 75 | 76 | func testHandleRedirectURLReturnsTokenOnAccessTokenResponse() { 77 | // Given 78 | let url = createRedirectURL(withAppKey: "foo", fragment: "access_token=thetoken&userid=42&locationid=1&hostname=api.pcloud.com") 79 | 80 | // When 81 | let result = OAuth.handleRedirectURL(url, appKey: "foo")! 82 | 83 | // Expect 84 | validate(result, against: .success(OAuth.User(id: 42, token: "thetoken", serverRegionId: 1, httpAPIHostName: "api.pcloud.com"))) 85 | } 86 | 87 | func testHandleRedirectURLReturnsErrorOnErrorResponse() { 88 | for (code, error) in oAuth2ErrorCodes() { 89 | // Given 90 | let url = createRedirectURL(withAppKey: "foo", fragment: "error=\(code)") 91 | 92 | // When 93 | let result = OAuth.handleRedirectURL(url, appKey: "foo")! 94 | 95 | // Expect 96 | validate(result, against: .failure(error)) 97 | } 98 | } 99 | 100 | func testHandleRedirectURLReturnsUnknownErrorOnUnknownErrorResponse() { 101 | // Given 102 | let url = createRedirectURL(withAppKey: "foo", fragment: "error=krokodil") 103 | 104 | // When 105 | let result = OAuth.handleRedirectURL(url, appKey: "foo")! 106 | 107 | // Expect 108 | validate(result, against: .failure(OAuth.Error.unknown)) 109 | } 110 | 111 | func testAuthorizationFlowPresentsWebView() { 112 | // Given 113 | let view = AuthorizationFlowViewMock() 114 | 115 | // When 116 | OAuth.performAuthorizationFlow(with: view, appKey: "") { _ in } 117 | 118 | // Expect 119 | XCTAssert(view.presentInvoked, "should present web view") 120 | } 121 | 122 | func testAuthorizationFlowDoesNothingWhenInterceptingNonOAuthRedirect() { 123 | // Given 124 | let view = AuthorizationFlowViewMock() 125 | let url = URL(string: "https://google.com")! 126 | 127 | // When 128 | OAuth.performAuthorizationFlow(with: view, appKey: "") { _ in 129 | // Expect 130 | XCTFail("should not invoke completion block") 131 | } 132 | 133 | let result = view.invokeInterceptBlock(url: url) 134 | 135 | // Expect 136 | XCTAssert(!result, "should not handle url") 137 | XCTAssert(!view.dismissInvoked, "should not dismiss view") 138 | } 139 | 140 | func testAuthorizationFlowInvokesCompletionBlockWhenCancelled() { 141 | // Given 142 | let view = AuthorizationFlowViewMock() 143 | let invokeExpectation = expectation(description: "to invoke completion block") 144 | 145 | // When 146 | OAuth.performAuthorizationFlow(with: view, appKey: "") { result in 147 | // Expect 148 | invokeExpectation.fulfill() 149 | 150 | switch result { 151 | case .cancel: break 152 | default: XCTFail("invalid result; expected cancel, got \(result)") 153 | } 154 | } 155 | 156 | view.invokeCancelBlock() 157 | 158 | waitForExpectations(timeout: 1, handler: nil) 159 | } 160 | 161 | func testAuthorizationFlowInvokesCompletionBlockWhenInterceptingOAuthRedirect() { 162 | // Given 163 | let url = createRedirectURL(withAppKey: "foo", fragment: "access_token=thetoken&userid=42&locationid=1&hostname=api.pcloud.com") 164 | let view = AuthorizationFlowViewMock() 165 | let invokeExpectation = expectation(description: "to invoke completion block") 166 | 167 | // When 168 | OAuth.performAuthorizationFlow(with: view, appKey: "foo") { _ in 169 | // Expect 170 | invokeExpectation.fulfill() 171 | } 172 | 173 | let result = view.invokeInterceptBlock(url: url) 174 | 175 | // Expect 176 | XCTAssert(result, "should handle url") 177 | 178 | waitForExpectations(timeout: 1, handler: nil) 179 | } 180 | 181 | func oAuth2ErrorCodes() -> [String: OAuth.Error] { 182 | return [ 183 | "invalid_request": .invalidRequest, 184 | "unauthorized_client": .unauthorizedClient, 185 | "access_denied": .accessDenied, 186 | "unsupported_response_type": .unsupportedResponseType, 187 | "invalid_scope": .invalidScope, 188 | "server_error": .serverError, 189 | "temporarily_unavailable": .temporarilyUnavailable 190 | ] 191 | } 192 | 193 | func validate(_ result: OAuth.Result, against expected: OAuth.Result) { 194 | switch (result, expected) { 195 | case let (.success(lhsPayload), .success(rhsPayload)) where lhsPayload == rhsPayload: break 196 | case let (.failure(lhsError), .failure(rhsError)) where lhsError == rhsError: break 197 | default: XCTFail("invalid authorization result; expected \(expected), got \(result)") 198 | } 199 | } 200 | } 201 | 202 | final class AuthorizationFlowViewMock: OAuthAuthorizationFlowView { 203 | private(set) var presentInvoked = false 204 | private(set) var dismissInvoked = false 205 | 206 | private var interceptBlock: ((URL) -> Bool)? 207 | private var cancelBlock: (() -> Void)? 208 | 209 | func invokeInterceptBlock(url: URL) -> Bool { 210 | return interceptBlock!(url) 211 | } 212 | 213 | func invokeCancelBlock() { 214 | cancelBlock!() 215 | } 216 | 217 | func presentWebView(url: URL, interceptNavigation: @escaping (URL) -> Bool, didCancel: @escaping () -> Void) { 218 | presentInvoked = true 219 | interceptBlock = interceptNavigation 220 | cancelBlock = didCancel 221 | } 222 | 223 | func dismissWebView() { 224 | dismissInvoked = true 225 | } 226 | } 227 | 228 | extension Collection { 229 | func dictionary(transform: (_ element: Iterator.Element) -> (K, V)) -> [K: V] { 230 | var result: [K: V] = [:] 231 | 232 | for element in self { 233 | let tuple = transform(element) 234 | result[tuple.0] = tuple.1 235 | } 236 | 237 | return result 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Tests/Tests/TaskTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TaskTests.swift 3 | // PCloudSDKSwift 4 | // 5 | // Created by Todor Pitekov on 03/01/2017 6 | // Copyright © 2017 pCloud LTD. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import PCloudSDKSwift 11 | 12 | final class CallTaskTests: XCTestCase { 13 | var operation: CallOperationMock! 14 | var task: CallTask! 15 | 16 | override func setUp() { 17 | super.setUp() 18 | 19 | operation = CallOperationMock() 20 | task = createTask(parser: { _ in .success(()) }) 21 | } 22 | 23 | func createTask(parser: @escaping ([String: Any]) throws -> Result>) -> CallTask { 24 | return CallTask(operation: operation, responseParser: parser) 25 | } 26 | 27 | func testTaskCompletesWithSuccessWhenOperationCompletesWithSuccessAndParserDoesNotFail() { 28 | let invokeExpectation = expectation(description: "to invoke completion block") 29 | 30 | // Given 31 | task.addCompletionBlock { result in 32 | // Expect 33 | invokeExpectation.fulfill() 34 | 35 | XCTAssert(Thread.isMainThread, "expected block to be invoked on the main thread") 36 | XCTAssert(result.isSuccess, "incorrect task result; expected success; got \(result)") 37 | } 38 | 39 | // When 40 | operation.invokeCompletion(response: .success([:])) 41 | 42 | waitForExpectations(timeout: 1, handler: nil) 43 | } 44 | 45 | func testTaskCompletesWithFailureWhenOperationCompletesWithFailure() { 46 | let invokeExpectation = expectation(description: "to invoke completion block") 47 | 48 | // Given 49 | task.addCompletionBlock { result in 50 | // Expect 51 | invokeExpectation.fulfill() 52 | 53 | XCTAssert(Thread.isMainThread, "expected block to be invoked on the main thread") 54 | XCTAssert(result.isFailure, "incorrect task result; expected failure; got \(result)") 55 | } 56 | 57 | // When 58 | operation.invokeCompletion(response: .failure(.clientError(NSError.void()))) 59 | 60 | waitForExpectations(timeout: 1, handler: nil) 61 | } 62 | 63 | func testTaskCompletesWithFailureWhenOperationCompletesWithSuccessAndParserFails() { 64 | let invokeExpectation = expectation(description: "to invoke completion block") 65 | 66 | // Given 67 | task = createTask(parser: { _ in 68 | throw NSError.void() 69 | }) 70 | 71 | task.addCompletionBlock { result in 72 | // Expect 73 | invokeExpectation.fulfill() 74 | 75 | XCTAssert(Thread.isMainThread, "expected block to be invoked on the main thread") 76 | XCTAssert(result.isFailure, "incorrect task result; expected failure; got \(result)") 77 | } 78 | 79 | // When 80 | operation.invokeCompletion(response: .failure(.clientError(NSError.void()))) 81 | 82 | waitForExpectations(timeout: 1, handler: nil) 83 | } 84 | 85 | func testStartsOperationOnStart() { 86 | // When 87 | task.start() 88 | 89 | // Expect 90 | XCTAssert(operation.startInvoked, "operation should have been started") 91 | } 92 | 93 | func testCancelsOperationOnCancel() { 94 | // When 95 | task.cancel() 96 | 97 | // Expect 98 | XCTAssert(operation.cancelInvoked, "operation should have been cancelled") 99 | XCTAssert(task.isCancelled, "task should have its cancelled status updated") 100 | } 101 | } 102 | 103 | 104 | final class UploadTaskTests: XCTestCase { 105 | var operation: UploadOperationMock! 106 | var task: UploadTask! 107 | 108 | override func setUp() { 109 | super.setUp() 110 | 111 | operation = UploadOperationMock() 112 | task = createTask(parser: { _ in .success(()) }) 113 | } 114 | 115 | func createTask(parser: @escaping ([String: Any]) throws -> Result>) -> UploadTask { 116 | return UploadTask(operation: operation, responseParser: parser) 117 | } 118 | 119 | func testStartsOperationOnStart() { 120 | // When 121 | task.start() 122 | 123 | // Expect 124 | XCTAssert(operation.startInvoked, "operation should have been started") 125 | } 126 | 127 | func testCancelsOperationOnCancel() { 128 | // When 129 | task.cancel() 130 | 131 | // Expect 132 | XCTAssert(operation.cancelInvoked, "operation should have been cancelled") 133 | XCTAssert(task.isCancelled, "task should have its cancelled status updated") 134 | } 135 | 136 | func testCompletesWithSuccessWhenOperationCompletesWithSuccessAndParserDoesNotFail() { 137 | let invokeExpectation = expectation(description: "to invoke completion block") 138 | 139 | // Given 140 | task.addCompletionBlock { result in 141 | // Expect 142 | invokeExpectation.fulfill() 143 | 144 | XCTAssert(Thread.isMainThread, "expected block to be invoked on the main thread") 145 | XCTAssert(result.isSuccess, "incorrect task result; expected success; got \(result)") 146 | } 147 | 148 | // When 149 | operation.invokeCompletion(response: .success([:])) 150 | 151 | waitForExpectations(timeout: 1, handler: nil) 152 | } 153 | 154 | func testCompletesWithFailureWhenOperationCompletesWithFailure() { 155 | let invokeExpectation = expectation(description: "to invoke completion block") 156 | 157 | // Given 158 | task.addCompletionBlock { result in 159 | // Expect 160 | invokeExpectation.fulfill() 161 | 162 | XCTAssert(Thread.isMainThread, "expected block to be invoked on the main thread") 163 | XCTAssert(result.isFailure, "incorrect task result; expected failure; got \(result)") 164 | } 165 | 166 | // When 167 | operation.invokeCompletion(response: .failure(.clientError(NSError.void()))) 168 | 169 | waitForExpectations(timeout: 1, handler: nil) 170 | } 171 | 172 | func testCompletesWithFailureWhenOperationCompletesWithSuccessAndParserFails() { 173 | let invokeExpectation = expectation(description: "to invoke completion block") 174 | 175 | // Given 176 | task = createTask(parser: { _ in 177 | throw NSError.void() 178 | }) 179 | 180 | task.addCompletionBlock { result in 181 | // Expect 182 | invokeExpectation.fulfill() 183 | 184 | XCTAssert(Thread.isMainThread, "expected block to be invoked on the main thread") 185 | XCTAssert(result.isFailure, "incorrect task result; expected failure; got \(result)") 186 | } 187 | 188 | // When 189 | operation.invokeCompletion(response: .failure(.clientError(NSError.void()))) 190 | 191 | waitForExpectations(timeout: 1, handler: nil) 192 | } 193 | 194 | func testInvokesProgressBlockWhenOperationProgresses() { 195 | let invokeExpectation = expectation(description: "to invoke progress block") 196 | 197 | // Given 198 | task.addProgressBlock { _,_ in 199 | // Expect 200 | invokeExpectation.fulfill() 201 | XCTAssert(Thread.isMainThread, "expected block to be invoked on the main thread") 202 | } 203 | 204 | // When 205 | operation.invokeProgress(sent: 0, total: 0) 206 | 207 | waitForExpectations(timeout: 1, handler: nil) 208 | } 209 | } 210 | 211 | 212 | final class DownloadTaskTest: XCTestCase { 213 | var operation: DownloadOperationMock! 214 | var task: DownloadTask! 215 | 216 | override func setUp() { 217 | super.setUp() 218 | 219 | operation = DownloadOperationMock() 220 | task = DownloadTask(operation: operation) 221 | } 222 | 223 | func testStartsOperationOnStart() { 224 | // When 225 | task.start() 226 | 227 | // Expect 228 | XCTAssert(operation.startInvoked, "operation should have been started") 229 | } 230 | 231 | func testCancelsOperationOnCancel() { 232 | // When 233 | task.cancel() 234 | 235 | // Expect 236 | XCTAssert(operation.cancelInvoked, "operation should have been cancelled") 237 | } 238 | 239 | func testCompletesWithSuccessWhenDownloadSucceeds() { 240 | let invokeExpectation = expectation(description: "to call completion block") 241 | 242 | task.addCompletionBlock { result in 243 | // Expect 244 | invokeExpectation.fulfill() 245 | XCTAssert(Thread.isMainThread, "block should be called on the main thread") 246 | XCTAssert(result.isSuccess, "download task should succeed when operation succeeds") 247 | } 248 | 249 | // When 250 | task.start() 251 | operation.invokeCompletion(response: .success(URL(fileURLWithPath: "/dev/null"))) 252 | 253 | waitForExpectations(timeout: 1, handler: nil) 254 | } 255 | 256 | func testCompletesWithFailureWhenDownloadFails() { 257 | let invokeExpectation = expectation(description: "to invoke completion") 258 | 259 | task.addCompletionBlock { result in 260 | // Expect 261 | invokeExpectation.fulfill() 262 | XCTAssert(Thread.isMainThread, "block should be called on the main thread") 263 | XCTAssert(result.isFailure, "invalid download result; expected failure, got \(result)") 264 | } 265 | 266 | // When 267 | task.start() 268 | operation.invokeCompletion(response: .failure(.clientError(NSError.void()))) 269 | 270 | waitForExpectations(timeout: 1, handler: nil) 271 | } 272 | 273 | func testInvokesProgressBlockWhenDownloadProgresses() { 274 | let invokeExpectation = expectation(description: "to invoke progress block") 275 | 276 | task.addProgressBlock { _,_ in 277 | // Expect 278 | invokeExpectation.fulfill() 279 | } 280 | 281 | // When 282 | task.start() 283 | operation.invokeProgress(written: 0, total: 0) 284 | 285 | waitForExpectations(timeout: 1, handler: nil) 286 | } 287 | } 288 | 289 | 290 | struct VoidAPIMethod: PCloudAPIMethod { 291 | var requiresAuthentication: Bool = false 292 | 293 | func createCommand() -> Call.Command { 294 | return Call.Command(name: "nop", parameters: []) 295 | } 296 | 297 | func createResponseParser() -> ([String : Any]) throws -> Result> { 298 | return { _ in .success(()) } 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Tests/Tests/URLSessionEventHubTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLSessionEventHubTests.swift 3 | // PCloudSDKSwift 4 | // 5 | // Created by Todor Pitekov on 03/01/2017 6 | // Copyright © 2017 pCloud LTD. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import PCloudSDKSwift 11 | 12 | 13 | final class URLSessionEventHubTests: XCTestCase { 14 | var session: URLSession! 15 | var hub: URLSessionEventHub! 16 | 17 | override func setUp() { 18 | super.setUp() 19 | session = URLSession(configuration: URLSessionConfiguration.default) 20 | hub = URLSessionEventHub() 21 | } 22 | 23 | func createDataTask() -> URLSessionDataTask { 24 | return session.dataTask(with: URL(string: "http://google.com")!) 25 | } 26 | 27 | func createDownloadTask() -> URLSessionDownloadTask { 28 | return session.downloadTask(with: URL(string: "http://google.com")!) 29 | } 30 | 31 | func testNotifiesObserverOnDidSendBodyData() { 32 | // Given 33 | let spy = EventHubSpy() 34 | let task = createDataTask() 35 | hub.setObserver(spy, for: task) 36 | 37 | // When 38 | hub.urlSession(session, task: task, didSendBodyData: 0, totalBytesSent: 0, totalBytesExpectedToSend: 0) 39 | 40 | // Expect 41 | XCTAssert(spy.didSendBodyDataCalled, "did not invoke callback") 42 | } 43 | 44 | func testNotifiesObserverOnDidCompleteWithError() { 45 | // Given 46 | let spy = EventHubSpy() 47 | let task = createDataTask() 48 | hub.setObserver(spy, for: task) 49 | 50 | // When 51 | hub.urlSession(session, task: task, didCompleteWithError: nil) 52 | 53 | // Expect 54 | XCTAssert(spy.didCompleteWithErrorCalled, "did not invoke callback") 55 | } 56 | 57 | func testNotifiesObserverOnDidReceiveData() { 58 | // Given 59 | let spy = EventHubSpy() 60 | let task = createDataTask() 61 | hub.setObserver(spy, for: task) 62 | 63 | // When 64 | hub.urlSession(session, dataTask: task, didReceive: Data()) 65 | 66 | // Expect 67 | XCTAssert(spy.didReceiveDataCalled, "did not invoke callback") 68 | } 69 | 70 | func testNotifiesObserverOnDidFinishDownloading() { 71 | // Given 72 | let spy = EventHubSpy() 73 | let task = createDownloadTask() 74 | hub.setObserver(spy, for: task) 75 | 76 | // When 77 | hub.urlSession(session, downloadTask: task, didFinishDownloadingTo: URL(fileURLWithPath: "/")) 78 | 79 | // Expect 80 | XCTAssert(spy.didFinishDownloadingCalled, "did not invoke callback") 81 | } 82 | 83 | func testNotifiesObserverOnDidWriteData() { 84 | // Given 85 | let spy = EventHubSpy() 86 | let task = createDownloadTask() 87 | hub.setObserver(spy, for: task) 88 | 89 | // When 90 | hub.urlSession(session, downloadTask: task, didWriteData: 0, totalBytesWritten: 0, totalBytesExpectedToWrite: 0) 91 | 92 | // Expect 93 | XCTAssert(spy.didWriteDataCalled, "did not invoke callback") 94 | } 95 | 96 | func testDoesNotNotifyObserverAfterObserverIsRemoved() { 97 | // Given 98 | let spy = EventHubSpy() 99 | let task = createDataTask() 100 | hub.setObserver(spy, for: task) 101 | 102 | // When 103 | hub.removeObserver(for: task) 104 | hub.urlSession(session, dataTask: task, didReceive: Data()) 105 | 106 | // Expect 107 | XCTAssert(!spy.didReceiveDataCalled, "should not invoke callback") 108 | } 109 | 110 | func testRemovesObserverOnDidCompleteWithError() { 111 | // Given 112 | let spy = EventHubSpy() 113 | let task = createDataTask() 114 | hub.setObserver(spy, for: task) 115 | 116 | // When 117 | hub.urlSession(session, task: task, didCompleteWithError: nil) 118 | hub.urlSession(session, dataTask: task, didReceive: Data()) 119 | 120 | // Expect 121 | XCTAssert(!spy.didReceiveDataCalled, "should not invoke callback") 122 | } 123 | } 124 | 125 | 126 | 127 | 128 | final class EventHubSpy { 129 | private(set) var didSendBodyDataCalled = false 130 | private(set) var didCompleteWithErrorCalled = false 131 | private(set) var didReceiveDataCalled = false 132 | private(set) var didFinishDownloadingCalled = false 133 | private(set) var didWriteDataCalled = false 134 | } 135 | 136 | extension EventHubSpy: URLSessionObserver { 137 | func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { 138 | didSendBodyDataCalled = true 139 | } 140 | 141 | func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { 142 | didCompleteWithErrorCalled = true 143 | } 144 | 145 | func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { 146 | didReceiveDataCalled = true 147 | } 148 | 149 | func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { 150 | didFinishDownloadingCalled = true 151 | } 152 | 153 | func urlSession(_ session: URLSession, 154 | downloadTask: URLSessionDownloadTask, 155 | didWriteData bytesWritten: Int64, 156 | totalBytesWritten: Int64, 157 | totalBytesExpectedToWrite: Int64) { 158 | didWriteDataCalled = true 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /PCloudSDKSwift/Tests/Tests/URLSessionTaskBuilderTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLSessionTaskBuilderTests.swift 3 | // SDK Tests 4 | // 5 | // Created by Todor Pitekov on 5.03.20. 6 | // Copyright © 2020 pCloud LTD. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import PCloudSDKSwift 11 | 12 | final class URLSessionTaskBuilderTests: XCTestCase { 13 | private var session: URLSession! 14 | 15 | override func setUp() { 16 | super.setUp() 17 | 18 | session = URLSession(configuration: .ephemeral) 19 | } 20 | 21 | override func tearDown() { 22 | session = nil 23 | 24 | super.tearDown() 25 | } 26 | 27 | func testBuilderPercentEncodesQueryForDataTask() { 28 | // Given 29 | let method = PCloudAPI.CreateFolder(name: "my fancy&new+folder@", parentFolderId: 0) 30 | 31 | // When 32 | let request = createDataTask(with: method).originalRequest! 33 | 34 | // Expect 35 | let queryString = String(bytes: request.httpBody!, encoding: .utf8)! 36 | let query = createDictionary(fromQueryString: queryString) 37 | 38 | XCTAssertEqual(query["name"], "my%20fancy%26new%2Bfolder%40", "data task query is not percent encoded or percent encoding is incorrect") 39 | } 40 | 41 | func testBuilderPercentEncodesQueryForUploadTask() { 42 | // Given 43 | let method = PCloudAPI.UploadFile(name: "my fancy&new+file@.txt", parentFolderId: 0, modificationDate: nil) 44 | 45 | // When 46 | let url = createUploadTask(with: method).originalRequest!.url! 47 | 48 | // Expect 49 | guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { 50 | XCTFail("incorrect upload task url: \"\(url)\"") 51 | return 52 | } 53 | 54 | let queryString = components.percentEncodedQuery! 55 | let query = createDictionary(fromQueryString: queryString) 56 | 57 | XCTAssertEqual(query["filename"], "my%20fancy%26new%2Bfile%40.txt", "upload task query is not percent encoded or percent encoding is incorrect") 58 | } 59 | 60 | private func createUploadTask(with method: Method) -> URLSessionUploadTask { 61 | let command = method.createCommand() 62 | let body = Upload.Request.Body.data("texty text".data(using: .utf8)!) 63 | let request = Upload.Request(command: command, body: body, hostName: "api.pcloud.com") 64 | return URLSessionTaskBuilder.createUploadTask(with: request, session: session, scheme: .http) 65 | } 66 | 67 | private func createDataTask(with method: Method) -> URLSessionDataTask { 68 | let command = method.createCommand() 69 | let request = Call.Request(command: command, hostName: "api.pcloud.com") 70 | return URLSessionTaskBuilder.createDataTask(with: request, session: session, scheme: .http) 71 | } 72 | 73 | private func createDictionary(fromQueryString query: String) -> [String: String?] { 74 | let pairs = query.components(separatedBy: "&") 75 | 76 | let entries: [(String, String?)] = pairs.map { 77 | let components = $0.components(separatedBy: "=") 78 | return (components.first!, components.last) 79 | } 80 | 81 | return Dictionary(uniqueKeysWithValues: entries) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "PCloudSDKSwift", 7 | platforms: [ 8 | // Only support the iOS platform. As of this writing (April 2021), I couldn't find a way to exclude the .xib file in the "macOS" directory 9 | // from the iOS version of the library but still keep it for the macOS version. You can exclude resources on a per-target basis, but each 10 | // target must support all platforms declared here. Until SPM makes this possible, we are only going to support one of the platforms. 11 | .iOS(.v9) 12 | ], 13 | products: [ 14 | .library(name: "PCloudSDKSwift", targets: ["PCloudSDKSwift"]), 15 | ], 16 | targets: [ 17 | .target(name: "PCloudSDKSwift", dependencies: [], path: "PCloudSDKSwift/Source", exclude: ["macOS", "iOS/Info.plist"]), 18 | .testTarget(name: "PCloudSDKSwiftTests", dependencies: ["PCloudSDKSwift"], path: "PCloudSDKSwift/Tests", exclude: ["Info.plist"]), 19 | ] 20 | ) 21 | -------------------------------------------------------------------------------- /docs/badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | documentation 17 | 18 | 19 | documentation 20 | 21 | 22 | 83% 23 | 24 | 25 | 83% 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /docs/css/highlight.css: -------------------------------------------------------------------------------- 1 | /* Credit to https://gist.github.com/wataru420/2048287 */ 2 | .highlight { 3 | /* Comment */ 4 | /* Error */ 5 | /* Keyword */ 6 | /* Operator */ 7 | /* Comment.Multiline */ 8 | /* Comment.Preproc */ 9 | /* Comment.Single */ 10 | /* Comment.Special */ 11 | /* Generic.Deleted */ 12 | /* Generic.Deleted.Specific */ 13 | /* Generic.Emph */ 14 | /* Generic.Error */ 15 | /* Generic.Heading */ 16 | /* Generic.Inserted */ 17 | /* Generic.Inserted.Specific */ 18 | /* Generic.Output */ 19 | /* Generic.Prompt */ 20 | /* Generic.Strong */ 21 | /* Generic.Subheading */ 22 | /* Generic.Traceback */ 23 | /* Keyword.Constant */ 24 | /* Keyword.Declaration */ 25 | /* Keyword.Pseudo */ 26 | /* Keyword.Reserved */ 27 | /* Keyword.Type */ 28 | /* Literal.Number */ 29 | /* Literal.String */ 30 | /* Name.Attribute */ 31 | /* Name.Builtin */ 32 | /* Name.Class */ 33 | /* Name.Constant */ 34 | /* Name.Entity */ 35 | /* Name.Exception */ 36 | /* Name.Function */ 37 | /* Name.Namespace */ 38 | /* Name.Tag */ 39 | /* Name.Variable */ 40 | /* Operator.Word */ 41 | /* Text.Whitespace */ 42 | /* Literal.Number.Float */ 43 | /* Literal.Number.Hex */ 44 | /* Literal.Number.Integer */ 45 | /* Literal.Number.Oct */ 46 | /* Literal.String.Backtick */ 47 | /* Literal.String.Char */ 48 | /* Literal.String.Doc */ 49 | /* Literal.String.Double */ 50 | /* Literal.String.Escape */ 51 | /* Literal.String.Heredoc */ 52 | /* Literal.String.Interpol */ 53 | /* Literal.String.Other */ 54 | /* Literal.String.Regex */ 55 | /* Literal.String.Single */ 56 | /* Literal.String.Symbol */ 57 | /* Name.Builtin.Pseudo */ 58 | /* Name.Variable.Class */ 59 | /* Name.Variable.Global */ 60 | /* Name.Variable.Instance */ 61 | /* Literal.Number.Integer.Long */ } 62 | .highlight .c { 63 | color: #999988; 64 | font-style: italic; } 65 | .highlight .err { 66 | color: #a61717; 67 | background-color: #e3d2d2; } 68 | .highlight .k { 69 | color: #000000; 70 | font-weight: bold; } 71 | .highlight .o { 72 | color: #000000; 73 | font-weight: bold; } 74 | .highlight .cm { 75 | color: #999988; 76 | font-style: italic; } 77 | .highlight .cp { 78 | color: #999999; 79 | font-weight: bold; } 80 | .highlight .c1 { 81 | color: #999988; 82 | font-style: italic; } 83 | .highlight .cs { 84 | color: #999999; 85 | font-weight: bold; 86 | font-style: italic; } 87 | .highlight .gd { 88 | color: #000000; 89 | background-color: #ffdddd; } 90 | .highlight .gd .x { 91 | color: #000000; 92 | background-color: #ffaaaa; } 93 | .highlight .ge { 94 | color: #000000; 95 | font-style: italic; } 96 | .highlight .gr { 97 | color: #aa0000; } 98 | .highlight .gh { 99 | color: #999999; } 100 | .highlight .gi { 101 | color: #000000; 102 | background-color: #ddffdd; } 103 | .highlight .gi .x { 104 | color: #000000; 105 | background-color: #aaffaa; } 106 | .highlight .go { 107 | color: #888888; } 108 | .highlight .gp { 109 | color: #555555; } 110 | .highlight .gs { 111 | font-weight: bold; } 112 | .highlight .gu { 113 | color: #aaaaaa; } 114 | .highlight .gt { 115 | color: #aa0000; } 116 | .highlight .kc { 117 | color: #000000; 118 | font-weight: bold; } 119 | .highlight .kd { 120 | color: #000000; 121 | font-weight: bold; } 122 | .highlight .kp { 123 | color: #000000; 124 | font-weight: bold; } 125 | .highlight .kr { 126 | color: #000000; 127 | font-weight: bold; } 128 | .highlight .kt { 129 | color: #445588; } 130 | .highlight .m { 131 | color: #009999; } 132 | .highlight .s { 133 | color: #d14; } 134 | .highlight .na { 135 | color: #008080; } 136 | .highlight .nb { 137 | color: #0086B3; } 138 | .highlight .nc { 139 | color: #445588; 140 | font-weight: bold; } 141 | .highlight .no { 142 | color: #008080; } 143 | .highlight .ni { 144 | color: #800080; } 145 | .highlight .ne { 146 | color: #990000; 147 | font-weight: bold; } 148 | .highlight .nf { 149 | color: #990000; } 150 | .highlight .nn { 151 | color: #555555; } 152 | .highlight .nt { 153 | color: #000080; } 154 | .highlight .nv { 155 | color: #008080; } 156 | .highlight .ow { 157 | color: #000000; 158 | font-weight: bold; } 159 | .highlight .w { 160 | color: #bbbbbb; } 161 | .highlight .mf { 162 | color: #009999; } 163 | .highlight .mh { 164 | color: #009999; } 165 | .highlight .mi { 166 | color: #009999; } 167 | .highlight .mo { 168 | color: #009999; } 169 | .highlight .sb { 170 | color: #d14; } 171 | .highlight .sc { 172 | color: #d14; } 173 | .highlight .sd { 174 | color: #d14; } 175 | .highlight .s2 { 176 | color: #d14; } 177 | .highlight .se { 178 | color: #d14; } 179 | .highlight .sh { 180 | color: #d14; } 181 | .highlight .si { 182 | color: #d14; } 183 | .highlight .sx { 184 | color: #d14; } 185 | .highlight .sr { 186 | color: #009926; } 187 | .highlight .s1 { 188 | color: #d14; } 189 | .highlight .ss { 190 | color: #990073; } 191 | .highlight .bp { 192 | color: #999999; } 193 | .highlight .vc { 194 | color: #008080; } 195 | .highlight .vg { 196 | color: #008080; } 197 | .highlight .vi { 198 | color: #008080; } 199 | .highlight .il { 200 | color: #009999; } 201 | -------------------------------------------------------------------------------- /docs/css/jazzy.css: -------------------------------------------------------------------------------- 1 | html, body, div, span, h1, h3, h4, p, a, code, em, img, ul, li, table, tbody, tr, td { 2 | background: transparent; 3 | border: 0; 4 | margin: 0; 5 | outline: 0; 6 | padding: 0; 7 | vertical-align: baseline; } 8 | 9 | body { 10 | background-color: #f2f2f2; 11 | font-family: Helvetica, freesans, Arial, sans-serif; 12 | font-size: 14px; 13 | -webkit-font-smoothing: subpixel-antialiased; 14 | word-wrap: break-word; } 15 | 16 | h1, h2, h3 { 17 | margin-top: 0.8em; 18 | margin-bottom: 0.3em; 19 | font-weight: 100; 20 | color: black; } 21 | 22 | h1 { 23 | font-size: 2.5em; } 24 | 25 | h2 { 26 | font-size: 2em; 27 | border-bottom: 1px solid #e2e2e2; } 28 | 29 | h4 { 30 | font-size: 13px; 31 | line-height: 1.5; 32 | margin-top: 21px; } 33 | 34 | h5 { 35 | font-size: 1.1em; } 36 | 37 | h6 { 38 | font-size: 1.1em; 39 | color: #777; } 40 | 41 | .section-name { 42 | color: gray; 43 | display: block; 44 | font-family: Helvetica; 45 | font-size: 22px; 46 | font-weight: 100; 47 | margin-bottom: 15px; } 48 | 49 | pre, code { 50 | font: 0.95em Menlo, monospace; 51 | color: #777; 52 | word-wrap: normal; } 53 | 54 | p code, li code { 55 | background-color: #eee; 56 | padding: 2px 4px; 57 | border-radius: 4px; } 58 | 59 | a { 60 | color: #0088cc; 61 | text-decoration: none; } 62 | 63 | ul { 64 | padding-left: 15px; } 65 | 66 | li { 67 | line-height: 1.8em; } 68 | 69 | img { 70 | max-width: 100%; } 71 | 72 | blockquote { 73 | margin-left: 0; 74 | padding: 0 10px; 75 | border-left: 4px solid #ccc; } 76 | 77 | .content-wrapper { 78 | margin: 0 auto; 79 | width: 980px; } 80 | 81 | header { 82 | font-size: 0.85em; 83 | line-height: 26px; 84 | background-color: #414141; 85 | position: fixed; 86 | width: 100%; 87 | z-index: 2; } 88 | header img { 89 | padding-right: 6px; 90 | vertical-align: -4px; 91 | height: 16px; } 92 | header a { 93 | color: #fff; } 94 | header p { 95 | float: left; 96 | color: #999; } 97 | header .header-right { 98 | float: right; 99 | margin-left: 16px; } 100 | 101 | #breadcrumbs { 102 | background-color: #f2f2f2; 103 | height: 27px; 104 | padding-top: 17px; 105 | position: fixed; 106 | width: 100%; 107 | z-index: 2; 108 | margin-top: 26px; } 109 | #breadcrumbs #carat { 110 | height: 10px; 111 | margin: 0 5px; } 112 | 113 | .sidebar { 114 | background-color: #f9f9f9; 115 | border: 1px solid #e2e2e2; 116 | overflow-y: auto; 117 | overflow-x: hidden; 118 | position: fixed; 119 | top: 70px; 120 | bottom: 0; 121 | width: 230px; 122 | word-wrap: normal; } 123 | 124 | .nav-groups { 125 | list-style-type: none; 126 | background: #fff; 127 | padding-left: 0; } 128 | 129 | .nav-group-name { 130 | border-bottom: 1px solid #e2e2e2; 131 | font-size: 1.1em; 132 | font-weight: 100; 133 | padding: 15px 0 15px 20px; } 134 | .nav-group-name > a { 135 | color: #333; } 136 | 137 | .nav-group-tasks { 138 | margin-top: 5px; } 139 | 140 | .nav-group-task { 141 | font-size: 0.9em; 142 | list-style-type: none; 143 | white-space: nowrap; } 144 | .nav-group-task a { 145 | color: #888; } 146 | 147 | .main-content { 148 | background-color: #fff; 149 | border: 1px solid #e2e2e2; 150 | margin-left: 246px; 151 | position: absolute; 152 | overflow: hidden; 153 | padding-bottom: 20px; 154 | top: 70px; 155 | width: 734px; } 156 | .main-content p, .main-content a, .main-content code, .main-content em, .main-content ul, .main-content table, .main-content blockquote { 157 | margin-bottom: 1em; } 158 | .main-content p { 159 | line-height: 1.8em; } 160 | .main-content section .section:first-child { 161 | margin-top: 0; 162 | padding-top: 0; } 163 | .main-content section .task-group-section .task-group:first-of-type { 164 | padding-top: 10px; } 165 | .main-content section .task-group-section .task-group:first-of-type .section-name { 166 | padding-top: 15px; } 167 | .main-content section .heading:before { 168 | content: ""; 169 | display: block; 170 | padding-top: 70px; 171 | margin: -70px 0 0; } 172 | .main-content .section-name p { 173 | margin-bottom: inherit; 174 | line-height: inherit; } 175 | .main-content .section-name code { 176 | background-color: inherit; 177 | padding: inherit; 178 | color: inherit; } 179 | 180 | .section { 181 | padding: 0 25px; } 182 | 183 | .highlight { 184 | background-color: #eee; 185 | padding: 10px 12px; 186 | border: 1px solid #e2e2e2; 187 | border-radius: 4px; 188 | overflow-x: auto; } 189 | 190 | .declaration .highlight { 191 | overflow-x: initial; 192 | padding: 0 40px 40px 0; 193 | margin-bottom: -25px; 194 | background-color: transparent; 195 | border: none; } 196 | 197 | .section-name { 198 | margin: 0; 199 | margin-left: 18px; } 200 | 201 | .task-group-section { 202 | padding-left: 6px; 203 | border-top: 1px solid #e2e2e2; } 204 | 205 | .task-group { 206 | padding-top: 0px; } 207 | 208 | .task-name-container a[name]:before { 209 | content: ""; 210 | display: block; 211 | padding-top: 70px; 212 | margin: -70px 0 0; } 213 | 214 | .section-name-container { 215 | position: relative; 216 | display: inline-block; } 217 | .section-name-container .section-name-link { 218 | position: absolute; 219 | top: 0; 220 | left: 0; 221 | bottom: 0; 222 | right: 0; 223 | margin-bottom: 0; } 224 | .section-name-container .section-name { 225 | position: relative; 226 | pointer-events: none; 227 | z-index: 1; } 228 | .section-name-container .section-name a { 229 | pointer-events: auto; } 230 | 231 | .item { 232 | padding-top: 8px; 233 | width: 100%; 234 | list-style-type: none; } 235 | .item a[name]:before { 236 | content: ""; 237 | display: block; 238 | padding-top: 70px; 239 | margin: -70px 0 0; } 240 | .item code { 241 | background-color: transparent; 242 | padding: 0; } 243 | .item .token, .item .direct-link { 244 | padding-left: 3px; 245 | margin-left: 15px; 246 | font-size: 11.9px; 247 | transition: all 300ms; } 248 | .item .token-open { 249 | margin-left: 0px; } 250 | .item .discouraged { 251 | text-decoration: line-through; } 252 | .item .declaration-note { 253 | font-size: .85em; 254 | color: gray; 255 | font-style: italic; } 256 | 257 | .pointer-container { 258 | border-bottom: 1px solid #e2e2e2; 259 | left: -23px; 260 | padding-bottom: 13px; 261 | position: relative; 262 | width: 110%; } 263 | 264 | .pointer { 265 | background: #f9f9f9; 266 | border-left: 1px solid #e2e2e2; 267 | border-top: 1px solid #e2e2e2; 268 | height: 12px; 269 | left: 21px; 270 | top: -7px; 271 | -webkit-transform: rotate(45deg); 272 | -moz-transform: rotate(45deg); 273 | -o-transform: rotate(45deg); 274 | transform: rotate(45deg); 275 | position: absolute; 276 | width: 12px; } 277 | 278 | .height-container { 279 | display: none; 280 | left: -25px; 281 | padding: 0 25px; 282 | position: relative; 283 | width: 100%; 284 | overflow: hidden; } 285 | .height-container .section { 286 | background: #f9f9f9; 287 | border-bottom: 1px solid #e2e2e2; 288 | left: -25px; 289 | position: relative; 290 | width: 100%; 291 | padding-top: 10px; 292 | padding-bottom: 5px; } 293 | 294 | .aside, .language { 295 | padding: 6px 12px; 296 | margin: 12px 0; 297 | border-left: 5px solid #dddddd; 298 | overflow-y: hidden; } 299 | .aside .aside-title, .language .aside-title { 300 | font-size: 9px; 301 | letter-spacing: 2px; 302 | text-transform: uppercase; 303 | padding-bottom: 0; 304 | margin: 0; 305 | color: #aaa; 306 | -webkit-user-select: none; } 307 | .aside p:last-child, .language p:last-child { 308 | margin-bottom: 0; } 309 | 310 | .language { 311 | border-left: 5px solid #cde9f4; } 312 | .language .aside-title { 313 | color: #4b8afb; } 314 | 315 | .aside-warning, .aside-deprecated, .aside-unavailable { 316 | border-left: 5px solid #ff6666; } 317 | .aside-warning .aside-title, .aside-deprecated .aside-title, .aside-unavailable .aside-title { 318 | color: #ff0000; } 319 | 320 | .graybox { 321 | border-collapse: collapse; 322 | width: 100%; } 323 | .graybox p { 324 | margin: 0; 325 | word-break: break-word; 326 | min-width: 50px; } 327 | .graybox td { 328 | border: 1px solid #e2e2e2; 329 | padding: 5px 25px 5px 10px; 330 | vertical-align: middle; } 331 | .graybox tr td:first-of-type { 332 | text-align: right; 333 | padding: 7px; 334 | vertical-align: top; 335 | word-break: normal; 336 | width: 40px; } 337 | 338 | .slightly-smaller { 339 | font-size: 0.9em; } 340 | 341 | #footer { 342 | position: relative; 343 | top: 10px; 344 | bottom: 0px; 345 | margin-left: 25px; } 346 | #footer p { 347 | margin: 0; 348 | color: #aaa; 349 | font-size: 0.8em; } 350 | 351 | html.dash header, html.dash #breadcrumbs, html.dash .sidebar { 352 | display: none; } 353 | 354 | html.dash .main-content { 355 | width: 980px; 356 | margin-left: 0; 357 | border: none; 358 | width: 100%; 359 | top: 0; 360 | padding-bottom: 0; } 361 | 362 | html.dash .height-container { 363 | display: block; } 364 | 365 | html.dash .item .token { 366 | margin-left: 0; } 367 | 368 | html.dash .content-wrapper { 369 | width: auto; } 370 | 371 | html.dash #footer { 372 | position: static; } 373 | -------------------------------------------------------------------------------- /docs/docsets/PCloudSDKSwift.docset/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleIdentifier 6 | com.jazzy.pcloudsdkswift 7 | CFBundleName 8 | PCloudSDKSwift 9 | DocSetPlatformFamily 10 | pcloudsdkswift 11 | isDashDocset 12 | 13 | dashIndexFilePath 14 | index.html 15 | isJavaScriptEnabled 16 | 17 | DashDocSetFamily 18 | dashtoc 19 | 20 | 21 | -------------------------------------------------------------------------------- /docs/docsets/PCloudSDKSwift.docset/Contents/Resources/Documents/css/highlight.css: -------------------------------------------------------------------------------- 1 | /* Credit to https://gist.github.com/wataru420/2048287 */ 2 | .highlight { 3 | /* Comment */ 4 | /* Error */ 5 | /* Keyword */ 6 | /* Operator */ 7 | /* Comment.Multiline */ 8 | /* Comment.Preproc */ 9 | /* Comment.Single */ 10 | /* Comment.Special */ 11 | /* Generic.Deleted */ 12 | /* Generic.Deleted.Specific */ 13 | /* Generic.Emph */ 14 | /* Generic.Error */ 15 | /* Generic.Heading */ 16 | /* Generic.Inserted */ 17 | /* Generic.Inserted.Specific */ 18 | /* Generic.Output */ 19 | /* Generic.Prompt */ 20 | /* Generic.Strong */ 21 | /* Generic.Subheading */ 22 | /* Generic.Traceback */ 23 | /* Keyword.Constant */ 24 | /* Keyword.Declaration */ 25 | /* Keyword.Pseudo */ 26 | /* Keyword.Reserved */ 27 | /* Keyword.Type */ 28 | /* Literal.Number */ 29 | /* Literal.String */ 30 | /* Name.Attribute */ 31 | /* Name.Builtin */ 32 | /* Name.Class */ 33 | /* Name.Constant */ 34 | /* Name.Entity */ 35 | /* Name.Exception */ 36 | /* Name.Function */ 37 | /* Name.Namespace */ 38 | /* Name.Tag */ 39 | /* Name.Variable */ 40 | /* Operator.Word */ 41 | /* Text.Whitespace */ 42 | /* Literal.Number.Float */ 43 | /* Literal.Number.Hex */ 44 | /* Literal.Number.Integer */ 45 | /* Literal.Number.Oct */ 46 | /* Literal.String.Backtick */ 47 | /* Literal.String.Char */ 48 | /* Literal.String.Doc */ 49 | /* Literal.String.Double */ 50 | /* Literal.String.Escape */ 51 | /* Literal.String.Heredoc */ 52 | /* Literal.String.Interpol */ 53 | /* Literal.String.Other */ 54 | /* Literal.String.Regex */ 55 | /* Literal.String.Single */ 56 | /* Literal.String.Symbol */ 57 | /* Name.Builtin.Pseudo */ 58 | /* Name.Variable.Class */ 59 | /* Name.Variable.Global */ 60 | /* Name.Variable.Instance */ 61 | /* Literal.Number.Integer.Long */ } 62 | .highlight .c { 63 | color: #999988; 64 | font-style: italic; } 65 | .highlight .err { 66 | color: #a61717; 67 | background-color: #e3d2d2; } 68 | .highlight .k { 69 | color: #000000; 70 | font-weight: bold; } 71 | .highlight .o { 72 | color: #000000; 73 | font-weight: bold; } 74 | .highlight .cm { 75 | color: #999988; 76 | font-style: italic; } 77 | .highlight .cp { 78 | color: #999999; 79 | font-weight: bold; } 80 | .highlight .c1 { 81 | color: #999988; 82 | font-style: italic; } 83 | .highlight .cs { 84 | color: #999999; 85 | font-weight: bold; 86 | font-style: italic; } 87 | .highlight .gd { 88 | color: #000000; 89 | background-color: #ffdddd; } 90 | .highlight .gd .x { 91 | color: #000000; 92 | background-color: #ffaaaa; } 93 | .highlight .ge { 94 | color: #000000; 95 | font-style: italic; } 96 | .highlight .gr { 97 | color: #aa0000; } 98 | .highlight .gh { 99 | color: #999999; } 100 | .highlight .gi { 101 | color: #000000; 102 | background-color: #ddffdd; } 103 | .highlight .gi .x { 104 | color: #000000; 105 | background-color: #aaffaa; } 106 | .highlight .go { 107 | color: #888888; } 108 | .highlight .gp { 109 | color: #555555; } 110 | .highlight .gs { 111 | font-weight: bold; } 112 | .highlight .gu { 113 | color: #aaaaaa; } 114 | .highlight .gt { 115 | color: #aa0000; } 116 | .highlight .kc { 117 | color: #000000; 118 | font-weight: bold; } 119 | .highlight .kd { 120 | color: #000000; 121 | font-weight: bold; } 122 | .highlight .kp { 123 | color: #000000; 124 | font-weight: bold; } 125 | .highlight .kr { 126 | color: #000000; 127 | font-weight: bold; } 128 | .highlight .kt { 129 | color: #445588; } 130 | .highlight .m { 131 | color: #009999; } 132 | .highlight .s { 133 | color: #d14; } 134 | .highlight .na { 135 | color: #008080; } 136 | .highlight .nb { 137 | color: #0086B3; } 138 | .highlight .nc { 139 | color: #445588; 140 | font-weight: bold; } 141 | .highlight .no { 142 | color: #008080; } 143 | .highlight .ni { 144 | color: #800080; } 145 | .highlight .ne { 146 | color: #990000; 147 | font-weight: bold; } 148 | .highlight .nf { 149 | color: #990000; } 150 | .highlight .nn { 151 | color: #555555; } 152 | .highlight .nt { 153 | color: #000080; } 154 | .highlight .nv { 155 | color: #008080; } 156 | .highlight .ow { 157 | color: #000000; 158 | font-weight: bold; } 159 | .highlight .w { 160 | color: #bbbbbb; } 161 | .highlight .mf { 162 | color: #009999; } 163 | .highlight .mh { 164 | color: #009999; } 165 | .highlight .mi { 166 | color: #009999; } 167 | .highlight .mo { 168 | color: #009999; } 169 | .highlight .sb { 170 | color: #d14; } 171 | .highlight .sc { 172 | color: #d14; } 173 | .highlight .sd { 174 | color: #d14; } 175 | .highlight .s2 { 176 | color: #d14; } 177 | .highlight .se { 178 | color: #d14; } 179 | .highlight .sh { 180 | color: #d14; } 181 | .highlight .si { 182 | color: #d14; } 183 | .highlight .sx { 184 | color: #d14; } 185 | .highlight .sr { 186 | color: #009926; } 187 | .highlight .s1 { 188 | color: #d14; } 189 | .highlight .ss { 190 | color: #990073; } 191 | .highlight .bp { 192 | color: #999999; } 193 | .highlight .vc { 194 | color: #008080; } 195 | .highlight .vg { 196 | color: #008080; } 197 | .highlight .vi { 198 | color: #008080; } 199 | .highlight .il { 200 | color: #009999; } 201 | -------------------------------------------------------------------------------- /docs/docsets/PCloudSDKSwift.docset/Contents/Resources/Documents/css/jazzy.css: -------------------------------------------------------------------------------- 1 | html, body, div, span, h1, h3, h4, p, a, code, em, img, ul, li, table, tbody, tr, td { 2 | background: transparent; 3 | border: 0; 4 | margin: 0; 5 | outline: 0; 6 | padding: 0; 7 | vertical-align: baseline; } 8 | 9 | body { 10 | background-color: #f2f2f2; 11 | font-family: Helvetica, freesans, Arial, sans-serif; 12 | font-size: 14px; 13 | -webkit-font-smoothing: subpixel-antialiased; 14 | word-wrap: break-word; } 15 | 16 | h1, h2, h3 { 17 | margin-top: 0.8em; 18 | margin-bottom: 0.3em; 19 | font-weight: 100; 20 | color: black; } 21 | 22 | h1 { 23 | font-size: 2.5em; } 24 | 25 | h2 { 26 | font-size: 2em; 27 | border-bottom: 1px solid #e2e2e2; } 28 | 29 | h4 { 30 | font-size: 13px; 31 | line-height: 1.5; 32 | margin-top: 21px; } 33 | 34 | h5 { 35 | font-size: 1.1em; } 36 | 37 | h6 { 38 | font-size: 1.1em; 39 | color: #777; } 40 | 41 | .section-name { 42 | color: gray; 43 | display: block; 44 | font-family: Helvetica; 45 | font-size: 22px; 46 | font-weight: 100; 47 | margin-bottom: 15px; } 48 | 49 | pre, code { 50 | font: 0.95em Menlo, monospace; 51 | color: #777; 52 | word-wrap: normal; } 53 | 54 | p code, li code { 55 | background-color: #eee; 56 | padding: 2px 4px; 57 | border-radius: 4px; } 58 | 59 | a { 60 | color: #0088cc; 61 | text-decoration: none; } 62 | 63 | ul { 64 | padding-left: 15px; } 65 | 66 | li { 67 | line-height: 1.8em; } 68 | 69 | img { 70 | max-width: 100%; } 71 | 72 | blockquote { 73 | margin-left: 0; 74 | padding: 0 10px; 75 | border-left: 4px solid #ccc; } 76 | 77 | .content-wrapper { 78 | margin: 0 auto; 79 | width: 980px; } 80 | 81 | header { 82 | font-size: 0.85em; 83 | line-height: 26px; 84 | background-color: #414141; 85 | position: fixed; 86 | width: 100%; 87 | z-index: 2; } 88 | header img { 89 | padding-right: 6px; 90 | vertical-align: -4px; 91 | height: 16px; } 92 | header a { 93 | color: #fff; } 94 | header p { 95 | float: left; 96 | color: #999; } 97 | header .header-right { 98 | float: right; 99 | margin-left: 16px; } 100 | 101 | #breadcrumbs { 102 | background-color: #f2f2f2; 103 | height: 27px; 104 | padding-top: 17px; 105 | position: fixed; 106 | width: 100%; 107 | z-index: 2; 108 | margin-top: 26px; } 109 | #breadcrumbs #carat { 110 | height: 10px; 111 | margin: 0 5px; } 112 | 113 | .sidebar { 114 | background-color: #f9f9f9; 115 | border: 1px solid #e2e2e2; 116 | overflow-y: auto; 117 | overflow-x: hidden; 118 | position: fixed; 119 | top: 70px; 120 | bottom: 0; 121 | width: 230px; 122 | word-wrap: normal; } 123 | 124 | .nav-groups { 125 | list-style-type: none; 126 | background: #fff; 127 | padding-left: 0; } 128 | 129 | .nav-group-name { 130 | border-bottom: 1px solid #e2e2e2; 131 | font-size: 1.1em; 132 | font-weight: 100; 133 | padding: 15px 0 15px 20px; } 134 | .nav-group-name > a { 135 | color: #333; } 136 | 137 | .nav-group-tasks { 138 | margin-top: 5px; } 139 | 140 | .nav-group-task { 141 | font-size: 0.9em; 142 | list-style-type: none; 143 | white-space: nowrap; } 144 | .nav-group-task a { 145 | color: #888; } 146 | 147 | .main-content { 148 | background-color: #fff; 149 | border: 1px solid #e2e2e2; 150 | margin-left: 246px; 151 | position: absolute; 152 | overflow: hidden; 153 | padding-bottom: 20px; 154 | top: 70px; 155 | width: 734px; } 156 | .main-content p, .main-content a, .main-content code, .main-content em, .main-content ul, .main-content table, .main-content blockquote { 157 | margin-bottom: 1em; } 158 | .main-content p { 159 | line-height: 1.8em; } 160 | .main-content section .section:first-child { 161 | margin-top: 0; 162 | padding-top: 0; } 163 | .main-content section .task-group-section .task-group:first-of-type { 164 | padding-top: 10px; } 165 | .main-content section .task-group-section .task-group:first-of-type .section-name { 166 | padding-top: 15px; } 167 | .main-content section .heading:before { 168 | content: ""; 169 | display: block; 170 | padding-top: 70px; 171 | margin: -70px 0 0; } 172 | .main-content .section-name p { 173 | margin-bottom: inherit; 174 | line-height: inherit; } 175 | .main-content .section-name code { 176 | background-color: inherit; 177 | padding: inherit; 178 | color: inherit; } 179 | 180 | .section { 181 | padding: 0 25px; } 182 | 183 | .highlight { 184 | background-color: #eee; 185 | padding: 10px 12px; 186 | border: 1px solid #e2e2e2; 187 | border-radius: 4px; 188 | overflow-x: auto; } 189 | 190 | .declaration .highlight { 191 | overflow-x: initial; 192 | padding: 0 40px 40px 0; 193 | margin-bottom: -25px; 194 | background-color: transparent; 195 | border: none; } 196 | 197 | .section-name { 198 | margin: 0; 199 | margin-left: 18px; } 200 | 201 | .task-group-section { 202 | padding-left: 6px; 203 | border-top: 1px solid #e2e2e2; } 204 | 205 | .task-group { 206 | padding-top: 0px; } 207 | 208 | .task-name-container a[name]:before { 209 | content: ""; 210 | display: block; 211 | padding-top: 70px; 212 | margin: -70px 0 0; } 213 | 214 | .section-name-container { 215 | position: relative; 216 | display: inline-block; } 217 | .section-name-container .section-name-link { 218 | position: absolute; 219 | top: 0; 220 | left: 0; 221 | bottom: 0; 222 | right: 0; 223 | margin-bottom: 0; } 224 | .section-name-container .section-name { 225 | position: relative; 226 | pointer-events: none; 227 | z-index: 1; } 228 | .section-name-container .section-name a { 229 | pointer-events: auto; } 230 | 231 | .item { 232 | padding-top: 8px; 233 | width: 100%; 234 | list-style-type: none; } 235 | .item a[name]:before { 236 | content: ""; 237 | display: block; 238 | padding-top: 70px; 239 | margin: -70px 0 0; } 240 | .item code { 241 | background-color: transparent; 242 | padding: 0; } 243 | .item .token, .item .direct-link { 244 | padding-left: 3px; 245 | margin-left: 15px; 246 | font-size: 11.9px; 247 | transition: all 300ms; } 248 | .item .token-open { 249 | margin-left: 0px; } 250 | .item .discouraged { 251 | text-decoration: line-through; } 252 | .item .declaration-note { 253 | font-size: .85em; 254 | color: gray; 255 | font-style: italic; } 256 | 257 | .pointer-container { 258 | border-bottom: 1px solid #e2e2e2; 259 | left: -23px; 260 | padding-bottom: 13px; 261 | position: relative; 262 | width: 110%; } 263 | 264 | .pointer { 265 | background: #f9f9f9; 266 | border-left: 1px solid #e2e2e2; 267 | border-top: 1px solid #e2e2e2; 268 | height: 12px; 269 | left: 21px; 270 | top: -7px; 271 | -webkit-transform: rotate(45deg); 272 | -moz-transform: rotate(45deg); 273 | -o-transform: rotate(45deg); 274 | transform: rotate(45deg); 275 | position: absolute; 276 | width: 12px; } 277 | 278 | .height-container { 279 | display: none; 280 | left: -25px; 281 | padding: 0 25px; 282 | position: relative; 283 | width: 100%; 284 | overflow: hidden; } 285 | .height-container .section { 286 | background: #f9f9f9; 287 | border-bottom: 1px solid #e2e2e2; 288 | left: -25px; 289 | position: relative; 290 | width: 100%; 291 | padding-top: 10px; 292 | padding-bottom: 5px; } 293 | 294 | .aside, .language { 295 | padding: 6px 12px; 296 | margin: 12px 0; 297 | border-left: 5px solid #dddddd; 298 | overflow-y: hidden; } 299 | .aside .aside-title, .language .aside-title { 300 | font-size: 9px; 301 | letter-spacing: 2px; 302 | text-transform: uppercase; 303 | padding-bottom: 0; 304 | margin: 0; 305 | color: #aaa; 306 | -webkit-user-select: none; } 307 | .aside p:last-child, .language p:last-child { 308 | margin-bottom: 0; } 309 | 310 | .language { 311 | border-left: 5px solid #cde9f4; } 312 | .language .aside-title { 313 | color: #4b8afb; } 314 | 315 | .aside-warning, .aside-deprecated, .aside-unavailable { 316 | border-left: 5px solid #ff6666; } 317 | .aside-warning .aside-title, .aside-deprecated .aside-title, .aside-unavailable .aside-title { 318 | color: #ff0000; } 319 | 320 | .graybox { 321 | border-collapse: collapse; 322 | width: 100%; } 323 | .graybox p { 324 | margin: 0; 325 | word-break: break-word; 326 | min-width: 50px; } 327 | .graybox td { 328 | border: 1px solid #e2e2e2; 329 | padding: 5px 25px 5px 10px; 330 | vertical-align: middle; } 331 | .graybox tr td:first-of-type { 332 | text-align: right; 333 | padding: 7px; 334 | vertical-align: top; 335 | word-break: normal; 336 | width: 40px; } 337 | 338 | .slightly-smaller { 339 | font-size: 0.9em; } 340 | 341 | #footer { 342 | position: relative; 343 | top: 10px; 344 | bottom: 0px; 345 | margin-left: 25px; } 346 | #footer p { 347 | margin: 0; 348 | color: #aaa; 349 | font-size: 0.8em; } 350 | 351 | html.dash header, html.dash #breadcrumbs, html.dash .sidebar { 352 | display: none; } 353 | 354 | html.dash .main-content { 355 | width: 980px; 356 | margin-left: 0; 357 | border: none; 358 | width: 100%; 359 | top: 0; 360 | padding-bottom: 0; } 361 | 362 | html.dash .height-container { 363 | display: block; } 364 | 365 | html.dash .item .token { 366 | margin-left: 0; } 367 | 368 | html.dash .content-wrapper { 369 | width: auto; } 370 | 371 | html.dash #footer { 372 | position: static; } 373 | -------------------------------------------------------------------------------- /docs/docsets/PCloudSDKSwift.docset/Contents/Resources/Documents/img/carat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pCloud/pcloud-sdk-swift/cc81e0250a9f378019470c78ce9a8bb501dcaeda/docs/docsets/PCloudSDKSwift.docset/Contents/Resources/Documents/img/carat.png -------------------------------------------------------------------------------- /docs/docsets/PCloudSDKSwift.docset/Contents/Resources/Documents/img/dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pCloud/pcloud-sdk-swift/cc81e0250a9f378019470c78ce9a8bb501dcaeda/docs/docsets/PCloudSDKSwift.docset/Contents/Resources/Documents/img/dash.png -------------------------------------------------------------------------------- /docs/docsets/PCloudSDKSwift.docset/Contents/Resources/Documents/img/gh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pCloud/pcloud-sdk-swift/cc81e0250a9f378019470c78ce9a8bb501dcaeda/docs/docsets/PCloudSDKSwift.docset/Contents/Resources/Documents/img/gh.png -------------------------------------------------------------------------------- /docs/docsets/PCloudSDKSwift.docset/Contents/Resources/Documents/js/jazzy.js: -------------------------------------------------------------------------------- 1 | window.jazzy = {'docset': false} 2 | if (typeof window.dash != 'undefined') { 3 | document.documentElement.className += ' dash' 4 | window.jazzy.docset = true 5 | } 6 | if (navigator.userAgent.match(/xcode/i)) { 7 | document.documentElement.className += ' xcode' 8 | window.jazzy.docset = true 9 | } 10 | 11 | function toggleItem($link, $content) { 12 | var animationDuration = 300; 13 | $link.toggleClass('token-open'); 14 | $content.slideToggle(animationDuration); 15 | } 16 | 17 | function itemLinkToContent($link) { 18 | return $link.parent().parent().next(); 19 | } 20 | 21 | // On doc load + hash-change, open any targetted item 22 | function openCurrentItemIfClosed() { 23 | if (window.jazzy.docset) { 24 | return; 25 | } 26 | var $link = $(`.token[href="${location.hash}"]`); 27 | $content = itemLinkToContent($link); 28 | if ($content.is(':hidden')) { 29 | toggleItem($link, $content); 30 | } 31 | } 32 | 33 | $(openCurrentItemIfClosed); 34 | $(window).on('hashchange', openCurrentItemIfClosed); 35 | 36 | // On item link ('token') click, toggle its discussion 37 | $('.token').on('click', function(event) { 38 | if (window.jazzy.docset) { 39 | return; 40 | } 41 | var $link = $(this); 42 | toggleItem($link, itemLinkToContent($link)); 43 | 44 | // Keeps the document from jumping to the hash. 45 | var href = $link.attr('href'); 46 | if (history.pushState) { 47 | history.pushState({}, '', href); 48 | } else { 49 | location.hash = href; 50 | } 51 | event.preventDefault(); 52 | }); 53 | 54 | // Clicks on links to the current, closed, item need to open the item 55 | $("a:not('.token')").on('click', function() { 56 | if (location == this.href) { 57 | openCurrentItemIfClosed(); 58 | } 59 | }); 60 | -------------------------------------------------------------------------------- /docs/docsets/PCloudSDKSwift.docset/Contents/Resources/docSet.dsidx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pCloud/pcloud-sdk-swift/cc81e0250a9f378019470c78ce9a8bb501dcaeda/docs/docsets/PCloudSDKSwift.docset/Contents/Resources/docSet.dsidx -------------------------------------------------------------------------------- /docs/docsets/PCloudSDKSwift.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pCloud/pcloud-sdk-swift/cc81e0250a9f378019470c78ce9a8bb501dcaeda/docs/docsets/PCloudSDKSwift.tgz -------------------------------------------------------------------------------- /docs/img/carat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pCloud/pcloud-sdk-swift/cc81e0250a9f378019470c78ce9a8bb501dcaeda/docs/img/carat.png -------------------------------------------------------------------------------- /docs/img/dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pCloud/pcloud-sdk-swift/cc81e0250a9f378019470c78ce9a8bb501dcaeda/docs/img/dash.png -------------------------------------------------------------------------------- /docs/img/gh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pCloud/pcloud-sdk-swift/cc81e0250a9f378019470c78ce9a8bb501dcaeda/docs/img/gh.png -------------------------------------------------------------------------------- /docs/js/jazzy.js: -------------------------------------------------------------------------------- 1 | window.jazzy = {'docset': false} 2 | if (typeof window.dash != 'undefined') { 3 | document.documentElement.className += ' dash' 4 | window.jazzy.docset = true 5 | } 6 | if (navigator.userAgent.match(/xcode/i)) { 7 | document.documentElement.className += ' xcode' 8 | window.jazzy.docset = true 9 | } 10 | 11 | function toggleItem($link, $content) { 12 | var animationDuration = 300; 13 | $link.toggleClass('token-open'); 14 | $content.slideToggle(animationDuration); 15 | } 16 | 17 | function itemLinkToContent($link) { 18 | return $link.parent().parent().next(); 19 | } 20 | 21 | // On doc load + hash-change, open any targetted item 22 | function openCurrentItemIfClosed() { 23 | if (window.jazzy.docset) { 24 | return; 25 | } 26 | var $link = $(`.token[href="${location.hash}"]`); 27 | $content = itemLinkToContent($link); 28 | if ($content.is(':hidden')) { 29 | toggleItem($link, $content); 30 | } 31 | } 32 | 33 | $(openCurrentItemIfClosed); 34 | $(window).on('hashchange', openCurrentItemIfClosed); 35 | 36 | // On item link ('token') click, toggle its discussion 37 | $('.token').on('click', function(event) { 38 | if (window.jazzy.docset) { 39 | return; 40 | } 41 | var $link = $(this); 42 | toggleItem($link, itemLinkToContent($link)); 43 | 44 | // Keeps the document from jumping to the hash. 45 | var href = $link.attr('href'); 46 | if (history.pushState) { 47 | history.pushState({}, '', href); 48 | } else { 49 | location.hash = href; 50 | } 51 | event.preventDefault(); 52 | }); 53 | 54 | // Clicks on links to the current, closed, item need to open the item 55 | $("a:not('.token')").on('click', function() { 56 | if (location == this.href) { 57 | openCurrentItemIfClosed(); 58 | } 59 | }); 60 | --------------------------------------------------------------------------------