185 lines
6.7 KiB
Swift
185 lines
6.7 KiB
Swift
//
|
|
// UploadService.swift
|
|
// QuickLocation
|
|
//
|
|
|
|
import Foundation
|
|
import RxSwift
|
|
import SwiftyJSON
|
|
import CommonCrypto
|
|
|
|
enum UploadFileKind {
|
|
case jpeg
|
|
case mp4
|
|
case wav
|
|
|
|
var fileExtension: String {
|
|
switch self {
|
|
case .jpeg: return "jpg"
|
|
case .mp4: return "mp4"
|
|
case .wav: return "wav"
|
|
}
|
|
}
|
|
|
|
var mimeType: String {
|
|
switch self {
|
|
case .jpeg: return "image/jpeg"
|
|
case .mp4: return "video/mp4"
|
|
case .wav: return "audio/wav"
|
|
}
|
|
}
|
|
}
|
|
|
|
struct UploadService {
|
|
|
|
private static let boundary = "YLQH"
|
|
private static let jiaMiKey = "857d69d374694c1de46486d3bdaaac43"
|
|
|
|
static func upload(_ data: Data, kind: UploadFileKind, scene: String? = nil) -> Observable<String> {
|
|
Observable.create { observer in
|
|
do {
|
|
let request = try makeRequest(fileData: data, kind: kind, scene: scene)
|
|
let session = URLSession(configuration: .default)
|
|
let task = session.uploadTask(with: request.request, from: request.body) { data, response, error in
|
|
if let error {
|
|
DispatchQueue.main.async {
|
|
observer.onError(error)
|
|
}
|
|
return
|
|
}
|
|
guard let data else {
|
|
DispatchQueue.main.async {
|
|
observer.onError(makeError("上传失败"))
|
|
}
|
|
return
|
|
}
|
|
let decrypted = ApiManager.decryptIfNeeded(data) ?? data
|
|
let json = JSON(decrypted)
|
|
let code = json["code"].intValue
|
|
guard code == 0 || json["success"].boolValue else {
|
|
let message = json["message"].string ?? "上传失败"
|
|
DispatchQueue.main.async {
|
|
observer.onError(makeError(message))
|
|
}
|
|
return
|
|
}
|
|
let nestedId = json["data"]["id"].stringValue
|
|
let nestedFileId = json["data"]["file_id"].stringValue
|
|
let dataString = json["data"].stringValue
|
|
let fileId: String? = {
|
|
if !nestedId.isEmpty { return nestedId }
|
|
if !nestedFileId.isEmpty { return nestedFileId }
|
|
if !dataString.isEmpty, json["data"].type != .dictionary { return dataString }
|
|
return nil
|
|
}()
|
|
guard let fileId, !fileId.isEmpty else {
|
|
DispatchQueue.main.async {
|
|
observer.onError(makeError("上传成功但未返回文件 id"))
|
|
}
|
|
return
|
|
}
|
|
DispatchQueue.main.async {
|
|
observer.onNext(fileId)
|
|
observer.onCompleted()
|
|
}
|
|
}
|
|
task.resume()
|
|
return Disposables.create {
|
|
task.cancel()
|
|
}
|
|
} catch {
|
|
observer.onError(error)
|
|
return Disposables.create()
|
|
}
|
|
}
|
|
}
|
|
|
|
private static func makeRequest(fileData: Data, kind: UploadFileKind, scene: String?) throws -> (request: URLRequest, body: Data) {
|
|
let formatter = DateFormatter()
|
|
formatter.dateFormat = "yyyyMMddHHmmss"
|
|
let fileName = "\(formatter.string(from: Date())).\(kind.fileExtension)"
|
|
let body = multipartBody(fileData: fileData, fileName: fileName, mimeType: kind.mimeType)
|
|
|
|
var query: [String: String] = [
|
|
"timestamp": String(format: "%.0lf", Date().timeIntervalSince1970),
|
|
"nonce": randomString(length: 32)
|
|
]
|
|
if let scene, !scene.isEmpty {
|
|
query["scene"] = scene
|
|
}
|
|
let queryString = sortedQueryString(query)
|
|
let signature = sign(queryString: queryString, body: body)
|
|
let signedQuery = queryString + "&signature=" + signature
|
|
let encodedQuery = signedQuery.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? signedQuery
|
|
|
|
let base = AppNetworkConfig.shared.baseURL
|
|
guard let url = URL(string: base + "api/user/upload?" + encodedQuery) else {
|
|
throw makeError("上传地址无效")
|
|
}
|
|
|
|
var request = URLRequest(url: url)
|
|
request.httpMethod = "POST"
|
|
request.timeoutInterval = 180
|
|
var headers = ApiManager.shared.httpHeader()
|
|
headers["Content-Type"] = "multipart/form-data; boundary=\(boundary)"
|
|
headers.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) }
|
|
return (request, body)
|
|
}
|
|
|
|
private static func multipartBody(fileData: Data, fileName: String, mimeType: String) -> Data {
|
|
var data = Data()
|
|
func append(_ string: String) {
|
|
if let part = string.data(using: .utf8) {
|
|
data.append(part)
|
|
}
|
|
}
|
|
append("--\(boundary)\r\n")
|
|
append("Content-Disposition:form-data; name=\"file\"; filename=\"\(fileName)\"\r\n")
|
|
append("Content-Type:\(mimeType)\r\n")
|
|
append("\r\n")
|
|
data.append(fileData)
|
|
append("\r\n")
|
|
append("--\(boundary)--\r\n")
|
|
return data
|
|
}
|
|
|
|
private static func sign(queryString: String, body: Data) -> String {
|
|
var combined = Data()
|
|
combined.append(Data((queryString + "&").utf8))
|
|
combined.append(body)
|
|
combined.append(Data(("&" + md5(jiaMiKey)).utf8))
|
|
return md5(combined)
|
|
}
|
|
|
|
private static func sortedQueryString(_ dict: [String: String]) -> String {
|
|
dict.sorted { $0.key < $1.key }
|
|
.map { "\($0.key)=\($0.value)" }
|
|
.joined(separator: "&")
|
|
}
|
|
|
|
private static func randomString(length: Int) -> String {
|
|
let chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
|
return String((0..<length).map { _ in chars.randomElement()! })
|
|
}
|
|
|
|
private static func md5(_ string: String) -> String {
|
|
md5(Data(string.utf8))
|
|
}
|
|
|
|
private static func md5(_ data: Data) -> String {
|
|
var digest = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH))
|
|
data.withUnsafeBytes { buffer in
|
|
_ = CC_MD5(buffer.baseAddress, CC_LONG(data.count), &digest)
|
|
}
|
|
return digest.map { String(format: "%02x", $0) }.joined()
|
|
}
|
|
|
|
private static func makeError(_ message: String) -> NSError {
|
|
NSError(
|
|
domain: "UploadService",
|
|
code: -1,
|
|
userInfo: [NSLocalizedDescriptionKey: message]
|
|
)
|
|
}
|
|
}
|