jsdw_ios/QuickLocation/Service/UploadService.swift

225 lines
8.3 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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 struct UploadedFile {
let id: String
let url: String
}
private static let boundary = "YLQH"
private static let jiaMiKey = "857d69d374694c1de46486d3bdaaac43"
static func upload(_ data: Data, kind: UploadFileKind, scene: String? = nil) -> Observable<String> {
uploadFile(data, kind: kind, scene: scene)
.map(\.id)
}
/// 访 URL 使
static func uploadURL(_ data: Data, kind: UploadFileKind, scene: String? = nil) -> Observable<String> {
uploadFile(data, kind: kind, scene: scene)
.map { file in
guard !file.url.isEmpty else {
throw makeError("上传成功但未返回文件地址")
}
return file.url
}
}
private static func uploadFile(
_ data: Data,
kind: UploadFileKind,
scene: String?
) -> Observable<UploadedFile> {
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 nestedURL = json["data"]["url"].stringValue
let nestedFileURL = json["data"]["file_url"].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 }
if !nestedURL.isEmpty { return nestedURL }
if !nestedFileURL.isEmpty { return nestedFileURL }
return nil
}()
guard let fileId, !fileId.isEmpty else {
DispatchQueue.main.async {
observer.onError(makeError("上传成功但未返回文件 id"))
}
return
}
let fileURL: String = {
if !nestedURL.isEmpty { return nestedURL }
if !nestedFileURL.isEmpty { return nestedFileURL }
if dataString.hasPrefix("http://") || dataString.hasPrefix("https://") {
return dataString
}
if fileId.hasPrefix("http://") || fileId.hasPrefix("https://") {
return fileId
}
return ""
}()
DispatchQueue.main.async {
observer.onNext(UploadedFile(id: fileId, url: fileURL))
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]
)
}
}