Swift https POST request for the PlantNet API - swift

Currently I'm trying to program an https post request for the Plant Net API (https://my.plantnet.org/account/doc#) in Swift. I want to have one to five images inside my https post request and each image has as parameter organ (in my case this is always "leaf"). When I try to run my application, I get the error code 400 "Invalid multipart payload format". I'm not sure how I can solve this problem.
//
// PlantRecognition.swift
// Happy Plant
//
// Insert several images of the same plant (maximum of 5) and get the result list
//
import Foundation
import UIKit
class PlantRecognition {
//List with images of the plant to be recognized
var imageList: [UIImage] = []
//Api key for the plant net access
var API_KEY: String = "MY_API_KEY"
var plantNetURL: String = ""
init() {
//Set plant net url after class initialization
plantNetURL = "https://my-api.plantnet.org/v2/identify/all?api-key{\(API_KEY)}"
}
func recognizeImages(images: [UIImage]) {
//Try to create the api endpoint url
guard let apiEndpoint = URL(string: "https://my-api.plantnet.org/v2/identify/all?api-key=\(API_KEY)") else {print("Error creating api endpoint"); return }
//Convert the UIImages to jpeg
let imagesJPG = convertImagesToJPEG(images: images)
//Create url request
var request = URLRequest(url: apiEndpoint)
//Set the http method to post (local images)
request.httpMethod = "POST"
//Create boundary which marks the start/end of the request body
let boundary = "Boundary-\(UUID().uuidString)"
//Set value for our specific http header field
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
//Set the mime type to jpg (we will convert our UIImages to jpg)
let mimeType = "image/jpg"
//Create http body
var body = Data()
let boundaryPrefix = " — \(boundary)\r\n"
//Add the parameter to the body (Currently just leaf recognition is possible)
body.append("--\(boundaryPrefix)\r\n")
body.append("Content-Disposition: form-data; name=\"organs\r\n\r\n")
body.append("leaf\r\n")
//Add the images to the body
var filename = String()
for (index,data) in imagesJPG.enumerated(){
filename = "image\(index)"
body.append(boundaryPrefix)
body.append("Content-Disposition: form-data; name=\"image[]\"; filename=\"\(filename)\"\r\n")
body.append("Content-Type: \(mimeType)\r\n\r\n")
body.append(data)
body.append("\r\n")
}
body.append("--\(boundary)--\r\n")
//Post the request
let session = URLSession.shared
session.dataTask(with: request) { (data, response, error) in
if let response = response {
print(response)
}
if let data = data {
do {
let json = try JSONSerialization.jsonObject(with: data, options: [])
print(json)
} catch {
print(error)
}
}
}.resume()
}
func convertImagesToJPEG(images: [UIImage]) -> [Data] {
var dataArray = [Data]()
for i in images {
if let data = i.jpegData(compressionQuality: 1.0) {
dataArray.append(data)
}
}
return dataArray
}
}
extension Data {
mutating func append(_ string: String) {
if let data = string.data(using: .utf8) {
append(data)
}
}
}

Related

Upload form-data with SwiftUI and Express backend

