как отправить файл через URLRequest с iOS (на стороне клиента)

#swift #file-upload #python-requests #urlsession #urlrequest

#swift #загрузка файла #python-запросы #urlsession #urlrequest

Вопрос:

Вот мой 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})
 

Приведенный выше код работает нормально, поскольку я тестировал с 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 = "rn"
        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)
    }
}
 

Комментарии:

1. Ммм… Это не Swift. Вы должны удалить [swift] тег из своего вопроса.

2. «Я пытаюсь использовать URLSession». И есть ли у вас код Swift? Знаете ли вы, что POSTMAN может генерировать Swift-код для вашего запроса? Не красивый код, но код, который вы могли бы использовать / вдохновиться?

3. переход к неправильному тегу может быть неприятным.. поэтому, пожалуйста, удалите [swift]

4. Я думаю, что автор хочет, чтобы в конце был код Swift, но не прилагал никаких усилий, просто показывал свой серверный код на другом языке.

5. Мне нужна помощь или подсказки для ресурса, который расскажет мне, как написать клиентский код iOS для отправки изображения с помощью URLSession.

Ответ №1:

Ошибка заключается в том, что вы boundary() каждый раз вызываете функцию в своем коде, которая генерирует вам новый UUID, но ресурс должен иметь один. Итак, просто сгенерируйте UUID для своего ресурса один раз, а затем вставьте это значение туда, где вам нужно:

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

Комментарии:

1. Я попробовал ваше предложение, но это тоже не сработало.

2. @Natasha Вы должны использовать ту же границу в том же запросе! Итак, лучше определить его как некоторое значение let и использовать его в составных данных, как предложил Юрий 😉 Это нормально, чтобы иметь новую границу для каждого запроса. Это будет определено в заголовке запроса типа содержимого. Это должна быть последовательность символов, которая не встречается внутри полезной нагрузки. Затем указанная граница должна использоваться в составном элементе, как определено в заголовке запроса типа содержимого.

3. Да, я именно так и сделал, но это не сработало. Я обновил свой пост предложенным обновлением.

Ответ №2:

Настройка содержимого данных формы из нескольких частей может быть сложной задачей. Особенно могут быть незначительные ошибки при объединении многих частей тела запроса.

Значение заголовка запроса типа содержимого:

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

Здесь параметру границы не должен предшествовать префикс «—«. Кроме того, удалите все WS, которые явно не разрешены в соответствии с соответствующим RFC. Кроме того, заключение граничного параметра в двойные кавычки делает его более надежным и никогда не повредит:

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, определение составных / форм-данных

Комментарии:

1. Я обновил все ваши предложения, но все равно не повезло.

2. До сих пор мы обнаружили несколько ошибок, которые фактически не позволяли отправлять действительный запрос. Я не уверен, нашли ли мы все проблемы. Теперь вы можете захотеть зарегистрировать URLRequest (в модульном тестировании) после его создания. Также распечатайте декодированное тело в формате UTF-8 и проверьте, является ли оно допустимым составным телом / формой данных. Вы можете проверить это в модульном тестировании. Возможно, также используйте такой инструмент, как Charles Proxy, чтобы проверить, распознает ли этот инструмент запрос. Если это нормально, вам нужно проверить свой сервер. Обратите внимание, что мы понятия не имеем, что ожидает ваш сервер, и «Файл не найден в запросе» — это ваша пользовательская обработка ошибок.

3. Кроме того, вы можете попробовать макет сервера, который может обрабатывать составные / данные формы. Отправьте короткий файл, чтобы проверить ваш подход к клиенту. Примечание: на этом уровне создание сетей может быть сложным, поэтому я не удивлен возникающими проблемами 🙂 Большинство разработчиков используют клиентские библиотеки, когда им нужно отправить составные / данные формы. 😉

Ответ №3:

Комментарии:

1. Нет alamofire. Я хотел сделать с URLSession.

Ответ №4:


Вот хороший пример Multipart. Я думаю, что это может быть что-то не так со сборкой multipart:

 let body = NSMutableData()
        
        if parameters != nil {
            for (key, value) in parameters! {
                body.appendString("--(boundary)rn")
                body.appendString("Content-Disposition: form-data; name="(key)"rnrn")
                body.appendString("(value)rn")
            }
        }
        
        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)rn")
                body.appendString("Content-Disposition: form-data; name="(fileKeyName!)"; filename="(filename!)"rn")
                body.appendString("Content-Type: (mimetype)rnrn")
                body.appendData(data)
                body.appendString("rn")
            }
        }
        
        body.appendString("--(boundary)--rn")
        return body