diff --git a/AliyunpanSDK.xcodeproj/project.pbxproj b/AliyunpanSDK.xcodeproj/project.pbxproj index f30bd1a..7e38004 100644 --- a/AliyunpanSDK.xcodeproj/project.pbxproj +++ b/AliyunpanSDK.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + F447857D2BBAB9AE00FE8247 /* TestFile1.txt in Resources */ = {isa = PBXBuildFile; fileRef = F447857C2BBAB9AE00FE8247 /* TestFile1.txt */; }; F47FADDE2B14767D00EC0D8D /* AliyunpanSDK.swift in Sources */ = {isa = PBXBuildFile; fileRef = F47FADB72B14767D00EC0D8D /* AliyunpanSDK.swift */; }; F47FADDF2B14767D00EC0D8D /* AliyunpanPKCECredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = F47FADB92B14767D00EC0D8D /* AliyunpanPKCECredentials.swift */; }; F47FADE12B14767D00EC0D8D /* AliyunpanAppJumper.swift in Sources */ = {isa = PBXBuildFile; fileRef = F47FADBB2B14767D00EC0D8D /* AliyunpanAppJumper.swift */; }; @@ -60,6 +61,7 @@ F498215C2B188A6D006559CC /* GetVipFeatureList.swift in Sources */ = {isa = PBXBuildFile; fileRef = F498213D2B188A6D006559CC /* GetVipFeatureList.swift */; }; F498215E2B188A6D006559CC /* GetVipFeatureTrial.swift in Sources */ = {isa = PBXBuildFile; fileRef = F498213F2B188A6D006559CC /* GetVipFeatureTrial.swift */; }; F49821602B188C4C006559CC /* AliyunpanSpaceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = F498215F2B188C4C006559CC /* AliyunpanSpaceInfo.swift */; }; + F4A0FF742BA2F95800E83A46 /* AliyunpanLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4A0FF732BA2F95800E83A46 /* AliyunpanLogger.swift */; }; F4A37F1C2B562081009AC6AA /* AliyunpanTokenCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4A37F1B2B562081009AC6AA /* AliyunpanTokenCredentials.swift */; }; F4B3F7332B29859100D9E122 /* CredentialTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4B3F7322B29859100D9E122 /* CredentialTests.swift */; }; F4BD1B642B29A11B002BEA2A /* Platform.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4BD1B632B29A11B002BEA2A /* Platform.swift */; }; @@ -79,6 +81,7 @@ F4BD1C622B34366E002BEA2A /* DownloaderOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4BD1C602B343423002BEA2A /* DownloaderOperationTests.swift */; }; F4BD1C652B34403F002BEA2A /* DownloaderTaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4BD1C632B343FD6002BEA2A /* DownloaderTaskTests.swift */; }; F4C6F2312B060C4B003A06B3 /* AliyunpanSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4C6F2262B060C4B003A06B3 /* AliyunpanSDK.framework */; }; + F4DDB1CE2BB17E4C0005DA0C /* AliyunpanUploader.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4DDB1CD2BB17E4C0005DA0C /* AliyunpanUploader.swift */; }; F4E9F9692B148ABA00DC8DBF /* Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4E9F9672B148ABA00DC8DBF /* Crypto.swift */; }; F4E9F96A2B148ABA00DC8DBF /* URL+AliyunpanSDK.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4E9F9682B148ABA00DC8DBF /* URL+AliyunpanSDK.swift */; }; F4E9F96C2B148AEE00DC8DBF /* AliyunpanMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4E9F96B2B148AEE00DC8DBF /* AliyunpanMessage.swift */; }; @@ -97,6 +100,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + F447857C2BBAB9AE00FE8247 /* TestFile1.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = TestFile1.txt; path = Tests/TestFile1.txt; sourceTree = SOURCE_ROOT; }; F47FADB72B14767D00EC0D8D /* AliyunpanSDK.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AliyunpanSDK.swift; sourceTree = ""; }; F47FADB92B14767D00EC0D8D /* AliyunpanPKCECredentials.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AliyunpanPKCECredentials.swift; sourceTree = ""; }; F47FADBB2B14767D00EC0D8D /* AliyunpanAppJumper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AliyunpanAppJumper.swift; sourceTree = ""; }; @@ -152,6 +156,7 @@ F498213D2B188A6D006559CC /* GetVipFeatureList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetVipFeatureList.swift; sourceTree = ""; }; F498213F2B188A6D006559CC /* GetVipFeatureTrial.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetVipFeatureTrial.swift; sourceTree = ""; }; F498215F2B188C4C006559CC /* AliyunpanSpaceInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AliyunpanSpaceInfo.swift; sourceTree = ""; }; + F4A0FF732BA2F95800E83A46 /* AliyunpanLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AliyunpanLogger.swift; sourceTree = ""; }; F4A37F1B2B562081009AC6AA /* AliyunpanTokenCredentials.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AliyunpanTokenCredentials.swift; sourceTree = ""; }; F4B3F7142B202CB500D9E122 /* DownloaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloaderTests.swift; sourceTree = ""; }; F4B3F7322B29859100D9E122 /* CredentialTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialTests.swift; sourceTree = ""; }; @@ -172,6 +177,7 @@ F4BD1C632B343FD6002BEA2A /* DownloaderTaskTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloaderTaskTests.swift; sourceTree = ""; }; F4C6F2262B060C4B003A06B3 /* AliyunpanSDK.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AliyunpanSDK.framework; sourceTree = BUILT_PRODUCTS_DIR; }; F4C6F2302B060C4B003A06B3 /* AliyunpanSDKTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AliyunpanSDKTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F4DDB1CD2BB17E4C0005DA0C /* AliyunpanUploader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AliyunpanUploader.swift; sourceTree = ""; }; F4E9F9672B148ABA00DC8DBF /* Crypto.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Crypto.swift; sourceTree = ""; }; F4E9F9682B148ABA00DC8DBF /* URL+AliyunpanSDK.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URL+AliyunpanSDK.swift"; sourceTree = ""; }; F4E9F96B2B148AEE00DC8DBF /* AliyunpanMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AliyunpanMessage.swift; sourceTree = ""; }; @@ -217,8 +223,9 @@ F47FADC62B14767D00EC0D8D /* AliyunpanError.swift */, F47FADD62B14767D00EC0D8D /* AliyunpanClient.swift */, F47FADB72B14767D00EC0D8D /* AliyunpanSDK.swift */, - F47FADBE2B14767D00EC0D8D /* AliyunpanSDK.h */, + F4A0FF732BA2F95800E83A46 /* AliyunpanLogger.swift */, F4BD1B632B29A11B002BEA2A /* Platform.swift */, + F47FADBE2B14767D00EC0D8D /* AliyunpanSDK.h */, F47FADD72B14767D00EC0D8D /* Info.plist */, ); path = AliyunpanSDK; @@ -241,6 +248,7 @@ F47FADC02B14767D00EC0D8D /* HTTPRequest */ = { isa = PBXGroup; children = ( + F4A0FF722BA2F85700E83A46 /* Upload */, F4BD1C422B302EF0002BEA2A /* Download */, F49821192B1742B6006559CC /* DebugDescription.swift */, F47FADC12B14767D00EC0D8D /* URLConvertible.swift */, @@ -288,6 +296,7 @@ F47FADDA2B14767D00EC0D8D /* AliyunpanSDKTests */ = { isa = PBXGroup; children = ( + F447857C2BBAB9AE00FE8247 /* TestFile1.txt */, F47FADDC2B14767D00EC0D8D /* AliyunpanSDKTests.swift */, F49745FE2B1F100C0043A4D7 /* AliyunpanClientTests.swift */, F4B3F7322B29859100D9E122 /* CredentialTests.swift */, @@ -370,6 +379,14 @@ path = Vip; sourceTree = ""; }; + F4A0FF722BA2F85700E83A46 /* Upload */ = { + isa = PBXGroup; + children = ( + F4DDB1CD2BB17E4C0005DA0C /* AliyunpanUploader.swift */, + ); + path = Upload; + sourceTree = ""; + }; F4BD1C422B302EF0002BEA2A /* Download */ = { isa = PBXGroup; children = ( @@ -512,6 +529,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + F447857D2BBAB9AE00FE8247 /* TestFile1.txt in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -583,6 +601,7 @@ F47FADE72B14767D00EC0D8D /* HTTPRequest.swift in Sources */, F498214E2B188A6D006559CC /* TrashFileToRecyclebin.swift in Sources */, F47FADEB2B14767D00EC0D8D /* AliyunpanError.swift in Sources */, + F4A0FF742BA2F95800E83A46 /* AliyunpanLogger.swift in Sources */, F47FADE82B14767D00EC0D8D /* HTTPMethod.swift in Sources */, F498214B2B188A6D006559CC /* GetFileByPath.swift in Sources */, F498215A2B188A6D006559CC /* GetAsyncTask.swift in Sources */, @@ -598,6 +617,7 @@ F4BD1C362B2AD8E1002BEA2A /* Task+AliyunpanSDK.swift in Sources */, F4BD1B642B29A11B002BEA2A /* Platform.swift in Sources */, F49821432B188A6D006559CC /* GetVideoRecentList.swift in Sources */, + F4DDB1CE2BB17E4C0005DA0C /* AliyunpanUploader.swift in Sources */, F498214C2B188A6D006559CC /* MoveFile.swift in Sources */, F47FADE62B14767D00EC0D8D /* URLConvertible.swift in Sources */, F4E9F96A2B148ABA00DC8DBF /* URL+AliyunpanSDK.swift in Sources */, diff --git a/Demo/Demo/Demo-iOS/ViewController.swift b/Demo/Demo/Demo-iOS/ViewController.swift index c29504a..0bcd5b6 100644 --- a/Demo/Demo/Demo-iOS/ViewController.swift +++ b/Demo/Demo/Demo-iOS/ViewController.swift @@ -64,9 +64,6 @@ class ViewController: UIViewController { } private func uploadFile(withURL url: URL) { - /// 单片,小于 5G - /// 大于 5G 请分片上传 - /// https://www.yuque.com/aliyundrive/zpfszx/ezlzok#C8JdZ Task { do { let driveInfo = try await client @@ -75,35 +72,15 @@ class ViewController: UIViewController { let driveId = driveInfo.default_drive_id - let response = try await client - .authorize() - .send( - AliyunpanScope.File.CreateFile( - .init( - drive_id: driveId, - parent_file_id: "root", - name: url.lastPathComponent, - check_name_mode: .auto_rename))) + let file = try await client.uploader + .upload( + fileURL: url, + fileName: "test_\(Date().timeIntervalSince1970).pdf", + driveId: driveId, + folderId: "root", + useProof: true) - if let uploadURL = response.part_info_list?.first?.upload_url { - var urlRequest = URLRequest(url: uploadURL) - urlRequest.httpMethod = "put" - urlRequest.allHTTPHeaderFields = [ - "Content-Type": "" // 不能传 Cotent-Type,否则会失败 - ] - _ = try await URLSession.shared.upload(for: urlRequest, fromFile: url) - - let file = try await client - .authorize() - .send( - AliyunpanScope.File.CompleteUpload( - .init( - drive_id: driveId, - file_id: response.file_id, - upload_id: response.upload_id ?? ""))) - - showAlert(message: file.description) - } + showAlert(message: file.description) } catch { print(error) } diff --git a/Demo/Podfile.lock b/Demo/Podfile.lock index 26921c1..a4bf29c 100644 --- a/Demo/Podfile.lock +++ b/Demo/Podfile.lock @@ -1,5 +1,5 @@ PODS: - - AliyunpanSDK (0.1.18) + - AliyunpanSDK (0.2.0) DEPENDENCIES: - AliyunpanSDK (from `../`) @@ -9,8 +9,8 @@ EXTERNAL SOURCES: :path: "../" SPEC CHECKSUMS: - AliyunpanSDK: 59f71bc271322edb3eb39c6260a7a4ef616410c7 + AliyunpanSDK: 9677ed52f2519f512e91bf3097438c35772535e3 PODFILE CHECKSUM: 40334940ba0e4f081833162ea871b81051e79c17 -COCOAPODS: 1.14.3 +COCOAPODS: 1.15.2 diff --git a/README.md b/README.md index a5ad70a..dd6d326 100644 --- a/README.md +++ b/README.md @@ -56,27 +56,41 @@ client.authorize(credentials: credentials) ```swift // Concurrency -try await client - .authorize() // 默认 pkce - .send(AliyunpanScope.User.GetUsersInfo()) // -> GetUsersInfo.Response - -try await client - .authorize() - .send( - AliyunpanScope.File.GetFileList( - .init(drive_id: driveId, parent_file_id: "root")))) // -> GetFileList.Response +try await client.send( + AliyunpanScope.User.GetUsersInfo()) // -> GetUsersInfo.Response + +try await client.send( + AliyunpanScope.File.GetFileList( + .init(drive_id: driveId, parent_file_id: "root")))) // -> GetFileList.Response // Closure -client - .authorize() - .send( - AliyunpanScope.User.GetUsersInfo()) { result in - /// do something - } +client.send( + AliyunpanScope.User.GetUsersInfo()) { result in + /// do something +} ``` ## 高级功能 +### 上传 +```swift +let uploader = client.uploader + +// 上传 +let task = Task { + let file = try? await uploader.upload( + fileURL: url, + fileName: fileName, + driveId: driveId, + folderId: folderId, + useProof: true // 是否开启快传 + ) +} + +// 取消 +task.cancel() +``` + ### 下载 ```swift let downloader = client.downloader diff --git a/Sources/AliyunpanSDK/AliyunpanClient.swift b/Sources/AliyunpanSDK/AliyunpanClient.swift index d8a9b99..7510f5a 100644 --- a/Sources/AliyunpanSDK/AliyunpanClient.swift +++ b/Sources/AliyunpanSDK/AliyunpanClient.swift @@ -54,6 +54,13 @@ public class AliyunpanClient { return downloader }() + /// 上传器 + public lazy var uploader: AliyunpanUploader = { + let uploader = AliyunpanUploader() + uploader.client = self + return uploader + }() + public init(_ config: AliyunpanClientConfig) { self.config = config @@ -91,6 +98,40 @@ public class AliyunpanClient { } return token } + + /// 发送请求 + /// + /// - throws: + /// `DecodingError`: JSON 解析错误 + /// `AliyunpanAuthorizeError`: 授权错误 + /// `AliyunpanServerError`: 服务端错误 + /// `AliyunpanNetworkSystemError`: 网络系统错误 + public func send(_ command: T) async throws -> T.Response where T.Response: Decodable { + guard let token = await token else { + throw AliyunpanError.AuthorizeError.accessTokenInvalid + } + return try await token.send(command) + } + + /// 发送请求 + /// + /// - throws: + /// `DecodingError`: JSON 解析错误 + /// `AliyunpanAuthorizeError`: 授权错误 + /// `AliyunpanServerError`: 服务端错误 + /// `AliyunpanNetworkSystemError`: 网络系统错误 + public func send( + _ command: T, + completionHandle: @escaping (Result) -> Void) where T.Response: Decodable { + Task { + do { + let response = try await send(command) + completionHandle(.success(response)) + } catch { + completionHandle(.failure(error)) + } + } + } } extension AliyunpanToken { diff --git a/Sources/AliyunpanSDK/AliyunpanError.swift b/Sources/AliyunpanSDK/AliyunpanError.swift index 27bf508..fd35f9c 100644 --- a/Sources/AliyunpanSDK/AliyunpanError.swift +++ b/Sources/AliyunpanSDK/AliyunpanError.swift @@ -18,6 +18,8 @@ public struct AliyunpanError { case authorizeFailed(error: String?, errorMsg: String?) /// 验证码授权超时 case qrCodeAuthorizeTimeout + /// 未授权或授权已过期 + case accessTokenInvalid } /// 网络层错误 @@ -59,9 +61,11 @@ public struct AliyunpanError { case forbiddenDriveLocked = "ForbiddenDriveLocked" /// 非法访问drive case forbiddenDriveNotValid = "ForbiddenDriveNotValid" + /// 快传预检匹配成功 + case preHashMatched = "PreHashMatched" } - public let code: Code? + public let code: Code public let message: String? public let requestId: String? @@ -81,6 +85,14 @@ public struct AliyunpanError { /// 缺少 client case invalidClient } + + /// 上传错误 + public enum UploadError: Error { + /// 缺少 client + case invalidClient + /// 快传预检查失败 + case preHashNotMatched + } /// 系统级网络层错误 public enum NetworkSystemError: Error { diff --git a/Sources/AliyunpanSDK/AliyunpanLogger.swift b/Sources/AliyunpanSDK/AliyunpanLogger.swift new file mode 100644 index 0000000..b2af1b9 --- /dev/null +++ b/Sources/AliyunpanSDK/AliyunpanLogger.swift @@ -0,0 +1,37 @@ +// +// AliyunpanLogger.swift +// AliyunpanSDK +// +// Created by zhaixian on 2024/3/14. +// + +import Foundation + +public enum AliyunpanLogLevel: Int { + case debug + case info + case warn + case error + + var msg: String { + switch self { + case .debug: + return "DEBUG" + case .info: + return "INFO" + case .warn: + return "⚠️" + case .error: + return "❌" + } + } +} + +class Logger { + static func log(_ level: AliyunpanLogLevel, msg: String) { + guard level.rawValue >= Aliyunpan.logLevel.rawValue else { + return + } + print("[AliyunpanSDK][\(level.msg)]\(msg)") + } +} diff --git a/Sources/AliyunpanSDK/AliyunpanSDK.swift b/Sources/AliyunpanSDK/AliyunpanSDK.swift index bacf9d0..4cba072 100644 --- a/Sources/AliyunpanSDK/AliyunpanSDK.swift +++ b/Sources/AliyunpanSDK/AliyunpanSDK.swift @@ -7,35 +7,6 @@ import Foundation -public enum AliyunpanLogLevel: Int { - case debug - case info - case warn - case error - - var msg: String { - switch self { - case .debug: - return "DEBUG" - case .info: - return "INFO" - case .warn: - return "⚠️" - case .error: - return "❌" - } - } -} - -class Logger { - static func log(_ level: AliyunpanLogLevel, msg: String) { - guard level.rawValue >= Aliyunpan.logLevel.rawValue else { - return - } - print("[AliyunpanSDK][\(level.msg)]\(msg)") - } -} - public class Aliyunpan { public enum Environment { /// 预发 @@ -68,9 +39,12 @@ public class Aliyunpan { } public private(set) static var env: Environment = .product + + #if DEBUG public static func setEnvironment(_ env: Environment) { self.env = env } + #endif @discardableResult public static func handleOpenURL(_ url: URL) -> Bool { diff --git a/Sources/AliyunpanSDK/AliyunpanScope/File/CreateFile.swift b/Sources/AliyunpanSDK/AliyunpanScope/File/CreateFile.swift index 47b1116..fa1f9dd 100644 --- a/Sources/AliyunpanSDK/AliyunpanScope/File/CreateFile.swift +++ b/Sources/AliyunpanSDK/AliyunpanScope/File/CreateFile.swift @@ -23,6 +23,8 @@ extension AliyunpanFileScope { public let name: String /// file | folder public let type: AliyunpanFile.FileType + /// 文件类型 + public let content_type: String? /// 重名策略 public let check_name_mode: AliyunpanFile.CheckNameMode /// 最大分片数量 10000 @@ -51,6 +53,7 @@ extension AliyunpanFileScope { parent_file_id: String, name: String, type: AliyunpanFile.FileType = .file, + content_type: String? = nil, check_name_mode: AliyunpanFile.CheckNameMode, part_info_list: [AliyunpanFile.PartInfo]? = nil, streams_info: AliyunpanFile.StreamsInfo? = nil, @@ -66,6 +69,7 @@ extension AliyunpanFileScope { self.parent_file_id = parent_file_id self.name = name self.type = type + self.content_type = content_type self.check_name_mode = check_name_mode self.part_info_list = part_info_list self.streams_info = streams_info diff --git a/Sources/AliyunpanSDK/HTTPRequest/Download/AliyunpanDownloadChunk.swift b/Sources/AliyunpanSDK/HTTPRequest/Download/AliyunpanDownloadChunk.swift index 5f0ec66..0b1aee9 100644 --- a/Sources/AliyunpanSDK/HTTPRequest/Download/AliyunpanDownloadChunk.swift +++ b/Sources/AliyunpanSDK/HTTPRequest/Download/AliyunpanDownloadChunk.swift @@ -7,7 +7,6 @@ import Foundation -/// public struct AliyunpanDownloadChunk: Equatable { public let start: Int64 public let end: Int64 diff --git a/Sources/AliyunpanSDK/HTTPRequest/Download/AliyunpanDownloader.swift b/Sources/AliyunpanSDK/HTTPRequest/Download/AliyunpanDownloader.swift index 3a219f0..a5ea1a1 100644 --- a/Sources/AliyunpanSDK/HTTPRequest/Download/AliyunpanDownloader.swift +++ b/Sources/AliyunpanSDK/HTTPRequest/Download/AliyunpanDownloader.swift @@ -10,7 +10,7 @@ import Foundation public typealias DownloadTasks = [AliyunpanDownloadTask] extension DownloadTasks { - mutating func finish(_ task: Element) { + mutating func cancel(_ task: Element) { removeAll(where: { $0.id == task.id }) @@ -71,7 +71,6 @@ public class AliyunpanDownloader: NSObject { }() private var lastWritedSize: Int64 = 0 - private var currentWritedSize: Int64 = 0 weak var client: AliyunpanClient? @@ -79,10 +78,6 @@ public class AliyunpanDownloader: NSObject { deinit { networkSpeedTimer.invalidate() } - - override init() { - super.init() - } } extension AliyunpanDownloader { @@ -132,7 +127,7 @@ extension AliyunpanDownloader { public func cancel(_ task: AliyunpanDownloadTask) { Logger.log(.info, msg: "[Downloader] cancel \(task.file.name)") task.cancel() - tasks.finish(task) + tasks.cancel(task) } } @@ -142,7 +137,6 @@ extension AliyunpanDownloader: AliyunpanDownloadTaskDelegate { throw AliyunpanError.DownloadError.invalidClient } return try await client - .authorize() .send( AliyunpanScope.File.GetFileDownloadUrl( .init(drive_id: driveId, file_id: fileId))) diff --git a/Sources/AliyunpanSDK/HTTPRequest/Upload/AliyunpanUploader.swift b/Sources/AliyunpanSDK/HTTPRequest/Upload/AliyunpanUploader.swift new file mode 100644 index 0000000..7a51371 --- /dev/null +++ b/Sources/AliyunpanSDK/HTTPRequest/Upload/AliyunpanUploader.swift @@ -0,0 +1,249 @@ +// +// AliyunpanUploader.swift +// AliyunpanSDK +// +// Created by zhaixian on 2024/3/25. +// + +import Foundation + +extension FileManager { + func dataChunk(at path: URL, in range: Range) throws -> Data { + let fileHandle = try FileHandle(forReadingFrom: path) + try fileHandle.seek(toOffset: UInt64(range.lowerBound)) + let data = fileHandle.readData(ofLength: range.upperBound - range.lowerBound) + try fileHandle.close() + + return data + } + + func fileSizeOfItem(at path: URL) throws -> Int64 { + let fileAttributes = try FileManager.default.attributesOfItem(atPath: path.path) + return fileAttributes[.size] as? Int64 ?? 0 + } +} + +fileprivate extension Array where Element == AliyunpanFile.PartInfo { + init(fileSize: Int64, chunkSize: Int64) { + self = stride(from: 0, to: fileSize, by: Int64.Stride(chunkSize)).enumerated().map { + let partSize = Swift.min(fileSize - $0.element, chunkSize) + return AliyunpanFile.PartInfo( + part_number: $0.offset + 1, + part_size: partSize + ) + } + } +} + +/// 上传器 +public class AliyunpanUploader: NSObject { + weak var client: AliyunpanClient? + /// 每 2G 分片 + private static let chunkSize: Int64 = 2_000_000_000 + + /// 创建上传任务,并适当分片 + private func createUploadTask( + client: AliyunpanClient, + fileURL: URL, + fileName: String, + fileSize: Int64, + driveId: String, + folderId: String, + checkNameMode: AliyunpanFile.CheckNameMode + ) async throws -> AliyunpanScope.File.CreateFile.Response { + let partInfoList = [AliyunpanFile.PartInfo](fileSize: fileSize, chunkSize: Self.chunkSize) + + let task = try await client.send( + AliyunpanScope.File.CreateFile( + .init( + drive_id: driveId, + parent_file_id: folderId, + name: fileName, + check_name_mode: .ignore, + part_info_list: partInfoList + ) + ) + ) + return task + } + + private func preProofMatch( + client: AliyunpanClient, + fileURL: URL, + fileName: String, + fileSize: Int64, + driveId: String, + folderId: String, + checkNameMode: AliyunpanFile.CheckNameMode + ) async throws -> Bool { + let preData = try FileManager.default.dataChunk( + at: fileURL, + in: 0..<1024 + ) + + let preSHA1 = AliyunpanCrypto.sha1AndHex(preData) + do { + _ = try await client.send( + AliyunpanScope.File.CreateFile( + .init( + drive_id: driveId, + parent_file_id: folderId, + name: fileName, + check_name_mode: checkNameMode, + pre_hash: preSHA1, + size: Int(fileSize) + ) + ) + ) + return false + } catch { + guard let error = error as? AliyunpanError.ServerError else { + return false + } + return error.code == .preHashMatched + } + } + + /// 创建秒传任务 + private func createProofUploadTask( + client: AliyunpanClient, + fileURL: URL, + fileName: String, + fileSize: Int64, + driveId: String, + folderId: String, + checkNameMode: AliyunpanFile.CheckNameMode + ) async throws -> AliyunpanScope.File.CreateFile.Response { + guard let token = await client.token else { + throw AliyunpanError.AuthorizeError.accessTokenInvalid + } + + let partInfoList = [AliyunpanFile.PartInfo](fileSize: fileSize, chunkSize: Self.chunkSize) + + var isPreHashMatched = false + // 大于 10M 的文件先预校验 + if fileSize > 10_000_000 { + isPreHashMatched = try await preProofMatch(client: client, fileURL: fileURL, fileName: fileName, fileSize: fileSize, driveId: driveId, folderId: folderId, checkNameMode: checkNameMode) + } + + if !isPreHashMatched { + throw AliyunpanError.UploadError.preHashNotMatched + } + + let contentHash = AliyunpanCrypto.sha1AndHex(fileURL) + let proofCode = AliyunpanCrypto.getProofCode(accessToken: token.access_token, fileURL: fileURL) + + let task = try await client.send( + AliyunpanScope.File.CreateFile( + .init( + drive_id: driveId, + parent_file_id: folderId, + name: fileName, + check_name_mode: checkNameMode, + part_info_list: partInfoList, + size: Int(fileSize), + content_hash: contentHash, + content_hash_name: "sha1", + proof_code: proofCode, + proof_version: "v1" + ) + ) + ) + return task + } + + /// 完成上传任务 + private func completeUploadTask( + client: AliyunpanClient, + driveId: String, + fileId: String, + uploadId: String + ) async throws -> AliyunpanFile { + try await client.send( + AliyunpanScope.File.CompleteUpload( + .init(drive_id: driveId, file_id: fileId, upload_id: uploadId) + ) + ) + } + + /// 上传文件 + /// - Parameters: + /// - fileURL: 文件 URL + /// - fileName: 文件名,可选,不填时为 fileURL.lastPathComponent + /// - driveId: 目标 drive id + /// - folderId: 目标文件夹 id + /// - checkNameMode: 重名策略,默认 .ignore + /// - useProof: 使用秒传,默认 false + /// - session: 上传使用的 URLSession + /// - Returns: AliyunpanFile + public func upload( + fileURL: URL, + fileName: String? = nil, + driveId: String, + folderId: String = "root", + checkNameMode: AliyunpanFile.CheckNameMode = .ignore, + useProof: Bool = false, + session: URLSession = URLSession.shared + ) async throws -> AliyunpanFile { + guard let client else { + throw AliyunpanError.UploadError.invalidClient + } + + let fileName = fileName ?? fileURL.lastPathComponent + let fileSize = try FileManager.default.fileSizeOfItem(at: fileURL) + + let task: AliyunpanScope.File.CreateFile.Response + if useProof { + task = try await createProofUploadTask( + client: client, + fileURL: fileURL, + fileName: fileName, + fileSize: fileSize, + driveId: driveId, + folderId: folderId, + checkNameMode: checkNameMode + ) + } else { + task = try await createUploadTask( + client: client, + fileURL: fileURL, + fileName: fileName, + fileSize: fileSize, + driveId: driveId, + folderId: folderId, + checkNameMode: checkNameMode + ) + } + + if task.rapid_upload == true { + // 秒传成功 + } else { + // 正常上传 + // 不支持并发上传 + let partInfoEnumerated = (task.part_info_list ?? []) + .enumerated() + for (index, partInfo) in partInfoEnumerated { + guard let uploadURL = partInfo.upload_url else { + continue + } + var urlRequest = URLRequest(url: uploadURL) + urlRequest.httpMethod = "put" + urlRequest.allHTTPHeaderFields = [ + "Content-Length": "\(partInfo.part_size ?? 0)", + "Content-Type": "" // 不能传 Cotent-Type,否则会失败 + ] + let beginOffset = Int64(index) * Self.chunkSize + let endOffset = beginOffset + Int64(partInfo.part_size ?? 0) + let data = try FileManager.default.dataChunk(at: fileURL, in: Int(beginOffset).. String { +extension Digest { + var bytes: [UInt8] { Array(makeIterator()) } + + var hexString: String { + bytes.map { String(format: "%02X", $0) }.joined() + } +} + +public class AliyunpanCrypto { + /// 大数据量 sha1 and hex + public static func sha1AndHex(_ fileURL: URL) -> String? { + guard let inputStream = InputStream(url: fileURL) else { + return nil + } + + defer { + inputStream.close() + } + + // 10M 缓冲区 + let bufferSize = 10 * 1024 * 1024 + var buffer = [UInt8](repeating: 0, count: bufferSize) + + var sha1 = Insecure.SHA1() + + inputStream.open() + var error: Error? + while inputStream.hasBytesAvailable { + let read = inputStream.read(&buffer, maxLength: bufferSize) + if read == 0 { + break + } else if read < 0 { + error = inputStream.streamError + break + } + let data = Data(buffer.prefix(read)) + sha1.update(data: data) + } + + if error != nil { + return nil + } + + let hashedData = sha1.finalize() + return hashedData.hexString + } + + /// 低数据量 sha1 and hex + public static func sha1AndHex(_ data: Data) -> String { + Insecure.SHA1.hash(data: data).hexString + } + + public static func sha256AndBase64(_ message: String) -> String { let inputData = Data(message.utf8) let hashedData = SHA256.hash(data: inputData) var string = Data(hashedData).base64EncodedString() as NSString @@ -17,4 +68,33 @@ class AliyunpanCrypto { string = string.replacingOccurrences(of: "/", with: "_") as NSString return string as String } + + public static func md5(_ message: String) -> String { + Insecure.MD5.hash(data: Data(message.utf8)) + .hexString + } + + /// 获取秒传值 + /// - Parameters: + /// - accessToken: access_token + /// - fileSize: 文件 size + /// - Returns: 秒传值 + public static func getProofCode(accessToken: String, fileURL: URL) -> String? { + do { + let fileSize = try FileManager.default.fileSizeOfItem(at: fileURL) + + let string = String(md5(accessToken).prefix(16)) + let value = strtoul(string, nil, 16) + + let index = UInt64(value) % UInt64(fileSize) + + let data = try FileManager.default.dataChunk( + at: fileURL, + in: Int(index)..