I want to upload some user data for register new users and I want to upload a profile picture. I'v tried with this code but it doesn't work, on my backend im just receiving a request with nothing like a form-data inside.
This is my swift code
func UserRegisterRequest(firstname: String, lastname: String, username: String, email: String, password: String, image: UIImage , completionHandler: #escaping (ReturnMessage) -> Void) {
let parameters = ["name": "MyTestFile123321",
"id": "12345"]
// MARK: URL Components
var components = URLComponents()
components.scheme = "https"
components.host = "my-url"
components.path = "/register"
//MARK: Create URL
guard let url = components.url else {
print("Invalid URL")
return
}
guard let mediaImage = Media(withImage: image, forKey: "profilePicture") else { return }
var request = URLRequest(url: url)
request.httpMethod = "POST"
//create boundary
let boundary = generateBoundary()
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
//call createDataBody method
let dataBody = createDataBody(withParameters: parameters, media: [mediaImage], boundary: boundary)
request.httpBody = dataBody as Data
print(request.httpBody)
URLSession.shared.dataTask(with: request) {
data, response, error in
if let data = data {
do {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(DateFormatter.customFormatter)
let response = try decoder.decode(ReturnMessage.self, from: data)
completionHandler(response)
}catch(let error) {
print(error.localizedDescription)
}
} else {
print("No Data")
}
}.resume()
}
func generateBoundary() -> String {
return "Boundary-\(NSUUID().uuidString)"
}
func createDataBody(withParameters params: Parameters?, media: [Media]?, boundary: String) -> Data {
let lineBreak = "\r\n"
var body = Data()
if let parameters = params {
for (key, value) in parameters {
body.append("--\(boundary + lineBreak)")
body.append("Content-Disposition: form-data; name=\"\(key)\"\(lineBreak + lineBreak)")
body.append("\(value + lineBreak)")
}
}
if let media = media {
for photo in media {
print(photo)
body.append("--\(boundary + lineBreak)")
body.append("Content-Disposition: form-data; name=\"\(photo.key)\"; filename=\"\(photo.filename)\"\(lineBreak)")
body.append("Content-Type: \(photo.mimeType + lineBreak + lineBreak)")
body.append(photo.data)
body.append(lineBreak)
}
}
body.append("--\(boundary)--\(lineBreak)")
return body
}
I don't know if this is the right way to do it but any help is welcome.
thanks for you helping me guys

error: missing argument for parameter 'from' in call

I am trying to make an http get request and I want to go back to convert the data obtained in JSON to an Array and send it as a parameter to a leaf template from the routes.swift file, all this using Vapor framework but I get the following error:
Error: missing argument for parameter 'from' in call
let json = JSONDecoder().decode(Todos.self).
Here is my code:
app.get("datos") {req -> EventLoopFuture<View> in
let url = URL(string: "https://jsonplaceholder.typicode.com/posts")
guard let requestUrl = url else { fatalError("Error url") }
var request = URLRequest(url: requestUrl)
request.httpMethod = "GET"
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
if let error = error {
print("Error took place \(error.localizedDescription)")
return
}
if let response = response as? HTTPURLResponse {
print("Response HTTP Status code: \(response.statusCode)")
}
if let data = data, let dataString = String(data: data, encoding: .utf8) {
print("Response data string:\n \(dataString)")
}
}
task.resume()
let json = JSONDecoder().decode(Todos.self)
return req.view.render("objetos", json)
}
struct Todos: Codable{
var userId: Int
var id: Int
var title: String
var body: String
}
Don't use URLSession - it doesn't integrate nicely with Vapor's futures and Vapor's client built on top of AsyncHTTPClient is much more performant and integrated. You can then rewrite your code to look like:
app.get("datos") { req -> EventLoopFuture<View> in
req.client.get("https://jsonplaceholder.typicode.com/posts").flatMap { response in
let todos = try response.content.decode(Todos.self)
return req.view.render("objetos", json)
}
}

how to send file through urlRequest from iOS(client-end)

