Vapor upload multiple files at once - swift

I want to upload multiple images in one POST request. Currently, the part of my request related to the file upload is taking one file and looks like this:
return try req.content.decode(File.self).flatMap(to: Image.self) { (file) in
try file.data.write(to: URL(fileURLWithPath: DirectoryConfig.detect().workDir + localImageStorage + file.filename))
return try Image(userID: user.requireID(), url: imageStorage + file.filename, filename: file.filename).save(on: req)
}
This works just fine. Now, I tried to change .decode(File.self) to .decode([File].self), and do a loop for all files.
When trying to upload images using the data[] parameter in Postman, I get the following error:
Nested form-data decoding is not supported.
How do I solve this?

Example below works well, tested multiple times already 🙂
struct MyPayload: Content {
var somefiles: [File]
}
func myUpload(_ req: Request) -> Future<HTTPStatus> {
let user: User = try req.requireAuthenticated()
return try req.content.decode(MyPayload.self).flatMap { payload in
let workDir = DirectoryConfig.detect().workDir
return payload.somefiles.map { file in
let url = URL(fileURLWithPath: workDir + localImageStorage + file.filename)
try file.data.write(to: url)
return try Image(userID: user.requireID(), url: imageStorage + file.filename, filename: file.filename).save(on: req).transform(to: ())
}.flatten(on: req).transform(to: .ok)
}
}
btw also you could declare your payload exactly in the function params
func myUpload(_ req: Request, _ payload: MyPayload) -> Future<HTTPStatus> {
let user: User = try req.requireAuthenticated()
let workDir = DirectoryConfig.detect().workDir
return payload.somefiles.map { file in
let url = URL(fileURLWithPath: workDir + localImageStorage + file.filename)
try file.data.write(to: url)
return try Image(userID: user.requireID(), url: imageStorage + file.filename, filename: file.filename).save(on: req).transform(to: ())
}.flatten(on: req).transform(to: .ok)
}
the only difference is in declaring endpoint function on router
router.post("upload", use: myUpload)
vs
router.post(MyPayload.self, at: "upload", use: myUpload)
Then in Postman upload your files like this

Related

Auto create directory when writing a file to a directory that does not exist

I am using File Manager to write files to the users documents directory. Every file is downloaded to a users device via a URL and then I create a local URL by doing the following:
extension URL {
func getLocalUrl() -> URL {
let directoryURL = FileManager.default.getDocumentsDirectory()
let filePath = pathComponents.dropFirst().joined(separator: "-")
return directoryURL.appendingPathComponent(filePath)
}
}
This works perfectly fine. However, when I try to create a local URL by using slashes instead of dashes via the following:
extension URL {
func getLocalUrl() -> URL {
let directoryURL = FileManager.default.getDocumentsDirectory()
let filePath = pathComponents.dropFirst().joined(separator: "/")
return directoryURL.appendingPathComponent(filePath)
}
}
I get the following error when this code runs:
func save(url: URL, fileUrl: URL) {
do {
// fileUrl is a url in the temporary directory from a URLSession.downloadTask
try FileManager.default.moveItem(at: fileUrl, to: url.getLocalUrl())
} catch {
print("download.service.write.error: \(error)")
}
}
CFNetworkDownload_sKaBto.tmp” couldn’t be moved to “user-data” because either the former doesn’t exist, or the folder containing the latter doesn’t exist."
As you can see the error is because I am trying to write to a directory that does not exist. Is there a way to auto create the directory if it does not exist?

Uploading a PDF File From Swift Yields a Blank PDF

