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

562 lines
20 KiB
Swift
Raw Normal View History

2025-10-09 10:29:35 +00:00
//
// 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)
}
}
}