Here is my REST API for uploading file-
#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})
The above code works fine as I tested with postman but in postman I can set a file object.
However, when I try to upload from iOS app, it gives me the error-
NO File found in request
Here is my swift code to upload image-
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)
}
}
}
Also here is the NetworkRequest struct-
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
}
}
In The ImageLoaderViewController, an image is selected to be sent to be uploaded.
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)
}
}
The mistake is that you call boundary() function each time in your code that generates you new UUID but the resource must have a single one. So just generate UUID for your resource once and then insert this value where you need:
...
let boundary = boundary()
let contentType = "multipart/form-data; boundary = \(boundary)"
...
Setting up a multipart form-data content can be tricky. Especially there can be subtle errors when combining the many parts of the request body.
Content-Type request header value:
let contentType = "multipart/form-data; boundary = --\(boundary)"
Here, the boundary parameter should not be preceded with the prefix "--".
Also, remove any WS that are not explicitly allowed according the corresponding RFC.
Furthermore, enclosing the boundary parameter in double quotes makes it more robust and never hurts:
let contentType = "multipart/form-data; boundary=\"\(boundary)\""
Initial body:
httpBody.append("--\(boundary + lineBreak)")
This is the start of the body. Before the body, the request headers are written into the body stream. Each header is completed with a CRLF, and after the last header another CRLF must be written. Well, I am pretty sure, URLRequest will ensure this. Nonetheless, it might be worth checking this with a tool that shows the characters written over the wire.
Otherwise, add a preceding CRLF to the boundary (which conceptually belongs to the boundary anyway, and it does not hurt also):
httpBody.append("\(lineBreak)--\(boundary)\(lineBreak)")
Content-Disposition:
httpBody.append("Content-Disposition: form-data; name = \"file\"; \(lineBreak)")
Here, again you may remove the additional WS:
httpBody.append("Content-Disposition: form-data; name=\"file\"; \(lineBreak)")
Optionally, you may want to provide a filename parameter and a value. This is not mandatory, though.
Closing boundary
There's no error here:
httpBody.append(lineBreak)
httpBody.append("--\(boundary)--")
But you might want to make it clear, that the preceding CRLF belongs to the boundary:
httpBody.append("\(lineBreak)--\(boundary)--")
Characters after the closing boundary will be ignored by the server.
Encoding
extension Data{
mutating func append(_ string: String) {
if let data = string.data(using: .utf8){
self.append(data)
}
}
}
You cannot generally return utf8 encoded strings and embed this into the many different parts of a HTTP request body. Many parts of the HTTP protocol allow only a restricted set of characters. In many cases, UTF-8 is not allowed. You have to look-up the details in the corresponding RFCs - which is cumbersome, but also enlightened ;)
References:
RFC 7578, Definition of multipart/form-data
This is my way to upload a file from IOS Client using multipart form , with the help of Alamofire library
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 { }
Here is nice example of Multipart. I think it could be something wrong with building 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

Cannot upload zip file with multipart and URLSession Swift

