AWS3 分片上傳的解決方案
背景
背景:最近一次業(yè)務需求是通過本地錄制或者相冊視頻上傳到亞馬遜服務 研究:前端小伙伴嘗試接入 SDK 發(fā)現(xiàn) AWS3 的上傳部分還是需要做很多工作,比如切片部分 > 5M 及 ETAG 處理等 決策:為了減少前端工作,決定采用后端調(diào)用 S3 SDK 方式,前端通過后端預簽名后的 URL 直接進行文件分段上傳
Github:TWMultiUploadFileManager[1]
安裝
使用Cocoapods安裝,或手動拖入項目
pod 'TWMultiUploadFileManager'
方案
后端執(zhí)行執(zhí)行 AWS3 SDK API,前端通過后端預簽名后的 URL 直接進行文件分段上傳
步驟
-
step1: 前端對文件選取后進行切片 (> 5M ) -
step2: 請求后端上傳 aws3 資源 url -
step3: 切片后的文件足個上傳 aws3 服務器 -
step4: 請求后端對上傳完畢的資源文件做校驗
流程圖
TWMultiUploadFileManager 組件封裝
功能
封裝了對文件分片處理,以及上傳功能
-
具體功能?? -
maxConcurrentOperationCount:上傳線程并發(fā)個數(shù)(默認3 ) -
maxSize:文件大小限制(默認2GB ) -
perSlicedSize:每個分片大小(默認5M) -
retryTimes:每個分片上傳嘗試次數(shù)(默認3) -
timeoutInterval:請求時長 (默認 120 s) -
headerFields:附加 header -
mimeType:文件上傳類型 不為空 (默認 text/plain) -
TODO? -
上傳文件最大時長(秒s)默認7200 -
最大緩沖分片數(shù)(默認100,建議不低于10,不高于100) -
附加參數(shù), 目前封裝 put 請求,后續(xù)會補充 post 請求 -
并發(fā)隊列管理依賴 Queuer[2] 庫 -
根據(jù)場景:自定義 TWConcurrentOperation
步驟
step 1
從相冊中選擇視頻源(文件)
// MARK: - Action
/// 選擇影片
fileprivate func selectPhotoAction(animated: Bool = true) {
let imagePicker: TZImagePickerController! = TZImagePickerController(maxImagesCount: 9, delegate: self)
imagePicker.allowPickingVideo = true
imagePicker.allowPreview = false
imagePicker.videoMaximumDuration = Macro.videoMaximumDuration
imagePicker.maxCropVideoDuration = Int(Macro.videoMaximumDuration)
imagePicker.allowPickingOriginalPhoto = false
imagePicker.allowPickingImage = false
imagePicker.allowPickingMultipleVideo = false
imagePicker.autoDismiss = false
imagePicker.navLeftBarButtonSettingBlock = { leftButton in
leftButton?.isHidden = true
}
present(imagePicker, animated: animated, completion: nil)
}
/// 獲取視頻資源
fileprivate func handleRequestVideoURL(asset: PHAsset) {
/// loading
print("loading....")
self.requestVideoURL(asset: asset) { [weak self] (urlasset, url) in
guard let self = self else { return }
print("success....")
self.url = url
self.asset = asset
self.uploadVideoView.play(videoUrl: url)
} failure: { (info) in
print("fail....")
}
}
對視頻源文件進行切片并創(chuàng)建上傳資源對象(文件)
/// 上傳影片
fileprivate func uploadVideoAction() {
guard let url = url, let asset = asset ,let outputPath: String = self.fetchVideoPath(url: url) else { return }
let relativePath: String = TWMultiFileManager.copyVideoFile(atPath: outputPath, dirPathName: Macro.dirPathName)
// 創(chuàng)建上傳資源對象, 對文件進行切片
let fileSource: TWMultiUploadFileSource = TWMultiUploadFileSource(
configure: self.configure,
filePath: relativePath,
fileType: .video,
localIdentifier: asset.localIdentifier
)
// ?? 上傳前需要從服務端獲取每個分片的上傳到亞馬遜 url ,執(zhí)行上傳
// fileSource.setFileFragmentRequestUrls([])
uploadFileManager.uploadFileSource(fileSource)
}
切片的核心邏輯
/// 切片處理
- (void)cutFileForFragments {
NSUInteger offset = self.configure.perSlicedSize;
// 總片數(shù)
NSUInteger totalFileFragment = (self.totalFileSize%offset==0)?(self.totalFileSize/offset):(self.totalFileSize/(offset) + 1);
self.totalFileFragment = totalFileFragment;
NSMutableArray<TWMultiUploadFileFragment *> *fragments = [NSMutableArray array];
for (NSUInteger i = 0; i < totalFileFragment; i++) {
TWMultiUploadFileFragment *fFragment = [[TWMultiUploadFileFragment alloc] init];
fFragment.fragmentIndex = i+1; // 從 1 開始
fFragment.uploadStatus = TWMultiUploadFileUploadStatusWaiting;
fFragment.fragmentOffset = i * offset;
if (i != totalFileFragment - 1) {
fFragment.fragmentSize = offset;
} else {
fFragment.fragmentSize = self.totalFileSize - fFragment.fragmentOffset;
}
/// 關聯(lián)屬性
fFragment.localIdentifier = self.localIdentifier;
fFragment.fragmentId = [NSString stringWithFormat:@"%@-%ld",self.localIdentifier, (long)i];
fFragment.fragmentName = [NSString stringWithFormat:@"%@-%ld.%@",self.localIdentifier, (long)i, self.fileName.pathExtension];
fFragment.fileType = self.fileType;
fFragment.filePath = self.filePath;
fFragment.totalFileFragment = self.totalFileFragment ;
fFragment.totalFileSize = self.totalFileSize;
[fragments addObject:fFragment];
}
self.fileFragments = fragments;
}
step 2
-
業(yè)務邏輯:通過后端調(diào)用 AWS3 SDK 獲取資源文件分片上傳的 urls, 后端配合獲取上傳 aws3 的 url -
?? 這里也可以上傳到自己服務端的 urls ,組件已封裝的上傳邏輯 put 請求,具體按各自業(yè)務修改即可
// ?? 上傳前需要從服務端獲取每個分片的上傳到亞馬遜 url ,執(zhí)行上傳
fileSource.setFileFragmentRequestUrls([])
step 3
/// 執(zhí)行上傳到 AWS3 服務端
uploadFileManager.uploadFileSource(fileSource)
設置代理回調(diào),當然也支持 block
extension ViewController: TWMultiUploadFileManagerDelegate {
/// 準備開始上傳
func prepareStart(_ manager: TWMultiUploadFileManager!, fileSource: TWMultiUploadFileSource!) {
}
/// 文件上傳中進度
func uploadingFileManager(_ manager: TWMultiUploadFileManager!, progress: CGFloat) {
}
/// 完成上傳
func finish(_ manager: TWMultiUploadFileManager!, fileSource: TWMultiUploadFileSource!) {
}
/// 上傳失敗
func fail(_ manager: TWMultiUploadFileManager!, fileSource: TWMultiUploadFileSource!, fail code: TWMultiUploadFileUploadErrorCode) {
}
/// 取消上傳
func cancleUploadFileManager(_ manager: TWMultiUploadFileManager!, fileSource: TWMultiUploadFileSource!) {
}
/// 上傳中某片文件失敗
func failUploadingFileManager(_ manager: TWMultiUploadFileManager!, fileSource: TWMultiUploadFileSource!, fileFragment: TWMultiUploadFileFragment!, fail code: TWMultiUploadFileUploadErrorCode) {
}
}
step 4
業(yè)務邏輯:最后資源上傳完畢后,請求后端接口;對上傳完畢的資源文件做校驗
?? 說明
-
業(yè)務邏輯是各自的業(yè)務方處理, 本組件封裝的是上傳功能:包括切片,重試次數(shù),文件大小,分片大小,最大支持分片數(shù)等 -
具體看上傳資源的配置對象
@interface TWMultiUploadConfigure : NSObject
/// 同時上傳線程 默認3
@property (nonatomic, assign) NSInteger maxConcurrentOperationCount;
/// 上傳文件最大限制(字節(jié)B)默認2GB
@property (nonatomic, assign) NSUInteger maxSize;
/// todo: 上傳文件最大時長(秒s)默認7200
@property (nonatomic, assign) NSUInteger maxDuration;
/// todo: 最大緩沖分片數(shù)(默認100,建議不低于10,不高于100)
@property (nonatomic, assign) NSUInteger maxSliceds;
/// 每個分片占用大小(字節(jié)B)默認5M
@property (nonatomic, assign) NSUInteger perSlicedSize;
/// 每個分片上傳嘗試次數(shù)(默認3)
@property (nonatomic, assign) NSUInteger retryTimes;
/// 請求時長 默認 120 s
@property (nonatomic, assign) NSUInteger timeoutInterval;
/// todo: 附加參數(shù), 目前封裝 put ,后續(xù)會補充 post 請求
@property (nonatomic, strong) NSDictionary *parameters;
/// 附加 header
@property (nonatomic, strong) NSDictionary<NSString *, NSString *> *headerFields;
/// 文件上傳類型 不為空 默認 text/plain
@property (nonatomic, strong) NSString *mimeType;
@end
業(yè)務使用案例
場景
業(yè)務代碼示例
項目基于 ReactorKit[3] 框架
定義視頻各個狀態(tài)
/// 當前的各種狀態(tài)
enum SaleHouseVideoUploadStatus: Equatable {
/// 默認是未選視頻的狀態(tài)
case unseleted
/// 開始選擇視頻,為了解決兩次選擇視頻失敗沒有變化無法進入監(jiān)聽回調(diào)里
case beginSeletedVideo
/// 選擇視頻失敗
case seletedVideoFail(code: SaleHouseVideoUploadStatusSeletedVideoFailCode)
/// 選擇視頻成功,準備上傳 導航: "上傳"
case seletedVideoSuccess(asset: PHAsset, url: URL)
/// 點擊上傳,準備獲取服務器提交信息
case requestUploadData
/// 獲取上傳信息成功
case requestUploadDataSuccess(uploadInfo: SaleHouseVideoUploadInfoModel)
/// 分片校驗 & 獲取上傳信息失敗 導航: "重新上傳";code: 錯誤碼后端返回用于統(tǒng)計
case requestUploadDataFail(code: String?)
/// 上傳中, 導航: "取消上傳"
case uploading(progress: CGFloat)
/// 上傳失敗,導航: "重新上傳"
case uploadFail(code: TWMultiUploadFileUploadErrorCode)
/// 文件已上傳 aws3 下一步 提交合并
case uploadFilesComplete
/// 上傳完成后提交服務失敗 合并 aws3;code: 錯誤碼后端返回用于統(tǒng)計
case requestMergeFilesFail(code: String?)
/// 上傳完成后提交合并 aws3 服務成功 導航: "重新上傳" "提交"
case requestMergeFilesComplete(mergeInfo: SaleHouseVideoMergeFilesCompleteModel)
/// 上傳成功,但提交失敗 ;code: 錯誤碼后端返回用于統(tǒng)計
case requestCommitFail(mergeInfo: SaleHouseVideoMergeFilesCompleteModel, code: String?)
/// 上傳成功,并提交成功 導航: "重新上傳" "提交"
case requestCommitSuccess(mergeInfo: SaleHouseVideoMergeFilesCompleteModel)
/// 取消上傳
case cancel
/// 處于編輯狀態(tài)成功,刪除 && 編輯影片未完成預處理預覽 導航: "刪除影片" "重新上傳"
case requestEditInfoSuccess(fileInfo: SaleHouseVideoEditInfoModel)
/// 獲取編輯狀態(tài)失敗
case requestEditInfoFail
/// 刪除成功, 刪除失敗還是之前狀態(tài)
case deleteSuccess
}
SaleHouseVideoUploadReactor 實現(xiàn) Reactor 協(xié)議
// MARK: - Reactor
extension SaleHouseVideoUploadReactor: Reactor {
enum Action {
/// 視頻校驗
case checkSelectedVideo(asset: PHAsset)
/// 點擊上傳獲取上傳數(shù)據(jù)
case requestUploadData
/// 重置視頻選擇
case resetSelectedVideo
/// 取消上傳
case cancleUploadVideo
/// 上傳完成后,提交視頻
case commitUploadedVideo
/// 上傳失敗,重新上傳
case reuploadVideo
/// 刪除影片
case deleteVideo
/// 獲取影片信息
case requestEditVideoInfo
/// 恢復原編輯視頻信息
case resetEditVideoInfo
}
enum Mutation {
case setStatus(SaleHouseVideoUploadStatus)
case setHUDAction(HUDAction)
}
struct State {
/// 當前的操作的狀態(tài),默認是未選視頻
var status: SaleHouseVideoUploadStatus = .unseleted
}
func mutate(action: Action) -> Observable<Mutation> {
switch action {
case let .checkSelectedVideo(asset: asset):
return checkSelectedVideo(asset: asset)
case .cancleUploadVideo:
return cancleUploadVideo()
case .resetSelectedVideo:
return updateStatus(.unseleted)
case .requestUploadData:
return requestUploadData()
case .commitUploadedVideo:
return commitUploadedVideo()
case .reuploadVideo:
return reuploadVideo()
case .deleteVideo:
return deleteVideo()
case .requestEditVideoInfo:
return fetchEditVideoInfoRequest()
case .resetEditVideoInfo:
return resetEditVideoInfo()
}
}
func reduce(state: State, mutation: Mutation) -> State {
var newState = state
switch mutation {
case let .setStatus(status):
newState.status = status
case let .setHUDAction(action):
hudAction.accept(action)
}
return newState
}
}
步驟
step 1
基于 TZImagePickerController 獲取視頻,觸發(fā) checkSelectedVideo 校驗視頻事件
/// 單個視頻選擇回調(diào)
func imagePickerController(_ picker: TZImagePickerController!, didFinishPickingPhotos photos: [UIImage]!, sourceAssets assets: [Any]!, isSelectOriginalPhoto: Bool) {
picker.dismiss(animated: true, completion: nil)
guard let asset = assets.first as? PHAsset else { return }
self.pickerDissmissActionType = .dismiss
self.reactor?.action.onNext(.checkSelectedVideo(asset: asset))
}
/// 校驗視頻資源
fileprivate func checkSelectedVideo(asset: PHAsset) -> Observable<Mutation> {
DLog("assets :\(asset)")
let beginSeletedVideo: Observable<Mutation> = updateStatus(.beginSeletedVideo)
if asset.duration > Macro.videoMaximumDuration {
let seletedVideoFail = updateStatus(.seletedVideoFail(code: .overTime))
return .concat([beginSeletedVideo, seletedVideoFail])
}
let size = SaleHouseVideoUploadHelper.requestVideoSize(asset)
if size > Macro.videoMaximumSize {
let seletedVideoFail: Observable<Mutation> = updateStatus(.seletedVideoFail(code: .overSize))
return .concat([beginSeletedVideo, seletedVideoFail])
}
switch asset.mediaType {
case .video:
let showLoading = Observable.just(Mutation.setHUDAction(.showHint(Macro.loadingVideoSourceTips)))
let requestVideoURL = handleRequestVideoURL(asset: asset)
return .concat([showLoading, requestVideoURL])
default:
return .empty()
}
}
/// 請求視頻資源
fileprivate func handleRequestVideoURL(asset: PHAsset) -> Observable<Mutation> {
return Observable.create { observer in
SaleHouseVideoUploadHelper.requestVideoURL(asset: asset) { [weak self] (urlasset, url) in
self?.url = url
self?.asset = asset
// 視頻選擇成功
observer.onNext(.setStatus(.seletedVideoSuccess(asset: asset, url: url)))
observer.onNext(.setHUDAction(.hide))
observer.onCompleted()
} failure: { (info) in
observer.onNext(.setHUDAction(.hideHint(Macro.loadFailVideoSourceTips)))
observer.onCompleted()
}
return Disposables.create()
}
}
用戶點擊上傳,獲取切片上傳資源
/// 對上傳 aws3 的 rxswift 封裝
/// - Parameters:
/// - uploadFileManager: 分片上傳管理類
/// - fileSource: 分片資源類
/// - continueUpload: 是否繼續(xù)上傳
/// - Returns: Observable
static func startUpload(
uploadFileManager: TWMultiUploadFileManager,
fileSource: TWMultiUploadFileSource,
continueUpload: Bool = false
) -> Observable<SaleHouseVideoUploadStatus> {
return Observable.create { observer in
// 準備開始上傳
uploadFileManager.prepareStartUploadBlock = { (manager, fileSource) in
observer.onNext(.uploading(progress: 0))
}
// 文件上傳中進度
uploadFileManager.uploadingBlock = { (manager, progress) in
observer.onNext(.uploading(progress: progress))
}
// 完成上傳
uploadFileManager.finishUploadBlock = { (manager, fileSource) in
observer.onNext(.uploadFilesComplete)
observer.onCompleted()
}
// 上傳失敗
uploadFileManager.failUploadBlock = { (manager, fileSource, failErrorCode) in
observer.onNext(.uploadFail(code: failErrorCode))
observer.onCompleted()
}
// 取消上傳
uploadFileManager.cancleUploadBlock = { (manager, fileSource) in
observer.onNext(.cancel)
observer.onCompleted()
}
// 上傳中某片文件失敗
uploadFileManager.failUploadingBlock = { (manager, fileSource, fileFragment, failErrorCode) in
observer.onNext(.uploadFail(code: failErrorCode))
}
if continueUpload {
uploadFileManager.continue(fileSource)
} else {
uploadFileManager.uploadFileSource(fileSource)
}
return Disposables.create()
}
}
/// step1 點擊上傳獲取切片上傳資源文件
fileprivate func requestUploadData(continueUpload: Bool = false) -> Observable<Mutation> {
let fetchUploadDataFail: Observable<Mutation> = .concat([setRequestUploadDataStatus(), requestUploadDataFail()])
guard let url = url, let asset = asset else { return fetchUploadDataFail }
guard let outputPath: String = SaleHouseVideoUploadHelper.fetchVideoPath(url: url) else { return fetchUploadDataFail }
let fetchUploadData = Observable<Mutation>.deferred { [weak self] in
guard let self = self else { return .empty() }
if !continueUpload { // 不是繼續(xù)上傳當前這個資源
// 直接移動文件到指定目錄(相對路徑)
let relativePath: String = TWMultiFileManager.copyVideoFile(atPath: outputPath, dirPathName: Macro.dirPathName)
DLog("relativePath ===> \(relativePath)")
self.deleteFile() // 刪除無效文件, 并對視頻進行切片,創(chuàng)建上傳資源對象
let fileSource: TWMultiUploadFileSource = TWMultiUploadFileSource(
configure: self.configure,
filePath: relativePath,
fileType: .video,
localIdentifier: asset.localIdentifier
)
self.currentFileSource = fileSource // 更新文件
}
guard let fileSource = self.currentFileSource else { return fetchUploadDataFail }
let fetchUploadInfoModel = SaleHouseVideoFetchUploadInfoModel(
filename: fileSource.fileName,
category: SaleHouseVideoUploadHeader.video,
part: TWSwiftGuardValueString(fileSource.totalFileFragment),
size: TWSwiftGuardValueString(fileSource.totalFileSize),
pathExtension: TWSwiftGuardValueString(fileSource.pathExtension)
)
return self.fetchUploadDataRequest(fetchUploadInfoModel: fetchUploadInfoModel, continueUpload: continueUpload)
}
return .concat([setRequestUploadDataStatus(), fetchUploadData])
}
step 2
/// step2 獲取上傳 urls 信息請求
fileprivate func fetchUploadDataRequest(
fetchUploadInfoModel: SaleHouseVideoFetchUploadInfoModel,
continueUpload: Bool = false
) -> Observable<Mutation> {
// loading 獲取服務 url
let showLoading = Observable.just(Mutation.setHUDAction(.showLoading))
let params: [String: Any] = [
"filename" : TWSwiftGuardNullString(fetchUploadInfoModel.filename),
"part" : TWSwiftGuardNullString(fetchUploadInfoModel.part),
"size" : TWSwiftGuardNullString(fetchUploadInfoModel.size),
"category" : TWSwiftGuardNullString(fetchUploadInfoModel.category),
"type" : TWSwiftGuardNullString(fetchUploadInfoModel.type),
]
let fetchData = TWSwiftHttpTool.rx.request(
type: .RequestPost,
url: APIAWSUploadPartUtil,
parameters:params,
isCheckLogin: true,
checkNeedLoginHandler: checkNeedLoginHandler()
)
.mapResult()
.flatMap { [weak self] (status, result) -> Observable<Mutation> in
guard let self = self else { return .empty() }
var hud: Observable<Mutation> = .just(.setHUDAction(.hide))
var uploadInfoStatus: Observable<Mutation> = self.updateStatus(.requestUploadDataFail(code: nil)) // 默認獲取失敗
switch status {
case let .success(isSuccessStatus, data, msg, _):
if isSuccessStatus {
if let uploadInfoModel = self.getUploadInfoModel(data: data) {
uploadInfoStatus = self.startUploadVideo(uploadInfoModel: uploadInfoModel, continueUpload: continueUpload)
} else {
hud = .just(.setHUDAction(.hideHint(Macro.requestUploadDataFailTips)))
}
} else {
hud = .just(.setHUDAction(.hideHint(msg)))
uploadInfoStatus = self.updateStatus(.requestUploadDataFail(code: self.fetchErrorCode(data: data)))
}
case let .error(msg, _):
hud = .just(.setHUDAction(.hideHint(msg)))
case .noNet:
hud = .just(.setHUDAction(.hideHint(TWSwiftHttpTool.Macro.ErrorStr.noNet)))
}
return .concat([hud, uploadInfoStatus])
}
return .concat([showLoading, fetchData])
}
step 3
/// step3 設置獲取上傳 urls ,并開始上傳
/// - Parameters:
/// - uploadInfoModel: 上傳資源對象
/// - continueUpload: 是否斷點繼續(xù)上傳
fileprivate func startUploadVideo(
uploadInfoModel: SaleHouseVideoUploadInfoModel?,
continueUpload: Bool = false
) -> Observable<Mutation> {
var uploadInfoStatus: Observable<Mutation> = self.requestUploadDataFail() // 默認獲取失敗
if let uploadInfoModel = uploadInfoModel {
guard let parts = uploadInfoModel.parts , let fileSource = self.currentFileSource else { return uploadInfoStatus }
uploadInfoStatus = updateStatus(.requestUploadDataSuccess(uploadInfo: uploadInfoModel))
// step2 設置上傳服務的 urls
fileSource.setFileFragmentRequestUrls(parts.map({ $0.url}))
// step3 開始上傳 aws3
let uploadStatus = SaleHouseVideoUploadHelper.startUpload(uploadFileManager: self.uploadFileManager, fileSource: fileSource, continueUpload: continueUpload)
.flatMap { [weak self] status -> Observable<Mutation> in
var mutation: Observable<Mutation> = .empty()
guard let self = self else { return mutation }
switch status {
case .uploadFilesComplete: // 上傳完成,繼續(xù)下一步,合并操作
mutation = self.requestMergeFiles()
default:
break
}
return .concat([
self.updateStatus(status),
mutation
])
}
return .concat([uploadInfoStatus, uploadStatus])
}
return uploadInfoStatus
}
step 4
上傳 aws3 完畢后,請求后端服務接口對資源做合并校驗
/// step4 完成上傳提交合併請求
/// - Parameters:
/// - bucketKey: 存儲桶路徑
/// - uploadId: 上傳唯一標識,step1獲得
/// - category: 分類,如video
/// - parts: [{"partNumber":1,"Etag":"xxxxx"},{"partNumber":2,"Etag":"xxxxx"}]
fileprivate func mergeFilesUploadRequest(_ mergeFilesUploadInfoModel: SaleHouseVideoMergeFilesUploadInfoModel) -> Observable<Mutation> {
var params: [String: Any] = [
"key" : TWSwiftGuardNullString(mergeFilesUploadInfoModel.key),
"upload_id" : TWSwiftGuardNullString(mergeFilesUploadInfoModel.upload_id),
"category" : TWSwiftGuardNullString(mergeFilesUploadInfoModel.category),
"file_id" : TWSwiftGuardValueNumber(mergeFilesUploadInfoModel.file_id),
]
// etga 校驗
if let parts = mergeFilesUploadInfoModel.parts {
params["parts"] = parts.mj_JSONString()
}
let fetchData = TWSwiftHttpTool.rx.request(
type: .RequestPost,
url: APIAWSUploadComplete,
parameters:params,
isCheckLogin: true,
checkNeedLoginHandler: checkNeedLoginHandler()
)
.mapResult()
.flatMap { [weak self] (status, result) -> Observable<Mutation> in
guard let self = self else { return .empty() }
var hud: Observable<Mutation> = .just(.setHUDAction(.hide))
var mergeFilesInfoStatus: Observable<Mutation> = self.updateStatus(.requestMergeFilesFail(code: nil)) // 默認文件合并失敗
switch status {
case let .success(isSuccessStatus, data, msg, _):
if isSuccessStatus {
mergeFilesInfoStatus = self.getMergeUploadStatus(data: data)
} else {
hud = .just(.setHUDAction(.hideHint(msg)))
mergeFilesInfoStatus = self.updateStatus(.requestMergeFilesFail(code: self.fetchErrorCode(data: data)))
}
case let .error(msg, _):
hud = .just(.setHUDAction(.hideHint(msg)))
case .noNet:
hud = .just(.setHUDAction(.hideHint(TWSwiftHttpTool.Macro.ErrorStr.noNet)))
}
return .concat([hud, mergeFilesInfoStatus])
}
return .concat(fetchData)
}
參考
-
如何使用 AWS CLI 將文件分段上傳到 Amazon S3?[4] -
AWS3 API\_UploadPart[5] -
Amazon S3 Transfer Utility for iOS[6]
參考資料
https://github.com/zeqinjie/TWMultiUploadFileManager
[2]https://github.com/FabrizioBrancati/Queuer
[3]https://github.com/ReactorKit/ReactorKit
[4]https://aws.amazon.com/cn/premiumsupport/knowledge-center/s3-multipart-upload-cli/
[5]https://docs.aws.amazon.com/zh_cn/AmazonS3/latest/API/API_UploadPart.html
[6]https://aws.amazon.com/cn/blogs/mobile/amazon-s3-transfer-utility-for-ios/
轉(zhuǎn)自:掘金 zeqinjie
https://juejin.cn/post/7001041806339096590
-End-
最近有一些小伙伴,讓我?guī)兔φ乙恍?nbsp;面試題 資料,于是我翻遍了收藏的 5T 資料后,匯總整理出來,可以說是程序員面試必備!所有資料都整理到網(wǎng)盤了,歡迎下載!

面試題】即可獲取
