【问题标题】:how to send file through urlRequest from iOS(client-end)如何从iOS(客户端)通过urlRequest发送文件
【发布时间】:2021-03-26 09:41:11
【问题描述】:

这是我用于上传文件的 REST API-

@api.route('/update_profile_picture', methods=['POST'])
def update_profile_picture():

    if 'file' in request.files:
        image_file = request.files['file']
    else:
    return jsonify({'response': None, 'error' : 'NO File found in request.'})

    filename = secure_filename(image_file.filename)
    image_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
    image_file.save(image_path)

    try:
        current_user.image = filename
        db.session.commit()
    except Exception as e:
        return jsonify({'response': None, 'error' : str(e)})

    return jsonify({'response': ['{} profile picture update successful'.format(filename)], 'error': None})

上面的代码在我用邮递员测试时工作正常,但在邮递员中我可以设置一个文件对象。 但是,当我尝试从 iOS 应用程序上传时,它给了我错误-

NO File found in request

这是我上传图片的快速代码-

struct ImageFile {
    let fileName : String
    let data: Data
    let mimeType: String
    
    init?(withImage image: UIImage, andFileName fileName: String) {
        self.mimeType = "image/jpeg"
        self.fileName = fileName
        guard let data = image.jpegData(compressionQuality: 1.0) else {
            return nil
        }
        self.data = data
    }
}

class FileLoadingManager{
    
    static let sharedInstance = FileLoadingManager()
    private init(){}
    
    let utilityClaas = Utility()
    
    func uploadFile(atURL urlString: String, image: ImageFile, completed:@escaping(Result<NetworkResponse<String>, NetworkError>)->()){
        
        guard let url = URL(string: urlString) else{
            return completed(.failure(.invalidURL))
        }
        
        var httpBody =  Data()
        let boundary = self.getBoundary()
    
        let lineBreak = "\r\n"
        let contentType = "multipart/form-data; boundary = --\(boundary)"
   
         httpBody.append("--\(boundary + lineBreak)")
         httpBody.append("Content-Disposition: form-data; name = \"file\"; \(lineBreak)")
         httpBody.append("Content-Type: \(image.mimeType + lineBreak + lineBreak)")
         httpBody.append(image.data)
         httpBody.append(lineBreak)
         httpBody.append("--\(boundary)--")
        
        let requestManager = NetworkRequest(withURL: url, httpBody: httpBody, contentType: contentType, andMethod: "POST")
        let urlRequest = requestManager.urlRequest()
        
        let dataTask = URLSession.shared.dataTask(with: urlRequest) {  (data, response, error) in
            if let error = error as? NetworkError{
                completed(.failure(error))
                return
            }
            if let response = response as? HTTPURLResponse{
                if response.statusCode < 200 || response.statusCode > 299{
                    completed(.failure(self.utilityClaas.getNetworkError(from: response)))
                    return
                }
            }

            guard let responseData = data else{
                completed(.failure(NetworkError.invalidData))
                return
            }

            do{
                let jsonResponse = try JSONDecoder().decode(NetworkResponse<String>.self, from: responseData)
                completed(.success(jsonResponse))
            }catch{
                completed(.failure(NetworkError.decodingFailed))
            }
        }
        dataTask.resume()
    }
    
    private func boundary()->String{
        return "--\(NSUUID().uuidString)"
    }
}

extension Data{
    mutating func append(_ string: String) {
        if let data = string.data(using: .utf8){
            self.append(data)
        }
    }
}

这里还有 NetworkRequest 结构-

class NetworkRequest{
    
    var url: URL
    var httpBody: Data?
    var httpMethod: String
    var contentType = "application/json"
   
    
    init(withURL url:URL, httpBody body:Data, contentType type:String?, andMethod method:String) {
        self.url = url
        self.httpBody = body
        self.httpMethod = method
        if let contentType = type{
            self.contentType = contentType
        }
    }
    
    func urlRequest()->URLRequest{
        var request = URLRequest(url: self.url)
        
        request.addValue(contentType, forHTTPHeaderField: "Content-Type")
        request.httpBody = self.httpBody
        request.httpMethod = self.httpMethod
        return request
    }
    
}

ImageLoaderViewController中,选择一张图片发送上传。

