├── .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 |
44 |
45 |
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 |
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 |
--------------------------------------------------------------------------------