Visual_Novel_iOS/crush/Crush/Src/Utils/S3/CloudStorage.swift

562 lines
20 KiB
Swift
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

//
// CloudStorage.swift
// Crush
//
// Created by Leon on 2025/7/21.
//
import AWSS3
import Foundation
import SwiftDate
// MARK: - CloudStorage
class CloudStorage {
static let shared = CloudStorage()
private(set) var uploadAlbumItems: [UploadPhotoM] = []
private var uploadRoleItems: [UploadPhotoM] = []
private var uploadHeadImagesItems: [UploadPhotoM] = []
private var uploadIMImageItems: [UploadPhotoM] = []
private var uploadAudiosItems: [UploadModel] = []
var authAudioData: S3AuthData?
var requestAudioSysTokenData: Date?
var audioTransferUtility: AWSS3TransferUtility?
private init() {
NotificationCenter.default.addObserver(self, selector: #selector(receiveNetStatusChanged(_:)), name: AppNotificationName.networkChanged.notificationName, object: nil)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
private func needCheckImage(for bucket: BucketS3Enum) -> Bool {
switch bucket {
case .HEAD_IMAGE: // , .ALBUM, .ROLE
return true
default:
return false
}
}
// MARK: - Public Methods
func s3BatchAddPhotos(_ photos: [UploadPhotoM], bucket: BucketS3Enum, callback: ((Bool) -> Void)?) {
guard !photos.isEmpty else {
callback?(false)
return
}
var uploadItems: [Any] = []
switch bucket {
case .HEAD_IMAGE:
uploadItems = uploadHeadImagesItems
case .ALBUM:
uploadItems = uploadAlbumItems
case .ROLE:
uploadItems = uploadRoleItems
case .IM_IMG:
uploadItems = uploadIMImageItems
case .SOUND, .SOUND_PATH:
uploadItems = uploadAudiosItems
default:
callback?(false)
return
}
for photo in photos {
photo.isUploading = true
photo.bucketEnum = bucket
if !uploadItems.contains(where: { $0 as? UploadPhotoM === photo }) {
uploadItems.append(photo)
}
}
doS3UploadPhotos(bucket, photos: photos, block: callback)
}
func s3AddPhotos(_ photos: [UploadPhotoM], bucket: BucketS3Enum) {
guard !photos.isEmpty else { return }
var uploadItems: [Any] = []
switch bucket {
case .HEAD_IMAGE:
uploadItems = uploadHeadImagesItems
case .ALBUM:
uploadItems = uploadAlbumItems
case .ROLE:
uploadItems = uploadRoleItems
case .IM_IMG:
uploadItems = uploadIMImageItems
case .SOUND, .SOUND_PATH:
uploadItems = uploadAudiosItems
default:
return
}
for photo in photos {
photo.isUploading = true
photo.bucketEnum = bucket
if !uploadItems.contains(where: { $0 as? UploadPhotoM === photo }) {
uploadItems.append(photo)
}
}
doS3UploadPhotos(bucket, photos: photos, block: nil)
}
func s3AddUploadAudio(_ model: UploadModel, callback: ((Bool) -> Void)?) {
guard let fileData = model.fileData, fileData.count > 0 else {
assert(false, "fileData should be valid")
callback?(false)
return
}
model.isUploading = true
if !uploadAudiosItems.contains(where: { $0 === model }) {
uploadAudiosItems.append(model)
}
if judgeTokenValid(requestAudioSysTokenData){
guard let authData = authAudioData, let utility = audioTransferUtility else{
callback?(false)
return
}
dlog("利用之前的token上传Audio: \(model)")
self.s3UploadModel(utility, object: model, auth: authData) { result in
callback?(result)
}
}else{
requestAuthToken(.SOUND_PATH, suffix: .mp3) { [weak self] result, authData, utility in
guard let self = self, !self.uploadAudiosItems.isEmpty, let s3Utility = utility, let s3AuthData = authData else {
callback?(false)
return
}
if result {
self.requestAudioSysTokenData = Date()
self.s3UploadModel(s3Utility, object: model, auth: s3AuthData) { result in
callback?(result)
}
} else {
model.isUploading = false
DispatchQueue.main.async {
model.mm_uploadFailed()
}
callback?(false)
}
}
}
}
func cancelTasks(for bucket: BucketS3Enum) {
switch bucket {
case .ALBUM:
for photo in uploadAlbumItems {
cancelTask(for: photo)
uploadAlbumItems.removeAll { $0 === photo }
}
case .HEAD_IMAGE:
for photo in uploadHeadImagesItems {
cancelTask(for: photo)
uploadHeadImagesItems.removeAll { $0 === photo }
}
case .ROLE:
for model in uploadRoleItems {
cancelTask(for: model)
uploadRoleItems.removeAll { $0 === model }
}
case .IM_IMG:
for model in uploadIMImageItems {
cancelTask(for: model)
uploadIMImageItems.removeAll { $0 === model }
}
case .SOUND, .SOUND_PATH:
for model in uploadAudiosItems{
cancelTask(for: model)
uploadAudiosItems.removeAll {$0 === model}
}
default:
break
}
}
// MARK: - Private Methods
private func doS3UploadPhotos(_ bucket: BucketS3Enum, photos: [UploadPhotoM], block: ((Bool) -> Void)?) {
guard !photos.isEmpty else {
assert(false, "photos count 0")
block?(false)
return
}
let enableGCDGroup = block != nil
var group: DispatchGroup?
var isAllOKInGroup = true
if enableGCDGroup {
group = DispatchGroup()
}
var tempPhotos = photos
//
// let checkImageNeedOfBucket = needCheckImage(for: bucket)
for photo in photos {
if let tempString = photo.remoteImageUrlString, tempString.count > 0 {
tempPhotos.removeAll { $0 === photo }
removeObjFromUploadingItems(by: photo)
photo.mm_uploadDone(tempString)
continue
}
guard photo.image != nil || photo.imageData != nil else {
dlog("❌❌❌: 错误路径图片应不能为空【CloudStorage】")
continue
}
if enableGCDGroup {
group?.enter()
}
if photo.imageData == nil {
photo.imageData = convertImageToData(photo.image!, suffix: photo.suffixEnum)
}
let sizeInMB = Float(photo.imageData!.count) / (1000.0 * 1000.0)
if sizeInMB > 10 {
photo.setupPhotoOversizeLimmit()
isAllOKInGroup = false
DispatchQueue.main.async {
let message = photo.suffixEnum == .gif ? "Photo must be .JPG, .JPEG, .GIF or .PNG and cannot exceed 10MB" : "Photo must be .JPG, .JPEG or .PNG and cannot exceed 10MB"
photo.setupErrorMsg(message)
// Assuming Hud is a utility for showing alerts/toasts
// Unknown: Hud ShowPureTip implementation
dlog(message) // Placeholder for Hud.ShowPureTip
photo.mm_uploadFailed()
}
if enableGCDGroup {
group?.leave()
}
continue
}
// && checkImageNeedOfBucket
let isCheckImageInCallback = block != nil && photo.isAutoCheckImage
if isCheckImageInCallback {
photo.banCheckImageInDelegateMethod = true
}
let suffixType = photo.imageData != nil ? photo.suffixEnum : .jpeg
requestAuthToken(bucket, suffix: suffixType) { [weak self] result, authData, utility in
guard let self = self, let s3AuthData = authData, let s3Utility = utility else { return }
if result {
self.s3Upload(s3Utility, photoM: photo, authData: s3AuthData) { success in
photo.isUploading = false
if success {
if isCheckImageInCallback {
photo.checkImageOK { result2 in
if !result2 { //
isAllOKInGroup = false
}
if enableGCDGroup {
group?.leave()
}
}
} else {
//
if enableGCDGroup {
group?.leave()
}
}
} else {
//
isAllOKInGroup = false
if enableGCDGroup {
group?.leave()
}
}
}
} else {
// token
isAllOKInGroup = false
if enableGCDGroup {
group?.leave()
}
DispatchQueue.main.async {
photo.mm_uploadFailed()
}
}
}
}
if enableGCDGroup, let validGroup = group {
// DispatchQueue.main.async(group: group) {
validGroup.notify(queue: .main) {
block?(isAllOKInGroup)
}
}
}
private func s3Upload(_ utility: AWSS3TransferUtility, photoM im: UploadPhotoM, authData: S3AuthData, result: @escaping (Bool) -> Void) {
guard !authData.path.isEmpty else {
DispatchQueue.main.async {
dlog("❌authData.path is empty")
im.mm_uploadFailed()
result(false)
}
return
}
if LTNetworkManage.ltManage.reachable == false { // Placeholder for [EGApiManager shared].status == .notReachable
DispatchQueue.main.async {
im.mm_uploadFailed()
result(false)
}
return
}
im.s3AuthData = authData
im.utility = utility
let key = authData.path
dlog("uplading key: \(key)")
let expression = AWSS3TransferUtilityUploadExpression()
expression.progressBlock = { _, progress in
dlog("🚀 \(key) taskprogress: \(progress)")
}
expression.setValue("temp=1", forRequestHeader: "x-amz-tagging")
guard let imageData = convertImageToData(im.image!, suffix: im.suffixEnum) ?? im.imageData else { return }
let contentType = im.contentType()
utility.uploadData(imageData, bucket: authData.bucket, key: key, contentType: contentType, expression: expression) { task, error in
self.removeObjFromUploadingItems(by: im)
if let error = error {
dlog("❌S3上传任务失败: \(error.localizedDescription)")
DispatchQueue.main.async {
im.mm_uploadFailed()
result(false)
}
return
}
switch task.status {
case .completed:
im.remoteFullPath = authData.urlPath
im.remoteImageUrlString = key
dlog("💹 s3 image uploaded full: \(im.remoteFullPath ?? "")")
DispatchQueue.main.async {
im.mm_uploadDone(key)
result(true)
}
case .error, .cancelled:
DispatchQueue.main.async {
im.mm_uploadFailed()
result(false)
}
default:
assert(false, "上传异常分支、需要查查原因")
}
}
}
private func s3UploadModel(_ utility: AWSS3TransferUtility, object model: UploadModel, auth: S3AuthData, result: @escaping (Bool) -> Void) {
guard !auth.path.isEmpty, let fileData = model.fileData else {
DispatchQueue.main.async {
model.mm_uploadFailed()
}
result(false)
return
}
// Assuming EGApiManager is a network status manager
// Unknown: EGApiManager shared status implementation
if false { // Placeholder for [EGApiManager shared].status == .notReachable
DispatchQueue.main.async {
model.mm_uploadFailed()
result(false)
}
return
}
model.s3AuthData = auth
model.utility = utility
var key = auth.path
/// dev/main/sound/0/*
if key.hasSuffix("/*") {
// "/*"
key.removeLast(2)
// + .mp3
key += "/\(model.addThisItemTimeStamp).mp3"
}
var urlPath = auth.urlPath ?? ""
if urlPath.hasSuffix("/*") {
// "/*"
urlPath.removeLast(2)
// + .mp3
urlPath += "/\(model.addThisItemTimeStamp).mp3"
}
dlog("uplading file key: \(key) full: \(urlPath)")
let expression = AWSS3TransferUtilityUploadExpression()
expression.progressBlock = { _, progress in
dlog("🚀 \(key) file taskprogress: \(progress)")
}
expression.setValue("temp=1", forRequestHeader: "x-amz-tagging")
let contentType = model.contentType()
utility.uploadData(fileData, bucket: auth.bucket, key: key, contentType: contentType, expression: expression) { task, error in
self.uploadAudiosItems.removeAll { $0 === model }
if let error = error {
dlog("❌S3上传任务失败: \(error.localizedDescription)")
result(false)
return
}
switch task.status {
case .completed:
model.remoteFullPath = urlPath // auth.urlPath
model.remoteImageUrlString = key
dlog("💹 s3 file uploaded full: \(model.remoteFullPath ?? "")")
DispatchQueue.main.async {
model.mm_uploadDone(key)
result(true)
}
case .error, .cancelled:
DispatchQueue.main.async {
model.mm_uploadFailed()
result(false)
}
default:
assert(false, "上传异常分支、需要查查原因")
}
}
}
private func requestAuthToken(_ bucket: BucketS3Enum, suffix: SuffixS3Enum, callback: @escaping (Bool, S3AuthData?, AWSS3TransferUtility?) -> Void) {
OssProvider.request(.getS3Token(bucketNameEnum: bucket, suffix: suffix), modelType: S3AuthData.self) {[weak self] result in
switch result {
case let .success(success):
if let authData = success {
let utility = self?.generateTransferUtility(bucket, authData: authData)
callback(true, authData, utility)
if bucket == .SOUND_PATH{
self?.authAudioData = success
}
} else {
callback(false, nil, nil)
}
case .failure:
callback(false, nil, nil)
}
}
}
private func generateTransferUtility(_ bucket: BucketS3Enum, authData: S3AuthData) -> AWSS3TransferUtility? {
guard let accessKeyId = authData.accessKeyId, !accessKeyId.isEmpty,
let accessKeySecret = authData.accessKeySecret, !accessKeySecret.isEmpty,
let securityToken = authData.securityToken, !securityToken.isEmpty else {
assert(false, "invalid token")
return nil
}
/// Region(String) from server
// let region = authData.region
let provider = AWSBasicSessionCredentialsProvider(accessKey: accessKeyId, secretKey: accessKeySecret, sessionToken: securityToken)
// Integerregion .USWest2
let regionType: AWSRegionType = .USWest2;
guard let config = AWSServiceConfiguration(region: regionType, credentialsProvider: provider) else {
assert(false, "Invalid AWSServiceConfiguration")
return nil
}
config.timeoutIntervalForRequest = 30
config.maxRetryCount = 1
let key = authData.fileName
AWSS3TransferUtility.remove(forKey: key) // AWSS3TransferUtility.removeS3TransferUtility(forKey: key)
AWSS3TransferUtility.register(with: config, forKey: key)
let utility = AWSS3TransferUtility.s3TransferUtility(forKey: key) // AWSS3TransferUtility(forKey: key)
if utility == nil {
assert(false, "invalid utility")
}
switch bucket{
case .SOUND_PATH:
audioTransferUtility = utility
default:
break
}
return utility
}
private func judgeTokenValid(_ lastRequestDate: Date?) -> Bool {
guard let date = lastRequestDate else {
return false
}
let expireDate = date + 59.minutes
if expireDate.milliStamp > Date().milliStamp {
return true
}
return false
}
private func removeObjFromUploadingItems(by obj: UploadPhotoM) {
uploadAlbumItems.removeAll { $0 === obj }
uploadHeadImagesItems.removeAll { $0 === obj }
uploadRoleItems.removeAll { $0 === obj }
uploadAudiosItems.removeAll { $0 === obj }
}
private func cancelTask<T: UploadEntity>(for item: T) {
// Unknown: AWSTask and AWSS3TransferUtilityUploadTask implementation
if let utility = item.utility {
// Placeholder for task cancellation
dlog("🈲🈲, upload task canceled : \(utility)")
DispatchQueue.main.async {
item.mm_uploadFailed()
}
}
}
// MARK: - Notification Handling
@objc private func receiveNetStatusChanged(_ notification: Notification) {
let status = LTNetworkManage.ltManage.status
if status == .notReachable {
cancelTasks(for: .ALBUM)
cancelTasks(for: .HEAD_IMAGE)
cancelTasks(for: .ROLE)
cancelTasks(for: .SOUND)
cancelTasks(for: .IM_IMG)
}
}
// MARK: - Helper Methods
private func convertImageToData(_ image: UIImage, suffix: SuffixS3Enum) -> Data? {
// Unknown: SDWebImage or UIImage conversion implementation
switch suffix {
case .gif:
assert(false, "imageData应该不为空")
return nil
// return image.sd_imageData(as: .gif) // Placeholder
case .png:
return image.pngData()
default:
return image.jpegData(compressionQuality: 0.8)
}
}
}