Im trying to upload a PDF generated from a UIImage. I take a picture using my camera framework, uploads all ok but the PDF on S3 is blank. Below is the code I am using:
I use the following class to create the PDF from a UIImage, tested ok.
// Using PDFKit
func generatePDF(source: UIImage) -> PDFDocument {
let pdfDocument = PDFDocument()
let pdfPage = PDFPage(image: source)
pdfDocument.insert(pdfPage!, at: 0)
return pdfDocument
}
I used Alamofire to perform a multipart upload with the pdf, tested ok.
class NetworkManager {
static let shared = NetworkManager()
func upload(document: Data, name: String) {
let filename = "\(name).pdf"
let urlString = endpoint + "?filename=" + filename
let url = URL(string: urlString)!
var request = URLRequest(url: url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 30)
request.method = .post
request.setValue(key, forHTTPHeaderField: api)
AF.upload(multipartFormData: { multiPart in
multiPart.append(document, withName: name, fileName: filename, mimeType: "application/pdf")
}, with: request)
.uploadProgress(queue: .main, closure: { progress in
print("Upload Progress: \(progress.fractionCompleted)")
})
.responseJSON(completionHandler: { data in
print("data: \(data)")
})
}
}
On AWS I am using the following framework to reconstruct the multipart data back to a PDF
lambda-multipart-parser
https://www.npmjs.com/package/lambda-multipart-parser
How Im using it in Lambda:
let filename = event.queryStringParameters.filename;
let documentData = await imageParser.parse(event);
let document = documentData.files[0];
var data = {
Bucket: 'order-scanned-copy',
Key: filename,
Body: document.content,
ContentType: "application/pdf",
};
let uploadFile = await s3.putObject(data).promise();
No errors, no warnings just a blank PDF. I can tell by the size around 500kb-1mb that it has something in it. Any help would be appreciated.
Thanks.
I figured it out, since I am using AWS specifically API Gateway and Lambda I needed to add the "Binary Media Type" to API Gateway. To do this I opened Api Gateway console and choose my API. Then, selected Settings and added the following types.
I did a redeploy of my api and everything worked perfect, so in the end it was not a code problem but configuration mistake on AWS.
This blog post was very helpful:
AWS SAM Configuration for API Gateway Binary Response / Payloads
Thanks for your help everyone.

Vapor: how to not receive a particular upload?

In Vapor, how does one setup to check and decline an upload request prior to any part of such file being uploaded to the server?
My current attempt in Vapor 3 is with a route handler structured like:
func imagesUploadOneHandler(_ request: Request) throws -> EventLoopFuture<HTTPResponseStatus> {
let headers = request.http.headers
let headersUploadToken: [String] = headers["Upload-Token"]
if headersUploadToken.count != 1 || headersUploadToken[0] != aValidToken {
return HTTPResponseStatus.notAcceptable
}
// http body content type: 'application/octet-stream'
let dataFuture: EventLoopFuture<Data> = request.http.body.consumeData(max: 50_000_000, on: request)
let futureHTTPResponseStatus = dataFuture.map(to: HTTPResponseStatus.self, {
(data: Data) -> HTTPResponseStatus in
// ... other code
return HTTPResponseStatus.ok
})
return futureHTTPResponseStatus
}
Firstly, the above will not compile. The line return HTTPResponseStatus.notAcceptable has a compile time error "return HTTPResponseStatus.notAcceptable". How to convert HTTPResponseStatus to EventLoopFuture<HTTPResponseStatus> has been elusive.
Secondly, can some code prior to request.http.body.consumeData(...) in a route handler prevent an upload of the file content? Or, is some middleware needed instead to avoid uploading the data content (e.g. some large file) from the http.body?

File upload using Swift Vapor 3.0