class ImageLoaderViewController: UIViewController {
    
    @IBOutlet weak var selectedImageView: UIImageView!
       
    override func viewDidLoad() {
        super.viewDidLoad()
    }
   
    @IBAction func selectImage(){
        if selectedImageView.image != nil{
            selectedImageView.image = nil
        }
        let imagePicker = UIImagePickerController()
        imagePicker.sourceType = .photoLibrary
        imagePicker.delegate = self
        self.present(imagePicker, animated: true, completion: nil)
    }

    @IBAction func uploadImageToServer(){
        if let image = imageFile{
            DataProvider.sharedInstance.uploadPicture(image) { (msg, error) in
                if let error = error{
                    print(error)
                }
                else{
                    print(msg!)
                }
            }
        }
    }
   func completedWithImage(_ image: UIImage) -> Void {
        imageFile = ImageFile(withImage: image, andFileName: "test")
    }
}
extension ImageLoaderViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate{
    
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        if let image = info[.originalImage] as? UIImage{
            picker.dismiss(animated: true) {
                self.selectedImageView.image = image
                self.completedWithImage(image)
            }
        }
        picker.dismiss(animated: true, completion: nil)
    }
    
    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        picker.dismiss(animated: true, completion: nil)
    }
}

【问题讨论】:

  • 嗯...这不是 Swift。您应该从问题中删除[swift] 标签。
  • "我正在尝试使用 URLSession。"你有 Swift 代码吗?你知道 POSTMAN 可以为你的请求生成 Swift 代码吗?不是漂亮的代码,但您可能会使用/获得灵感的代码?
  • 跳转到错误的标签可能会令人沮丧..所以请删除 [swift]
  • 我认为作者最终想要的是Swift代码,但没有表现出任何努力,只是用另一种语言显示他/她的服务器代码。
  • 我需要一些帮助或资源提示,告诉我如何编写 iOS 客户端代码以使用 URLSession 发送图像。

标签: swift file-upload python-requests urlsession urlrequest


【解决方案1】:

错误是您每次在生成新 UUID 的代码中调用 boundary() 函数,但资源必须只有一个。因此,只需为您的资源生成一次 UUID,然后将此值插入您需要的位置:

...
let boundary = boundary()
let contentType = "multipart/form-data; boundary = \(boundary)"
...

【讨论】:

  • 我试过你的建议,但也没有用。
  • @Natasha 你必须在同一个请求中使用相同的边界!因此,最好将其定义为一些 let 值并在多部分数据中使用它,正如 iUrii 建议的那样;)为每个请求都有一个新的边界是可以的。它将在 Content-Type 请求标头中定义。它必须是一个不出现在有效负载中的字符序列。然后必须在 Content-Type 请求标头中定义的多部分中使用指定的边界。
  • 是的,我确实这样做了,但它没有用。我用建议的更新更新了我的帖子。
【解决方案2】:

设置多部分表单数据内容可能很棘手。尤其是在组合请求正文的许多部分时可能会出现细微的错误。

Content-Type 请求头值:

let contentType = "multipart/form-data; boundary = --\(boundary)"

这里,边界参数不应以前缀“--”开头。 此外,删除根据相应 RFC 未明确允许的任何 WS。 此外,将边界参数括在双引号中使其更加健壮并且永远不会受到伤害:

let contentType = "multipart/form-data; boundary=\"\(boundary)\""

初始正文:

httpBody.append("--\(boundary + lineBreak)")

这是身体的开始。在正文之前,请求标头被写入正文流。每个标头都以 CRLF 完成,在最后一个标头之后必须写入另一个 CRLF。好吧,我很确定,URLRequest 将确保这一点。尽管如此,可能值得使用一个工具来检查这一点,该工具可以显示写在电线上的字符。 否则,将前面的 CRLF 添加到边界(无论如何,它在概念上属于边界,并且它也不会受到伤害):

httpBody.append("\(lineBreak)--\(boundary)\(lineBreak)")

内容处置:

httpBody.append("Content-Disposition: form-data; name = \"file\"; \(lineBreak)")

在这里,您可以再次删除额外的 WS:

httpBody.append("Content-Disposition: form-data; name=\"file\"; \(lineBreak)")

