├── .github └── workflows │ ├── ci.yml │ ├── release.yml │ └── static.yml ├── .gitignore ├── .swiftformat ├── .swiftlint.yml ├── AliyunpanSDK.podspec ├── AliyunpanSDK.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── AliyunpanSDK.xcscheme ├── CONTRIBUTING.md ├── Demo ├── Demo.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Demo-iOS.xcscheme ├── Demo.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Demo │ ├── Client.swift │ ├── Demo-MacOS │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Base.lproj │ │ │ └── Main.storyboard │ │ ├── Demo_MacOS.entitlements │ │ ├── ExamplesViewController.swift │ │ └── ViewController.swift │ ├── Demo-TVOS │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── App Icon & Top Shelf Image.brandassets │ │ │ │ ├── App Icon - App Store.imagestack │ │ │ │ │ ├── Back.imagestacklayer │ │ │ │ │ │ ├── Content.imageset │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ └── Contents.json │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── Front.imagestacklayer │ │ │ │ │ │ ├── Content.imageset │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Middle.imagestacklayer │ │ │ │ │ │ ├── Content.imageset │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ └── Contents.json │ │ │ │ ├── App Icon.imagestack │ │ │ │ │ ├── Back.imagestacklayer │ │ │ │ │ │ ├── Content.imageset │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ └── Contents.json │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── Front.imagestacklayer │ │ │ │ │ │ ├── Content.imageset │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Middle.imagestacklayer │ │ │ │ │ │ ├── Content.imageset │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ └── Contents.json │ │ │ │ ├── Contents.json │ │ │ │ ├── Top Shelf Image Wide.imageset │ │ │ │ │ └── Contents.json │ │ │ │ └── Top Shelf Image.imageset │ │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Base.lproj │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ └── ViewController.swift │ ├── Demo-iOS │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Base.lproj │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── FileCell.swift │ │ ├── FileListViewController.swift │ │ ├── Info.plist │ │ ├── SceneDelegate.swift │ │ └── ViewController.swift │ ├── Demo-visionOS │ │ ├── Assets.xcassets │ │ │ ├── AppIcon.solidimagestack │ │ │ │ ├── Back.solidimagestacklayer │ │ │ │ │ ├── Content.imageset │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Contents.json │ │ │ │ ├── Contents.json │ │ │ │ ├── Front.solidimagestacklayer │ │ │ │ │ ├── Content.imageset │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Contents.json │ │ │ │ └── Middle.solidimagestacklayer │ │ │ │ │ ├── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── ContentView.swift │ │ ├── Demo_visionOSApp.swift │ │ ├── Info.plist │ │ └── Preview Content │ │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── DisplayItem.swift │ └── Example.swift ├── Package.swift ├── Podfile.lock └── podfile ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Package.swift ├── Package@swift-5.9.swift ├── README.md ├── Sources └── AliyunpanSDK │ ├── AliyunpanClient.swift │ ├── AliyunpanCredentials │ ├── AliyunpanAuthenticator.swift │ ├── AliyunpanCredentials.swift │ ├── AliyunpanMessage.swift │ ├── AliyunpanPKCECredentials.swift │ ├── AliyunpanQRCodeCredentials.swift │ ├── AliyunpanServerCredentials.swift │ └── AliyunpanTokenCredentials.swift │ ├── AliyunpanError.swift │ ├── AliyunpanLogger.swift │ ├── AliyunpanSDK.h │ ├── AliyunpanSDK.swift │ ├── AliyunpanScope │ ├── AliyunpanCommand.swift │ ├── AliyunpanScope.swift │ ├── File │ │ ├── BatchGet.swift │ │ ├── CompleteUpload.swift │ │ ├── CopyFile.swift │ │ ├── CreateFile.swift │ │ ├── DeleteFile.swift │ │ ├── GetAsyncTask.swift │ │ ├── GetFile.swift │ │ ├── GetFileByPath.swift │ │ ├── GetFileDownloadUrl.swift │ │ ├── GetFileList.swift │ │ ├── GetStarredList.swift │ │ ├── GetUploadURL.swift │ │ ├── ListUploadedParts.swift │ │ ├── MoveFile.swift │ │ ├── SearchFile.swift │ │ ├── TrashFileToRecyclebin.swift │ │ └── UpdateFile.swift │ ├── Internal │ │ ├── Authorize.swift │ │ ├── GetAccessToken.swift │ │ ├── GetAuthorizeQRCode.swift │ │ └── GetAuthorizeQRCodeStatus.swift │ ├── User │ │ ├── GetDriveInfo.swift │ │ ├── GetSpaceInfo.swift │ │ ├── GetUsersInfo.swift │ │ ├── GetUsersScopes.swift │ │ └── GetVipInfo.swift │ ├── Video │ │ ├── GetVideoPreviewPlayInfo.swift │ │ ├── GetVideoPreviewPlayMeta.swift │ │ ├── GetVideoRecentList.swift │ │ └── UpdateVideoRecord.swift │ └── Vip │ │ ├── GetVipFeatureList.swift │ │ └── GetVipFeatureTrial.swift │ ├── HTTPRequest │ ├── DebugDescription.swift │ ├── Download │ │ ├── AliyunpanDownloadChunk.swift │ │ ├── AliyunpanDownloadTask.swift │ │ ├── AliyunpanDownloader.swift │ │ └── DownloadChunkOperation.swift │ ├── HTTPHeaders.swift │ ├── HTTPMethod.swift │ ├── HTTPRequest.swift │ ├── JSON+Codable.swift │ ├── URLConvertible.swift │ └── Upload │ │ └── AliyunpanUploader.swift │ ├── Info.plist │ ├── Model │ ├── AliyunpanFile.swift │ ├── AliyunpanSpaceInfo.swift │ └── AliyunpanToken.swift │ ├── Platform.swift │ └── Utils │ ├── AsyncOperation.swift │ ├── Crypto.swift │ ├── Task+AliyunpanSDK.swift │ ├── ThreadSafe.swift │ ├── URL+AliyunpanSDK.swift │ └── Weak.swift ├── Tests ├── AliyunpanSDK.xctestplan ├── AliyunpanSDKTests │ ├── AliyunpanClientTests.swift │ ├── AliyunpanSDKTests.swift │ ├── CredentialTests.swift │ ├── CryptoTests.swift │ ├── DownloaderOperationTests.swift │ ├── DownloaderTaskTests.swift │ ├── DownloaderTests.swift │ ├── HTTPRequestTests.swift │ ├── JSONTest.swift │ ├── MessageTests.swift │ └── TaskTests.swift └── TestFile1.txt └── fastlane └── Fastfile /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: AliyunpanSDK CI 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["main"] 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | build: 15 | runs-on: macos-13 16 | timeout-minutes: 10 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | destination: [ 21 | 'macOS', 22 | 'iOS Simulator,name=iPhone 14', 23 | 'tvOS Simulator,name=Apple TV' 24 | ] 25 | steps: 26 | - uses: actions/checkout@v4 27 | - uses: ruby/setup-ruby@v1 28 | with: 29 | ruby-version: 2.7.4 30 | bundler-cache: true 31 | - name: Test 32 | env: 33 | DESTINATION: platform=${{ matrix.destination }} 34 | run: bundle exec fastlane test_ci 35 | 36 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: AliyunpanSDK Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: "version" 8 | required: false 9 | type: string 10 | 11 | jobs: 12 | Run: 13 | runs-on: macos-13 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: ruby/setup-ruby@v1 17 | with: 18 | ruby-version: 2.7.4 19 | bundler-cache: true 20 | - name: Release 21 | run: bundle exec fastlane release version:$VERSION 22 | env: 23 | VERSION: ${{ inputs.version }} 24 | FL_GITHUB_RELEASE_API_BEARER: ${{ secrets.GITHUB_TOKEN }} 25 | COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} 26 | 27 | -------------------------------------------------------------------------------- /.github/workflows/static.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to Pages 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: read 11 | pages: write 12 | id-token: write 13 | 14 | concurrency: 15 | group: "pages" 16 | cancel-in-progress: false 17 | 18 | jobs: 19 | deploy: 20 | environment: 21 | name: github-pages 22 | url: ${{ steps.deployment.outputs.page_url }} 23 | runs-on: macOS-latest 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v4 27 | - name: Install Jazzy 28 | run: gem install jazzy 29 | - name: Generate Document 30 | run: jazzy 31 | - name: Setup Pages 32 | uses: actions/configure-pages@v4 33 | - name: Upload artifact 34 | uses: actions/upload-pages-artifact@v2 35 | with: 36 | path: 'docs' 37 | - name: Deploy to GitHub Pages 38 | id: deployment 39 | uses: actions/deploy-pages@v3 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/cocoapods,swift,swiftpm,macos 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=cocoapods,swift,swiftpm,macos 3 | 4 | ### CocoaPods ### 5 | ## CocoaPods GitIgnore Template 6 | 7 | # CocoaPods - Only use to conserve bandwidth / Save time on Pushing 8 | # - Also handy if you have a large number of dependant pods 9 | # - AS PER https://guides.cocoapods.org/using/using-cocoapods.html NEVER IGNORE THE LOCK FILE 10 | Pods/ 11 | SourcePackages/ 12 | 13 | ### macOS ### 14 | # General 15 | .DS_Store 16 | .AppleDouble 17 | .LSOverride 18 | 19 | # Icon must end with two \r 20 | Icon 21 | 22 | 23 | # Thumbnails 24 | ._* 25 | 26 | # Files that might appear in the root of a volume 27 | .DocumentRevisions-V100 28 | .fseventsd 29 | .Spotlight-V100 30 | .TemporaryItems 31 | .Trashes 32 | .VolumeIcon.icns 33 | .com.apple.timemachine.donotpresent 34 | 35 | # Directories potentially created on remote AFP share 36 | .AppleDB 37 | .AppleDesktop 38 | Network Trash Folder 39 | Temporary Items 40 | .apdisk 41 | 42 | ### macOS Patch ### 43 | # iCloud generated files 44 | *.icloud 45 | 46 | ### Swift ### 47 | # Xcode 48 | # 49 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 50 | 51 | ## User settings 52 | xcuserdata/ 53 | 54 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 55 | *.xcscmblueprint 56 | *.xccheckout 57 | 58 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 59 | build/ 60 | DerivedData/ 61 | *.moved-aside 62 | *.pbxuser 63 | !default.pbxuser 64 | *.mode1v3 65 | !default.mode1v3 66 | *.mode2v3 67 | !default.mode2v3 68 | *.perspectivev3 69 | !default.perspectivev3 70 | 71 | ## Obj-C/Swift specific 72 | *.hmap 73 | 74 | ## App packaging 75 | *.ipa 76 | *.dSYM.zip 77 | *.dSYM 78 | 79 | ## Playgrounds 80 | timeline.xctimeline 81 | playground.xcworkspace 82 | 83 | # Swift Package Manager 84 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 85 | # Packages/ 86 | # Package.pins 87 | # Package.resolved 88 | # *.xcodeproj 89 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 90 | # hence it is not needed unless you have added a package configuration file to your project 91 | # .swiftpm 92 | 93 | .build/ 94 | 95 | # CocoaPods 96 | # We recommend against adding the Pods directory to your .gitignore. However 97 | # you should judge for yourself, the pros and cons are mentioned at: 98 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 99 | # Pods/ 100 | # Add this line if you want to avoid checking in source code from the Xcode workspace 101 | # *.xcworkspace 102 | 103 | # Carthage 104 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 105 | # Carthage/Checkouts 106 | 107 | Carthage/Build/ 108 | 109 | # Accio dependency management 110 | Dependencies/ 111 | .accio/ 112 | 113 | # fastlane 114 | # It is recommended to not store the screenshots in the git repo. 115 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 116 | # For more information about the recommended setup visit: 117 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 118 | 119 | fastlane/report.xml 120 | fastlane/Preview.html 121 | fastlane/screenshots/**/*.png 122 | fastlane/test_output 123 | fastlane/README.md 124 | 125 | # Code Injection 126 | # After new code Injection tools there's a generated folder /iOSInjectionProject 127 | # https://github.com/johnno1962/injectionforxcode 128 | 129 | iOSInjectionProject/ 130 | 131 | ### SwiftPM ### 132 | Packages 133 | xcuserdata 134 | # *.xcodeproj 135 | .swiftpm 136 | 137 | .bundle 138 | vendor 139 | # End of https://www.toptal.com/developers/gitignore/api/cocoapods,swift,swiftpm,macos 140 | n -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | # file options 2 | 3 | --symlinks ignore 4 | --swiftversion 5.7 5 | 6 | # format options 7 | 8 | --patternlet inline 9 | --decimalgrouping 3,5 10 | --operatorfunc spaced 11 | --nospaceoperators ..<, ... 12 | --someAny false 13 | --extensionacl on-declarations 14 | --ifdef outdent 15 | --commas inline 16 | --stripunusedargs closure-only 17 | --elseposition same-line 18 | --guardelse same-line 19 | --indentstrings false 20 | --anonymousforeach ignore 21 | 22 | # rules 23 | --enable isEmpty 24 | --disable sortImports,wrapMultilineStatementBraces,wrapArguments,enumNamespaces,indent,trailingSpace,conditionalAssignment -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: # rule identifiers to exclude from running 2 | - line_length 3 | - nesting 4 | - cyclomatic_complexity 5 | - shorthand_operator 6 | - large_tuple 7 | - private_over_fileprivate 8 | - fallthrough 9 | - multiple_closures_with_trailing_closure 10 | - block_based_kvo 11 | - unused_setter_value 12 | - no_space_in_method_call 13 | - inclusive_language 14 | included: 15 | - Sources 16 | excluded: # paths to ignore during linting. overridden by `included`. 17 | type_name: 18 | min_length: 2 # only warning 19 | max_length: # warning and error 20 | warning: 100 21 | error: 200 22 | excluded: 23 | - iPhone 24 | 25 | function_parameter_count: 10 26 | function_body_length: 27 | - 200 28 | - 300 29 | type_body_length: 30 | - 400 # warning 31 | - 500 # error 32 | file_length: 33 | - 1000 # warning 34 | - 1500 # error 35 | identifier_name: 36 | min_length: 1 37 | max_length: 40 38 | allowed_symbols: "_" 39 | 40 | trailing_whitespace: 41 | ignores_empty_lines: true 42 | ignores_comments: true 43 | -------------------------------------------------------------------------------- /AliyunpanSDK.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = "AliyunpanSDK" 3 | spec.version = "0.3.6" 4 | spec.summary = "Aliyunpan OpenSDK-iOS" 5 | 6 | spec.description = <<-DESC 7 | Aliyunpan OpenSDK-iOS 8 | DESC 9 | spec.homepage = "https://github.com/alibaba/aliyunpan-ios-sdk" 10 | spec.license = "MIT" 11 | spec.author = { "zhaixian" => "zixuan.wzx@alibaba-inc.com" } 12 | 13 | spec.swift_versions = '5.0' 14 | 15 | spec.ios.deployment_target = "13.0" 16 | spec.tvos.deployment_target = "13.0" 17 | spec.osx.deployment_target = "10.15" 18 | spec.visionos.deployment_target = "1.0" 19 | 20 | spec.source = { :git => "https://github.com/alibaba/aliyunpan-ios-sdk.git", :tag => "v#{spec.version}" } 21 | 22 | spec.source_files = "Sources/**/*.swift" 23 | end 24 | -------------------------------------------------------------------------------- /AliyunpanSDK.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /AliyunpanSDK.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /AliyunpanSDK.xcodeproj/xcshareddata/xcschemes/AliyunpanSDK.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 34 | 35 | 36 | 37 | 40 | 46 | 47 | 48 | 49 | 50 | 60 | 61 | 67 | 68 | 74 | 75 | 76 | 77 | 79 | 80 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 贡献指南 2 | 3 | 欢迎并感谢你对 ***AliyunpanSDK*** 的兴趣和贡献!我们鼓励任何形式的贡献,无论是通过开启讨论、提交代码、完善文档还是其他方式。 4 | 5 | ## 报告问题 6 | 7 | 遇到问题的时候,请先查阅我们的问题追踪器来确认是否已有人报告相同的问题。如果没有,可以创建一个新的问题,描述清楚遇到的问题、重现步骤、预期行为和实际行为。如果可能,请附上相关的代码片段或错误截图。 8 | 9 | ## 讨论功能请求 10 | 11 | 如果你有想要实现的新功能或者改进的建议,请在问题追踪器中开启一个新的讨论(issue)来描述你的想法。请尝试详细阐述功能的预期目标和它对现有项目的潜在影响。 12 | 13 | ## 提交代码 14 | 15 | 我们欢迎长期贡献者成为本项目的维护者,如果你愿意贡献代码,请遵循以下步骤: 16 | 17 | 1. Fork 本项目仓库。 18 | 2. 在自己的 fork 仓库中创建一个新的分支,分支名称尽量反映作用。 19 | 3. 请确保代码符合我们的代码规范和风格。 20 | 4. 提交您的代码,并在 ***pull request*** 中清晰准确地描述您的改动。 21 | 5. 对项目主分支发起 ***pull request***。 22 | 6. 在您的代码被合并之前,一个项目维护者可能会与您联系讨论或请求进一步的修正。 23 | 24 | ## 开源协议 25 | 26 | 您对项目的任何贡献都将受我们项目选用的开源协议保护,有关详细信息,请参阅项目的LICENSE文件。 27 | -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/xcshareddata/xcschemes/Demo-iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 42 | 44 | 50 | 51 | 52 | 53 | 59 | 61 | 67 | 68 | 69 | 70 | 72 | 73 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /Demo/Demo.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Demo/Demo.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Demo/Demo/Client.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Client.swift 3 | // Demo 4 | // 5 | // Created by zhaixian on 2024/2/20. 6 | // 7 | 8 | import Foundation 9 | import AliyunpanSDK 10 | 11 | let appId = "YOUR_APP_ID" // 替换成你的 AppID 12 | let scope = "user:base,file:all:read,file:all:write" 13 | 14 | /// 调试参数 15 | /// 环境 16 | let environment = Aliyunpan.Environment.product 17 | /// 日志等级 18 | let logLevel = AliyunpanLogLevel.info 19 | /// 运行时清空 token 20 | let cleanToken = true 21 | 22 | let client = AliyunpanClient( 23 | appId: appId, 24 | scope: scope 25 | ) 26 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-MacOS/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Demo-MacOS 4 | // 5 | // Created by zhaixian on 2023/12/13. 6 | // 7 | 8 | import Cocoa 9 | import AliyunpanSDK 10 | 11 | @main 12 | class AppDelegate: NSObject, NSApplicationDelegate { 13 | func applicationDidFinishLaunching(_ aNotification: Notification) { 14 | Aliyunpan.setEnvironment(environment) 15 | Aliyunpan.setLogLevel(logLevel) 16 | 17 | client.cleanToken() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-MacOS/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-MacOS/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "2x", 51 | "size" : "512x512" 52 | } 53 | ], 54 | "info" : { 55 | "author" : "xcode", 56 | "version" : 1 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-MacOS/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-MacOS/Demo_MacOS.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | com.apple.security.network.client 10 | 11 | com.apple.security.network.server 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-MacOS/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Demo-MacOS 4 | // 5 | // Created by zhaixian on 2023/12/13. 6 | // 7 | 8 | import Cocoa 9 | import AliyunpanSDK 10 | 11 | class ViewController: NSViewController { 12 | private var buttonContainer = NSStackView() 13 | private var pkceButton = NSButton() 14 | private var qrCodeButton = NSButton() 15 | 16 | private var imageView = NSImageView() 17 | 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | 21 | buttonContainer.orientation = .vertical 22 | view.addSubview(buttonContainer) 23 | buttonContainer.translatesAutoresizingMaskIntoConstraints = false 24 | NSLayoutConstraint.activate([ 25 | buttonContainer.centerXAnchor.constraint(equalTo: view.centerXAnchor), 26 | buttonContainer.centerYAnchor.constraint(equalTo: view.centerYAnchor), 27 | buttonContainer.widthAnchor.constraint(equalToConstant: 200), 28 | buttonContainer.heightAnchor.constraint(equalToConstant: 60) 29 | ]) 30 | 31 | pkceButton.title = "Authorize(PKCE)" 32 | pkceButton.bezelStyle = .roundRect 33 | pkceButton.action = #selector(pkceAuthorize) 34 | 35 | qrCodeButton.title = "Authorize(QR Code)" 36 | qrCodeButton.bezelStyle = .roundRect 37 | qrCodeButton.action = #selector(showQRCode) 38 | 39 | buttonContainer.addArrangedSubview(pkceButton) 40 | buttonContainer.addArrangedSubview(qrCodeButton) 41 | 42 | view.addSubview(imageView) 43 | imageView.isHidden = true 44 | imageView.translatesAutoresizingMaskIntoConstraints = false 45 | NSLayoutConstraint.activate([ 46 | imageView.centerXAnchor.constraint(equalTo: view.centerXAnchor), 47 | imageView.centerYAnchor.constraint(equalTo: view.centerYAnchor), 48 | imageView.widthAnchor.constraint(equalToConstant: 200), 49 | imageView.heightAnchor.constraint(equalToConstant: 200) 50 | ]) 51 | } 52 | 53 | override func viewWillAppear() { 54 | super.viewWillAppear() 55 | if client.accessToken != nil { 56 | // 已授权过 57 | navigateToExamples() 58 | } 59 | } 60 | 61 | @objc private func pkceAuthorize() { 62 | Task { 63 | do { 64 | try await client.authorize( 65 | credentials: .pkce) 66 | navigateToExamples() 67 | } catch { 68 | print(error) 69 | } 70 | } 71 | } 72 | 73 | @objc private func showQRCode() { 74 | Task { 75 | do { 76 | try await client.authorize( 77 | credentials: .qrCode(self)) 78 | } catch { 79 | print(error) 80 | } 81 | } 82 | } 83 | 84 | private func navigateToExamples() { 85 | let storyboard = NSStoryboard(name: NSStoryboard.Name("Main"), bundle: Bundle.main) 86 | let splitViewController = storyboard.instantiateController(withIdentifier: NSStoryboard.Name("MainSplitViewController")) as! MainSplitViewController 87 | view.window?.contentViewController = splitViewController 88 | } 89 | } 90 | 91 | extension ViewController: AliyunpanQRCodeContainer { 92 | func authorizeQRCodeStatusUpdated(_ status: AliyunpanSDK.AliyunpanAuthorizeQRCodeStatus) { 93 | print(status.rawValue) 94 | if status == .loginSuccess { 95 | navigateToExamples() 96 | } 97 | } 98 | 99 | func showAliyunpanAuthorizeQRCode(with url: URL) { 100 | buttonContainer.isHidden = true 101 | imageView.isHidden = false 102 | Task { 103 | let (data, _) = try await URLSession.shared.data(from: url) 104 | let image = NSImage(data: data) 105 | imageView.image = image 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-TVOS/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Demo-TVOS 4 | // 5 | // Created by zhaixian on 2023/12/13. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | var window: UIWindow? 13 | 14 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 15 | // Override point for customization after application launch. 16 | true 17 | } 18 | 19 | func applicationWillResignActive(_ application: UIApplication) { 20 | // 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. 21 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 22 | } 23 | 24 | func applicationDidEnterBackground(_ application: UIApplication) { 25 | // 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. 26 | } 27 | 28 | func applicationWillEnterForeground(_ application: UIApplication) { 29 | // 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. 30 | } 31 | 32 | func applicationDidBecomeActive(_ application: UIApplication) { 33 | // 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. 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-TVOS/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-TVOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-TVOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-TVOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "layers" : [ 7 | { 8 | "filename" : "Front.imagestacklayer" 9 | }, 10 | { 11 | "filename" : "Middle.imagestacklayer" 12 | }, 13 | { 14 | "filename" : "Back.imagestacklayer" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-TVOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-TVOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-TVOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-TVOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-TVOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "author" : "xcode", 14 | "version" : 1 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-TVOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-TVOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "layers" : [ 7 | { 8 | "filename" : "Front.imagestacklayer" 9 | }, 10 | { 11 | "filename" : "Middle.imagestacklayer" 12 | }, 13 | { 14 | "filename" : "Back.imagestacklayer" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-TVOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "author" : "xcode", 14 | "version" : 1 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-TVOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-TVOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "author" : "xcode", 14 | "version" : 1 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-TVOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-TVOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "assets" : [ 3 | { 4 | "filename" : "App Icon - App Store.imagestack", 5 | "idiom" : "tv", 6 | "role" : "primary-app-icon", 7 | "size" : "1280x768" 8 | }, 9 | { 10 | "filename" : "App Icon.imagestack", 11 | "idiom" : "tv", 12 | "role" : "primary-app-icon", 13 | "size" : "400x240" 14 | }, 15 | { 16 | "filename" : "Top Shelf Image Wide.imageset", 17 | "idiom" : "tv", 18 | "role" : "top-shelf-image-wide", 19 | "size" : "2320x720" 20 | }, 21 | { 22 | "filename" : "Top Shelf Image.imageset", 23 | "idiom" : "tv", 24 | "role" : "top-shelf-image", 25 | "size" : "1920x720" 26 | } 27 | ], 28 | "info" : { 29 | "author" : "xcode", 30 | "version" : 1 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-TVOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "author" : "xcode", 14 | "version" : 1 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-TVOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "author" : "xcode", 14 | "version" : 1 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-TVOS/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-TVOS/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 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-TVOS/Base.lproj/Main.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 | 29 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-TVOS/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Demo-TVOS 4 | // 5 | // Created by zhaixian on 2023/12/13. 6 | // 7 | 8 | import UIKit 9 | 10 | class ViewController: UIViewController { 11 | override func viewDidLoad() { 12 | super.viewDidLoad() 13 | // Do any additional setup after loading the view. 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-iOS/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Demo 4 | // 5 | // Created by zhaixian on 2023/11/23. 6 | // 7 | 8 | import UIKit 9 | import AliyunpanSDK 10 | 11 | @main 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | func applicationDidFinishLaunching(_ application: UIApplication) { 14 | Aliyunpan.setEnvironment(environment) 15 | Aliyunpan.setLogLevel(logLevel) 16 | 17 | client.cleanToken() 18 | } 19 | 20 | func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { 21 | Aliyunpan.handleOpenURL(url) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-iOS/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-iOS/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-iOS/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-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 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-iOS/Base.lproj/Main.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 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-iOS/FileCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileCell.swift 3 | // Demo 4 | // 5 | // Created by zhaixian on 2023/12/12. 6 | // 7 | 8 | import UIKit 9 | import AliyunpanSDK 10 | 11 | protocol FileCellDelegate: AnyObject { 12 | func fileCell(_ cell: FileCell, willOpen item: DisplayItem) 13 | func fileCell(_ cell: FileCell, willDownload item: DisplayItem) 14 | func fileCell(_ cell: FileCell, willPause item: DisplayItem) 15 | func fileCell(_ cell: FileCell, willResume item: DisplayItem) 16 | } 17 | 18 | class FileCell: UICollectionViewListCell { 19 | weak var delegate: FileCellDelegate? 20 | weak var client: AliyunpanClient? 21 | 22 | private var item: DisplayItem? 23 | 24 | private lazy var pauseButton: UIButton = { 25 | let pauseButton = UIButton() 26 | pauseButton.addTarget(self, action: #selector(pause), for: .touchUpInside) 27 | pauseButton.setImage(UIImage(systemName: "pause"), for: .normal) 28 | pauseButton.tintColor = .gray 29 | return pauseButton 30 | }() 31 | 32 | private lazy var downloadButton: UIButton = { 33 | let downloadButton = UIButton() 34 | downloadButton.addTarget(self, action: #selector(download), for: .touchUpInside) 35 | downloadButton.setImage(.add, for: .normal) 36 | return downloadButton 37 | }() 38 | 39 | private lazy var progressLabel: UILabel = { 40 | let progressLabel = UILabel() 41 | progressLabel.textColor = .gray 42 | progressLabel.font = .monospacedDigitSystemFont(ofSize: 12, weight: .regular) 43 | progressLabel.adjustsFontSizeToFitWidth = true 44 | return progressLabel 45 | }() 46 | 47 | private lazy var openButton: UIButton = { 48 | let openFileButton = UIButton() 49 | openFileButton.addTarget(self, action: #selector(openFile), for: .touchUpInside) 50 | openFileButton.setImage(.actions, for: .normal) 51 | return openFileButton 52 | }() 53 | 54 | @objc private func download() { 55 | guard let item else { 56 | return 57 | } 58 | delegate?.fileCell(self, willDownload: item) 59 | } 60 | 61 | @objc private func pause() { 62 | guard let item else { 63 | return 64 | } 65 | delegate?.fileCell(self, willPause: item) 66 | } 67 | 68 | @objc private func openFile() { 69 | guard let item else { 70 | return 71 | } 72 | delegate?.fileCell(self, willOpen: item) 73 | } 74 | 75 | func fill(_ item: DisplayItem) { 76 | self.item = item 77 | if item.file.isFolder { 78 | accessories = [.disclosureIndicator()] 79 | return 80 | } 81 | 82 | var views: [UIView] = [progressLabel] 83 | 84 | if let downloadState = item.downloadState { 85 | switch downloadState { 86 | case .waiting: 87 | progressLabel.text = "等待下载" 88 | 89 | views.append(pauseButton) 90 | case .downloading(let progress): 91 | progressLabel.text = "\(String(format: "%.2f", progress * 100))%" 92 | 93 | views.append(pauseButton) 94 | case .pause(let progress): 95 | progressLabel.text = "\(String(format: "%.2f", progress * 100))%" 96 | 97 | views.append(downloadButton) 98 | case .finished: 99 | progressLabel.text = nil 100 | 101 | views.append(openButton) 102 | case .failed: 103 | progressLabel.text = nil 104 | } 105 | } else { 106 | views.append(downloadButton) 107 | } 108 | 109 | accessories = views.map { 110 | UICellAccessory.customView( 111 | configuration: .init(customView: $0, placement: .trailing())) 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-iOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleURLTypes 6 | 7 | 8 | CFBundleTypeRole 9 | Editor 10 | CFBundleURLSchemes 11 | 12 | smartdriveYOUR_APP_ID 13 | 14 | 15 | 16 | LSApplicationQueriesSchemes 17 | 18 | smartdrive 19 | 20 | UIApplicationSceneManifest 21 | 22 | UIApplicationSupportsMultipleScenes 23 | 24 | UISceneConfigurations 25 | 26 | UIWindowSceneSessionRoleApplication 27 | 28 | 29 | UISceneConfigurationName 30 | Default Configuration 31 | UISceneDelegateClassName 32 | $(PRODUCT_MODULE_NAME).SceneDelegate 33 | UISceneStoryboardFile 34 | Main 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-iOS/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // Demo 4 | // 5 | // Created by zhaixian on 2023/11/23. 6 | // 7 | 8 | import UIKit 9 | import AliyunpanSDK 10 | 11 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 12 | var window: UIWindow? 13 | 14 | func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { 15 | if let url = URLContexts.first?.url { 16 | Aliyunpan.handleOpenURL(url) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-visionOS/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "vision", 5 | "scale" : "2x" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-visionOS/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-visionOS/Assets.xcassets/AppIcon.solidimagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "layers" : [ 7 | { 8 | "filename" : "Front.solidimagestacklayer" 9 | }, 10 | { 11 | "filename" : "Middle.solidimagestacklayer" 12 | }, 13 | { 14 | "filename" : "Back.solidimagestacklayer" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-visionOS/Assets.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "vision", 5 | "scale" : "2x" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-visionOS/Assets.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-visionOS/Assets.xcassets/AppIcon.solidimagestack/Middle.solidimagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "vision", 5 | "scale" : "2x" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-visionOS/Assets.xcassets/AppIcon.solidimagestack/Middle.solidimagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-visionOS/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Demo/Demo/Demo-visionOS/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // Demo-visionOS 4 | // 5 | // Created by zhaixian on 2024/2/19. 6 | // 7 | 8 | import SwiftUI 9 | import RealityKit 10 | import RealityKitContent 11 | import AliyunpanSDK 12 | 13 | struct AuthorizeView: View { 14 | @State var status: AliyunpanAuthorizeQRCodeStatus? 15 | @State var userInfo: AliyunpanScope.User.GetUsersInfo.Response? 16 | 17 | var isAuthorized: Bool { 18 | userInfo != nil 19 | } 20 | 21 | var body: some View { 22 | if !isAuthorized { 23 | Button("Authorize") { 24 | Task { 25 | do { 26 | let userInfo = try await client.authorize( 27 | credentials: .pkce) 28 | .send(AliyunpanScope.User.GetUsersInfo()) 29 | self.userInfo = userInfo 30 | } catch { 31 | print(error) 32 | } 33 | } 34 | } 35 | .font(.extraLargeTitle) 36 | } else { 37 | Text(userInfo?.name ?? "") 38 | .font(.extraLargeTitle) 39 | 40 | Text(userInfo?.id ?? "") 41 | .font(.largeTitle) 42 | } 43 | } 44 | } 45 | 46 | struct ContentView: View { 47 | @State var authorizeView = AuthorizeView() 48 | 49 | var body: some View { 50 | authorizeView 51 | } 52 | } 53 | 54 | #Preview(windowStyle: .automatic) { 55 | ContentView() 56 | } 57 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-visionOS/Demo_visionOSApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Demo_visionOSApp.swift 3 | // Demo-visionOS 4 | // 5 | // Created by zhaixian on 2024/2/19. 6 | // 7 | 8 | import SwiftUI 9 | import AliyunpanSDK 10 | 11 | class AppDelegate: NSObject, UIApplicationDelegate { 12 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { 13 | Aliyunpan.setEnvironment(environment) 14 | Aliyunpan.setLogLevel(logLevel) 15 | 16 | client.cleanToken() 17 | 18 | return true 19 | } 20 | } 21 | 22 | @main 23 | struct Demo_visionOSApp: App { 24 | @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate 25 | 26 | var body: some Scene { 27 | WindowGroup { 28 | ContentView() 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-visionOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSceneManifest 6 | 7 | UIApplicationPreferredDefaultSceneSessionRole 8 | UIWindowSceneSessionRoleApplication 9 | UIApplicationSupportsMultipleScenes 10 | 11 | UISceneConfigurations 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Demo/Demo/Demo-visionOS/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo/Demo/DisplayItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DisplayItem.swift 3 | // Demo 4 | // 5 | // Created by zhaixian on 2023/12/12. 6 | // 7 | 8 | import Foundation 9 | import AliyunpanSDK 10 | 11 | extension AliyunpanFile: Hashable { 12 | public func hash(into hasher: inout Hasher) { 13 | hasher.combine(name) 14 | } 15 | 16 | public static func == (lhs: AliyunpanFile, rhs: AliyunpanFile) -> Bool { 17 | lhs.hashValue == rhs.hashValue 18 | } 19 | } 20 | 21 | extension AliyunpanDownloadTask.State: Hashable { 22 | public func hash(into hasher: inout Hasher) { 23 | switch self { 24 | case .waiting: 25 | hasher.combine("waiting") 26 | case .downloading(let progress): 27 | hasher.combine("downloading") 28 | hasher.combine(progress) 29 | case .pause(let progress): 30 | hasher.combine("pause") 31 | hasher.combine(progress) 32 | case .finished(let url): 33 | hasher.combine("finished") 34 | hasher.combine(url) 35 | case .failed: 36 | hasher.combine("failed") 37 | } 38 | } 39 | 40 | public static func == (lhs: AliyunpanSDK.AliyunpanDownloadTask.State, rhs: AliyunpanSDK.AliyunpanDownloadTask.State) -> Bool { 41 | lhs.hashValue == rhs.hashValue 42 | } 43 | } 44 | 45 | struct DisplayItem: Hashable { 46 | let file: AliyunpanFile 47 | let downloadState: AliyunpanDownloadTask.State? 48 | 49 | public func hash(into hasher: inout Hasher) { 50 | hasher.combine(file) 51 | hasher.combine(downloadState) 52 | } 53 | 54 | public static func == (lhs: DisplayItem, rhs: DisplayItem) -> Bool { 55 | lhs.hashValue == rhs.hashValue 56 | } 57 | 58 | init(file: AliyunpanFile, downloadState: AliyunpanDownloadTask.State?) { 59 | self.file = file 60 | self.downloadState = downloadState 61 | } 62 | 63 | init(_ file: AliyunpanFile) { 64 | self.init(file: file, downloadState: nil) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Demo/Demo/Example.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Example.swift 3 | // Demo-MacOS 4 | // 5 | // Created by zhaixian on 2023/12/15. 6 | // 7 | 8 | import Foundation 9 | 10 | enum Example: String, CaseIterable { 11 | case getUserInfo = "获取用户信息" 12 | case getDriveInfo = "获取 Drive 信息" 13 | case getSpaceInfo = "获取空间信息" 14 | case getVIPInfo = "获取会员信息" 15 | case getVipFeatureList = "获取付费墙" 16 | case fetchFileList = "获取文件列表" 17 | case uploadFileToRoot = "上传文件到根目录" 18 | case createFolderOnRoot = "在根目录创建文件夹" 19 | } 20 | -------------------------------------------------------------------------------- /Demo/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.8 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | -------------------------------------------------------------------------------- /Demo/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - AliyunpanSDK (0.2.1) 3 | 4 | DEPENDENCIES: 5 | - AliyunpanSDK (from `../`) 6 | 7 | EXTERNAL SOURCES: 8 | AliyunpanSDK: 9 | :path: "../" 10 | 11 | SPEC CHECKSUMS: 12 | AliyunpanSDK: 8611714269b39ec8e2c41427f9b77a08fd85fc9b 13 | 14 | PODFILE CHECKSUM: 40334940ba0e4f081833162ea871b81051e79c17 15 | 16 | COCOAPODS: 1.15.2 17 | -------------------------------------------------------------------------------- /Demo/podfile: -------------------------------------------------------------------------------- 1 | target 'Demo-iOS' do 2 | use_frameworks! 3 | pod 'AliyunpanSDK', :path => "../" 4 | end 5 | 6 | target 'Demo-MacOS' do 7 | use_frameworks! 8 | pod 'AliyunpanSDK', :path => "../" 9 | end 10 | 11 | target 'Demo-TVOS' do 12 | use_frameworks! 13 | pod 'AliyunpanSDK', :path => "../" 14 | end 15 | 16 | target 'Demo-visionOS' do 17 | use_frameworks! 18 | pod 'AliyunpanSDK', :path => "../" 19 | end 20 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "fastlane" 4 | gem "cocoapods", ">= 1.15.2" 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-present Alibaba Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "AliyunpanSDK", 7 | platforms: [ 8 | .iOS(.v13), 9 | .macOS(.v10_15), 10 | .tvOS(.v13) 11 | ], 12 | products: [ 13 | .library( 14 | name: "AliyunpanSDK", 15 | targets: [ 16 | "AliyunpanSDK" 17 | ] 18 | ) 19 | ], 20 | targets: [ 21 | .target( 22 | name: "AliyunpanSDK" 23 | ), 24 | .testTarget( 25 | name: "AliyunpanSDKTests", 26 | dependencies: ["AliyunpanSDK"] 27 | ) 28 | ] 29 | ) 30 | -------------------------------------------------------------------------------- /Package@swift-5.9.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "AliyunpanSDK", 7 | platforms: [ 8 | .iOS(.v13), 9 | .macOS(.v10_15), 10 | .tvOS(.v13), 11 | .visionOS(.v1) 12 | ], 13 | products: [ 14 | .library( 15 | name: "AliyunpanSDK", 16 | targets: [ 17 | "AliyunpanSDK" 18 | ] 19 | ) 20 | ], 21 | targets: [ 22 | .target( 23 | name: "AliyunpanSDK" 24 | ), 25 | .testTarget( 26 | name: "AliyunpanSDKTests", 27 | dependencies: ["AliyunpanSDK"] 28 | ) 29 | ] 30 | ) 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