I'm trying to create a simple vapor service so I can upload video files (one at a time) through an api.
From my app I'm uploading the video file using Alamofire:
func uploadVideo(video: URL) {
Alamofire.upload(videoFileURL, to: "http://localhost:8080/upload").responseString { response in
debugPrint(response)
}
}
The vapor controller method is like this (this is where I don't know how to do it):
func upload(_ req: Request) throws -> String {
let data = try req.content.decode(Data.self).map(to: Data.self) { video in
try Data(video).write(to: URL(fileURLWithPath: "/Users/eivindml/Desktop/video.mp4"))
return Data(video)
}
return "Video uploaded";
}
How do I get the video file from the request and into the right format so I can write it do disk?
The method upload() is called correctly etc, as it works if I just have the last return statement.
Looking at your function it appears you're not handling your future response correctly, or extracting the data.
func upload(_ req: Request) throws -> Future<String> {
return try req.content.decode(File.self).map(to: String.self) { (file) in
try file.data.write(to: URL(fileURLWithPath: "/Users/eivindml/Desktop/\(file.filename)"))
return "File uploaded"
}
}
See if this helps.

Pass an uploaded image directly from json file

I am trying to upload an image from my app to my heroku backend and then pass that to Stripe for verification. Here is my swift code to upload and pass the image:
#IBAction func registerAccountButtonWasPressed(sender: UIButton) {
let dob = self.dobTextField.text!.components(separatedBy: "/")
let URL = "https://splitterstripeservertest.herokuapp.com/account/create"
var imageData = UIImageJPEGRepresentation(photoID,1)
let params = ["year": UInt(dob[2])! as UInt] as [String : Any]
let manager = AFHTTPSessionManager()
let operation = manager.post(URL, parameters: params, constructingBodyWith: { (formData: AFMultipartFormData!) -> Void in
formData.appendPart(withFileData: imageData!, name: "file", fileName: "photoID.jpg", mimeType: "image/jpeg")
}, success: { (operation, responseObject) -> Void in
print(responseObject!)
}) { (operation, error) -> Void in
self.handleError(error as NSError)
}
}
I've deleted the list of params above out and left one for readability.
Is there a way to then receive this file and upload it to stripe without having to save it by passing the file parameter? like so:
Stripe::FileUpload.create(
{
:purpose => 'identity_document',
:file => params[file]
},
{:stripe_account => params[stripe_account]}
)
Also in the stripe docs it says to upload the file to 'https://uploads.stripe.com/v1/files' but then shows code to put in your backend, does Stripe::FileUpload.create do the uploading to stripe for me?
Any insight on either would be great thanks.
You need to first upload the file to Stripe's API using the "create file upload" call. You can then use the file upload's ID (file_...) to update the account's legal_entity.verification.document attribute.
(This process is explained here:
https://stripe.com/docs/connect/identity-verification#uploading-a-file
https://stripe.com/docs/connect/identity-verification#attaching-the-file)
Since the file is provided by the user, you have two choices for creating the file upload:
have your app upload the file to your backend server, then on your backend, use the file to create the file upload
create the file upload directly from the app (using your publishable API key), and send the resulting file_upload's ID (file_...) to your backend
Here's an example for creating file uploads client-side, using jQuery: https://jsfiddle.net/captainapollo/d8yd3761/.
You could do the same thing from your iOS app's code. Basically all you need to do is send a POST request to https://uploads.stripe.com/v1/files with an Authorization header with value Bearer pk_... (where pk_... is your publishable API key) and type multipart/form-data, with the file's contents as the request's body. This blog post should be helpful for sending multipart/form-data requests using Swift: http://www.kaleidosblog.com/how-to-upload-images-using-swift-2-send-multipart-post-request
Thanks to #Ywain I was able to come up with this solution for IOS Swift 4. I created the file upload directly from the app and retrieved the file_upload's ID to send to my backend to attach to the Connect Account. I did this by importing Alamofire and SwiftyJSON.
let heads = ["Authorization": "Bearer \(stripePublishableKey)"]
let imageData = image!.jpegData(compressionQuality: 1.0)
let fileName = "\(NSUUID().uuidString).jpeg"
Alamofire.upload(multipartFormData: { multipart in
multipart.append(imageData!, withName: "file", fileName: fileName, mimeType: "image/jpeg")
multipart.append(("identity_document").data(using: .utf8)!, withName :"purpose")
}, to: fileUrl!, method: .post, headers: heads) { encodingResult in
switch encodingResult {
case .success(let upload, _, _):
upload.responseJSON { answer in
let value = JSON(answer.value!)
let FileID = value["id"].stringValue
// Use fileID and Connect Account ID to create params to send to backend.
let params: [String: Any] = [
"acct_id": ConnectID!,
"fileID": FileID,
]
print("statusCode: \(String(describing: answer.response?.statusCode))")
}
case .failure(let encodingError):
print("multipart upload encodingError: \(encodingError)")
}
}