2022-10-31 13:41:19 +01:00
//
// A t t a c h m e n t V i e w M o d e l + U p l o a d . s w i f t
//
//
// C r e a t e d b y M a i n a s u K o n 2 0 2 1 - 1 1 - 2 6 .
//
import os . log
import UIKit
import Kingfisher
import UniformTypeIdentifiers
import MastodonCore
import MastodonSDK
// o b j c . i o
// r e f : h t t p s : / / t a l k . o b j c . i o / e p i s o d e s / S 0 1 E 2 6 9 - s w i f t - c o n c u r r e n c y - a s y n c - s e q u e n c e s - p a r t - 1
struct Chunked < Base : AsyncSequence > : AsyncSequence where Base . Element = = UInt8 {
var base : Base
var chunkSize : Int = 1 * 1024 * 1024 // 1 M i B
typealias Element = Data
struct AsyncIterator : AsyncIteratorProtocol {
var base : Base . AsyncIterator
var chunkSize : Int
mutating func next ( ) async throws -> Data ? {
var result = Data ( )
while let element = try await base . next ( ) {
result . append ( element )
if result . count = = chunkSize { return result }
}
return result . isEmpty ? nil : result
}
}
func makeAsyncIterator ( ) -> AsyncIterator {
AsyncIterator ( base : base . makeAsyncIterator ( ) , chunkSize : chunkSize )
}
}
extension AsyncSequence where Element = = UInt8 {
var chunked : Chunked < Self > {
Chunked ( base : self )
}
}
extension Data {
fileprivate func chunks ( size : Int ) -> [ Data ] {
return stride ( from : 0 , to : count , by : size ) . map {
Data ( self [ $0 . . < Swift . min ( count , $0 + size ) ] )
}
}
}
extension AttachmentViewModel {
2022-11-11 11:10:13 +01:00
public enum UploadState {
case none
2022-11-11 14:28:19 +01:00
case compressing
2022-11-11 11:10:13 +01:00
case ready
case uploading
case fail
case finish
}
2022-10-31 13:41:19 +01:00
struct UploadContext {
let apiService : APIService
let authContext : AuthContext
}
2022-11-08 12:40:58 +01:00
public typealias UploadResult = Mastodon . Entity . Attachment
2022-10-31 13:41:19 +01:00
}
extension AttachmentViewModel {
2022-11-11 11:10:13 +01:00
@ MainActor
func upload ( isRetry : Bool = false ) async throws {
do {
let result = try await upload (
context : . init (
apiService : self . api ,
authContext : self . authContext
) ,
isRetry : isRetry
)
update ( uploadResult : result )
} catch {
self . error = error
}
2022-10-31 13:41:19 +01:00
}
2022-11-11 11:10:13 +01:00
@ MainActor
private func upload ( context : UploadContext , isRetry : Bool ) async throws -> UploadResult {
if isRetry {
guard uploadState = = . fail else { throw AppError . badRequest }
self . error = nil
self . fractionCompleted = 0
} else {
guard uploadState = = . ready else { throw AppError . badRequest }
}
do {
update ( uploadState : . uploading )
let result = try await uploadMastodonMedia (
context : context
)
update ( uploadState : . finish )
return result
} catch {
update ( uploadState : . fail )
throw error
}
}
2022-11-08 12:40:58 +01:00
// M a i n A c t o r i s r e q u i r e d h e r e t o t r i g g e r s t r e a m u p l o a d t a s k
@ MainActor
2022-10-31 13:41:19 +01:00
private func uploadMastodonMedia (
context : UploadContext
) async throws -> UploadResult {
guard let output = self . output else {
throw AppError . badRequest
}
let attachment = output . asAttachment
let query = Mastodon . API . Media . UploadMediaQuery (
file : attachment ,
thumbnail : nil ,
description : {
let caption = caption . trimmingCharacters ( in : . whitespacesAndNewlines )
return caption . isEmpty ? nil : caption
} ( ) ,
focus : nil // TODO:
)
// u p l o a d + N * c h e c k u p l o a d
// u p l o a d : c h e c k = 9 : 1
let uploadTaskCount : Int64 = 540
let checkUploadTaskCount : Int64 = 1
let checkUploadTaskRetryLimit : Int64 = 60
progress . totalUnitCount = uploadTaskCount + checkUploadTaskCount * checkUploadTaskRetryLimit
progress . completedUnitCount = 0
let attachmentUploadResponse : Mastodon . Response . Content < Mastodon . Entity . Attachment > = try await {
do {
AttachmentViewModel . logger . log ( level : . debug , " \( ( #file as NSString ) . lastPathComponent , privacy : . public ) [ \( #line , privacy : . public ) ], \( #function , privacy : . public ) : [V2] upload attachment... " )
progress . addChild ( query . progress , withPendingUnitCount : uploadTaskCount )
return try await context . apiService . uploadMedia (
domain : context . authContext . mastodonAuthenticationBox . domain ,
query : query ,
mastodonAuthenticationBox : context . authContext . mastodonAuthenticationBox ,
needsFallback : false
) . singleOutput ( )
} catch {
// c h e c k n e e d s f a l l b a c k
guard let apiError = error as ? Mastodon . API . Error ,
apiError . httpResponseStatus = = . notFound
else { throw error }
AttachmentViewModel . logger . log ( level : . debug , " \( ( #file as NSString ) . lastPathComponent , privacy : . public ) [ \( #line , privacy : . public ) ], \( #function , privacy : . public ) : [V1] upload attachment... " )
progress . addChild ( query . progress , withPendingUnitCount : uploadTaskCount )
return try await context . apiService . uploadMedia (
domain : context . authContext . mastodonAuthenticationBox . domain ,
query : query ,
mastodonAuthenticationBox : context . authContext . mastodonAuthenticationBox ,
needsFallback : true
) . singleOutput ( )
}
} ( )
// c h e c k n e e d s w a i t p r o c e s s i n g ( u n t i l g e t t h e ` u r l ` )
if attachmentUploadResponse . statusCode = = 202 {
// n o t e :
// t h e M a s t o d o n s e r v e r a p p e n d t h e a t t a c h m e n t s i n o r d e r b y u p l o a d t i m e
2022-11-11 11:10:13 +01:00
// c a n n o t u p l o a d p a r a l l e l s
2022-10-31 13:41:19 +01:00
let waitProcessRetryLimit = checkUploadTaskRetryLimit
var waitProcessRetryCount : Int64 = 0
repeat {
defer {
// m a k e s u r e a l w a y s c o u n t + 1
waitProcessRetryCount += checkUploadTaskCount
}
AttachmentViewModel . logger . log ( level : . debug , " \( ( #file as NSString ) . lastPathComponent , privacy : . public ) [ \( #line , privacy : . public ) ], \( #function , privacy : . public ) : check attachment process status " )
let attachmentStatusResponse = try await context . apiService . getMedia (
attachmentID : attachmentUploadResponse . value . id ,
mastodonAuthenticationBox : context . authContext . mastodonAuthenticationBox
) . singleOutput ( )
progress . completedUnitCount += checkUploadTaskCount
if let url = attachmentStatusResponse . value . url {
AttachmentViewModel . logger . log ( level : . debug , " \( ( #file as NSString ) . lastPathComponent , privacy : . public ) [ \( #line , privacy : . public ) ], \( #function , privacy : . public ) : attachment process finish: \( url ) " )
// e s c a p e h e r e
progress . completedUnitCount = progress . totalUnitCount
2022-11-08 12:40:58 +01:00
return attachmentStatusResponse . value
2022-10-31 13:41:19 +01:00
} else {
AttachmentViewModel . logger . log ( level : . debug , " \( ( #file as NSString ) . lastPathComponent , privacy : . public ) [ \( #line , privacy : . public ) ], \( #function , privacy : . public ) : attachment processing. Retry \( waitProcessRetryCount ) / \( waitProcessRetryLimit ) " )
await Task . sleep ( 1_000_000_000 * 3 ) // 3 s
}
} while waitProcessRetryCount < waitProcessRetryLimit
AttachmentViewModel . logger . log ( level : . debug , " \( ( #file as NSString ) . lastPathComponent , privacy : . public ) [ \( #line , privacy : . public ) ], \( #function , privacy : . public ) : attachment processing result discard due to exceed retry limit " )
throw AppError . badRequest
} else {
AttachmentViewModel . logger . log ( level : . debug , " \( ( #file as NSString ) . lastPathComponent , privacy : . public ) [ \( #line , privacy : . public ) ], \( #function , privacy : . public ) : upload attachment success: \( attachmentUploadResponse . value . url ? ? " <nil> " ) " )
2022-11-08 12:40:58 +01:00
return attachmentUploadResponse . value
2022-10-31 13:41:19 +01:00
}
}
}
extension AttachmentViewModel . Output {
var asAttachment : Mastodon . Query . MediaAttachment {
switch self {
case . image ( let data , let kind ) :
switch kind {
case . png : return . png ( data )
case . jpg : return . jpeg ( data )
}
case . video ( let url , _ ) :
return . other ( url , fileExtension : url . pathExtension , mimeType : " video/mp4 " )
}
}
}