HuaweiOBSUploader.swift
7.94 KB
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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
import Foundation
import OBS
enum HuaweiOBSResourceType {
case image
case video
fileprivate var fileType: String {
switch self {
case .image: return "png"
case .video: return "mp4"
}
}
fileprivate var contentType: OBSContentType {
switch self {
case .image: return .PNG
case .video: return .MP4
}
}
}
enum HuaweiOBSUploadError: LocalizedError {
case invalidServerURL
case invalidFilePath(String)
case missingAccessToken
case tokenRequestFailed(statusCode: Int, message: String?)
case invalidTokenResponse
case uploadFailed(statusCode: String?, underlyingError: Error?)
var errorDescription: String? {
switch self {
case .invalidServerURL:
return "Invalid OBS token server URL."
case .invalidFilePath(let path):
return "Upload file does not exist or is not a regular file: \(path)"
case .missingAccessToken:
return "An access token is required before uploading a file."
case .tokenRequestFailed(let statusCode, let message):
return "Requesting OBS credentials failed (HTTP \(statusCode))\(message.map { ": \($0)" } ?? "")."
case .invalidTokenResponse:
return "The OBS credential response is incomplete."
case .uploadFailed(let statusCode, let underlyingError):
if let underlyingError {
return "OBS upload failed: \(underlyingError.localizedDescription)"
}
return "OBS upload failed with status \(statusCode ?? "unknown")."
}
}
}
/// Requests a short-lived OBS credential from DoubleFeel and uploads a local file
/// with Huawei Cloud's OBS SDK.
final class HuaweiOBSUploader {
static let shared = HuaweiOBSUploader()
private let session: URLSession
private let clientLock = NSLock()
private var retainedClients: [UUID: OBSClient] = [:]
init(session: URLSession = .shared) {
self.session = session
}
func upload(filePath: String, resourceType: HuaweiOBSResourceType) async throws -> String {
var isDirectory: ObjCBool = false
guard FileManager.default.fileExists(atPath: filePath, isDirectory: &isDirectory),
!isDirectory.boolValue else {
throw HuaweiOBSUploadError.invalidFilePath(filePath)
}
let config = try await requestUploadConfig(resourceType: resourceType)
try await putFile(at: filePath, resourceType: resourceType, config: config)
return config.fileURL
}
private func requestUploadConfig(resourceType: HuaweiOBSResourceType) async throws -> UploadConfig {
guard let baseURL = URL(string: AppShared.shared.baseUrl) else {
throw HuaweiOBSUploadError.invalidServerURL
}
guard let accessToken = AppShared.shared.token,
!accessToken.isEmpty else {
throw HuaweiOBSUploadError.missingAccessToken
}
let url = baseURL.appendingPathComponent("client/doublefeel/obs/temp_token/")
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue(accessToken, forHTTPHeaderField: "access_token")
request.httpBody = try JSONSerialization.data(withJSONObject: [
"file_type": resourceType.fileType,
"scene": "avatar",
"count": 1
])
let (data, response) = try await session.data(for: request)
guard let response = response as? HTTPURLResponse else {
throw HuaweiOBSUploadError.tokenRequestFailed(statusCode: -1, message: nil)
}
guard 200..<300 ~= response.statusCode else {
throw HuaweiOBSUploadError.tokenRequestFailed(
statusCode: response.statusCode,
message: String(data: data, encoding: .utf8)
)
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let tokenInfo = try decoder.decode(OBSTempTokenInfo.self, from: data)
guard let token = tokenInfo.token,
let accessKey = token.accessKey, !accessKey.isEmpty,
let secretKey = token.secretKey, !secretKey.isEmpty,
let securityToken = token.securityToken, !securityToken.isEmpty,
let objectKey = tokenInfo.keys?.first, !objectKey.isEmpty,
let host = tokenInfo.host, !host.isEmpty,
let endpoint = tokenInfo.endpoint, !endpoint.isEmpty,
let bucket = tokenInfo.bucket, !bucket.isEmpty else {
throw HuaweiOBSUploadError.invalidTokenResponse
}
return UploadConfig(
accessKey: accessKey,
secretKey: secretKey,
securityToken: securityToken,
objectKey: objectKey,
host: normalizedHTTPSURL(host),
endpoint: normalizedHTTPSURL(endpoint),
bucket: bucket
)
}
private func putFile(
at filePath: String,
resourceType: HuaweiOBSResourceType,
config: UploadConfig
) async throws {
let credentialProvider = OBSStaticCredentialProvider(
accessKey: config.accessKey,
secretKey: config.secretKey
)
credentialProvider?.securityToken = config.securityToken
let serviceConfiguration = OBSServiceConfiguration(
urlString: config.endpoint,
credentialProvider: credentialProvider
)
guard let client = OBSClient(configuration: serviceConfiguration),
let uploadRequest = OBSPutObjectWithFileRequest(
bucketName: config.bucket,
objectKey: config.objectKey,
uploadFilePath: filePath
) else {
throw HuaweiOBSUploadError.invalidTokenResponse
}
uploadRequest.contentType = resourceType.contentType
let clientID = UUID()
retain(client, id: clientID)
defer { releaseClient(id: clientID) }
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
client.putObject(uploadRequest) { response, error in
if let error {
continuation.resume(throwing: HuaweiOBSUploadError.uploadFailed(
statusCode: response?.statusCode,
underlyingError: error
))
} else if response?.statusCode == "200" {
continuation.resume(returning: ())
} else {
continuation.resume(throwing: HuaweiOBSUploadError.uploadFailed(
statusCode: response?.statusCode,
underlyingError: nil
))
}
}
}
}
private func normalizedHTTPSURL(_ value: String) -> String {
value.hasPrefix("http://") || value.hasPrefix("https://") ? value : "https://\(value)"
}
private func retain(_ client: OBSClient, id: UUID) {
clientLock.lock()
retainedClients[id] = client
clientLock.unlock()
}
private func releaseClient(id: UUID) {
clientLock.lock()
retainedClients[id] = nil
clientLock.unlock()
}
}
private extension HuaweiOBSUploader {
struct UploadConfig {
let accessKey: String
let secretKey: String
let securityToken: String
let objectKey: String
let host: String
let endpoint: String
let bucket: String
var fileURL: String {
"\(host.trimmingCharacters(in: CharacterSet(charactersIn: "/")))/\(objectKey)"
}
}
struct OBSTempTokenInfo: Decodable {
let token: OBSToken?
let keys: [String]?
let host: String?
let endpoint: String?
let bucket: String?
}
struct OBSToken: Decodable {
let accessKey: String?
let secretKey: String?
let securityToken: String?
}
}