Swift 如何从iOS(客户端)通过URL请求发送文件

Swift 如何从iOS(客户端)通过URL请求发送文件,swift,file-upload,python-requests,urlsession,urlrequest,Swift,File Upload,Python Requests,Urlsession,Urlrequest,这是我上传文件的RESTAPI- @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 foun

这是我上传文件的RESTAPI-

@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})
上面的代码在我使用postman测试时运行良好,但是在postman中我可以设置一个file对象。 然而,当我尝试从iOS应用程序上传时,它会给我错误信息-

NO File found in request
这是我上传图片的swift代码-

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)
        }
    }
}
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)
    }
}

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

。。。
设boundary=boundary()
let contentType=“多部分/表单数据;边界=\(边界)”
...

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

内容类型请求标头值:

let contentType=“多部分/表单数据;边界=-->(边界)”

这里,边界参数前面不应加前缀“-”。 此外,根据相应的RFC删除任何不明确允许的WS。 此外,将边界参数括在双引号中可使其更加健壮,而且不会造成任何伤害:

let contentType=“多部分/表单数据;边界=\”\(边界)\“”

初始正文:

httpBody.append(“--\(边界+换行符)”)

这是身体的开始。在主体之前,请求头被写入主体流。每个标头都用一个CRLF完成,在最后一个标头之后,必须写入另一个CRLF。嗯,我很肯定,URL请求将确保这一点。尽管如此,还是值得使用一个工具来检查这一点,该工具显示了在导线上书写的字符。 否则,将前面的CRLF添加到边界(从概念上讲,它无论如何都属于该边界,并且不会造成任何伤害):

httpBody.append(“\(换行符)--\(边界)\(换行符)”)

内容配置:

httpBody.append(“内容处置:表单数据;名称=\“文件\”;\(换行符)”)

在此,您也可以删除其他WS:

httpBody.append(“内容处置:表单数据;名称=\“文件\”;\(换行符)”)

或者,您可能需要提供
文件名
参数和值。不过,这不是强制性的

关闭边界 这里没有错误:

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中查找详细信息-这很麻烦,但也很有启发性;)

参考资料:


这是我在library的帮助下,使用
多部分表单从IOS客户端上传文件的方法

let url=“url here”
let头:HTTPHeaders=[
“授权”:“此处为持票人代币”,
“接受”:“应用程序/x-www-form-urlencoded”
]
AF.upload(multipartFormData:{(multipartFormData)在
multipartFormData.append(imageData,名称为:“image”,文件名为:“image.png”,mimeType:“image/png”)
},to:url,method:.post,headers:headers).validate(状态码:200..是多部分的好例子。我认为构建多部分可能有问题:

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

嗯……这不是Swift。你应该从你的问题中删除
[Swift]
标记。“我正在尝试使用URLSession。”你有Swift代码吗?你知道邮递员可以为你的请求生成Swift代码吗?这不是漂亮的代码,但你可能会使用/受到启发的代码?跳转到错误的标记可能会令人沮丧。因此,请删除[Swift]我想作者最终想要Swift代码,但没有表现出任何努力,只是用另一种语言显示他/她的服务器代码。我需要一些帮助或资源提示,告诉我如何编写iOS客户端代码以使用URLSession发送图像。我尝试了你的建议,但也没有效果。@Natasha你必须在同一个re中使用相同的边界quest!所以,最好将它定义为一些let值,并在多部分数据中使用它,正如iUrii所建议的;)每个请求都有一个新的边界是可以的。它将在内容类型请求标头中定义。它必须是一系列不在有效负载内发生的字符。然后,指定的边界必须在内容类型请求头中定义的多部分中使用。我用建议的更新更新了我的帖子。我用你所有的建议更新了帖子,但仍然没有成功。到目前为止,我们发现了一些错误,这些错误实际上阻止了发送有效的请求。我不确定我们是否发现了所有问题。您现在可能希望在创建URLRequest后将其记录(在单元测试中)。还打印出UTF-8解码的正文,并检查它是否是有效的多部分/表单数据正文。您可以在单元测试中检查这一点。还可能使用Charles Proxy之类的工具来检查此工具是否识别请求。如果这是确定的,您需要检查您的服务器。请注意,我们不知道您的服务器需要什么,而“请求中找不到文件”是您的自定义错误处理。此外,您可以尝试一个可以处理多部分/表单数据的模拟服务器。发送一个简短的文件来检查您的客户方法。旁注:在这个级别上,联网可能很棘手,因此我对出现的问题并不感到惊讶:)大多数开发人员在必须发送多部分/表单数据时使用客户端库。;)没有阿拉莫菲尔。我想处理URLSession。
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 { }
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