I am experiencing a really strange issue with uploading zip file to s3... Here is my sample code and all details:
private func uploadZipFile(_ photos:URL,_ step:String,_ presignedUrlModel: PresignedUrlModel) {
var uploadingBackgroundTask = UIBackgroundTaskIdentifier(rawValue: 3)
uploadingBackgroundTask = UIApplication.shared.beginBackgroundTask(expirationHandler: {
UIApplication.shared.endBackgroundTask(uploadingBackgroundTask)
uploadingBackgroundTask = .invalid
})
let success: () -> Void = {
DispatchQueue.main.async {
UIApplication.shared.endBackgroundTask(uploadingBackgroundTask)
uploadingBackgroundTask = .invalid
}
}
let failure: (ErrorResponse) -> Void = { error in
DispatchQueue.main.async {
UIApplication.shared.endBackgroundTask(uploadingBackgroundTask)
uploadingBackgroundTask = .invalid
}
}
let boundary = "Boundary-\(UUID().uuidString)"
let uploadingRequest = URL(string: presignedUrlModel.url)!
var request = APIhelper().getMultipartRequestBody(_type: "POST", _url: uploadingRequest, boundary: boundary)
let body = self.formBackgroundPhotoData(presignedUrlModel, step, photos, boundary)
request.httpBody = body
self.session.configuration.shouldUseExtendedBackgroundIdleMode = true
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
guard let data = data else {
return
}
if let httpResponse = response as? HTTPURLResponse {
if(httpResponse.statusCode==200)
{
success()
}
else if(httpResponse.statusCode>=400 && httpResponse.statusCode<500)
{
//getting 403 errorr
let error = ErrorResponse(message:
self.errorFormatingClass.getLocalizedErrorMessage(identifier: self.errorFormatingClass.ERROR)
,
identifier: self.errorFormatingClass.ERROR)
failure(error)
}
}
}
)
task.resume()
}
I always getting 403 response:AccessDeniedInvalid according to Policy: Policy Condition failed: ["eq", "$Content-Type", "application/zip"]
​
Here is how I form data:
private func formBackgroundPhotoData(_ presignedUrlModel: PresignedUrlModel,_ step: String,_ zip: URL,_ boundary: String) -> Data {
var body = Data()
var key = "key"
var value = presignedUrlModel.key
body.append("--\(boundary)\r\n")
body.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n")
body.append("\(value)\r\n")
key = "AWSAccessKeyId"
value = presignedUrlModel.AWSAccessKeyId
body.append("--\(boundary)\r\n")
body.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n")
body.append("\(value)\r\n")
key = "signature"
value = presignedUrlModel.signature
body.append("--\(boundary)\r\n")
body.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n")
body.append("\(value)\r\n")
key = "policy"
value = presignedUrlModel.policy
body.append("--\(boundary)\r\n")
body.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n")
body.append("\(value)\r\n")
let filename = zip.lastPathComponent
guard let data = try? Data(contentsOf: zip) else {
#if DEBUG
debugPrint("backgroundupload", "dataisnull")
#endif
return Data()
}
let mimetype = mimeTypeForPath(url: zip)
body.append("--\(boundary)\r\n")
body.append("Content-Disposition: form-data; name=\"file\"; filename=\"\(filename)\"\r\n")
body.append("Content-Type: \(mimetype)\r\n\r\n")
body.append(data)
body.append("\r\n")
body.append("--\(boundary)--\r\n")
return body
}
Here is multipart request:
public func getMultipartRequestBody(_type: String, _url: URL, boundary: String) ->URLRequest
{
var request = URLRequest(url: _url)
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
request.timeoutInterval = 300
request.httpMethod = _type
return request
}
Now, I know this tons of code, and perhaps nobody will take a look... All I am asking: please provide an example for uploading zip or images to AWS S3, without adding S3 framework for uploading files..
Our whole team does not know what to do :(

HTTP status 415 when using Alamofire multipart upload

I'm trying to get an image uploaded to a RESTful web api and I'm getting a HTTP status 415 (unsupported media type). The strange thing is I did have it working the other day and i'm not to sure what i've changed to make it stop accepting the POST request.
My swift code:
func uploadImage(image: UIImage, userId: String, completion: () -> Void) {
parameters["userId"] = userId
let imageData = UIImageJPEGRepresentation(image, 70)
let urlRequest = urlRequestWithComponents(myUrl, parameters: parameters, imageData: imageData!)
Alamofire.upload(urlRequest.0, data: urlRequest.1)
.responseJSON { (response) in
print(response)
if let result = response.result.value as? Dictionary<String, String>{
print("have a result from uploading!")
print(result)
if let result = result["success"] {
if (result == "true") {
completion()
}
}
}
}
}
func urlRequestWithComponents(urlString:String, parameters:Dictionary<String, String>, imageData:NSData) -> (URLRequestConvertible, NSData) {
// create url request to send
let mutableURLRequest = NSMutableURLRequest(URL: NSURL(string: urlString)!)
mutableURLRequest.HTTPMethod = Alamofire.Method.POST.rawValue
let boundaryConstant = "myRandomBoundary12345";
let contentType = "multipart/form-data"
mutableURLRequest.setValue(contentType, forHTTPHeaderField: "Content-Type")
// create upload data to send
let uploadData = NSMutableData()
// add image
uploadData.appendData("\r\n--\(boundaryConstant)\r\n".dataUsingEncoding(NSUTF8StringEncoding)!)
uploadData.appendData("Content-Disposition: form-data; name=\"file\"; filename=\"file.png\"\r\n".dataUsingEncoding(NSUTF8StringEncoding)!)
uploadData.appendData("Content-Type: image/png\r\n\r\n".dataUsingEncoding(NSUTF8StringEncoding)!)
uploadData.appendData(imageData)
// add parameters
for (key, value) in parameters {
uploadData.appendData("\r\n--\(boundaryConstant)\r\n".dataUsingEncoding(NSUTF8StringEncoding)!)
uploadData.appendData("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n\(value)".dataUsingEncoding(NSUTF8StringEncoding)!)
}
uploadData.appendData("\r\n--\(boundaryConstant)--\r\n".dataUsingEncoding(NSUTF8StringEncoding)!)
// return URLRequestConvertible and NSData
return (Alamofire.ParameterEncoding.URL.encode(mutableURLRequest, parameters: nil).0, uploadData)
}
And here is what my Jersey Rest service accepts for that url:
#POST
#Path("/uploadImage")
#Produces(MediaType.APPLICATION_JSON)
#Consumes(MediaType.MULTIPART_FORM_DATA)
public String uploadImage(FormDataMultiPart form) throws IOException
{
//do stuff
}
Its driving me crazy trying to figure out what is going wrong, any help is muchly appreciated!