jsdw_ios/QuickLocation/Service/UploadService.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]
)
}
}