AliyunpanSDK

3 |

4 | 5 | 6 |

7 | 8 |

9 | This is the open-source SDK for Aliyunpan OpenAPI. 10 |

11 |

12 | 示例 13 | · 14 | 反馈 Bug 15 | · 16 | 提交需求 17 |

18 |
19 | 20 | ## 准备工作 21 | 22 | 在开始前,请查看阿里云盘开放平台接入指南: 23 | 24 | [👉 如何注册三方开发者](https://www.yuque.com/aliyundrive/zpfszx/tyzl591kxmft4e81) 25 | 26 | ## 快速开始 27 | 28 | ### 1. 创建 Client 29 | ```swift 30 | let client: AliyunpanClient = AliyunpanClient( 31 | .init( 32 | appId: "YOUR_APP_ID", 33 | scope: "YOUR_SCOPE", // e.g. user:base,file:all:read 34 | ) 35 | ) 36 | ``` 37 | 38 | ### 2. 授权 39 | 你可以使用 SDK 提供的多种授权方式授权 40 | #### [Credentials](https://alibaba.github.io/aliyunpan-ios-sdk/Enums/AliyunpanCredentials.html) 41 | 42 | | 授权方式 | 描述 | **不需要** Server | **不需要**阿里云盘客户端 | 43 | | :----: | :----: | :----: | :----: | 44 | | pkce | pkce 授权 | ✅ | ✅ | 45 | | sso | sso pkce 授权 | ✅ | ✅ | 46 | | server | 业务后端授权 | ❌ | ✅ | 47 | | qrCode | 二维码授权 | ✅ | ✅ | 48 | | token | 注入 token 授权 | ✅ | ✅ | 49 | 50 | ```swift 51 | client.authorize(credentials: credentials) 52 | ``` 53 | 54 | ### 3. 发送命令 55 | 56 | 使用 SDK,你可以轻松使用所有已提供的 OpenAPI 和它们的请求体、返回体模型 57 | 58 | ```swift 59 | // Concurrency 60 | try await client.send( 61 | AliyunpanScope.User.GetUsersInfo()) // -> GetUsersInfo.Response 62 | 63 | try await client.send( 64 | AliyunpanScope.File.GetFileList( 65 | .init(drive_id: driveId, parent_file_id: "root")))) // -> GetFileList.Response 66 | 67 | // Closure 68 | client.send( 69 | AliyunpanScope.User.GetUsersInfo()) { result in 70 | /// do something 71 | } 72 | ``` 73 | 74 | ## 高级功能 75 | 76 | ### 上传 77 | ```swift 78 | let uploader = client.uploader 79 | 80 | // 上传 81 | let task = Task { 82 | let file = try? await uploader.upload( 83 | fileURL: url, 84 | fileName: fileName, 85 | driveId: driveId, 86 | folderId: folderId, 87 | useProof: true // 是否开启快传 88 | ) 89 | } 90 | 91 | // 取消 92 | task.cancel() 93 | ``` 94 | 95 | ### 下载 96 | ```swift 97 | let downloader = client.downloader 98 | 99 | // 下载 100 | let task = downloader.download(file: file, to: destination) 101 | // let task = downloader.tasks.first 102 | 103 | // 修改并发数,默认为10 104 | downloader.maxConcurrentOperationCount = 10 105 | 106 | // 暂停 107 | downloader.pause(task) 108 | // 恢复 109 | downloader.resume(task) 110 | // 取消 111 | downloader.cancel(task) 112 | 113 | // AliyunpanDownloadDelegate 114 | // 下载速度变化 115 | // func downloader(_ downloader: AliyunpanDownloader, didUpdatedNetworkSpeed networkSpeed: Int64) 116 | // 下载任务状态变化 117 | // func downloader(_ downloader: AliyunpanDownloader, didUpdateTaskState state: AliyunpanDownloadTask.State, for task: AliyunpanDownloadTask) 118 | downloadr.addDelegate(DELEGATE) 119 | ``` 120 | 121 | #### 示例 122 | [FileListViewController](Demo/Demo/Demo-iOS/FileListViewController.swift) 123 | 124 | ## 安装方式 125 | 126 | #### Swift Package Manager 127 | 128 | - File > Swift Packages > Add Package Dependency 129 | - 添加 `https://github.com/alibaba/aliyunpan-ios-sdk.git` 130 | 131 | #### CocoaPods 132 | 133 | ```ruby 134 | target 'MyApp' do 135 | pod 'AliyunpanSDK', '~> 0.2' 136 | end 137 | ``` 138 | 139 | ## 要求 140 | 141 | - iOS 13.0+ (CocoaPods) 142 | - Swift 5.0+ 143 | 144 | ## 文档 145 | 146 | [👉 文档](https://alibaba.github.io/aliyunpan-ios-sdk/) 147 | 148 | ## TODO 149 | - Alamofire、URLSession 拓展 150 | 151 | ## License 152 | 153 | This project is licensed under the [MIT License](LICENSE). 154 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AliyunpanClient.swift 3 | // AliyunpanSDK 4 | // 5 | // Created by zhaixian on 2023/11/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct AliyunpanClientConfig { 11 | /// 应用 ID 12 | public let appId: String 13 | /// 申请权限 14 | public let scope: String 15 | /// 业务方自定义 id,可实现账号互绑 16 | public var identifier: String? 17 | 18 | /// - Parameters: 19 | /// - appId: 应用 ID 20 | /// - scope: 申请权限 21 | /// - identifier: 业务方自定义 id 22 | public init(appId: String, scope: String, identifier: String? = nil) { 23 | self.appId = appId 24 | self.scope = scope 25 | self.identifier = identifier 26 | } 27 | } 28 | 29 | public class AliyunpanClient { 30 | private let config: AliyunpanClientConfig 31 | 32 | private var tokenStorageKey: String { 33 | "com.aliyunpanSDK.accessToken_\(config.appId)_\(config.identifier ?? "-")" 34 | } 35 | 36 | @MainActor var token: AliyunpanToken? { 37 | willSet { 38 | if token != newValue, 39 | let data = try? JSONParameterEncoder().encode(newValue) { 40 | UserDefaults.standard.set(data, forKey: tokenStorageKey) 41 | } 42 | } 43 | } 44 | 45 | /// 获取当前持久化的 accessToken 46 | @MainActor public var accessToken: String? { 47 | token?.access_token 48 | } 49 | 50 | /// 下载器 51 | public lazy var downloader: AliyunpanDownloader = { 52 | let downloader = AliyunpanDownloader() 53 | downloader.client = self 54 | return downloader 55 | }() 56 | 57 | /// 上传器 58 | public lazy var uploader: AliyunpanUploader = { 59 | let uploader = AliyunpanUploader() 60 | uploader.client = self 61 | return uploader 62 | }() 63 | 64 | public init(_ config: AliyunpanClientConfig) { 65 | self.config = config 66 | 67 | if let tokenData = UserDefaults.standard.data(forKey: tokenStorageKey) { 68 | token = try? JSONParameterDecoder().decode(AliyunpanToken.self, from: tokenData) 69 | } 70 | } 71 | 72 | public convenience init(appId: String, scope: String, identifier: String? = nil) { 73 | self.init(.init(appId: appId, scope: scope, identifier: identifier)) 74 | } 75 | 76 | /// 强制清除 token 持久化 77 | @MainActor public func cleanToken() { 78 | token = nil 79 | } 80 | 81 | /// 授权 82 | /// 如本地持久化未过期会取持久化,否则会开始授权 83 | /// - Parameter credentials: 授权方式 84 | /// - Returns: token 85 | @discardableResult 86 | public func authorize( 87 | credentials: AliyunpanCredentials = .pkce 88 | ) async throws -> AliyunpanToken { 89 | if let token = await token, !token.isExpired { 90 | return token 91 | } 92 | let token = try await credentials.implement.authorize( 93 | appId: config.appId, 94 | scope: config.scope 95 | ) 96 | await MainActor.run { 97 | self.token = token 98 | } 99 | return token 100 | } 101 | 102 | /// 发送请求 103 | /// 104 | /// - throws: 105 | /// `DecodingError`: JSON 解析错误 106 | /// `AliyunpanAuthorizeError`: 授权错误 107 | /// `AliyunpanServerError`: 服务端错误 108 | /// `AliyunpanNetworkSystemError`: 网络系统错误 109 | public func send(_ command: T) async throws -> T.Response where T.Response: Decodable { 110 | guard let token = await token else { 111 | throw AliyunpanError.AuthorizeError.accessTokenInvalid 112 | } 113 | return try await token.send(command) 114 | } 115 | 116 | /// 发送请求 117 | /// 118 | /// - throws: 119 | /// `DecodingError`: JSON 解析错误 120 | /// `AliyunpanAuthorizeError`: 授权错误 121 | /// `AliyunpanServerError`: 服务端错误 122 | /// `AliyunpanNetworkSystemError`: 网络系统错误 123 | public func send( 124 | _ command: T, 125 | completionHandle: @escaping (Result) -> Void) where T.Response: Decodable { 126 | Task { 127 | do { 128 | let response = try await send(command) 129 | completionHandle(.success(response)) 130 | } catch { 131 | completionHandle(.failure(error)) 132 | } 133 | } 134 | } 135 | } 136 | 137 | extension AliyunpanToken { 138 | /// 发送请求 139 | /// 140 | /// - throws: 141 | /// `DecodingError`: JSON 解析错误 142 | /// `AliyunpanAuthorizeError`: 授权错误 143 | /// `AliyunpanServerError`: 服务端错误 144 | /// `AliyunpanNetworkSystemError`: 网络系统错误 145 | public func send(_ command: T) async throws -> T.Response where T.Response: Decodable { 146 | let result = try await HTTPRequest(command: command) 147 | .headers([.authorization(bearerToken: access_token)]) 148 | .response() 149 | return result 150 | } 151 | 152 | /// 发送请求 153 | /// 154 | /// - throws: 155 | /// `DecodingError`: JSON 解析错误 156 | /// `AliyunpanAuthorizeError`: 授权错误 157 | /// `AliyunpanServerError`: 服务端错误 158 | /// `AliyunpanNetworkSystemError`: 网络系统错误 159 | public func send( 160 | _ command: T, 161 | completionHandle: @escaping (Result) -> Void) where T.Response: Decodable { 162 | Task { 163 | do { 164 | let response = try await send(command) 165 | completionHandle(.success(response)) 166 | } catch { 167 | completionHandle(.failure(error)) 168 | } 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanCredentials/AliyunpanAuthenticator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AliyunpanAuthenticator.swift 3 | // AliyunpanSDK 4 | // 5 | // Created by zhaixian on 2023/11/23. 6 | // 7 | 8 | import Foundation 9 | import AuthenticationServices 10 | 11 | extension Notification.Name { 12 | static let hasReceivedMessage = Notification.Name("AliyunpanSDK.Notification.hasReceivedMessage") 13 | } 14 | 15 | /// 用于获取 AuthCode 16 | class AliyunpanAuthenticator: NSObject { 17 | private var webAuthorizeHandler: ((Result) -> Void)? 18 | 19 | @MainActor private func openURL(_ url: URL) async { 20 | await Platform.open(url) 21 | } 22 | 23 | /// 获取 AuthCode 24 | func authorize(_ url: URL) async throws -> String { 25 | let isInstalledApp = await Aliyunpan.isInstalled 26 | 27 | if isInstalledApp { 28 | return try await authorize(withAppLink: url) 29 | } else { 30 | // sso 授权 31 | return try await authorize(withSSO: url) 32 | } 33 | } 34 | 35 | /// 根据 AppLink 获取 AuthCode 36 | @MainActor 37 | func authorize(withAppLink appLink: URL) async throws -> String { 38 | var observer: NSObjectProtocol? 39 | defer { 40 | if let observer { 41 | NotificationCenter.default.removeObserver(observer) 42 | } 43 | } 44 | 45 | await openURL(appLink) 46 | 47 | let result = try await withCheckedThrowingContinuation { continuation in 48 | observer = NotificationCenter.default.addObserver(forName: .hasReceivedMessage, object: nil, queue: nil) { notification in 49 | guard let authMessage = notification.object as? AliyunpanAuthorizeMessage else { 50 | return 51 | } 52 | if let authCode = authMessage.authCode { 53 | continuation.resume(with: .success(authCode)) 54 | } else { 55 | continuation.resume(with: .failure( 56 | AliyunpanError.AuthorizeError.authorizeFailed( 57 | error: authMessage.error, 58 | errorMsg: authMessage.errorMsg))) 59 | } 60 | } 61 | } 62 | return result 63 | } 64 | 65 | @MainActor 66 | func authorize(withSSO url: URL) async throws -> String { 67 | let urlString = url.absoluteString.replacingOccurrences( 68 | of: "alipan.com/applink/authorize", 69 | with: "alipan.com/o/oauth/authorize") 70 | // TODO: - auto_login 服务修复后需要去除主动 auto_login 参数 71 | guard let url = URL(string: urlString + "&source=app_link&auto_login=true") else { 72 | throw AliyunpanError.AuthorizeError.invalidAuthorizeURL 73 | } 74 | return try await startAuthenticationSession(url) 75 | } 76 | 77 | func startAuthenticationSession(_ url: URL) async throws -> String { 78 | return try await withCheckedThrowingContinuation { continuation in 79 | if #available(iOS 13, macOS 10.15, tvOS 16.0, visionOS 1, *) { 80 | let session = ASWebAuthenticationSession(url: url, callbackURLScheme: "smartdrive") { url, error in 81 | if let error { 82 | continuation.resume(with: .failure(error)) 83 | return 84 | } 85 | 86 | let components = URLComponents(string: url?.absoluteString ?? "") 87 | if let code = components?.queryItems?.first(where: { $0.name == "code" })?.value { 88 | continuation.resume(with: .success(code)) 89 | } else { 90 | continuation.resume(with: .failure(AliyunpanError.AuthorizeError.invalidCode)) 91 | } 92 | } 93 | #if canImport(TVUIKit) 94 | #else 95 | session.presentationContextProvider = self 96 | #endif 97 | DispatchQueue.main.async { 98 | session.start() 99 | } 100 | } else { 101 | continuation.resume(with: .failure(AliyunpanError.AuthorizeError.invalidPlatform)) 102 | } 103 | } 104 | } 105 | 106 | static func handle(url: URL) -> Bool { 107 | guard let message = try? AliyunpanAuthorizeMessage(url) else { 108 | return false 109 | } 110 | NotificationCenter.default.post(name: .hasReceivedMessage, object: message) 111 | return true 112 | } 113 | } 114 | 115 | #if canImport(TVUIKit) 116 | #else 117 | extension AliyunpanAuthenticator: ASWebAuthenticationPresentationContextProviding { 118 | @MainActor 119 | func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { 120 | Platform.mainPresentationAnchor 121 | } 122 | } 123 | #endif 124 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanCredentials/AliyunpanCredentials.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AliyunpanCredentials.swift 3 | // AliyunpanSDK 4 | // 5 | // Created by zhaixian on 2023/11/21. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol AliyunpanCredentialsProtocol { 11 | func authorize(appId: String, scope: String) async throws -> AliyunpanToken 12 | } 13 | 14 | public enum AliyunpanCredentials { 15 | /// PKCE 授权,无需服务端,需要安装阿里云盘客户端 16 | /// 有阿里云盘客户端会唤端授权,否则会唤起 h5 进行 sso 授权 17 | /// https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow-with-proof-key-for-code-exchange-pkce 18 | case pkce 19 | /// 强制 sso pkce 授权,会唤起 h5 进行 sso 授权 20 | case sso 21 | /// 标准授权,需要服务端 22 | /// 有阿里云盘客户端会唤端授权,否则会唤起 h5 进行 sso 授权 23 | case server(AliyunpanBizServer) 24 | /// 二维码授权,无需服务端,无需安装阿里云盘客户端 25 | case qrCode(AliyunpanQRCodeContainer) 26 | /// 手动注入 token 27 | case token(AliyunpanToken) 28 | 29 | var implement: AliyunpanCredentialsProtocol { 30 | switch self { 31 | case .pkce: 32 | return AliyunpanPKCECredentials(forceSSO: false) 33 | case .sso: 34 | return AliyunpanPKCECredentials(forceSSO: true) 35 | case .server(let server): 36 | return AliyunpanServerCredentials(server) 37 | case .qrCode(let container): 38 | return AliyunpanQRCodeCredentials(container) 39 | case .token(let token): 40 | return AliyunpanTokenCredentials(token) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanCredentials/AliyunpanMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AliyunpanMessage.swift 3 | // 4 | // 5 | // Created by zhaixian on 2023/11/27. 6 | // 7 | 8 | import Foundation 9 | 10 | class AliyunpanMessage { 11 | let state: String 12 | let originalURL: URL 13 | 14 | init(_ url: URL) throws { 15 | let isSmartDriveScheme = url.scheme?.lowercased().starts(with: "smartdrive") == true 16 | let isSmartDriveUniversalLink = url.host?.hasSuffix(".alipan.com") == true 17 | guard isSmartDriveScheme || isSmartDriveUniversalLink else { 18 | throw AliyunpanError.AuthorizeError.invalidAuthorizeURL 19 | } 20 | let queryItems = url.queryItems 21 | originalURL = url 22 | state = queryItems.first(where: { $0.name == "state" })?.value ?? "Unknown" 23 | } 24 | } 25 | 26 | class AliyunpanAuthorizeMessage: AliyunpanMessage { 27 | let authCode: String? 28 | let error: String? 29 | let errorMsg: String? 30 | 31 | override init(_ url: URL) throws { 32 | let queryItems = url.queryItems 33 | authCode = queryItems.first(where: { $0.name == "code" })?.value 34 | error = queryItems.first(where: { $0.name == "error" })?.value 35 | errorMsg = queryItems.first(where: { $0.name == "errorMsg" })?.value 36 | try super.init(url) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanCredentials/AliyunpanPKCECredentials.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AliyunpanPKCECredentials.swift 3 | // AliyunpanSDK 4 | // 5 | // Created by zhaixian on 2023/11/21. 6 | // 7 | 8 | import Foundation 9 | 10 | class AliyunpanPKCECredentials: AliyunpanCredentialsProtocol { 11 | let codeVerifier: String 12 | let codeChallenge: String 13 | 14 | let forceSSO: Bool 15 | 16 | /// - Parameter forceSSO: 强制 SSO 17 | init(forceSSO: Bool) { 18 | codeVerifier = "\(Int.random(in: 43...128))" 19 | codeChallenge = AliyunpanCrypto.sha256AndBase64(codeVerifier) 20 | 21 | self.forceSSO = forceSSO 22 | } 23 | 24 | func authorize(appId: String, scope: String) async throws -> AliyunpanToken { 25 | let redirectUri = try await HTTPRequest( 26 | command: 27 | AliyunpanScope.Internal.Authorize( 28 | .init( 29 | client_id: appId, 30 | redirect_uri: "oob", 31 | scope: scope, 32 | response_type: "code", 33 | code_challenge: codeChallenge, 34 | code_challenge_method: "S256"))) 35 | .response() 36 | .redirectUri 37 | 38 | let authenticator = AliyunpanAuthenticator() 39 | 40 | let authCode: String 41 | if forceSSO { 42 | authCode = try await authenticator.authorize(withSSO: redirectUri) 43 | } else { 44 | authCode = try await authenticator.authorize(redirectUri) 45 | } 46 | var token = try await HTTPRequest(command: AliyunpanScope.Internal.GetAccessToken( 47 | .init( 48 | client_id: appId, 49 | grant_type: "authorization_code", 50 | code: authCode, 51 | code_verifier: codeVerifier))) 52 | .response() 53 | token.expires_in += Date().timeIntervalSince1970 54 | 55 | return token 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanCredentials/AliyunpanQRCodeCredentials.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AliyunpanQRCodeCredentials.swift 3 | // AliyunpanSDK 4 | // 5 | // Created by zhaixian on 2023/12/14. 6 | // 7 | 8 | import Foundation 9 | 10 | /// 二维码授权容器协议 11 | public protocol AliyunpanQRCodeContainer { 12 | /// 展示二维码 13 | @MainActor func showAliyunpanAuthorizeQRCode(with url: URL) 14 | 15 | /// 授权状态发生变化,可以在这里做一些 UI 变更 16 | @MainActor func authorizeQRCodeStatusUpdated(_ status: AliyunpanAuthorizeQRCodeStatus) 17 | } 18 | 19 | class AliyunpanQRCodeCredentials: AliyunpanCredentialsProtocol { 20 | let codeVerifier: String 21 | let codeChallenge: String 22 | 23 | private let container: AliyunpanQRCodeContainer 24 | 25 | init(_ container: AliyunpanQRCodeContainer) { 26 | codeVerifier = "\(Int.random(in: 43...128))" 27 | codeChallenge = AliyunpanCrypto.sha256AndBase64(codeVerifier) 28 | self.container = container 29 | } 30 | 31 | private func pollingWaitAuthorize(sid: String, timeout: TimeInterval) -> AsyncThrowingStream<(status: AliyunpanAuthorizeQRCodeStatus, authCode: String?), Error> { 32 | AsyncThrowingStream { continuation in 33 | Task { 34 | do { 35 | try await withTimeout(seconds: timeout) { 36 | var authCode: String? 37 | while authCode == nil, !Task.isCancelled { 38 | let response = try? await HTTPRequest( 39 | command: 40 | AliyunpanScope.Internal.GetAuthorizeQRCodeStatus(sid: sid)) 41 | .response() 42 | 43 | if response?.status == .qrCodeExpired { 44 | throw CancellationError() 45 | } 46 | 47 | if let response { 48 | authCode = response.authCode 49 | let status = response.status 50 | continuation.yield((status, authCode)) 51 | } 52 | 53 | if authCode == nil { 54 | try await Task.sleep(seconds: 1) 55 | } 56 | } 57 | continuation.finish() 58 | } 59 | } catch is CancellationError { 60 | continuation.finish(throwing: AliyunpanError.AuthorizeError.qrCodeAuthorizeTimeout) 61 | } catch { 62 | continuation.finish(throwing: error) 63 | } 64 | } 65 | } 66 | } 67 | 68 | func authorize(appId: String, scope: String) async throws -> AliyunpanToken { 69 | // 请求二维码 70 | let response = try await HTTPRequest( 71 | command: 72 | AliyunpanScope.Internal.GetAuthorizeQRCode( 73 | .init( 74 | client_id: appId, 75 | scopes: Array(scope.split(separator: ",").map { String($0) }), 76 | code_challenge: codeChallenge, 77 | code_challenge_method: "S256"))) 78 | .response() 79 | 80 | // 展示二维码 81 | await container.showAliyunpanAuthorizeQRCode(with: response.qrCodeUrl) 82 | 83 | // 轮询等待,180s有效 84 | var authCode: String? 85 | for try await result in pollingWaitAuthorize(sid: response.sid, timeout: 180) { 86 | authCode = result.authCode 87 | let status = result.status 88 | await container.authorizeQRCodeStatusUpdated(status) 89 | } 90 | 91 | if let authCode { 92 | // authcode 换 token 93 | var token = try await HTTPRequest(command: AliyunpanScope.Internal.GetAccessToken( 94 | .init( 95 | client_id: appId, 96 | grant_type: "authorization_code", 97 | code: authCode, 98 | code_verifier: codeVerifier))) 99 | .response() 100 | token.expires_in += Date().timeIntervalSince1970 101 | return token 102 | } else { 103 | // 实际不会走到 104 | throw AliyunpanError.AuthorizeError.qrCodeAuthorizeTimeout 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanCredentials/AliyunpanServerCredentials.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AliyunpanServerCredentials.swift 3 | // AliyunpanSDK 4 | // 5 | // Created by zhaixian on 2023/11/21. 6 | // 7 | 8 | import Foundation 9 | 10 | /// 三方业务 Server 抽象协议 11 | public protocol AliyunpanBizServer { 12 | /// 根据 appId、authCode 请求 token 13 | func requestToken(appId: String, authCode: String) async throws -> AliyunpanToken 14 | } 15 | 16 | class AliyunpanServerCredentials: AliyunpanCredentialsProtocol { 17 | private let server: AliyunpanBizServer 18 | 19 | init(_ server: AliyunpanBizServer) { 20 | self.server = server 21 | } 22 | 23 | func authorize(appId: String, scope: String) async throws -> AliyunpanToken { 24 | let redirectUri = try await HTTPRequest( 25 | command: 26 | AliyunpanScope.Internal.Authorize( 27 | .init( 28 | client_id: appId, 29 | redirect_uri: "oob", 30 | scope: scope, 31 | response_type: "code"))) 32 | .response() 33 | .redirectUri 34 | 35 | let authenticator = AliyunpanAuthenticator() 36 | let authCode = try await authenticator.authorize(redirectUri) 37 | 38 | var token = try await server.requestToken(appId: appId, authCode: authCode) 39 | token.expires_in += Date().timeIntervalSince1970 40 | return token 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanCredentials/AliyunpanTokenCredentials.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AliyunpanTokenCredentials.swift 3 | // AliyunpanSDK 4 | // 5 | // Created by zhaixian on 2024/1/16. 6 | // 7 | 8 | import Foundation 9 | 10 | class AliyunpanTokenCredentials: AliyunpanCredentialsProtocol { 11 | let token: AliyunpanToken 12 | 13 | init(_ token: AliyunpanToken) { 14 | self.token = token 15 | } 16 | 17 | func authorize(appId: String, scope: String) async throws -> AliyunpanToken { 18 | token 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AliyunpanError.swift 3 | // AliyunpanSDK 4 | // 5 | // Created by zhaixian on 2023/11/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct AliyunpanError { 11 | /// 授权错误 12 | public enum AuthorizeError: Error { 13 | /// 错误的授权链接 14 | case invalidAuthorizeURL 15 | /// 当前设备未安装阿里云盘 16 | case notInstalledApp 17 | /// 授权错误 18 | case authorizeFailed(error: String?, errorMsg: String?) 19 | /// 验证码授权超时 20 | case qrCodeAuthorizeTimeout 21 | /// 未授权或授权已过期 22 | case accessTokenInvalid 23 | /// 授权 code 错误 24 | case invalidCode 25 | /// 当前平台不支持 26 | case invalidPlatform 27 | } 28 | 29 | /// 网络层错误 30 | public struct ServerError: Error, Decodable { 31 | public enum Code: String, Decodable { 32 | /// 二维码过期 33 | case qrCodeExpired = "QRCodeExpired" 34 | /// 容量超限 35 | case quotaExhaustedDrive = "QuotaExhausted.Drive" 36 | /// access_token 过期 37 | case accessTokenExpired = "AccessTokenExpired" 38 | /// access_token 格式不对 39 | case accessTokenInvalid = "AccessTokenInvalid" 40 | /// refresh_token 过期 41 | case refreshTokenExpired = "RefreshTokenExpired" 42 | /// refresh_token 格式不对 43 | case refreshTokenInvalid = "RefreshTokenInvalid" 44 | /// 用户已取消授权,或权限已失效,或 token 无效。需要重新发起授权 45 | case permissionDenied = "PermissionDenied" 46 | /// 回收站文件不允许操作 47 | case forbiddenFileInTheRecycleBin = "ForbiddenFileInTheRecycleBin" 48 | /// 用户容量超限,限制播放,需要扩容或者删除不必要的文件释放空间 49 | case exceedCapacityForbidden = "ExceedCapacityForbidden" 50 | /// 文件找不到 51 | case notFound = "NotFound.FileId" 52 | /// 请求过快 53 | case tooManyRequests = "TooManyRequests" 54 | /// 应用不存在 55 | case appNotExists = "AppNotExists" 56 | /// 应用密钥不对 57 | case invalidClientSecret = "InvalidClientSecret" 58 | /// 授权码为空或过期 59 | case invalidCode = "InvalidCode" 60 | /// 应用ID和构造授权链接时填的不一致 61 | case invalidClientId = "InvalidClientId" 62 | /// 无效的担保类型,目前仅支持 authorization_code 和 refresh_token 63 | case invalidGrantType = "InvalidGrantType" 64 | /// 文件drive被锁,操作无法执行 65 | case forbiddenDriveLocked = "ForbiddenDriveLocked" 66 | /// 非法访问drive 67 | case forbiddenDriveNotValid = "ForbiddenDriveNotValid" 68 | /// 快传预检匹配成功 69 | case preHashMatched = "PreHashMatched" 70 | } 71 | 72 | public let code: Code 73 | public let message: String? 74 | public let requestId: String? 75 | 76 | public var isAccessTokenInvalidOrExpired: Bool { 77 | code == .accessTokenExpired || code == .accessTokenInvalid 78 | } 79 | } 80 | 81 | /// 下载错误 82 | public enum DownloadError: Error { 83 | /// 下载链接过期 84 | case downloadURLExpired 85 | /// 错误的下载链接 86 | case invalidDownloadURL 87 | /// 主动取消 88 | case userCancelled 89 | /// 缺少 client 90 | case invalidClient 91 | /// 服务器错误 92 | case serverError 93 | /// 未知错误 94 | case unknownError 95 | } 96 | 97 | /// 上传错误 98 | public enum UploadError: Error { 99 | /// 缺少 client 100 | case invalidClient 101 | /// 快传预检查失败 102 | case preHashNotMatched 103 | } 104 | 105 | /// 系统级网络层错误 106 | public enum NetworkSystemError: Error { 107 | case invalidURL 108 | case httpError(statusCode: Int, data: Data, response: HTTPURLResponse) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanLogger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AliyunpanLogger.swift 3 | // AliyunpanSDK 4 | // 5 | // Created by zhaixian on 2024/3/14. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum AliyunpanLogLevel: Int { 11 | case debug 12 | case info 13 | case warn 14 | case error 15 | 16 | var msg: String { 17 | switch self { 18 | case .debug: 19 | return "DEBUG" 20 | case .info: 21 | return "INFO" 22 | case .warn: 23 | return "⚠️" 24 | case .error: 25 | return "❌" 26 | } 27 | } 28 | } 29 | 30 | class Logger { 31 | static func log(_ level: AliyunpanLogLevel, msg: String) { 32 | guard level.rawValue >= Aliyunpan.logLevel.rawValue else { 33 | return 34 | } 35 | print("[AliyunpanSDK][\(level.msg)]\(msg)") 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanSDK.h: -------------------------------------------------------------------------------- 1 | // 2 | // AliyunpanSDK.h 3 | // AliyunpanSDK 4 | // 5 | // Created by zhaixian on 2023/11/16. 6 | // 7 | 8 | #import 9 | 10 | //! Project version number for AliyunpanSDK. 11 | FOUNDATION_EXPORT double AliyunpanSDKVersionNumber; 12 | 13 | //! Project version string for AliyunpanSDK. 14 | FOUNDATION_EXPORT const unsigned char AliyunpanSDKVersionString[]; 15 | 16 | // In this header, you should import all the public headers of your framework using statements like #import 17 | 18 | 19 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanSDK.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AliyunpanSDK.swift 3 | // AliyunpanSDK 4 | // 5 | // Created by zhaixian on 2023/11/16. 6 | // 7 | 8 | import Foundation 9 | 10 | public class Aliyunpan { 11 | public enum Environment { 12 | /// 预发 13 | case pre 14 | /// 线上 15 | case product 16 | 17 | var host: String { 18 | switch self { 19 | case .pre: 20 | return "https://stg-openapi.alipan.com" 21 | case .product: 22 | return "https://openapi.alipan.com" 23 | } 24 | } 25 | } 26 | 27 | /// 是否已安装阿里云盘 28 | @MainActor 29 | public static var isInstalled: Bool { 30 | guard let url = URL(string: "smartdrive://") else { 31 | return false 32 | } 33 | return Platform.canOpenURL(url) 34 | } 35 | 36 | private(set) static var logLevel: AliyunpanLogLevel = .warn 37 | public static func setLogLevel(_ level: AliyunpanLogLevel) { 38 | logLevel = level 39 | } 40 | 41 | public private(set) static var env: Environment = .product 42 | 43 | #if DEBUG 44 | public static func setEnvironment(_ env: Environment) { 45 | self.env = env 46 | } 47 | #endif 48 | 49 | @discardableResult 50 | public static func handleOpenURL(_ url: URL) -> Bool { 51 | AliyunpanAuthenticator.handle(url: url) 52 | } 53 | } 54 | 55 | let version = "0.3.5" 56 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanScope/AliyunpanCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AliyunpanCommand.swift 3 | // AliyunpanSDK 4 | // 5 | // Created by zhaixian on 2023/11/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol AliyunpanCommand { 11 | associatedtype Request 12 | associatedtype Response: Decodable 13 | 14 | var httpMethod: HTTPMethod { get } 15 | var uri: String { get } 16 | 17 | var request: Request? { get } 18 | var requestData: Data? { get } 19 | } 20 | 21 | extension AliyunpanCommand where Request == Void { 22 | public var request: Request? { nil } 23 | public var requestData: Data? { nil } 24 | } 25 | 26 | extension AliyunpanCommand where Request: Encodable { 27 | public var requestData: Data? { 28 | if let request { 29 | return try? JSONParameterEncoder().encode(request) 30 | } else { 31 | return nil 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanScope/AliyunpanScope.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AliyunpanScope.swift 3 | // AliyunpanSDK 4 | // 5 | // Created by zhaixian on 2023/11/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct AliyunpanScope { 11 | public typealias User = AliyunpanUserScope 12 | public typealias VIP = AliyunpanVIPScope 13 | public typealias File = AliyunpanFileScope 14 | public typealias Video = AliyunpanVideoScope 15 | typealias Internal = AliyunpanInternalScope 16 | } 17 | 18 | public class AliyunpanUserScope {} 19 | 20 | public class AliyunpanVIPScope {} 21 | 22 | public class AliyunpanFileScope { 23 | public enum OrderBy: String, Codable { 24 | case created_at 25 | case updated_at 26 | case name 27 | case size 28 | case name_enhanced 29 | } 30 | 31 | public enum OrderDirection: String, Codable { 32 | case desc = "DESC" 33 | case asc = "ASC" 34 | } 35 | } 36 | 37 | public class AliyunpanVideoScope {} 38 | 39 | class AliyunpanInternalScope {} 40 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanScope/File/BatchGet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BatchGet.swift 3 | // AliyunpanSDK 4 | // gen code 5 | // 6 | 7 | import Foundation 8 | 9 | extension AliyunpanFileScope { 10 | /// 批量获取文件详情 11 | public class BatchGet: AliyunpanCommand { 12 | public var httpMethod: HTTPMethod { .post } 13 | public var uri: String { 14 | "/adrive/v1.0/openFile/batch/get" 15 | } 16 | 17 | public struct Request: Codable { 18 | public struct Item: Codable { 19 | public let drive_id: String 20 | public let file_id: String 21 | 22 | public init(drive_id: String, file_id: String) { 23 | self.drive_id = drive_id 24 | self.file_id = file_id 25 | } 26 | } 27 | 28 | public let file_list: [Item] 29 | 30 | public init(file_list: [Item]) { 31 | self.file_list = file_list 32 | } 33 | } 34 | 35 | public struct Response: Codable { 36 | public let items: [AliyunpanFile] 37 | } 38 | 39 | public let request: Request? 40 | public init(_ request: Request) { 41 | self.request = request 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanScope/File/CompleteUpload.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompleteUpload.swift 3 | // AliyunpanSDK 4 | // gen code 5 | // 6 | 7 | import Foundation 8 | 9 | extension AliyunpanFileScope { 10 | /// 标记文件上传完毕 11 | public class CompleteUpload: AliyunpanCommand { 12 | public var httpMethod: HTTPMethod { .post } 13 | public var uri: String { 14 | "/adrive/v1.0/openFile/complete" 15 | } 16 | 17 | public struct Request: Codable { 18 | /// drive id 19 | public let drive_id: String 20 | /// file_id 21 | public let file_id: String 22 | /// 文件创建获取的upload_id 23 | public let upload_id: String 24 | 25 | public init(drive_id: String, file_id: String, upload_id: String) { 26 | self.drive_id = drive_id 27 | self.file_id = file_id 28 | self.upload_id = upload_id 29 | } 30 | } 31 | 32 | public typealias Response = AliyunpanFile 33 | 34 | public let request: Request? 35 | public init(_ request: Request) { 36 | self.request = request 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanScope/File/CopyFile.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CopyFile.swift 3 | // AliyunpanSDK 4 | // gen code 5 | // 6 | 7 | import Foundation 8 | 9 | extension AliyunpanFileScope { 10 | /// 复制文件或文件夹 11 | public class CopyFile: AliyunpanCommand { 12 | public var httpMethod: HTTPMethod { .post } 13 | public var uri: String { 14 | "/adrive/v1.0/openFile/copy" 15 | } 16 | 17 | public struct Request: Codable { 18 | /// drive id 19 | public let drive_id: String 20 | /// file_id 21 | public let file_id: String 22 | /// 父文件ID、根目录为 root 23 | public let to_parent_file_id: String 24 | /// 目标drive,默认是当前drive_id 25 | public let to_drive_id: String? 26 | /// 当目标文件夹下存在同名文件时,是否自动重命名,默认为 false,默认允许同名文件 27 | public let auto_rename: Bool? 28 | 29 | public init(drive_id: String, file_id: String, to_parent_file_id: String, to_drive_id: String? = nil, auto_rename: Bool? = nil) { 30 | self.drive_id = drive_id 31 | self.file_id = file_id 32 | self.to_parent_file_id = to_parent_file_id 33 | self.to_drive_id = to_drive_id 34 | self.auto_rename = auto_rename 35 | } 36 | } 37 | 38 | public struct Response: Codable { 39 | /// drive id 40 | public let drive_id: String 41 | /// file_id 42 | public let file_id: String 43 | /// 异步任务id。当复制的是文件时,不返回该字段;当复制的是文件夹时,为后台异步复制,会返回该字段 44 | public var async_task_id: String? 45 | } 46 | 47 | public let request: Request? 48 | public init(_ request: Request) { 49 | self.request = request 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanScope/File/CreateFile.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CreateFile.swift 3 | // AliyunpanSDK 4 | // gen code 5 | // 6 | 7 | import Foundation 8 | 9 | extension AliyunpanFileScope { 10 | /// 创建文件 11 | public class CreateFile: AliyunpanCommand { 12 | public var httpMethod: HTTPMethod { .post } 13 | public var uri: String { 14 | "/adrive/v1.0/openFile/create" 15 | } 16 | 17 | public struct Request: Codable { 18 | /// drive id 19 | public let drive_id: String 20 | /// 根目录为root 21 | public let parent_file_id: String 22 | /// 文件名称,按照 utf8 编码最长 1024 字节,不能以 / 结尾 23 | public let name: String 24 | /// file | folder 25 | public let type: AliyunpanFile.FileType 26 | /// 文件类型 27 | public let content_type: String? 28 | /// 重名策略 29 | public let check_name_mode: AliyunpanFile.CheckNameMode 30 | /// 最大分片数量 10000 31 | public let part_info_list: [AliyunpanFile.PartInfo]? 32 | /// 仅上传livp格式的时候需要,常见场景不需要 33 | public let streams_info: AliyunpanFile.StreamsInfo? 34 | /// 针对大文件sha1计算非常耗时的情况, 可以先在读取文件的前1k的sha1, 如果前1k的sha1没有匹配的, 那么说明文件无法做秒传, 如果1ksha1有匹配再计算文件sha1进行秒传,这样有效边避免无效的sha1计算。 35 | public let pre_hash: String? 36 | /// 秒传必须, 文件大小,单位为 byte 37 | public let size: Int? 38 | /// 秒传必须, 文件内容 hash 值,需要根据 content_hash_name 指定的算法计算,当前都是sha1算法 39 | public let content_hash: String? 40 | /// 秒传必须, 默认都是 sha1 41 | public let content_hash_name: String? 42 | /// 秒传必须 43 | public let proof_code: String? 44 | /// 固定 v1 45 | public let proof_version: String? 46 | /// 本地创建时间,格式yyyy-MM-dd'T'HH:mm:ss.SSS'Z' 47 | public let local_created_at: Date? 48 | /// 本地修改时间,格式yyyy-MM-dd'T'HH:mm:ss.SSS'Z' 49 | public let local_modified_at: Date? 50 | 51 | public init( 52 | drive_id: String, 53 | parent_file_id: String, 54 | name: String, 55 | type: AliyunpanFile.FileType = .file, 56 | content_type: String? = nil, 57 | check_name_mode: AliyunpanFile.CheckNameMode, 58 | part_info_list: [AliyunpanFile.PartInfo]? = nil, 59 | streams_info: AliyunpanFile.StreamsInfo? = nil, 60 | pre_hash: String? = nil, 61 | size: Int? = nil, 62 | content_hash: String? = nil, 63 | content_hash_name: String? = nil, 64 | proof_code: String? = nil, 65 | proof_version: String? = nil, 66 | local_created_at: Date? = nil, 67 | local_modified_at: Date? = nil) { 68 | self.drive_id = drive_id 69 | self.parent_file_id = parent_file_id 70 | self.name = name 71 | self.type = type 72 | self.content_type = content_type 73 | self.check_name_mode = check_name_mode 74 | self.part_info_list = part_info_list 75 | self.streams_info = streams_info 76 | self.pre_hash = pre_hash 77 | self.size = size 78 | self.content_hash = content_hash 79 | self.content_hash_name = content_hash_name 80 | self.proof_code = proof_code 81 | self.proof_version = proof_version 82 | self.local_created_at = local_created_at 83 | self.local_modified_at = local_modified_at 84 | } 85 | } 86 | 87 | public struct Response: Codable { 88 | public let drive_id: String 89 | public let file_id: String 90 | public let status: String? 91 | public let parent_file_id: String 92 | public let file_name: String 93 | public let available: Bool? 94 | /// 是否存在同名文件 95 | public let exist: Bool? 96 | /// 是否秒传 97 | public let rapid_upload: Bool? 98 | public let part_info_list: [AliyunpanFile.PartInfo]? 99 | /// 创建文件夹返回空 100 | public var upload_id: String? 101 | } 102 | 103 | public let request: Request? 104 | public init(_ request: Request) { 105 | self.request = request 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanScope/File/DeleteFile.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeleteFile.swift 3 | // AliyunpanSDK 4 | // gen code 5 | // 6 | 7 | import Foundation 8 | 9 | extension AliyunpanFileScope { 10 | /// 删除文件 11 | public class DeleteFile: AliyunpanCommand { 12 | public var httpMethod: HTTPMethod { .post } 13 | public var uri: String { 14 | "/adrive/v1.0/openFile/delete" 15 | } 16 | 17 | public struct Request: Codable { 18 | /// drive id 19 | public let drive_id: String 20 | /// file_id 21 | public let file_id: String 22 | 23 | public init(drive_id: String, file_id: String) { 24 | self.drive_id = drive_id 25 | self.file_id = file_id 26 | } 27 | } 28 | 29 | public struct Response: Codable { 30 | /// drive id 31 | public let drive_id: String 32 | /// file_id 33 | public let file_id: String 34 | /// 异步任务id,有的话表示需要经过异步处理。 35 | public var async_task_id: String? 36 | } 37 | 38 | public let request: Request? 39 | public init(_ request: Request) { 40 | self.request = request 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanScope/File/GetAsyncTask.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetAsyncTask.swift 3 | // AliyunpanSDK 4 | // gen code 5 | // 6 | 7 | import Foundation 8 | 9 | extension AliyunpanFileScope { 10 | /// 获取异步任务状态 11 | public class GetAsyncTask: AliyunpanCommand { 12 | public var httpMethod: HTTPMethod { .post } 13 | public var uri: String { 14 | "/adrive/v1.0/openFile/async_task/get" 15 | } 16 | 17 | public struct Request: Codable { 18 | /// 异步任务ID 19 | public let async_task_id: String 20 | 21 | public init(async_task_id: String) { 22 | self.async_task_id = async_task_id 23 | } 24 | } 25 | 26 | public struct Response: Codable { 27 | /// Succeed 成功,Running 处理中,Failed 已失败 28 | public let state: String 29 | /// 异步任务id。 30 | public let async_task_id: String 31 | } 32 | 33 | public let request: Request? 34 | public init(_ request: Request) { 35 | self.request = request 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanScope/File/GetFile.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetFile.swift 3 | // AliyunpanSDK 4 | // gen code 5 | // 6 | 7 | import Foundation 8 | 9 | extension AliyunpanFileScope { 10 | /// 获取文件详情 11 | public class GetFile: AliyunpanCommand { 12 | public var httpMethod: HTTPMethod { .post } 13 | public var uri: String { 14 | "/adrive/v1.0/openFile/get" 15 | } 16 | 17 | public struct Request: Codable { 18 | /// drive id 19 | public let drive_id: String 20 | /// file_id 21 | public let file_id: String 22 | /// 生成的视频缩略图截帧时间,单位ms,默认120000ms 23 | public var video_thumbnail_time: Int? 24 | /// 生成的视频缩略图宽度,默认480px 25 | public var video_thumbnail_width: Int? 26 | /// 生成的图片缩略图宽度,默认480px 27 | public var image_thumbnail_width: Int? 28 | 29 | public init(drive_id: String, file_id: String, video_thumbnail_time: Int? = nil, video_thumbnail_width: Int? = nil, image_thumbnail_width: Int? = nil) { 30 | self.drive_id = drive_id 31 | self.file_id = file_id 32 | self.video_thumbnail_time = video_thumbnail_time 33 | self.video_thumbnail_width = video_thumbnail_width 34 | self.image_thumbnail_width = image_thumbnail_width 35 | } 36 | } 37 | 38 | public typealias Response = AliyunpanFile 39 | 40 | public let request: Request? 41 | public init(_ request: Request) { 42 | self.request = request 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanScope/File/GetFileByPath.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetFileByPath.swift 3 | // AliyunpanSDK 4 | // gen code 5 | // 6 | 7 | import Foundation 8 | 9 | extension AliyunpanFileScope { 10 | /// 根据文件路径查找文件 11 | public class GetFileByPath: AliyunpanCommand { 12 | public var httpMethod: HTTPMethod { .post } 13 | public var uri: String { 14 | "/adrive/v1.0/openFile/get_by_path" 15 | } 16 | 17 | public struct Request: Codable { 18 | /// drive id 19 | public let drive_id: String 20 | /// file_path 21 | public let file_path: String 22 | 23 | public init(drive_id: String, file_path: String) { 24 | self.drive_id = drive_id 25 | self.file_path = file_path 26 | } 27 | } 28 | 29 | public typealias Response = AliyunpanFile 30 | 31 | public let request: Request? 32 | public init(_ request: Request) { 33 | self.request = request 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanScope/File/GetFileDownloadUrl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetFileDownloadUrl.swift 3 | // AliyunpanSDK 4 | // gen code 5 | // 6 | 7 | import Foundation 8 | 9 | extension AliyunpanFileScope { 10 | /// 获取文件下载详情 11 | public class GetFileDownloadUrl: AliyunpanCommand { 12 | public var httpMethod: HTTPMethod { .post } 13 | public var uri: String { 14 | "/adrive/v1.0/openFile/getDownloadUrl" 15 | } 16 | 17 | public struct Request: Codable { 18 | /// drive id 19 | public let drive_id: String 20 | /// file_id 21 | public let file_id: String 22 | /// 下载地址过期时间,单位为秒,默认为 900 秒, 最长4h(14400秒) 23 | public var expire_sec: Int? 24 | 25 | public init(drive_id: String, file_id: String, expire_sec: Int? = nil) { 26 | self.drive_id = drive_id 27 | self.file_id = file_id 28 | self.expire_sec = expire_sec 29 | } 30 | } 31 | 32 | public struct Response: Codable { 33 | /// 下载地址 34 | public let url: URL 35 | /// 过期时间 格式:yyyy-MM-dd'T'HH:mm:ss.SSS'Z' 36 | public let expiration: Date 37 | /// 下载方法 38 | public let method: String 39 | } 40 | 41 | public let request: Request? 42 | public init(_ request: Request) { 43 | self.request = request 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanScope/File/GetFileList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetFileList.swift 3 | // AliyunpanSDK 4 | // gen code 5 | // 6 | 7 | import Foundation 8 | 9 | extension AliyunpanFileScope { 10 | /// 获取文件列表 11 | public class GetFileList: AliyunpanCommand { 12 | public var httpMethod: HTTPMethod { .post } 13 | public var uri: String { 14 | "/adrive/v1.0/openFile/list" 15 | } 16 | 17 | public struct Request: Codable { 18 | /// drive id 19 | public let drive_id: String 20 | /// 根目录为root 21 | public let parent_file_id: String 22 | /// 返回文件数量,默认 50,最大 100 23 | public var limit: Int? 24 | /// 分页标记 25 | public var marker: String? 26 | /// created_at, updated_at, name, size, name_enhanced 27 | public var order_by: OrderBy? 28 | /// DESC ASC 29 | public var order_direction: OrderDirection? 30 | /// 分类,目前有枚举:video | doc | audio | zip | others | image, 可任意组合,按照逗号分割,例如 video,doc,audio, image,doc 31 | public var category: AliyunpanFile.FileCategory? 32 | /// all | file | folder, 默认所有类型, type为folder时,category不做检查 33 | public var type: AliyunpanFile.FileType? 34 | /// 生成的视频缩略图截帧时间,单位ms,默认120000ms 35 | public var video_thumbnail_time: Int? 36 | /// 生成的视频缩略图宽度,默认480px 37 | public var video_thumbnail_width: Int? 38 | /// 生成的图片缩略图宽度,默认480px 39 | public var image_thumbnail_width: Int? 40 | /// 当填 * 时,返回文件所有字段 41 | public var fields: String? 42 | 43 | public init(drive_id: String, parent_file_id: String, limit: Int? = nil, marker: String? = nil, order_by: OrderBy? = nil, order_direction: OrderDirection? = nil, category: AliyunpanFile.FileCategory? = nil, type: AliyunpanFile.FileType? = nil, video_thumbnail_time: Int? = nil, video_thumbnail_width: Int? = nil, image_thumbnail_width: Int? = nil, fields: String? = nil) { 44 | self.drive_id = drive_id 45 | self.parent_file_id = parent_file_id 46 | self.limit = limit 47 | self.marker = marker 48 | self.order_by = order_by 49 | self.order_direction = order_direction 50 | self.category = category 51 | self.type = type 52 | self.video_thumbnail_time = video_thumbnail_time 53 | self.video_thumbnail_width = video_thumbnail_width 54 | self.image_thumbnail_width = image_thumbnail_width 55 | self.fields = fields 56 | } 57 | } 58 | 59 | public struct Response: Codable { 60 | public let items: [AliyunpanFile] 61 | /// 下个分页标记 62 | public var next_marker: String? 63 | } 64 | 65 | public let request: Request? 66 | public init(_ request: Request) { 67 | self.request = request 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanScope/File/GetStarredList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetStarredList.swift 3 | // AliyunpanSDK 4 | // gen code 5 | // 6 | 7 | import Foundation 8 | 9 | extension AliyunpanFileScope { 10 | /// 获取收藏文件列表 11 | public class GetStarredList: AliyunpanCommand { 12 | public var httpMethod: HTTPMethod { .post } 13 | public var uri: String { 14 | "/adrive/v1.0/openFile/starredList" 15 | } 16 | 17 | public struct Request: Codable { 18 | /// drive id 19 | public let drive_id: String 20 | /// 默认100,最大 100 21 | public var limit: Int? 22 | /// 分页标记 23 | public var marker: String? 24 | /// created_at, updated_at, name, size 25 | public var order_by: OrderBy? 26 | /// DESC ASC 27 | public var order_direction: OrderDirection? 28 | /// 生成的视频缩略图截帧时间,单位ms,默认120000ms 29 | public var video_thumbnail_time: Int? 30 | /// 生成的视频缩略图宽度,默认480px 31 | public var video_thumbnail_width: Int? 32 | /// 生成的图片缩略图宽度,默认480px 33 | public var image_thumbnail_width: Int? 34 | /// file 或 folder , 默认所有类型 35 | public var type: AliyunpanFile.FileType? 36 | 37 | public init(drive_id: String, limit: Int? = nil, marker: String? = nil, order_by: OrderBy? = nil, order_direction: OrderDirection? = nil, video_thumbnail_time: Int? = nil, video_thumbnail_width: Int? = nil, image_thumbnail_width: Int? = nil, type: AliyunpanFile.FileType? = nil) { 38 | self.drive_id = drive_id 39 | self.limit = limit 40 | self.marker = marker 41 | self.order_by = order_by 42 | self.order_direction = order_direction 43 | self.video_thumbnail_time = video_thumbnail_time 44 | self.video_thumbnail_width = video_thumbnail_width 45 | self.image_thumbnail_width = image_thumbnail_width 46 | self.type = type 47 | } 48 | } 49 | 50 | public struct Response: Codable { 51 | public let items: [AliyunpanFile] 52 | /// 下个分页标记 53 | public var next_marker: String? 54 | } 55 | 56 | public let request: Request? 57 | public init(_ request: Request) { 58 | self.request = request 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanScope/File/GetUploadURL.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetUploadURL.swift 3 | // AliyunpanSDK 4 | // gen code 5 | // 6 | 7 | import Foundation 8 | 9 | extension AliyunpanFileScope { 10 | /// 刷新获取上传地址 11 | public class GetUploadURL: AliyunpanCommand { 12 | public var httpMethod: HTTPMethod { .post } 13 | public var uri: String { 14 | "/adrive/v1.0/openFile/getUploadUrl" 15 | } 16 | 17 | public struct Request: Codable { 18 | /// drive id 19 | public let drive_id: String 20 | /// file_id 21 | public let file_id: String 22 | /// 文件创建获取的upload_id 23 | public let upload_id: String 24 | /// 分片信息列表 25 | public var part_info_list: [AliyunpanFile.PartInfo]? 26 | } 27 | 28 | public struct Response: Codable { 29 | /// drive id 30 | public let drive_id: String 31 | /// file_id 32 | public let file_id: String 33 | /// 上传ID 34 | public let upload_id: String 35 | /// 格式:yyyy-MM-dd'T'HH:mm:ss.SSS'Z' 36 | public let created_at: String 37 | /// 分片信息列表 38 | public let part_info_list: [AliyunpanFile.PartInfo] 39 | } 40 | 41 | public let request: Request? 42 | public init(_ request: Request) { 43 | self.request = request 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanScope/File/ListUploadedParts.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListUploadedParts.swift 3 | // AliyunpanSDK 4 | // gen code 5 | // 6 | 7 | import Foundation 8 | 9 | extension AliyunpanFileScope { 10 | /// 列举已上传分片 11 | public class ListUploadedParts: AliyunpanCommand { 12 | public var httpMethod: HTTPMethod { .post } 13 | public var uri: String { 14 | "/adrive/v1.0/openFile/listUploadedParts" 15 | } 16 | 17 | public struct Request: Codable { 18 | /// drive id 19 | public let drive_id: String 20 | /// file_id 21 | public let file_id: String 22 | /// 文件创建获取的upload_id 23 | public let upload_id: String 24 | public var part_number_marker: String? 25 | 26 | public init(drive_id: String, file_id: String, upload_id: String, part_number_marker: String? = nil) { 27 | self.drive_id = drive_id 28 | self.file_id = file_id 29 | self.upload_id = upload_id 30 | self.part_number_marker = part_number_marker 31 | } 32 | } 33 | 34 | public struct Response: Codable { 35 | /// drive id 36 | public let drive_id: String 37 | /// upload_id 38 | public let upload_id: String 39 | /// 是否并行上传 40 | public let parallelUpload: Bool 41 | /// 已经上传分片列表 42 | public let uploaded_parts: [AliyunpanFile.PartInfo] 43 | /// 下一页起始资源标识符, 最后一页该值为空 44 | public var next_part_number_marker: String? 45 | } 46 | 47 | public let request: Request? 48 | public init(_ request: Request) { 49 | self.request = request 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanScope/File/MoveFile.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MoveFile.swift 3 | // AliyunpanSDK 4 | // gen code 5 | // 6 | 7 | import Foundation 8 | 9 | extension AliyunpanFileScope { 10 | /// 移动文件或文件夹 11 | public class MoveFile: AliyunpanCommand { 12 | public var httpMethod: HTTPMethod { .post } 13 | public var uri: String { 14 | "/adrive/v1.0/openFile/move" 15 | } 16 | 17 | public struct Request: Codable { 18 | /// 当前drive id 19 | public let drive_id: String 20 | /// file_id 21 | public let file_id: String 22 | /// 父文件ID、根目录为 root 23 | public let to_parent_file_id: String 24 | /// 目标drive,默认是当前drive_id 目前只能在当前drive操作 25 | public let to_drive_id: String? 26 | /// 重名策略,默认为 .refuse 27 | public let check_name_mode: AliyunpanFile.CheckNameMode? 28 | /// 当云端存在同名文件时,使用的新名字 29 | public let new_name: String? 30 | 31 | public init(drive_id: String, file_id: String, to_parent_file_id: String, to_drive_id: String? = nil, check_name_mode: AliyunpanFile.CheckNameMode? = nil, new_name: String? = nil) { 32 | self.drive_id = drive_id 33 | self.file_id = file_id 34 | self.to_parent_file_id = to_parent_file_id 35 | self.to_drive_id = to_drive_id 36 | self.check_name_mode = check_name_mode 37 | self.new_name = new_name 38 | } 39 | } 40 | 41 | public struct Response: Codable { 42 | /// drive id 43 | public let drive_id: String 44 | /// file_id 45 | public let file_id: String 46 | /// 文件是否已存在 47 | public let exist: Bool 48 | /// 异步任务id。如果返回为空字符串,表示直接移动成功。如果返回非空字符串,表示需要经过异步处理。 49 | public var async_task_id: String? 50 | } 51 | 52 | public let request: Request? 53 | public init(_ request: Request) { 54 | self.request = request 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanScope/File/SearchFile.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchFile.swift 3 | // AliyunpanSDK 4 | // gen code 5 | // 6 | 7 | import Foundation 8 | 9 | extension AliyunpanFileScope { 10 | /// 文件搜索 11 | public class SearchFile: AliyunpanCommand { 12 | public var httpMethod: HTTPMethod { .post } 13 | public var uri: String { 14 | "/adrive/v1.0/openFile/search" 15 | } 16 | 17 | public struct Request: Codable { 18 | /// drive id 19 | public let drive_id: String 20 | /// 返回文件数量,默认 100,最大100 21 | public let limit: Int? 22 | /// 分页标记 23 | public let marker: String? 24 | /// 查询语句,样例:固定目录搜索,只搜索一级 parent_file_id = '123' 精确查询 name = '123' 模糊匹配 name match '123' 搜索指定后缀文件 file_extension = 'apk' 范围查询 created_at < '2019-01-14T00:00:00' 复合查询: type = 'folder' or name = '123' parent_file_id = 'root' and name = '123' and category = 'video' 25 | public let query: String? 26 | /// created_at ASC | DESC updated_at ASC | DESC name ASC | DESC size ASC | DESC 27 | public let order_by: String? 28 | /// 生成的视频缩略图截帧时间,单位ms,默认120000ms 29 | public let video_thumbnail_time: Int? 30 | /// 生成的视频缩略图宽度,默认480px 31 | public let video_thumbnail_width: Int? 32 | /// 生成的图片缩略图宽度,默认480px 33 | public let image_thumbnail_width: Int? 34 | /// 是否返回总数 35 | public let return_total_count: Bool? 36 | 37 | public init(drive_id: String, limit: Int?, marker: String?, query: String?, order_by: OrderBy?, order_direction: OrderDirection?, video_thumbnail_time: Int?, video_thumbnail_width: Int?, image_thumbnail_width: Int?, return_total_count: Bool?) { 38 | self.drive_id = drive_id 39 | self.limit = limit 40 | self.marker = marker 41 | self.query = query 42 | self.video_thumbnail_time = video_thumbnail_time 43 | self.video_thumbnail_width = video_thumbnail_width 44 | self.image_thumbnail_width = image_thumbnail_width 45 | self.return_total_count = return_total_count 46 | if let order_by = order_by, let order_direction = order_direction { 47 | self.order_by = "\(order_by) \(order_direction)" 48 | } else { 49 | self.order_by = nil 50 | } 51 | } 52 | } 53 | 54 | public struct Response: Codable { 55 | public let items: [AliyunpanFile] 56 | /// 下个分页标记 57 | public var next_marker: String? 58 | public var total_count: Int? 59 | } 60 | 61 | public let request: Request? 62 | public init(_ request: Request) { 63 | self.request = request 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanScope/File/TrashFileToRecyclebin.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TrashFileToRecyclebin.swift 3 | // AliyunpanSDK 4 | // gen code 5 | // 6 | 7 | import Foundation 8 | 9 | extension AliyunpanFileScope { 10 | /// 放入回收站 11 | public class TrashFileToRecyclebin: AliyunpanCommand { 12 | public var httpMethod: HTTPMethod { .post } 13 | public var uri: String { 14 | "/adrive/v1.0/openFile/recyclebin/trash" 15 | } 16 | 17 | public struct Request: Codable { 18 | /// drive id 19 | public let drive_id: String 20 | /// file_id 21 | public let file_id: String 22 | 23 | public init(drive_id: String, file_id: String) { 24 | self.drive_id = drive_id 25 | self.file_id = file_id 26 | } 27 | } 28 | 29 | public struct Response: Codable { 30 | /// drive id 31 | public let drive_id: String 32 | /// file_id 33 | public let file_id: String 34 | /// 异步任务id,有的话表示需要经过异步处理。 35 | public var async_task_id: String? 36 | } 37 | 38 | public let request: Request? 39 | public init(_ request: Request) { 40 | self.request = request 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanScope/File/UpdateFile.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UpdateFile.swift 3 | // AliyunpanSDK 4 | // gen code 5 | // 6 | 7 | import Foundation 8 | 9 | extension AliyunpanFileScope { 10 | /// 文件更新 11 | public class UpdateFile: AliyunpanCommand { 12 | public var httpMethod: HTTPMethod { .post } 13 | public var uri: String { 14 | "/adrive/v1.0/openFile/update" 15 | } 16 | 17 | public struct Request: Codable { 18 | /// drive id 19 | public let drive_id: String 20 | /// file_id 21 | public let file_id: String 22 | /// 新的文件名 23 | public let name: String? 24 | /// 重名策略 25 | public let check_name_mode: AliyunpanFile.CheckNameMode? 26 | /// 收藏 true,移除收藏 false 27 | public let starred: Bool? 28 | 29 | public init(drive_id: String, file_id: String, name: String? = nil, check_name_mode: AliyunpanFile.CheckNameMode? = nil, starred: Bool? = nil) { 30 | self.drive_id = drive_id 31 | self.file_id = file_id 32 | self.name = name 33 | self.check_name_mode = check_name_mode 34 | self.starred = starred 35 | } 36 | } 37 | 38 | public typealias Response = AliyunpanFile 39 | 40 | public let request: Request? 41 | public init(_ request: Request) { 42 | self.request = request 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanScope/Internal/Authorize.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Authorize.swift 3 | // AliyunpanSDK 4 | // gen code 5 | // 6 | 7 | import Foundation 8 | 9 | extension AliyunpanInternalScope { 10 | /// 将用户浏览器重定向到云盘登录授权页面上 11 | public class Authorize: AliyunpanCommand { 12 | public var httpMethod: HTTPMethod { .get } 13 | public var uri: String { 14 | "/oauth/authorize" 15 | } 16 | 17 | public struct Request: Codable { 18 | /// 创建应用时分配的appId 19 | public let client_id: String 20 | /// 授权后要回调的URI,即接收Authorization Code的URI。请使用urlEncode对链接进行处理 21 | public let redirect_uri: String 22 | /// 申请的授权范围,多个用逗号分隔(示例:user:base,file:all:read) 23 | public let scope: String 24 | /// 仅支持code 25 | public let response_type: String 26 | public let bundle_id: String 27 | public let code_challenge: String? 28 | public let code_challenge_method: String? 29 | /// 用于保持请求和回调的状态,在重定向用户浏览器到redirect_uri时原样回传该参数 30 | public let state: String? 31 | /// h5下true强制用户登录,默认false 32 | public let relogin: Bool? 33 | public let source: String? 34 | /// 已授权后,后续无需用户主动点击授权。true 开启,nil 关闭,默认 nil 35 | public let auto_login: String? 36 | 37 | public init(client_id: String, redirect_uri: String, scope: String, response_type: String, code_challenge: String? = nil, code_challenge_method: String? = nil, state: String? = nil, relogin: Bool? = nil, bundle_id: String = Bundle.main.bundleId, source: String? = "appLink", auto_login: String? = nil) { 38 | self.client_id = client_id 39 | self.redirect_uri = redirect_uri 40 | self.scope = scope 41 | self.response_type = response_type 42 | self.code_challenge = code_challenge 43 | self.code_challenge_method = code_challenge_method 44 | self.state = state 45 | self.relogin = relogin 46 | self.bundle_id = bundle_id 47 | self.source = source 48 | self.auto_login = auto_login 49 | } 50 | } 51 | 52 | public struct Response: Codable { 53 | public let redirectUri: URL 54 | } 55 | 56 | public let request: Request? 57 | public init(_ request: Request) { 58 | self.request = request 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanScope/Internal/GetAccessToken.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetAccessToken.swift 3 | // AliyunpanSDK 4 | // gen code 5 | // 6 | 7 | import Foundation 8 | 9 | extension AliyunpanInternalScope { 10 | /// 通过code获取access_token或通过refresh_token刷新access_token。code10分钟内有效,只能用一次 11 | public class GetAccessToken: AliyunpanCommand { 12 | public var httpMethod: HTTPMethod { .post } 13 | public var uri: String { 14 | "/oauth/access_token" 15 | } 16 | 17 | public struct Request: Codable { 18 | /// 应用标识,创建应用时分配的appId 19 | public let client_id: String 20 | /// 身份类型 authorization_code 或 refresh_token 21 | public let grant_type: String 22 | /// 授权码 23 | public let code: String? 24 | /// 应用密钥,创建应用时分配的secret 25 | public let client_secret: String? 26 | /// 刷新token,单次请求有效 27 | public let refresh_token: String? 28 | /// pkce code_verifier 29 | public let code_verifier: String? 30 | 31 | public init(client_id: String, grant_type: String, code: String? = nil, client_secret: String? = nil, refresh_token: String? = nil, code_verifier: String? = nil) { 32 | self.client_id = client_id 33 | self.grant_type = grant_type 34 | self.code = code 35 | self.client_secret = client_secret 36 | self.refresh_token = refresh_token 37 | self.code_verifier = code_verifier 38 | } 39 | } 40 | 41 | public typealias Response = AliyunpanToken 42 | 43 | public let request: Request? 44 | public init(_ request: Request) { 45 | self.request = request 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanScope/Internal/GetAuthorizeQRCode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetAuthorizeQRCode.swift 3 | // AliyunpanSDK 4 | // 5 | // Created by zhaixian on 2023/12/14. 6 | // 7 | 8 | import Foundation 9 | 10 | extension AliyunpanInternalScope { 11 | /// 将用户浏览器重定向到云盘登录授权页面上 12 | public class GetAuthorizeQRCode: AliyunpanCommand { 13 | public var httpMethod: HTTPMethod { .post } 14 | public var uri: String { 15 | "/oauth/authorize/qrcode" 16 | } 17 | 18 | public struct Request: Codable { 19 | /// 创建应用时分配的appId 20 | public let client_id: String 21 | /// 申请的授权范围,多个用逗号分隔(示例:user:base,file:all:read) 22 | public let scopes: [String] 23 | public let code_challenge: String? 24 | public let code_challenge_method: String? 25 | 26 | public init(client_id: String, scopes: [String], code_challenge: String?, code_challenge_method: String?) { 27 | self.client_id = client_id 28 | self.scopes = scopes 29 | self.code_challenge = code_challenge 30 | self.code_challenge_method = code_challenge_method 31 | } 32 | } 33 | 34 | public struct Response: Codable { 35 | public let qrCodeUrl: URL 36 | public let sid: String 37 | } 38 | 39 | public let request: Request? 40 | public init(_ request: Request) { 41 | self.request = request 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanScope/Internal/GetAuthorizeQRCodeStatus.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetAuthorizeQRCodeStatus.swift 3 | // AliyunpanSDK 4 | // 5 | // Created by zhaixian on 2023/12/15. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum AliyunpanAuthorizeQRCodeStatus: String, Codable { 11 | /// 等待扫码 12 | case waitLogin = "WaitLogin" 13 | /// 扫码成功,待确认 14 | case scanSuccess = "ScanSuccess" 15 | /// 授权成功 16 | case loginSuccess = "LoginSuccess" 17 | /// 二维码失效 18 | case qrCodeExpired = "QRCodeExpired" 19 | } 20 | 21 | extension AliyunpanInternalScope { 22 | /// 将用户浏览器重定向到云盘登录授权页面上 23 | public class GetAuthorizeQRCodeStatus: AliyunpanCommand { 24 | public var httpMethod: HTTPMethod { .get } 25 | public var uri: String { 26 | "/oauth/qrcode/\(sid)/status" 27 | } 28 | 29 | public struct Response: Codable { 30 | public let status: AliyunpanAuthorizeQRCodeStatus 31 | public let authCode: String? 32 | } 33 | 34 | typealias Request = Void 35 | 36 | let sid: String 37 | public init(sid: String) { 38 | self.sid = sid 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanScope/User/GetDriveInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetDriveInfo.swift 3 | // AliyunpanSDK 4 | // gen code 5 | // 6 | 7 | import Foundation 8 | 9 | extension AliyunpanUserScope { 10 | /// 获取用户信息和drive信息 11 | public class GetDriveInfo: AliyunpanCommand { 12 | public var httpMethod: HTTPMethod { .post } 13 | public var uri: String { 14 | "/adrive/v1.0/user/getDriveInfo" 15 | } 16 | 17 | public typealias Request = Void 18 | 19 | public struct Response: Codable { 20 | /// 用户ID,具有唯一性 21 | public let user_id: String 22 | /// 昵称 23 | public let name: String 24 | /// 头像地址 25 | public let avatar: String 26 | /// 默认drive 27 | public let default_drive_id: String 28 | /// 资源库 29 | public var resource_drive_id: String? 30 | /// 备份盘 31 | public var backup_drive_id: String? 32 | } 33 | 34 | public init() {} 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanScope/User/GetSpaceInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetSpaceInfo.swift 3 | // AliyunpanSDK 4 | // gen code 5 | // 6 | 7 | import Foundation 8 | 9 | extension AliyunpanUserScope { 10 | /// 获取用户空间信息 11 | public class GetSpaceInfo: AliyunpanCommand { 12 | public var httpMethod: HTTPMethod { .post } 13 | public var uri: String { 14 | "/adrive/v1.0/user/getSpaceInfo" 15 | } 16 | 17 | public typealias Request = Void 18 | 19 | public struct Response: Codable { 20 | public let personal_space_info: AliyunpanSpaceInfo 21 | } 22 | 23 | public init() {} 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanScope/User/GetUsersInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetUsersInfo.swift 3 | // AliyunpanSDK 4 | // gen code 5 | // 6 | 7 | import Foundation 8 | 9 | extension AliyunpanUserScope { 10 | /// 通过 access_token 获取用户信息 11 | public class GetUsersInfo: AliyunpanCommand { 12 | public var httpMethod: HTTPMethod { .get } 13 | public var uri: String { 14 | "/oauth/users/info" 15 | } 16 | 17 | public typealias Request = Void 18 | 19 | public struct Response: Codable { 20 | /// 用户ID,具有唯一性 21 | public let id: String 22 | /// 昵称 23 | public let name: String 24 | /// 头像地址 25 | public let avatar: String 26 | /// 需要联系运营申请 user:phone 权限 27 | public var phone: String? 28 | } 29 | 30 | public init() {} 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanScope/User/GetUsersScopes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetUsersScopes.swift 3 | // AliyunpanSDK 4 | // gen code 5 | // 6 | 7 | import Foundation 8 | 9 | extension AliyunpanUserScope { 10 | /// 通过 access_token 获取用户权限信息 11 | public class GetUsersScopes: AliyunpanCommand { 12 | public var httpMethod: HTTPMethod { .get } 13 | public var uri: String { 14 | "/oauth/users/scopes" 15 | } 16 | 17 | public typealias Request = Void 18 | 19 | public struct Response: Codable { 20 | public struct ScopeItem: Codable { 21 | let scope: String 22 | } 23 | 24 | /// 用户ID 25 | public let id: String 26 | /// 数组为空时,没有权限。 27 | public let scopes: [ScopeItem] 28 | } 29 | 30 | public init() {} 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanScope/User/GetVipInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetVipInfo.swift 3 | // AliyunpanSDK 4 | // gen code 5 | // 6 | 7 | import Foundation 8 | 9 | extension AliyunpanUserScope { 10 | /// 获取用户vip信息 11 | public class GetVipInfo: AliyunpanCommand { 12 | public var httpMethod: HTTPMethod { .post } 13 | public var uri: String { 14 | "/v1.0/user/getVipInfo" 15 | } 16 | 17 | public typealias Request = Void 18 | 19 | public struct Response: Codable { 20 | /// 枚举:member, vip, svip 21 | public let identity: String 22 | /// 过期时间,时间戳,单位秒 23 | public let expire: TimeInterval? 24 | /// 20t、8t 25 | public var level: String? 26 | } 27 | 28 | public init() {} 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanScope/Video/GetVideoPreviewPlayInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetVideoPreviewPlayInfo.swift 3 | // AliyunpanSDK 4 | // gen code 5 | // 6 | 7 | import Foundation 8 | 9 | extension AliyunpanVideoScope { 10 | /// 获取文件播放详情 11 | public class GetVideoPreviewPlayInfo: AliyunpanCommand { 12 | public var httpMethod: HTTPMethod { .post } 13 | public var uri: String { 14 | "/adrive/v1.0/openFile/getVideoPreviewPlayInfo" 15 | } 16 | 17 | public struct Request: Codable { 18 | /// drive id 19 | public let drive_id: String 20 | /// file_id 21 | public let file_id: String 22 | /// live_transcoding 边转边播 23 | public let category: String 24 | /// 默认true 25 | public let get_subtitle_info: Bool? 26 | /// 默认所有类型,枚举 LD|SD|HD|FHD|QHD 27 | public let template_id: String? 28 | /// 单位秒,最长4小时,默认15分钟。 29 | public let url_expire_sec: Int? 30 | /// 默认fale,为true,仅会员可以查看所有内容 31 | public let only_vip: Bool? 32 | /// 是否获取视频的播放进度,默认为false 33 | public let with_play_cursor: Bool? 34 | 35 | public init(drive_id: String, file_id: String, category: String = "live_transcoding", get_subtitle_info: Bool? = nil, template_id: String? = nil, url_expire_sec: Int? = nil, only_vip: Bool? = nil, with_play_cursor: Bool? = nil) { 36 | self.drive_id = drive_id 37 | self.file_id = file_id 38 | self.category = category 39 | self.get_subtitle_info = get_subtitle_info 40 | self.template_id = template_id 41 | self.url_expire_sec = url_expire_sec 42 | self.only_vip = only_vip 43 | self.with_play_cursor = with_play_cursor 44 | } 45 | } 46 | 47 | public struct Response: Codable { 48 | public let domain_id: String? 49 | public let drive_id: String 50 | public let file_id: String 51 | public let video_preview_play_info: AliyunpanFile.VideoPreviewPlayInfo 52 | } 53 | 54 | public let request: Request? 55 | public init(_ request: Request) { 56 | self.request = request 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanScope/Video/GetVideoPreviewPlayMeta.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetVideoPreviewPlayMeta.swift 3 | // AliyunpanSDK 4 | // gen code 5 | // 6 | 7 | import Foundation 8 | 9 | extension AliyunpanVideoScope { 10 | /// 获取文件播放元数据 11 | public class GetVideoPreviewPlayMeta: AliyunpanCommand { 12 | public var httpMethod: HTTPMethod { .post } 13 | public var uri: String { 14 | "/adrive/v1.0/openFile/getVideoPreviewPlayMeta" 15 | } 16 | 17 | public struct Request: Codable { 18 | /// drive id 19 | public let drive_id: String 20 | /// file_id 21 | public let file_id: String 22 | /// live_transcoding 边转边播 23 | public let category: String? 24 | /// 默认所有类型,枚举 LD|SD|HD|FHD|QHD 25 | public let template_id: String? 26 | 27 | public init(drive_id: String, file_id: String, category: String?, template_id: String?) { 28 | self.drive_id = drive_id 29 | self.file_id = file_id 30 | self.category = category 31 | self.template_id = template_id 32 | } 33 | } 34 | 35 | public struct Response: Codable { 36 | public let domain_id: String 37 | public let drive_id: String 38 | public let file_id: String 39 | public let video_preview_play_meta: AliyunpanFile.VideoPreviewPlayInfo 40 | } 41 | 42 | public let request: Request? 43 | public init(_ request: Request) { 44 | self.request = request 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanScope/Video/GetVideoRecentList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetVideoRecentList.swift 3 | // AliyunpanSDK 4 | // gen code 5 | // 6 | 7 | import Foundation 8 | 9 | extension AliyunpanVideoScope { 10 | /// 获取最近播放列表 11 | public class GetVideoRecentList: AliyunpanCommand { 12 | public var httpMethod: HTTPMethod { .post } 13 | public var uri: String { 14 | "/adrive/v1.1/openFile/video/recentList" 15 | } 16 | 17 | public struct Request: Codable { 18 | /// 缩略图宽度 19 | public let video_thumbnail_width: Int? 20 | 21 | public init(video_thumbnail_width: Int? = nil) { 22 | self.video_thumbnail_width = video_thumbnail_width 23 | } 24 | } 25 | 26 | public struct Response: Codable { 27 | public let items: [AliyunpanFile] 28 | } 29 | 30 | public let request: Request? 31 | public init(_ request: Request) { 32 | self.request = request 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanScope/Video/UpdateVideoRecord.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UpdateVideoRecord.swift 3 | // AliyunpanSDK 4 | // gen code 5 | // 6 | 7 | import Foundation 8 | 9 | extension AliyunpanVideoScope { 10 | /// 更新播放进度 11 | public class UpdateVideoRecord: AliyunpanCommand { 12 | public var httpMethod: HTTPMethod { .post } 13 | public var uri: String { 14 | "/adrive/v1.0/openFile/video/updateRecord" 15 | } 16 | 17 | public struct Request: Codable { 18 | /// drive id 19 | public let drive_id: String 20 | /// file_id 21 | public let file_id: String 22 | /// 播放进度,单位s,可为小数 23 | public let play_cursor: String 24 | /// 视频总时长,单位s,可为小数 25 | public var duration: String? 26 | 27 | public init(drive_id: String, file_id: String, play_cursor: String, duration: String? = nil) { 28 | self.drive_id = drive_id 29 | self.file_id = file_id 30 | self.play_cursor = play_cursor 31 | self.duration = duration 32 | } 33 | } 34 | 35 | public struct Response: Codable { 36 | public let domain_id: String 37 | public let drive_id: String 38 | public let file_id: String 39 | public let name: String 40 | } 41 | 42 | public let request: Request? 43 | public init(_ request: Request) { 44 | self.request = request 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanScope/Vip/GetVipFeatureList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetVipFeatureList.swift 3 | // AliyunpanSDK 4 | // gen code 5 | // 6 | 7 | import Foundation 8 | 9 | extension AliyunpanVIPScope { 10 | /// 开始试用付费功能 11 | public class GetVipFeatureList: AliyunpanCommand { 12 | public var httpMethod: HTTPMethod { .get } 13 | public var uri: String { 14 | "/business/v1.0/vip/feature/list" 15 | } 16 | 17 | public typealias Request = Void 18 | 19 | public struct Response: Codable { 20 | public struct FeatureItem: Codable { 21 | /// 付费功能标记 22 | public let code: String 23 | /// 是否拦截 24 | public let intercept: Bool 25 | /// noTrial 不允许试用 26 | /// onTrial 试用中 27 | /// endTrial 试用结束 28 | /// allowTrial 允许试用,还未开始 29 | public let trialStatus: String 30 | /// 允许试用的时间,单位分钟。 31 | public let trialDuration: Int 32 | /// 开始试用的时间戳 33 | public let trialStartTime: TimeInterval? 34 | } 35 | 36 | /// 付费功能数组 37 | public let result: [FeatureItem] 38 | } 39 | 40 | public init() {} 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/AliyunpanScope/Vip/GetVipFeatureTrial.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetVipFeatureTrial.swift 3 | // AliyunpanSDK 4 | // gen code 5 | // 6 | 7 | import Foundation 8 | 9 | extension AliyunpanVIPScope { 10 | /// 开始试用付费功能 11 | public class GetVipFeatureTrial: AliyunpanCommand { 12 | public var httpMethod: HTTPMethod { .post } 13 | public var uri: String { 14 | "/business/v1.0/vip/feature/trial" 15 | } 16 | 17 | public struct Request: Codable { 18 | /// 付费功能。枚举列表 19 | public let featureCode: String 20 | 21 | public init(featureCode: String) { 22 | self.featureCode = featureCode 23 | } 24 | } 25 | 26 | public struct Response: Codable { 27 | /// noTrial 不允许试用 onTrial 试用中 endTrial 试用结束 allowTrial 允许试用,还未开始 28 | public let trialStatus: String 29 | /// 允许试用的时间,单位分钟。 30 | public let trialDuration: Int 31 | /// 开始试用的时间戳 32 | public let trialStartTime: Int 33 | } 34 | 35 | public let request: Request? 36 | public init(_ request: Request) { 37 | self.request = request 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/HTTPRequest/DebugDescription.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DebugDescription.swift 3 | // AliyunpanSDK 4 | // 5 | // Created by zhaixian on 2023/11/28. 6 | // 7 | // Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | // 27 | 28 | import Foundation 29 | 30 | class DebugDescription { 31 | static func description(of request: URLRequest) -> String { 32 | let requestSummary = "\(request.httpMethod ?? "Unknown") \(request)" 33 | let requestHeadersDescription = DebugDescription.description(for: request.headers) 34 | let requestBodyDescription = DebugDescription.description(for: request.httpBody) 35 | 36 | return """ 37 | [Request]: \(requestSummary) 38 | \(requestHeadersDescription.indentingNewlines()) 39 | \(requestBodyDescription.indentingNewlines()) 40 | """ 41 | } 42 | 43 | static func description(of response: URLResponse, data: Data) -> String { 44 | guard let response = response as? HTTPURLResponse else { 45 | return "" 46 | } 47 | 48 | let responseBodyDescription = DebugDescription.description(for: data) 49 | 50 | return """ 51 | [Response]: 52 | [Status Code]: \(response.statusCode) 53 | \(DebugDescription.description(for: response.headers).indentingNewlines()) 54 | \(responseBodyDescription.indentingNewlines()) 55 | """ 56 | } 57 | 58 | static func description(for headers: HTTPHeaders) -> String { 59 | guard !headers.isEmpty else { return "[Headers]: None" } 60 | 61 | let headerDescription = "\(headers.sorted().description)".indentingNewlines() 62 | return """ 63 | [Headers]: 64 | \(headerDescription) 65 | """ 66 | } 67 | 68 | static func description(for data: Data?, 69 | maximumLength: Int = 10_000) -> String { 70 | guard let data, !data.isEmpty else { 71 | return "[Body]: None" 72 | } 73 | 74 | guard data.count <= maximumLength else { 75 | return "[Body]: \(data.count) bytes" 76 | } 77 | 78 | return """ 79 | [Body]: 80 | \(String(decoding: data, as: UTF8.self) 81 | .trimmingCharacters(in: .whitespacesAndNewlines) 82 | .indentingNewlines()) 83 | """ 84 | } 85 | } 86 | 87 | extension String { 88 | fileprivate func indentingNewlines(by spaceCount: Int = 4) -> String { 89 | let spaces = String(repeating: " ", count: spaceCount) 90 | return replacingOccurrences(of: "\n", with: "\n\(spaces)") 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/HTTPRequest/Download/AliyunpanDownloadChunk.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AliyunpanDownloadChunk.swift 3 | // AliyunpanSDK 4 | // 5 | // Created by zhaixian on 2023/12/19. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct AliyunpanDownloadChunk: Equatable, CustomStringConvertible { 11 | public let start: Int64 12 | public let end: Int64 13 | public let index: Int 14 | 15 | init(start: Int64, end: Int64) { 16 | self.start = start 17 | self.end = end 18 | self.index = -1 19 | } 20 | 21 | init(start: Int64, end: Int64, index: Int) { 22 | self.start = start 23 | self.end = end 24 | self.index = index 25 | } 26 | 27 | init?(rangeString: String, fileSize: Int64) { 28 | let rangeValue = rangeString.split(separator: "=").last ?? "" 29 | let array = rangeValue.split(separator: "-") 30 | guard array.count >= 1, 31 | let start = Int64(array[0]) else { 32 | return nil 33 | } 34 | let end: Int64 35 | if array.count == 2, let value = Int64(array[1]) { 36 | end = value + 1 37 | } else { 38 | end = fileSize 39 | } 40 | self = Self(start: start, end: end, index: -1) 41 | } 42 | 43 | public var description: String { 44 | "[chunk-\(index)]: \(start)-\(end)" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/HTTPRequest/Download/AliyunpanDownloader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AliyunpanDownloader.swift 3 | // AliyunpanSDK 4 | // 5 | // Created by zhaixian on 2023/12/18. 6 | // 7 | 8 | import Foundation 9 | 10 | public typealias DownloadTasks = [AliyunpanDownloadTask] 11 | 12 | extension DownloadTasks { 13 | mutating func cancel(_ task: Element) { 14 | removeAll(where: { 15 | $0.id == task.id 16 | }) 17 | } 18 | } 19 | 20 | public protocol AliyunpanDownloadDelegate: AnyObject { 21 | /// 下载速度更新 22 | @MainActor 23 | func downloader(_ downloader: AliyunpanDownloader, didUpdatedNetworkSpeed networkSpeed: Int64) 24 | 25 | /// 下载进度发生变化 26 | @MainActor 27 | func downloader(_ downloader: AliyunpanDownloader, didUpdateTaskState state: AliyunpanDownloadTask.State, for task: AliyunpanDownloadTask) 28 | } 29 | 30 | /// 下载器 31 | public class AliyunpanDownloader: NSObject { 32 | /// 最大并发数,默认为10 33 | public var maxConcurrentOperationCount: Int { 34 | get { 35 | operationQueue.maxConcurrentOperationCount 36 | } 37 | set { 38 | operationQueue.maxConcurrentOperationCount = newValue 39 | } 40 | } 41 | 42 | /// 当前下载任务 43 | public private(set) var tasks: DownloadTasks = [] 44 | 45 | private var operationQueue = OperationQueue( 46 | name: "com.aliyunpanSDK.downloader.queue", 47 | maxConcurrentOperationCount: 10) 48 | 49 | private var delegates: [Weak] = [] 50 | 51 | /// 网速监听 Timer 52 | private lazy var networkSpeedTimer: Timer = { 53 | let timer = Timer(timeInterval: 1, repeats: true) { [weak self] _ in 54 | guard let self else { 55 | return 56 | } 57 | let offset = self.currentWritedSize - self.lastWritedSize 58 | self.lastWritedSize = self.currentWritedSize 59 | self.delegates.compactMap { $0.value as? AliyunpanDownloadDelegate } 60 | .forEach { delegate in 61 | DispatchQueue.main.async { [weak self] in 62 | guard let self else { 63 | return 64 | } 65 | delegate.downloader(self, didUpdatedNetworkSpeed: offset) 66 | } 67 | } 68 | } 69 | RunLoop.current.add(timer, forMode: .common) 70 | return timer 71 | }() 72 | 73 | private var lastWritedSize: Int64 = 0 74 | private var currentWritedSize: Int64 = 0 75 | 76 | weak var client: AliyunpanClient? 77 | 78 | deinit { 79 | networkSpeedTimer.invalidate() 80 | } 81 | } 82 | 83 | extension AliyunpanDownloader { 84 | /// 添加代理 85 | public func addDelegate(_ delegate: AliyunpanDownloadDelegate) { 86 | delegates = (delegates + [.init(value: delegate)]).filter { 87 | $0.value != nil 88 | } 89 | } 90 | 91 | /// 开启网速监听 92 | public func enableNetworkSpeedMonitor() { 93 | networkSpeedTimer.fire() 94 | } 95 | 96 | /// 下载文件 97 | /// - Parameters: 98 | /// - file: 目标文件 99 | /// - destination: 目标目录 100 | /// - Returns: DownloadTask 101 | @discardableResult 102 | public func download(file: AliyunpanFile, to destination: URL) -> AliyunpanDownloadTask { 103 | Logger.log(.info, msg: "[Downloader] download \(file.name), to:\(destination)") 104 | 105 | let task = AliyunpanDownloadTask(file: file, destination: destination, delegate: self) 106 | tasks.append(task) 107 | task.start() 108 | return task 109 | } 110 | 111 | /// 暂停下载 112 | /// - Parameter task: 目标任务 113 | public func pause(_ task: AliyunpanDownloadTask) { 114 | Logger.log(.info, msg: "[Downloader] pause \(task.file.name)") 115 | task.pause() 116 | } 117 | 118 | /// 恢复下载 119 | /// - Parameter task: 目标任务 120 | public func resume(_ task: AliyunpanDownloadTask) { 121 | Logger.log(.info, msg: "[Downloader] resume \(task.file.name)") 122 | task.start() 123 | } 124 | 125 | /// 取消下载,会清空已下载内容 126 | /// - Parameter task: 目标任务 127 | public func cancel(_ task: AliyunpanDownloadTask) { 128 | Logger.log(.info, msg: "[Downloader] cancel \(task.file.name)") 129 | task.cancel() 130 | tasks.cancel(task) 131 | } 132 | } 133 | 134 | extension AliyunpanDownloader: AliyunpanDownloadTaskDelegate { 135 | func getFileDownloadUrl(driveId: String, fileId: String) async throws -> AliyunpanScope.File.GetFileDownloadUrl.Response { 136 | guard let client else { 137 | throw AliyunpanError.DownloadError.invalidClient 138 | } 139 | return try await client 140 | .send( 141 | AliyunpanScope.File.GetFileDownloadUrl( 142 | .init(drive_id: driveId, file_id: fileId))) 143 | } 144 | 145 | func getOperationQueue() -> OperationQueue { 146 | operationQueue 147 | } 148 | 149 | func downloadTask(_ task: AliyunpanDownloadTask, didUpdateState state: AliyunpanDownloadTask.State) { 150 | delegates.compactMap { $0.value as? AliyunpanDownloadDelegate } 151 | .forEach { delegate in 152 | DispatchQueue.main.async { [weak self] in 153 | guard let self else { 154 | return 155 | } 156 | delegate.downloader(self, didUpdateTaskState: state, for: task) 157 | } 158 | } 159 | } 160 | 161 | func downloadTask(task: AliyunpanDownloadTask, didWriteData bytesWritten: Int64) { 162 | currentWritedSize += bytesWritten 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/HTTPRequest/HTTPMethod.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPMethod.swift 3 | // AliyunpanSDK 4 | // 5 | // Created by zhaixian on 2023/11/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum HTTPMethod: String { 11 | case get = "GET" 12 | case post = "POST" 13 | case put = "PUT" 14 | case delete = "DELETE" 15 | case head = "HEAD" 16 | } 17 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/HTTPRequest/HTTPRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPRequest.swift 3 | // AliyunpanSDK 4 | // 5 | // Created by zhaixian on 2023/11/21. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Dictionary where Key == String { 11 | fileprivate func jsonToQueryItems() -> [URLQueryItem] { 12 | keys.sorted(by: <).compactMap { key -> [URLQueryItem]? in 13 | guard let value = self[key] else { 14 | return nil 15 | } 16 | if let arrayValue = value as? [Any] { 17 | return arrayValue.map { 18 | URLQueryItem(name: "\(key)[]", value: "\($0)") 19 | } 20 | } else { 21 | return [URLQueryItem(name: key, value: "\(value)")] 22 | } 23 | }.flatMap { $0 } 24 | } 25 | } 26 | 27 | extension OperationQueue { 28 | convenience init( 29 | name: String, 30 | qualityOfService: QualityOfService = .default, 31 | maxConcurrentOperationCount: Int = OperationQueue.defaultMaxConcurrentOperationCount) { 32 | self.init() 33 | self.qualityOfService = qualityOfService 34 | self.maxConcurrentOperationCount = maxConcurrentOperationCount 35 | self.name = name 36 | } 37 | } 38 | 39 | extension OperationQueue { 40 | static let rootQueue = OperationQueue(name: "com.aliyunpanSDK.session.rootQueue") 41 | } 42 | 43 | extension URLSession { 44 | static let rootSession = URLSession(configuration: .default) 45 | } 46 | 47 | class HTTPRequest { 48 | private var headers = HTTPHeaders.default 49 | private var decoder = JSONParameterDecoder.default 50 | private var serializationQueue = OperationQueue.rootQueue 51 | private var urlSession = URLSession.rootSession 52 | 53 | private let command: Command 54 | 55 | init(command: Command) { 56 | self.command = command 57 | } 58 | 59 | func headers(_ headers: HTTPHeaders) -> Self { 60 | self.headers.concat(headers) 61 | return self 62 | } 63 | 64 | func asURLRequest() throws -> URLRequest { 65 | guard let url = try? AliyunpanURL(uri: command.uri).asURL() else { 66 | throw AliyunpanError.NetworkSystemError.invalidURL 67 | } 68 | var urlRequest = URLRequest(url: url) 69 | // set httpMethod 70 | urlRequest.httpMethod = command.httpMethod.rawValue 71 | // set headerField 72 | urlRequest.headers = headers 73 | // set parameters 74 | if let requestData = command.requestData { 75 | if command.httpMethod == .post { 76 | urlRequest.httpBody = requestData 77 | } else { 78 | if let json = try? JSONSerialization.jsonObject(with: requestData) as? [String: Any] { 79 | if var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) { 80 | let queryItems = json.jsonToQueryItems() 81 | urlComponents.queryItems = (urlComponents.queryItems ?? []) + queryItems 82 | urlRequest.url = urlComponents.url 83 | } 84 | } 85 | } 86 | } 87 | return urlRequest 88 | } 89 | 90 | func responseData() async throws -> Data { 91 | let urlRequest = try asURLRequest() 92 | Logger.log(.debug, msg: DebugDescription.description(of: urlRequest)) 93 | let (data, response) = try await urlSession.data(for: urlRequest) 94 | Logger.log(.debug, msg: DebugDescription.description(of: response, data: data)) 95 | 96 | if let response = response as? HTTPURLResponse, response.statusCode != 200 { 97 | if let error = try? decoder.decode(AliyunpanError.ServerError.self, from: data) { 98 | throw error 99 | } else { 100 | throw AliyunpanError.NetworkSystemError.httpError(statusCode: response.statusCode, data: data, response: response) 101 | } 102 | } 103 | return data 104 | } 105 | 106 | func response() async throws -> Command.Response { 107 | let data = try await responseData() 108 | return try decoder.decode(Command.Response.self, from: data) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/HTTPRequest/JSON+Codable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSON+Codable.swift 3 | // AliyunpanSDK 4 | // 5 | // Created by zhaixian on 2023/11/23. 6 | // 7 | 8 | import Foundation 9 | 10 | class JSONParameterEncoder: JSONEncoder { 11 | static let `default` = JSONParameterEncoder() 12 | 13 | override init() { 14 | super.init() 15 | 16 | let dateFormatter = DateFormatter() 17 | dateFormatter.locale = Locale.current 18 | dateFormatter.calendar = Calendar(identifier: .gregorian) 19 | dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ" 20 | dateEncodingStrategy = .formatted(dateFormatter) 21 | } 22 | } 23 | 24 | class JSONParameterDecoder: JSONDecoder { 25 | static let `default` = JSONParameterDecoder() 26 | 27 | override init() { 28 | super.init() 29 | 30 | let dateFormatter = DateFormatter() 31 | dateFormatter.locale = Locale.current 32 | dateFormatter.calendar = Calendar(identifier: .gregorian) 33 | dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ" 34 | dateDecodingStrategy = .formatted(dateFormatter) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/HTTPRequest/URLConvertible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLConvertible.swift 3 | // AliyunpanSDK 4 | // 5 | // Created by zhaixian on 2023/11/23. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol URLConvertible { 11 | func asURL() throws -> URL 12 | } 13 | 14 | extension String: URLConvertible { 15 | func asURL() throws -> URL { 16 | guard let url = URL(string: self) else { 17 | throw AliyunpanError.NetworkSystemError.invalidURL 18 | } 19 | return url 20 | } 21 | } 22 | 23 | extension URL: URLConvertible { 24 | func asURL() throws -> URL { self } 25 | } 26 | 27 | struct AliyunpanURL: URLConvertible { 28 | var host: String = Aliyunpan.env.host 29 | let uri: String 30 | 31 | func asURL() throws -> URL { 32 | try "\(host)\(uri)".asURL() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/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 | 0.3.5 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/Model/AliyunpanSpaceInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AliyunpanSpaceInfo.swift 3 | // AliyunpanSDK 4 | // 5 | // Created by zhaixian on 2023/11/30. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct AliyunpanSpaceInfo: Codable { 11 | public let used_size: Int64 12 | public let total_size: Int64 13 | } 14 | 15 | extension AliyunpanSpaceInfo: CustomStringConvertible { 16 | public var description: String { 17 | """ 18 | [AliyunpanSpaceInfo] 19 | used_size: \(used_size) 20 | total_size: \(total_size) 21 | """ 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/Model/AliyunpanToken.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AliyunpanToken.swift 3 | // AliyunpanSDK 4 | // 5 | // Created by zhaixian on 2023/11/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct AliyunpanToken: Codable { 11 | /// Bearer 12 | public let token_type: String 13 | /// 用来获取用户信息的access_token。刷新后,旧access_token不会立即失效 14 | public let access_token: String 15 | /// 单次有效,用来刷新access_token,90天有效期。刷新后,返回新的refresh_token,请保存以便下一次刷新使用 16 | public let refresh_token: String? 17 | /// access_token的过期时间,单位秒 18 | public internal(set) var expires_in: TimeInterval 19 | 20 | /// 是否已过期 21 | public var isExpired: Bool { 22 | Date().timeIntervalSince1970 > expires_in 23 | } 24 | 25 | public init( 26 | token_type: String = "Bearer", 27 | access_token: String, 28 | refresh_token: String? = nil, 29 | expires_in: TimeInterval = .greatestFiniteMagnitude) { 30 | self.token_type = token_type 31 | self.access_token = access_token 32 | self.refresh_token = refresh_token 33 | self.expires_in = expires_in 34 | } 35 | } 36 | 37 | extension AliyunpanToken: Hashable, Equatable { 38 | public func hash(into hasher: inout Hasher) { 39 | hasher.combine(token_type) 40 | hasher.combine(access_token) 41 | hasher.combine(refresh_token) 42 | hasher.combine(expires_in) 43 | } 44 | } 45 | 46 | extension AliyunpanToken: CustomStringConvertible { 47 | public var description: String { 48 | """ 49 | [AliyunpanToken] 50 | token_type: \(token_type) 51 | access_token: \(access_token) 52 | refresh_token: \(refresh_token ?? "nil") 53 | expires_in: \(expires_in) 54 | """ 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/Platform.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Platform.swift 3 | // AliyunpanSDK 4 | // 5 | // Created by zhaixian on 2023/12/13. 6 | // 7 | 8 | #if canImport(UIKit) 9 | import UIKit 10 | #endif 11 | 12 | #if canImport(AppKit) && !targetEnvironment(macCatalyst) 13 | import AppKit 14 | #endif 15 | 16 | #if canImport(TVUIKit) 17 | import TVUIKit 18 | #endif 19 | 20 | import AuthenticationServices 21 | 22 | class Platform: NSObject { 23 | @MainActor 24 | static func canOpenURL(_ url: URL) -> Bool { 25 | #if canImport(UIKit) || canImport(TVUIKit) 26 | return UIApplication.shared.canOpenURL(url) 27 | #endif 28 | 29 | #if canImport(AppKit) && !targetEnvironment(macCatalyst) 30 | return NSWorkspace.shared.urlForApplication(toOpen: url) != nil 31 | #endif 32 | } 33 | 34 | @MainActor 35 | static func open(_ url: URL) async { 36 | #if canImport(UIKit) || canImport(TVUIKit) 37 | await UIApplication.shared.open(url) 38 | #endif 39 | 40 | #if canImport(AppKit) && !targetEnvironment(macCatalyst) 41 | NSWorkspace.shared.open(url) 42 | #endif 43 | } 44 | 45 | @MainActor 46 | static var mainPresentationAnchor: ASPresentationAnchor { 47 | #if canImport(UIKit) || canImport(TVUIKit) 48 | return UIApplication.shared.connectedScenes 49 | .compactMap { $0 as? UIWindowScene } 50 | .filter { $0.activationState == .foregroundActive } 51 | .first?.windows.first ?? ASPresentationAnchor() 52 | #endif 53 | 54 | #if canImport(AppKit) && !targetEnvironment(macCatalyst) 55 | return NSApplication.shared.mainWindow ?? ASPresentationAnchor() 56 | #endif 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/Utils/AsyncOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncOperation.swift 3 | // AliyunpanSDK 4 | // 5 | // Created by zhaixian on 2023/12/19. 6 | // 7 | 8 | import Foundation 9 | 10 | class AsyncOperation: Operation { 11 | @objc enum State: Int { 12 | case ready 13 | case executing 14 | case finished 15 | case cancel 16 | } 17 | 18 | @ThreadSafe 19 | @objc var state: State = .ready { 20 | willSet { 21 | willChangeValue(for: \.isReady) 22 | willChangeValue(for: \.isExecuting) 23 | willChangeValue(for: \.isFinished) 24 | willChangeValue(for: \.isCancelled) 25 | } 26 | didSet { 27 | didChangeValue(for: \.isReady) 28 | didChangeValue(for: \.isExecuting) 29 | didChangeValue(for: \.isFinished) 30 | didChangeValue(for: \.isCancelled) 31 | 32 | updateState(state: state, oldValue: oldValue) 33 | } 34 | } 35 | 36 | override var isAsynchronous: Bool { 37 | true 38 | } 39 | 40 | override var isReady: Bool { 41 | state == .ready && super.isReady 42 | } 43 | 44 | override var isExecuting: Bool { 45 | state == .executing 46 | } 47 | 48 | override var isFinished: Bool { 49 | state == .finished || state == .cancel 50 | } 51 | 52 | override var isCancelled: Bool { 53 | state == .cancel 54 | } 55 | 56 | override func start() { 57 | guard !isCancelled else { 58 | cancel() 59 | return 60 | } 61 | 62 | state = .executing 63 | 64 | main() 65 | } 66 | 67 | func finish() { 68 | state = .finished 69 | } 70 | 71 | override func cancel() { 72 | state = .cancel 73 | } 74 | 75 | func updateState(state: State, oldValue: State) {} 76 | } 77 | 78 | class AsyncThrowOperation: AsyncOperation { 79 | var result: Result? 80 | } 81 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/Utils/Crypto.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Crypto.swift 3 | // AliyunpanSDK 4 | // 5 | // Created by zhaixian on 2023/11/22. 6 | // 7 | 8 | import Foundation 9 | import CryptoKit 10 | 11 | extension Digest { 12 | var bytes: [UInt8] { Array(makeIterator()) } 13 | 14 | var hexString: String { 15 | bytes.map { String(format: "%02X", $0) }.joined() 16 | } 17 | } 18 | 19 | public class AliyunpanCrypto { 20 | /// 大数据量 sha1 and hex 21 | public static func sha1AndHex(_ fileURL: URL) -> String? { 22 | guard let inputStream = InputStream(url: fileURL) else { 23 | return nil 24 | } 25 | 26 | defer { 27 | inputStream.close() 28 | } 29 | 30 | // 10M 缓冲区 31 | let bufferSize = 10 * 1024 * 1024 32 | var buffer = [UInt8](repeating: 0, count: bufferSize) 33 | 34 | var sha1 = Insecure.SHA1() 35 | 36 | inputStream.open() 37 | var error: Error? 38 | while inputStream.hasBytesAvailable { 39 | let read = inputStream.read(&buffer, maxLength: bufferSize) 40 | if read == 0 { 41 | break 42 | } else if read < 0 { 43 | error = inputStream.streamError 44 | break 45 | } 46 | let data = Data(buffer.prefix(read)) 47 | sha1.update(data: data) 48 | } 49 | 50 | if error != nil { 51 | return nil 52 | } 53 | 54 | let hashedData = sha1.finalize() 55 | return hashedData.hexString 56 | } 57 | 58 | /// 低数据量 sha1 and hex 59 | public static func sha1AndHex(_ data: Data) -> String { 60 | Insecure.SHA1.hash(data: data).hexString 61 | } 62 | 63 | public static func sha256AndBase64(_ message: String) -> String { 64 | let inputData = Data(message.utf8) 65 | let hashedData = SHA256.hash(data: inputData) 66 | var string = Data(hashedData).base64EncodedString() as NSString 67 | string = string.replacingOccurrences(of: "+", with: "-") as NSString 68 | string = string.replacingOccurrences(of: "/", with: "_") as NSString 69 | return string as String 70 | } 71 | 72 | public static func md5(_ message: String) -> String { 73 | Insecure.MD5.hash(data: Data(message.utf8)) 74 | .hexString 75 | } 76 | 77 | /// 获取秒传值 78 | /// - Parameters: 79 | /// - accessToken: access_token 80 | /// - fileSize: 文件 size 81 | /// - Returns: 秒传值 82 | public static func getProofCode(accessToken: String, fileURL: URL) -> String? { 83 | do { 84 | let fileSize = try FileManager.default.fileSizeOfItem(at: fileURL) 85 | 86 | let string = String(md5(accessToken).prefix(16)) 87 | let value = strtoul(string, nil, 16) 88 | 89 | let index = UInt64(value) % UInt64(fileSize) 90 | 91 | let data = try FileManager.default.dataChunk( 92 | at: fileURL, 93 | in: Int(index)..( 19 | seconds: TimeInterval, 20 | operation: @escaping @Sendable () async throws -> R 21 | ) async throws -> R { 22 | try await withThrowingTaskGroup(of: R.self) { group in 23 | defer { 24 | group.cancelAll() 25 | } 26 | 27 | group.addTask { 28 | let result = try await operation() 29 | try Task.checkCancellation() 30 | return result 31 | } 32 | 33 | group.addTask { 34 | if seconds > 0 { 35 | try await Task.sleep(seconds: seconds) 36 | } 37 | try Task.checkCancellation() 38 | throw CancellationError() 39 | } 40 | 41 | if let result = try await group.next() { 42 | return result 43 | } else { 44 | throw CancellationError() 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/Utils/ThreadSafe.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThreadSafe.swift 3 | // Pods 4 | // 5 | // Created by zhaixian on 2023/12/20. 6 | // 7 | 8 | import Foundation 9 | 10 | @propertyWrapper 11 | struct ThreadSafe { 12 | var wrappedValue: Element { 13 | get { 14 | queue.sync { 15 | projectValue 16 | } 17 | } 18 | set { 19 | queue.sync { 20 | projectValue = newValue 21 | } 22 | } 23 | } 24 | 25 | private let queue = DispatchQueue( 26 | label: "com.aliyunpanSDK.\(String(describing: Element.self))", 27 | attributes: .concurrent) 28 | 29 | var projectValue: Element 30 | 31 | init(wrappedValue: Element) { 32 | projectValue = wrappedValue 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/Utils/URL+AliyunpanSDK.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URL+AliyunpanSDK.swift 3 | // 4 | // 5 | // Created by zhaixian on 2023/11/27. 6 | // 7 | 8 | import Foundation 9 | 10 | extension URL { 11 | var queryItems: [URLQueryItem] { 12 | guard let urlComponents = URLComponents(url: self, resolvingAgainstBaseURL: false) else { 13 | return [] 14 | } 15 | return urlComponents.queryItems ?? [] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/AliyunpanSDK/Utils/Weak.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Weak.swift 3 | // AliyunpanSDK 4 | // 5 | // Created by zhaixian on 2023/12/18. 6 | // 7 | 8 | import Foundation 9 | 10 | class Weak { 11 | weak var value: T? 12 | init(value: T) { 13 | self.value = value 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Tests/AliyunpanSDK.xctestplan: -------------------------------------------------------------------------------- 1 | { 2 | "configurations" : [ 3 | { 4 | "id" : "793A1A9C-6D23-48AD-A8EF-A521CC2C8924", 5 | "name" : "Configuration 1", 6 | "options" : { 7 | 8 | } 9 | } 10 | ], 11 | "defaultOptions" : { 12 | 13 | }, 14 | "testTargets" : [ 15 | { 16 | "target" : { 17 | "containerPath" : "container:AliyunpanSDK.xcodeproj", 18 | "identifier" : "F4C6F22F2B060C4B003A06B3", 19 | "name" : "AliyunpanSDKTests" 20 | } 21 | }, 22 | { 23 | "target" : { 24 | "containerPath" : "container:", 25 | "identifier" : "AliyunpanSDKTests", 26 | "name" : "AliyunpanSDKTests" 27 | } 28 | } 29 | ], 30 | "version" : 1 31 | } 32 | -------------------------------------------------------------------------------- /Tests/AliyunpanSDKTests/AliyunpanClientTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AliyunpanClientTests.swift 3 | // AliyunpanSDKTests 4 | // 5 | // Created by zhaixian on 2023/12/5. 6 | // 7 | 8 | import XCTest 9 | @testable import AliyunpanSDK 10 | 11 | class AliyunpanClientTests: XCTestCase { 12 | @MainActor func testToken() { 13 | let client1 = AliyunpanClient(.init(appId: "app", scope: "scope", identifier: "user1")) 14 | let client2 = AliyunpanClient(.init(appId: "app", scope: "scope", identifier: "user2")) 15 | let client3 = AliyunpanClient(appId: "app", scope: "scope") 16 | 17 | let now = Date() 18 | let token = AliyunpanToken( 19 | token_type: "token_type1", 20 | access_token: "access_token1", 21 | refresh_token: "refresh_token1", 22 | expires_in: now.timeIntervalSince1970) 23 | client1.token = token 24 | XCTAssertEqual(client1.accessToken, "access_token1") 25 | XCTAssertEqual(client1.token?.token_type, "token_type1") 26 | XCTAssertEqual(client1.token?.refresh_token, "refresh_token1") 27 | XCTAssertEqual(client1.token?.expires_in, now.timeIntervalSince1970) 28 | XCTAssertEqual(client2.accessToken, nil) 29 | XCTAssertEqual(client3.accessToken, nil) 30 | client1.cleanToken() 31 | XCTAssertEqual(client1.accessToken, nil) 32 | XCTAssertEqual(client2.accessToken, nil) 33 | XCTAssertEqual(client3.accessToken, nil) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Tests/AliyunpanSDKTests/AliyunpanSDKTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AliyunpanSDKTests.swift 3 | // AliyunpanSDKTests 4 | // 5 | // Created by zhaixian on 2023/11/16. 6 | // 7 | 8 | import XCTest 9 | @testable import AliyunpanSDK 10 | 11 | class AliyunpanSDKTests: XCTestCase { 12 | func testSDK() throws { 13 | XCTAssertEqual(Aliyunpan.logLevel, .warn) 14 | Aliyunpan.setLogLevel(.error) 15 | XCTAssertEqual(Aliyunpan.logLevel, .error) 16 | 17 | XCTAssertEqual(Aliyunpan.env, .product) 18 | XCTAssertEqual(Aliyunpan.env.host, "https://openapi.alipan.com") 19 | Aliyunpan.setEnvironment(.pre) 20 | XCTAssertEqual(Aliyunpan.env, .pre) 21 | XCTAssertEqual(Aliyunpan.env.host, "https://stg-openapi.alipan.com") 22 | 23 | let url1 = URL(string: "smartdrive123456://authorize?state=abc&code=anycode")! 24 | XCTAssertTrue(Aliyunpan.handleOpenURL(url1)) 25 | let url2 = URL(string: "taobao://authorize?state=abc&code=anycode")! 26 | XCTAssertFalse(Aliyunpan.handleOpenURL(url2)) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tests/AliyunpanSDKTests/CredentialTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CredentialTests.swift 3 | // AliyunpanSDKTests 4 | // 5 | // Created by zhaixian on 2023/12/13. 6 | // 7 | 8 | import Foundation 9 | 10 | import XCTest 11 | @testable import AliyunpanSDK 12 | 13 | class TestBizServer: AliyunpanBizServer { 14 | func requestToken(appId: String, authCode: String) async throws -> AliyunpanToken { 15 | .init(token_type: "", access_token: "", refresh_token: nil, expires_in: 0) 16 | } 17 | } 18 | 19 | class TestQRCodeContainer: AliyunpanQRCodeContainer { 20 | func authorizeQRCodeStatusUpdated(_ status: AliyunpanSDK.AliyunpanAuthorizeQRCodeStatus) {} 21 | 22 | func showAliyunpanAuthorizeQRCode(with url: URL) {} 23 | } 24 | 25 | class CredentialTests: XCTestCase { 26 | func testCredential() throws { 27 | XCTAssertTrue(AliyunpanCredentials.pkce.implement is AliyunpanPKCECredentials) 28 | XCTAssertTrue(AliyunpanCredentials.server(TestBizServer()).implement is AliyunpanServerCredentials) 29 | XCTAssertTrue(AliyunpanCredentials.qrCode(TestQRCodeContainer()).implement is AliyunpanQRCodeCredentials) 30 | XCTAssertTrue(AliyunpanCredentials.token(.init(access_token: "accessToken")).implement is AliyunpanTokenCredentials) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Tests/AliyunpanSDKTests/CryptoTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CryptoTests.swift 3 | // AliyunpanSDK 4 | // 5 | // Created by zhaixian on 2023/11/24. 6 | // 7 | 8 | import XCTest 9 | @testable import AliyunpanSDK 10 | 11 | class CryptoTests: XCTestCase { 12 | func testSHA256() throws { 13 | XCTAssertEqual(AliyunpanCrypto.sha256AndBase64("1"), "a4ayc_80_OGda4BO_1o_V0etpOqiLx1JwB5S3beHW0s=") 14 | 15 | XCTAssertEqual(AliyunpanCrypto.sha256AndBase64("82"), "pG43Yy-mylGhP-OaVns8I7KML0fYr2vpvWPgMOIUujg=") 16 | 17 | XCTAssertEqual(AliyunpanCrypto.sha256AndBase64("123"), "pmWkWSBCL51Bfkhn79xPuKBKHz__H6B-mY6G9_eieuM=") 18 | 19 | XCTAssertEqual(AliyunpanCrypto.sha256AndBase64("1234"), "A6xnQhbz4Vx2HuGl4lXwZ5U2I8iziLRFnhP5eNfIRvQ=") 20 | } 21 | 22 | func testProofCode() throws { 23 | guard let url = Bundle(for: self.classForCoder).url(forResource: "TestFile1", withExtension: "txt") else { 24 | return 25 | } 26 | 27 | XCTAssertEqual( 28 | AliyunpanCrypto.getProofCode( 29 | accessToken: "Bearer eyJraWQiOiJLcU8iLCJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIwZWJkMjUyY2EyNTg0MmY0OTBmYmJhYWJlZTkwOGE5YyIsImF1ZCI6IjQ2NWJjNzMzZmQ2MTQxNDJiZmQ1Y2MxNGYzZjk1NWFlIiwicyI6ImNkYSIsImQiOiIxMDE4NDAsMTAyOTM0MjAiLCJpc3MiOiJhbGlwYW4iLCJleHAiOjE3MTQwMzg0NDYsImwiOjIsImlhdCI6MTcxMTQ0NjQ0MywianRpIjoiODM2ZmVkZDExMjhkNDU2MTg0NjA4OTgwMzBlZGM5NTkifQ.yZJFeeUriWOcPhg-CHHO1XyotwuFR0v", 30 | fileURL: url), 31 | "bG8gV29ybGQ=" 32 | ) 33 | } 34 | 35 | func testSHA1() throws { 36 | guard let url = Bundle(for: self.classForCoder).url(forResource: "TestFile1", withExtension: "txt") else { 37 | return 38 | } 39 | 40 | XCTAssertEqual( 41 | AliyunpanCrypto.sha1AndHex(url), 42 | "2EF7BDE608CE5404E97D5F042F95F89F1C232871" 43 | ) 44 | 45 | XCTAssertEqual( 46 | AliyunpanCrypto.sha1AndHex(try Data(contentsOf: url)), 47 | "2EF7BDE608CE5404E97D5F042F95F89F1C232871" 48 | ) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Tests/AliyunpanSDKTests/DownloaderOperationTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DownloaderOperationTests.swift 3 | // AliyunpanSDK 4 | // 5 | // Created by zhaixian on 2023/12/21. 6 | // 7 | 8 | import XCTest 9 | @testable import AliyunpanSDK 10 | 11 | class DownloaderOperationTests: XCTestCase { 12 | let destination = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first! 13 | 14 | let operationQueue = OperationQueue( 15 | name: "DownloaderOperationTests.queue", 16 | maxConcurrentOperationCount: 10) 17 | 18 | var completionHandle: ((DownloadChunkOperation, AsyncOperation.State) -> Void)? 19 | 20 | var chunkOperation: DownloadChunkOperation? 21 | 22 | func testSuccess() async throws { 23 | let chunkOperation = { 24 | let chunk = AliyunpanDownloadChunk(start: 0, end: 1000) 25 | let chunkOperation = DownloadChunkOperation(chunk: chunk, destination: destination.appendingPathComponent("testfile"), taskIdentifier: "") 26 | chunkOperation.delegate = self 27 | chunkOperation.dataSource = self 28 | return chunkOperation 29 | }() 30 | self.chunkOperation = chunkOperation 31 | 32 | XCTAssertEqual(chunkOperation.state, .ready) 33 | 34 | let stream = AsyncThrowingStream<(DownloadChunkOperation, AsyncOperation.State), Error> { continuation in 35 | completionHandle = { operation, state in 36 | continuation.yield((operation, state)) 37 | if state == .finished { 38 | continuation.finish() 39 | } 40 | } 41 | operationQueue.addOperation(chunkOperation) 42 | } 43 | 44 | var index = 0 45 | for try await result in stream { 46 | let url = try? result.0.result?.get() 47 | if index == 0 { 48 | XCTAssertEqual(url, nil) 49 | XCTAssertEqual(result.1, .executing) 50 | } else if index == 1 { 51 | XCTAssertEqual(result.1, .finished) 52 | } 53 | index += 1 54 | } 55 | } 56 | 57 | func testFailure() async throws { 58 | let chunkOperation = { 59 | let chunk = AliyunpanDownloadChunk(start: 1000, end: 4000) 60 | let chunkOperation = DownloadChunkOperation(chunk: chunk, destination: destination, taskIdentifier: "") 61 | chunkOperation.delegate = self 62 | chunkOperation.dataSource = self 63 | return chunkOperation 64 | }() 65 | self.chunkOperation = chunkOperation 66 | 67 | XCTAssertEqual(chunkOperation.state, .ready) 68 | 69 | let stream = AsyncThrowingStream<(DownloadChunkOperation, AsyncOperation.State), Error> { continuation in 70 | completionHandle = { operation, state in 71 | continuation.yield((operation, state)) 72 | if state == .finished { 73 | continuation.finish() 74 | } 75 | } 76 | operationQueue.addOperation(chunkOperation) 77 | } 78 | 79 | var index = 0 80 | for try await result in stream { 81 | if index == 0 { 82 | let url = try? result.0.result?.get() 83 | XCTAssertEqual(url, nil) 84 | XCTAssertEqual(result.1, .executing) 85 | } else if index == 1 { 86 | XCTAssertThrowsError(try result.0.result?.get()) 87 | XCTAssertEqual(result.1, .finished) 88 | } 89 | index += 1 90 | } 91 | } 92 | } 93 | 94 | extension DownloaderOperationTests: DownloadChunkOperationDelegate { 95 | func chunkOperationDidWriteData(_ bytesWritten: Int64) {} 96 | 97 | func chunkOperation(_ operation: AliyunpanSDK.DownloadChunkOperation, didUpdatedState state: AliyunpanSDK.AsyncOperation.State) { 98 | completionHandle?(operation, state) 99 | } 100 | } 101 | 102 | extension DownloaderOperationTests: DownloadChunkOperationDataSource { 103 | func getFileDownloadUrl() async throws -> URL { 104 | URL(string: "https://aliyunpan.com")! 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Tests/AliyunpanSDKTests/DownloaderTaskTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DownloaderTaskTests.swift 3 | // AliyunpanSDK 4 | // 5 | // Created by zhaixian on 2023/12/21. 6 | // 7 | 8 | import XCTest 9 | @testable import AliyunpanSDK 10 | 11 | let file = AliyunpanFile( 12 | drive_id: "drive_id", 13 | file_id: "file_id", 14 | parent_file_id: "", 15 | name: "", 16 | size: 12_345_678, 17 | file_extension: nil, 18 | content_hash: nil, 19 | type: .file, 20 | mime_type: nil, 21 | thumbnail: nil, 22 | url: nil, 23 | created_at: nil, 24 | updated_at: nil, 25 | play_cursor: nil, 26 | image_media_metadata: nil, 27 | video_media_metadata: nil) 28 | 29 | class DownloaderTaskTests: XCTestCase, AliyunpanDownloadTaskDelegate { 30 | let destination = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first! 31 | 32 | private let operationQueue = OperationQueue( 33 | name: "test", 34 | maxConcurrentOperationCount: 10) 35 | 36 | func testTask() throws { 37 | let task = AliyunpanDownloadTask( 38 | file: file, 39 | destination: destination, 40 | delegate: self) 41 | 42 | task.start() 43 | XCTAssertEqual(operationQueue.operations.count, 4) 44 | 45 | task.cancel() 46 | XCTAssertEqual(operationQueue.operations.count, 0) 47 | 48 | task.start() 49 | XCTAssertEqual(operationQueue.operations.count, 4) 50 | 51 | task.pause() 52 | XCTAssertEqual(operationQueue.operations.count, 0) 53 | } 54 | 55 | func getFileDownloadUrl(driveId: String, fileId: String) async throws -> AliyunpanScope.File.GetFileDownloadUrl.Response { 56 | try await Task.sleep(seconds: 1) 57 | return AliyunpanScope.File.GetFileDownloadUrl.Response( 58 | url: URL(string: "https://alipan.com")!, 59 | expiration: Date().addingTimeInterval(100), 60 | method: "GET") 61 | } 62 | 63 | func getOperationQueue() -> OperationQueue { 64 | operationQueue 65 | } 66 | 67 | func downloadTask(_ task: AliyunpanDownloadTask, didUpdateState state: AliyunpanDownloadTask.State) {} 68 | 69 | func downloadTask(task: AliyunpanDownloadTask, didWriteData bytesWritten: Int64) {} 70 | } 71 | 72 | class DownloaderActorTests: XCTestCase, AliyunpanDownloadTaskDelegate { 73 | private var getDownloadURLCount = 0 74 | 75 | func testDownloadActor() async throws { 76 | let actor = DownloadURLActor() 77 | _ = try await actor.getDownloadURL(with: file, by: self) 78 | _ = try await actor.getDownloadURL(with: file, by: self) 79 | _ = try await actor.getDownloadURL(with: file, by: self) 80 | _ = try await actor.getDownloadURL(with: file, by: self) 81 | _ = try await actor.getDownloadURL(with: file, by: self) 82 | _ = try await actor.getDownloadURL(with: file, by: self) 83 | _ = try await actor.getDownloadURL(with: file, by: self) 84 | _ = try await actor.getDownloadURL(with: file, by: self) 85 | _ = try await actor.getDownloadURL(with: file, by: self) 86 | _ = try await actor.getDownloadURL(with: file, by: self) 87 | _ = try await actor.getDownloadURL(with: file, by: self) 88 | 89 | XCTAssertEqual(getDownloadURLCount, 1) 90 | 91 | try await Task.sleep(seconds: 0.06) 92 | 93 | _ = try await actor.getDownloadURL(with: file, by: self) 94 | _ = try await actor.getDownloadURL(with: file, by: self) 95 | _ = try await actor.getDownloadURL(with: file, by: self) 96 | _ = try await actor.getDownloadURL(with: file, by: self) 97 | _ = try await actor.getDownloadURL(with: file, by: self) 98 | _ = try await actor.getDownloadURL(with: file, by: self) 99 | _ = try await actor.getDownloadURL(with: file, by: self) 100 | _ = try await actor.getDownloadURL(with: file, by: self) 101 | _ = try await actor.getDownloadURL(with: file, by: self) 102 | _ = try await actor.getDownloadURL(with: file, by: self) 103 | _ = try await actor.getDownloadURL(with: file, by: self) 104 | 105 | XCTAssertEqual(getDownloadURLCount, 2) 106 | 107 | _ = try await actor.getDownloadURL(with: file, by: self) 108 | _ = try await actor.getDownloadURL(with: file, by: self) 109 | _ = try await actor.getDownloadURL(with: file, by: self) 110 | 111 | XCTAssertEqual(getDownloadURLCount, 2) 112 | } 113 | 114 | func getFileDownloadUrl(driveId: String, fileId: String) async throws -> AliyunpanScope.File.GetFileDownloadUrl.Response { 115 | try await Task.sleep(seconds: 0.05) 116 | 117 | getDownloadURLCount += 1 118 | 119 | return AliyunpanScope.File.GetFileDownloadUrl.Response( 120 | url: URL(string: "https://alipan.com")!, 121 | expiration: Date().addingTimeInterval(0.05), 122 | method: "GET") 123 | } 124 | 125 | func getOperationQueue() -> OperationQueue { 126 | OperationQueue() 127 | } 128 | 129 | func downloadTask(_ task: AliyunpanDownloadTask, didUpdateState state: AliyunpanDownloadTask.State) {} 130 | 131 | func downloadTask(task: AliyunpanDownloadTask, didWriteData bytesWritten: Int64) {} 132 | } 133 | -------------------------------------------------------------------------------- /Tests/AliyunpanSDKTests/DownloaderTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DownloaderTests.swift 3 | // AliyunpanSDKTests 4 | // 5 | // Created by zhaixian on 2023/12/6. 6 | // 7 | 8 | import XCTest 9 | @testable import AliyunpanSDK 10 | 11 | class DownloaderTests: XCTestCase { 12 | let file = AliyunpanFile( 13 | drive_id: "drive_id", 14 | file_id: "file_id", 15 | parent_file_id: "", 16 | name: "", 17 | size: 12_345_678, 18 | file_extension: nil, 19 | content_hash: nil, 20 | type: .file, 21 | mime_type: nil, 22 | thumbnail: nil, 23 | url: nil, 24 | created_at: nil, 25 | updated_at: nil, 26 | play_cursor: nil, 27 | image_media_metadata: nil, 28 | video_media_metadata: nil) 29 | 30 | let destination = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first! 31 | 32 | func testChunk() { 33 | let chunk1 = AliyunpanDownloadChunk(rangeString: "bytes=0-999", fileSize: file.size ?? 0) 34 | XCTAssertEqual(chunk1?.start, 0) 35 | XCTAssertEqual(chunk1?.end, 1000) 36 | 37 | let chunk2 = AliyunpanDownloadChunk(rangeString: "bytes=1000-", fileSize: file.size ?? 0) 38 | XCTAssertEqual(chunk2?.start, 1000) 39 | XCTAssertEqual(chunk2?.end, 12_345_678) 40 | 41 | let chunk3 = AliyunpanDownloadChunk(rangeString: "bytes=-", fileSize: file.size ?? 0) 42 | XCTAssertNil(chunk3) 43 | 44 | let chunk4 = AliyunpanDownloadChunk(rangeString: "bytes", fileSize: file.size ?? 0) 45 | XCTAssertNil(chunk4) 46 | 47 | let downloadTask = AliyunpanDownloadTask( 48 | file: file, 49 | destination: destination, 50 | delegate: nil) 51 | let chunks = downloadTask.chunks 52 | XCTAssertEqual(chunks[0], .init(start: 0, end: 4_000_000)) 53 | XCTAssertEqual(chunks[1], .init(start: 4_000_000, end: 8_000_000)) 54 | XCTAssertEqual(chunks[2], .init(start: 8_000_000, end: 12_000_000)) 55 | XCTAssertEqual(chunks[3], .init(start: 12_000_000, end: 12_345_678)) 56 | XCTAssertEqual(chunks.count, 4) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Tests/AliyunpanSDKTests/HTTPRequestTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPRequestTests.swift 3 | // AliyunpanSDKTests 4 | // 5 | // Created by zhaixian on 2023/11/24. 6 | // 7 | 8 | import XCTest 9 | @testable import AliyunpanSDK 10 | 11 | extension [URLQueryItem] { 12 | subscript(key: String) -> String? { 13 | first(where: { $0.name == key })?.value 14 | } 15 | } 16 | 17 | class HTTPRequestTests: XCTestCase { 18 | func testURLExtension() throws { 19 | let url = URL(string: "https://alipan.com?a=1&b=2&c=3&d=4")! 20 | let queryItem = url.queryItems 21 | XCTAssertEqual(queryItem.count, 4) 22 | XCTAssertEqual(queryItem[0].name, "a") 23 | XCTAssertEqual(queryItem[0].value, "1") 24 | XCTAssertEqual(queryItem[1].name, "b") 25 | XCTAssertEqual(queryItem[1].value, "2") 26 | XCTAssertEqual(queryItem[2].name, "c") 27 | XCTAssertEqual(queryItem[2].value, "3") 28 | XCTAssertEqual(queryItem[3].name, "d") 29 | XCTAssertEqual(queryItem[3].value, "4") 30 | 31 | let url2 = URL(string: "https://alipan.com")! 32 | XCTAssertEqual(url2.queryItems.count, 0) 33 | } 34 | 35 | func testHTTPHeader1() throws { 36 | let request = HTTPRequest(command: AliyunpanScope.User.GetDriveInfo()) 37 | let urlRequest = try request.asURLRequest() 38 | XCTAssertEqual(urlRequest.headers.count, HTTPHeaders.default.count) 39 | HTTPHeaders.default.forEach { 40 | XCTAssertTrue(urlRequest.headers.contains($0)) 41 | } 42 | } 43 | 44 | func testHTTPHeader2() throws { 45 | let request = HTTPRequest(command: AliyunpanScope.User.GetVipInfo()) 46 | .headers([ 47 | .acceptEncoding("other"), 48 | .authorization(bearerToken: "abc") 49 | ]) 50 | let urlRequest = try request.asURLRequest() 51 | XCTAssertEqual(urlRequest.allHTTPHeaderFields!["Accept-Encoding"], "other") 52 | XCTAssertEqual(urlRequest.allHTTPHeaderFields!["Authorization"], "Bearer abc") 53 | } 54 | 55 | func testHTTPBody1() throws { 56 | let request = HTTPRequest( 57 | command: AliyunpanScope.File.GetFileList( 58 | .init(drive_id: "drive_id1", 59 | parent_file_id: "parent_file_id1", 60 | limit: 300, 61 | marker: "next_marker", 62 | order_by: .created_at, 63 | order_direction: .desc))) 64 | let urlRequest = try request.asURLRequest() 65 | XCTAssertEqual(urlRequest.httpMethod?.lowercased(), "post") 66 | 67 | let json = try JSONSerialization.jsonObject(with: urlRequest.httpBody!) as! [String: Any] 68 | XCTAssertEqual(json["drive_id"] as! String, "drive_id1") 69 | XCTAssertEqual(json["parent_file_id"] as! String, "parent_file_id1") 70 | XCTAssertEqual(json["limit"] as! Int, 300) 71 | XCTAssertEqual(json["marker"] as! String, "next_marker") 72 | XCTAssertEqual(json["order_by"] as! String, "created_at") 73 | XCTAssertEqual(json["order_direction"] as! String, "DESC") 74 | } 75 | 76 | func testHTTPBody2() throws { 77 | let request = HTTPRequest( 78 | command: AliyunpanScope.File.GetStarredList( 79 | .init( 80 | drive_id: "drive_id1", 81 | limit: 300, 82 | marker: "next_marker", 83 | order_by: .created_at, 84 | order_direction: .desc))) 85 | let urlRequest = try request.asURLRequest() 86 | XCTAssertEqual(urlRequest.httpMethod?.lowercased(), "post") 87 | 88 | let json = try JSONSerialization.jsonObject(with: urlRequest.httpBody!) as! [String: Any] 89 | XCTAssertEqual(json["drive_id"] as! String, "drive_id1") 90 | XCTAssertEqual(json["limit"] as! Int, 300) 91 | XCTAssertEqual(json["marker"] as! String, "next_marker") 92 | XCTAssertEqual(json["order_by"] as! String, "created_at") 93 | XCTAssertEqual(json["order_direction"] as! String, "DESC") 94 | } 95 | 96 | func testHTTPParams() throws { 97 | let request = HTTPRequest( 98 | command: AliyunpanScope.Internal.Authorize( 99 | .init( 100 | client_id: "client_id1", 101 | redirect_uri: "redirect_uri1", 102 | scope: "scope1", 103 | response_type: "response_type1", 104 | relogin: true))) 105 | let urlRequest = try request.asURLRequest() 106 | XCTAssertEqual(urlRequest.httpMethod?.lowercased(), "get") 107 | 108 | let queryItem = urlRequest.url!.queryItems 109 | 110 | XCTAssertEqual(queryItem["client_id"], "client_id1") 111 | XCTAssertEqual(queryItem["redirect_uri"], "redirect_uri1") 112 | XCTAssertEqual(queryItem["scope"], "scope1") 113 | XCTAssertEqual(queryItem["response_type"], "response_type1") 114 | XCTAssertEqual(queryItem["relogin"], "1") 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Tests/AliyunpanSDKTests/MessageTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageTests.swift 3 | // AliyunpanSDKTests 4 | // 5 | // Created by zhaixian on 2023/12/5. 6 | // 7 | 8 | import XCTest 9 | @testable import AliyunpanSDK 10 | 11 | extension AliyunpanError.AuthorizeError: Equatable { 12 | public static func == (lhs: AliyunpanError.AuthorizeError, rhs: AliyunpanError.AuthorizeError) -> Bool { 13 | lhs.localizedDescription == rhs.localizedDescription 14 | } 15 | } 16 | 17 | class MessageTests: XCTestCase { 18 | func testAliyunpanMessage() throws { 19 | let state = Date().timeIntervalSince1970 20 | 21 | let url1 = URL(string: "smartdrive://authorize?state=\(state)")! 22 | let message1 = try AliyunpanMessage(url1) 23 | XCTAssertEqual(message1.state, "\(state)") 24 | XCTAssertEqual(message1.originalURL, url1) 25 | 26 | let url2 = URL(string: "abcd://authorize?state=\(state)")! 27 | XCTAssertThrowsError(try AliyunpanMessage(url2)) { error in 28 | XCTAssertEqual(error as! AliyunpanError.AuthorizeError, AliyunpanError.AuthorizeError.invalidAuthorizeURL) 29 | } 30 | 31 | let url3 = URL(string: "https://stg.alipan.com/applink/authorize?state=\(state)")! 32 | let message3 = try AliyunpanMessage(url3) 33 | XCTAssertEqual(message3.state, "\(state)") 34 | XCTAssertEqual(message3.originalURL, url3) 35 | 36 | let url4 = URL(string: "https://www.alipan.com/applink/authorize?state=\(state)")! 37 | let message4 = try AliyunpanMessage(url4) 38 | XCTAssertEqual(message4.state, "\(state)") 39 | XCTAssertEqual(message4.originalURL, url4) 40 | } 41 | 42 | func testAliyunpanAuthMessage() throws { 43 | let state = Date().timeIntervalSince1970 44 | let code = "abcd" 45 | 46 | let url1 = URL(string: "smartdrive123456://authorize?state=\(state)&code=\(code)")! 47 | let message1 = try AliyunpanAuthorizeMessage(url1) 48 | XCTAssertEqual(message1.state, "\(state)") 49 | XCTAssertEqual(message1.authCode, code) 50 | XCTAssertEqual(message1.originalURL, url1) 51 | 52 | let url2 = URL(string: "abcd://authorize?state=\(state)&code=\(code)")! 53 | XCTAssertThrowsError(try AliyunpanAuthorizeMessage(url2)) { error in 54 | XCTAssertEqual(error as! AliyunpanError.AuthorizeError, AliyunpanError.AuthorizeError.invalidAuthorizeURL) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Tests/AliyunpanSDKTests/TaskTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TaskTests.swift 3 | // AliyunpanSDK 4 | // 5 | // Created by zhaixian on 2023/12/15. 6 | // 7 | 8 | import Foundation 9 | import XCTest 10 | @testable import AliyunpanSDK 11 | 12 | class TaskTests: XCTestCase { 13 | func testTimeout() async throws { 14 | do { 15 | let _ = try await withTimeout(seconds: 0.05) { 16 | try await Task.sleep(seconds: 0.1) 17 | return 1 18 | } 19 | } catch { 20 | XCTAssertTrue(error is CancellationError) 21 | } 22 | 23 | let value1 = try await withTimeout(seconds: 0.05) { 24 | Task { 25 | try await Task.sleep(seconds: 0.05) 26 | return 1 27 | } 28 | }.value 29 | XCTAssertEqual(1, value1) 30 | 31 | let value2 = try await withTimeout(seconds: 0.05) { 32 | Task { 33 | try await Task.sleep(seconds: 0.025) 34 | return 1 35 | } 36 | }.value 37 | XCTAssertEqual(1, value2) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Tests/TestFile1.txt: -------------------------------------------------------------------------------- 1 | Hello World! -------------------------------------------------------------------------------- /fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | default_platform(:ios) 2 | 3 | platform :ios do 4 | lane :tests do 5 | test(destination: "platform=macOS") 6 | test(destination: "platform=iOS Simulator,name=iPhone 14") 7 | test(destination: "platform=tvOS Simulator,name=Apple TV") 8 | end 9 | 10 | lane :test_ci do 11 | test(destination: ENV["DESTINATION"]) 12 | end 13 | 14 | lane :test do |options| 15 | scan( 16 | scheme: "AliyunpanSDK", 17 | clean: true, 18 | destination: options[:destination] 19 | ) 20 | end 21 | 22 | lane :bumpVersion do |options| 23 | target_version = options[:version] 24 | 25 | if target_version.nil? 26 | increment_version_number 27 | target_version = get_version_number 28 | else 29 | increment_version_number(version_number: target_version) 30 | end 31 | version_bump_podspec(path: "AliyunpanSDK.podspec", version_number: target_version) 32 | 33 | Actions.sh("sed -i '' 's/let version = \"[^\"]*\"/let version = \"#{target_version}\"/g' ../Sources/AliyunpanSDK/AliyunpanSDK.swift") 34 | 35 | # git commit all 36 | message = "Bump version to #{target_version}" 37 | Action.sh "git add -A" 38 | Actions.sh "git commit -am \"#{message}\"" 39 | end 40 | 41 | desc "Release new version" 42 | lane :release do |options| 43 | ensure_git_status_clean 44 | 45 | ensure_git_branch( 46 | branch: 'main' 47 | ) 48 | 49 | target_version = options[:version] 50 | 51 | # git tag 52 | Actions.sh("git tag v#{target_version}") 53 | 54 | sh('git fetch --tags') 55 | 56 | most_recent_tags = sh("git tag --sort=-version:refname | head -2").split("\n").reverse() 57 | release_log = sh("git log --pretty=format:'- %s' #{most_recent_tags[0]}...#{most_recent_tags[1]}") 58 | 59 | Actions.sh("git push origin v#{target_version}") 60 | 61 | xcframework(version: target_version) 62 | set_github_release( 63 | repository_name: "alibaba/aliyunpan-ios-sdk", 64 | api_token: ENV['GITHUB_TOKEN'], 65 | name: target_version, 66 | tag_name: "v#{target_version}", 67 | description: release_log, 68 | upload_assets: ["build/AliyunpanSDK-#{target_version}.zip"] 69 | ) 70 | 71 | pod_push( 72 | allow_warnings: true 73 | ) 74 | end 75 | 76 | lane :xcframework do |options| 77 | target_version = "AliyunpanSDK-#{options[:version]}" 78 | 79 | FileUtils.rm_rf '../build' 80 | 81 | frameworks = {} 82 | 83 | ["macosx", 84 | "iphoneos", 85 | "iphonesimulator", 86 | "appletvos", 87 | "appletvsimulator", 88 | # "watchos", 89 | # "watchsimulator", 90 | # "xros", 91 | # "xrsimulator" 92 | ].each do |sdk| 93 | archive_path = "build/AliyunpanSDK-#{sdk}.xcarchive" 94 | xcodebuild( 95 | archive: true, 96 | archive_path: archive_path, 97 | scheme: "AliyunpanSDK", 98 | sdk: sdk, 99 | build_settings: { 100 | "BUILD_LIBRARY_FOR_DISTRIBUTION" => "YES", 101 | "SKIP_INSTALL" => "NO" 102 | } 103 | ) 104 | 105 | dSYM_path = "#{Dir.pwd}/../#{archive_path}/dSYMs/AliyunpanSDK.framework.dSYM" 106 | frameworks["#{archive_path}/Products/Library/Frameworks/AliyunpanSDK.framework"] = { dsyms: dSYM_path } 107 | end 108 | 109 | create_xcframework( 110 | frameworks_with_dsyms: frameworks, 111 | output: "build/#{target_version}/AliyunpanSDK.xcframework" 112 | ) 113 | 114 | zip( 115 | path: "build/#{target_version}", 116 | output_path: "build/#{target_version}.zip", 117 | symlinks: true 118 | ) 119 | end 120 | 121 | lane :doc do |options| 122 | jazzy 123 | end 124 | end --------------------------------------------------------------------------------