如何使用 AlamoFire 下载 blob URI

Posted

技术标签:

【中文标题】如何使用 AlamoFire 下载 blob URI【英文标题】:How to download a blob URI using AlamoFire 【发布时间】:2022-01-15 14:48:17 【问题描述】:

我正在尝试快速使用 WKWebView,目前有一个使用 AlamoFire 的下载引擎。我遇到了一个使用 blob: url 方案来下载项目的站点。一般有没有办法使用 AlamoFire 或 WKWebView 下载 blob 文件?

我的具体目标是将此 blob URI 中的内容下载到文件中。

我将不胜感激。谢谢。

下面附上所有相关代码。

这是我遇到问题的网址:

blob:https://cubari.moe/87d49857-dfef-4f0f-bb83-db8517fd3094

这是我的日志中的错误:

2021-12-10 22:41:45.382527-0500 Asobi[14529:358202] -canOpenURL: failed for URL: "blob:https://cubari.moe/87d49857-dfef-4f0f-bb83-db8517fd3094" - error: "This app is not allowed to query for scheme blob"
2021-12-10 22:41:45.474214-0500 Asobi[14529:358357] Task <4B011CC1-60E9-4AAD-98F0-BB6A6D0C92FB>.<1> finished with error [-1002] Error Domain=NSURLErrorDomain Code=-1002 "unsupported URL" UserInfo=NSLocalizedDescription=unsupported URL, NSErrorFailingURLStringKey=blob:https://cubari.moe/87d49857-dfef-4f0f-bb83-db8517fd3094, NSErrorFailingURLKey=blob:https://cubari.moe/87d49857-dfef-4f0f-bb83-db8517fd3094, _NSURLErrorRelatedURLSessionTaskErrorKey=(
    "LocalDownloadTask <4B011CC1-60E9-4AAD-98F0-BB6A6D0C92FB>.<1>"
), _NSURLErrorFailingURLSessionTaskErrorKey=LocalDownloadTask <4B011CC1-60E9-4AAD-98F0-BB6A6D0C92FB>.<1>, NSUnderlyingError=0x6000017e99b0 Error Domain=kCFErrorDomainCFNetwork Code=-1002 "(null)"
2021-12-10 22:41:45.476703-0500 Asobi[14529:358202] [Process] 0x124034e18 - [pageProxyID=6, webPageID=7, PID=14540] WebPageProxy::didFailProvisionalLoadForFrame: frameID=3, domain=WebKitErrorDomain, code=102
Failed provisional nav: Error Domain=WebKitErrorDomain Code=102 "Frame load interrupted" UserInfo=_WKRecoveryAttempterErrorKey=<WKReloadFrameErrorRecoveryAttempter: 0x6000019a88c0>, NSErrorFailingURLStringKey=blob:https://cubari.moe/87d49857-dfef-4f0f-bb83-db8517fd3094, NSErrorFailingURLKey=blob:https://cubari.moe/87d49857-dfef-4f0f-bb83-db8517fd3094, NSLocalizedDescription=Frame load interrupted

这是我在 WKNavigation 决策策略中的下载决策处理程序的代码

// Check if a page can be downloaded
func webView(_ webView: WKWebView,
             decidePolicyFor navigationResponse: WKNavigationResponse,
             decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) 
    
    if navigationResponse.canShowMIMEType 
        decisionHandler(.allow)
     else 
        let url = navigationResponse.response.url
        
        // Alternative to decisionHandler(.download) since that's ios 15 and up
        //let documentUrl = url?.appendingPathComponent(navigationResponse.response.suggestedFilename!)
        parent.webModel.downloadDocumentFrom(url: url!)
        decisionHandler(.cancel)
    

这是我的下载数据函数的代码(它使用 AF.download 方法)

// Download file from page
func downloadDocumentFrom(url downloadUrl : URL) 
    if currentDownload != nil 
        showDuplicateDownloadAlert = true
        return
    
    
    let queue = DispatchQueue(label: "download", qos: .userInitiated)
    var lastTime = Date()
    
    let destination: DownloadRequest.Destination =  tempUrl, response in
        let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
        let suggestedName = response.suggestedFilename ?? "unknown"
        
        let fileURL = documentsURL.appendingPathComponent(suggestedName)

        return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
    
    
    self.showDownloadProgress = true
    
    currentDownload = AF.download(downloadUrl, to: destination)
        .downloadProgress(queue: queue)  progress in
            if Date().timeIntervalSince(lastTime) > 1.5 
                lastTime = Date()
                
                DispatchQueue.main.async 
                    self.downloadProgress = progress.fractionCompleted
                
            
        
        .response  response in
            DispatchQueue.main.asyncAfter(deadline: .now() + 1) 
                self.showDownloadProgress = false
                self.downloadProgress = 0.0
            
            
            if response.error == nil, let currentPath = response.fileURL 
                self.downloadFileUrl = currentPath
                self.showFileMover = true
            
            
            if let error = response.error 
                self.errorDescription = "Download could not be completed. \(error)"
                self.showError = true
            
        