或者,您可能想要提供一个filename 参数和一个值。不过,这不是强制性的。

关闭边界 这里没有错误:

httpBody.append(lineBreak)
httpBody.append("--\(boundary)--")

但是你可能要说清楚,前面的 CRLF 属于边界:

httpBody.append("\(lineBreak)--\(boundary)--")

关闭边界之后的字符将被服务器忽略。

编码

extension Data{
    mutating func append(_ string: String) {
        if let data = string.data(using: .utf8){
            self.append(data)
        }
    }
}

您通常不能返回 utf8 编码的字符串并将其嵌入到 HTTP 请求正文的许多不同部分。 HTTP 协议的许多部分只允许有限的字符集。在许多情况下,UTF-8 是不允许的。您必须在相应的 RFC 中查找详细信息 - 这很麻烦,但也很有启发性;)

参考资料:

RFC 7578, Definition of multipart/form-data

【讨论】:

  • 我更新了您的所有建议,但仍然没有运气。
  • 到目前为止,我们发现了一些实际上阻止发送有效请求的错误。我不确定我们是否已经找到了所有问题。您现在可能希望在创建 URLRequest 后记录它(在单元测试中)。还打印出 UTF-8 解码的正文并检查它是否是有效的多部分/表单数据正文。您可以在单元测试中检查这一点。可能还使用诸如 Charles Proxy 之类的工具来检查此工具是否识别请求。如果这没问题,您需要检查您的服务器。请注意,我们不知道您的服务器期望什么,“请求中未找到文件”是您的自定义错误处理。
  • 另外,您可以尝试一个可以处理多部分/表单数据的模拟服务器。发送一个简短的文件来检查您的客户方法。附注:在这个级别上,网络可能会很棘手,所以我对出现的问题并不感到惊讶 :) 大多数开发人员在必须发送多部分/表单数据时使用客户端库。 ;)
【解决方案3】:

这是我使用 multipart form 从 IOS 客户端上传文件的方法,在 Alamofire 库的帮助下

let url = "url here"
let headers: HTTPHeaders = [
        "Authorization": "Bearer Token Here",
        "Accept": "application/x-www-form-urlencoded"
    ]
AF.upload(multipartFormData: { (multipartFormData) in
            multipartFormData.append(imageData, withName: "image" ,fileName: "image.png" , mimeType: "image/png")
        }, to: url, method: .post ,headers: headers).validate(statusCode: 200..<300).response { }

【讨论】:

  • 没有阿拉莫火。我想处理 URLSession。
【解决方案4】:

Here 是 Multipart 的一个很好的例子。我认为构建多部分可能有问题:

let body = NSMutableData()
        
        if parameters != nil {
            for (key, value) in parameters! {
                body.appendString("--\(boundary)\r\n")
                body.appendString("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n")
                body.appendString("\(value)\r\n")
            }
        }
        
        if fileURLs != nil {
            if fileKeyName == nil {
                throw NSError(domain: NSBundle.mainBundle().bundleIdentifier ?? "NSURLSession+Multipart", code: -1, userInfo: [NSLocalizedDescriptionKey: "If fileURLs supplied, fileKeyName must not be nil"])
            }
            
            for fileURL in fileURLs! {
                let filename = fileURL.lastPathComponent
                guard let data = NSData(contentsOfURL: fileURL) else {
                    throw NSError(domain: NSBundle.mainBundle().bundleIdentifier ?? "NSURLSession+Multipart", code: -1, userInfo: [NSLocalizedDescriptionKey: "Unable to open \(fileURL.path)"])
                }
                
                let mimetype = NSURLSession.mimeTypeForPath(fileURL.path!)
                
                body.appendString("--\(boundary)\r\n")
                body.appendString("Content-Disposition: form-data; name=\"\(fileKeyName!)\"; filename=\"\(filename!)\"\r\n")
                body.appendString("Content-Type: \(mimetype)\r\n\r\n")
                body.appendData(data)
                body.appendString("\r\n")
            }
        }
        
        body.appendString("--\(boundary)--\r\n")
        return body

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-10-15
    • 2019-03-20
    • 2020-04-17
    • 1970-01-01
    • 1970-01-01
    • 2018-10-27
    • 1970-01-01
    • 2012-06-27
    相关资源
    最近更新 更多