【问题讨论】:

另一个注意事项:我的目标是 iOS 14 及更高版本,因此我无法使用 WKDownloadDelegate,因为它仅适用于 iOS 15 及更高版本。 这是您的错误:“此应用不允许查询方案 blob”。您需要将 blob 添加到 LSApplicationQueriesSchemes。 所以,我刚刚尝试了这个,应用程序现在可以打开 blob URL,但这不是我想要做的。相反,我想下载该 blob URL 格式的内容。当我尝试正常打开 URL 时,我现在收到此错误 -canOpenURL: failed for URL: "blob:https://cubari.moe/6d964a07-c4fe-4b22-95ac-7e3a6da88c6f" - error: "The operation couldn’t be completed. 我不知道 blob 是什么,但这是一个有效的网址吗? 是的,blob URL 是有效的,这里是 MDN spec 【参考方案1】:

几天后,我能够弄清楚如何在没有 WKDownloadDelegate 的情况下下载 blob URL。以下代码基于this answer。

需要创建一个消息处理程序来响应 JS 消息。我在makeUIView 函数中创建了这个

webModel.webView.configuration.userContentController.add(context.coordinator, name: "jsListener")

在您的 WKNavigationDelegate 中,您需要将此代码添加到导航操作中。

注意:由于我使用 SwiftUI,我的所有变量/模型都位于父类(UIViewRepresentable 协调器)中。

func webView(_ webView: WKWebView,
             decidePolicyFor navigationAction: WKNavigationAction,
             decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) 
    if let url = navigationAction.request.url, let scheme = url.scheme?.lowercased() 
        if scheme == "blob" 
            // Defer to JS handling
            parent.webModel.executeBlobDownloadJS(url: url)
            
            decisionHandler(.cancel)
         else 
            decisionHandler(.allow)
        
    

这是请求存储在浏览器内存中的 blob 的 JS。我将此 JS 添加到一个名为 evaluatejavascript 的包装函数中,并带有 url 以使我的代码保持整洁。

function blobToDataURL(blob, callback) 
    var reader = new FileReader()
    reader.onload = function(e) callback(e.target.result.split(",")[1])
    reader.readAsDataURL(blob)


async function run() 
    const url = "\(url)"
    const blob = await fetch(url).then(r => r.blob())

    blobToDataURL(blob, datauri => 
        const responseObj = 
            url: url,
            mimeType: blob.type,
            size: blob.size,
            dataString: datauri
        
        window.webkit.messageHandlers.jsListener.postMessage(JSON.stringify(responseObj))
    )


run()

除了返回的 JS 对象之外,我还必须创建一个可以反序列化 JSON 字符串的结构:

struct BlobComponents: Codable 
    let url: String
    let mimeType: String
    let size: Int64
    let dataString: String

然后我将发送到 WKScriptMessageHandler 的消息并解释它们以保存到文件中。我在这里使用了 SwiftUI 文件移动器,但是你可以对这个内容做任何你想做的事情。

func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) 
    guard let jsonString = message.body as? String else 
        return
    
    
    parent.webModel.blobDownloadWith(jsonString: jsonString)

在我的网络模型中(需要导入CoreServices):

func blobDownloadWith(jsonString: String) 
    guard let jsonData = jsonString.data(using: .utf8) else 
        print("Cannot convert blob JSON into data!")
        return
    

    let decoder = JSONDecoder()
    
    do 
        let file = try decoder.decode(BlobComponents.self, from: jsonData)
        
        guard let data = Data(base64Encoded: file.dataString),
            let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, file.mimeType as CFString, nil),
            let ext = UTTypeCopyPreferredTagWithClass(uti.takeRetainedValue(), kUTTagClassFilenameExtension)
        else 
            print("Error! \(error)")
            return
        
        
        let fileName = file.url.components(separatedBy: "/").last ?? "unknown"
        let path = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
        let url = path.appendingPathComponent("blobDownload-\(fileName).\(ext.takeRetainedValue())")
        
        try data.write(to: url)
        
        downloadFileUrl = url
        showFileMover = true
     catch 
        print("Error! \(error)")
        return
    

【讨论】:

以上是关于如何使用 AlamoFire 下载 blob URI的主要内容,如果未能解决你的问题,请参考以下文章

AlamoFire + ObjectMapper,如何让函数获取 Mappable 类型的参数?

Alamofire:如何按顺序下载文件

如何暂停应用程序直到使用 Alamofire 下载 json 数据?

使用 alamofire 首次单击时未获得 json 响应

使用 Alamofire 的下载速率

使用 Swift 3.0 的 Alamofire 4 失败:错误域 = NSURLErrorDomain 代码 = -999 “已取消”