Vapor: how to not receive a particular upload? - swift

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?

Related

Code improvement - how to serve use pictures in Vapor?

I have a working directory that contains every user's picture and I am trying to implement a call that returns data containing the user's picture, defined in this structure:
struct ImageData: Content {
var picture: Data // UIImage data
}
I tried to implement a solution also partially using what I found in the book 'Server Side Swift with Vapor' (version 3) in chapter 26 but that's different for me because I am not using Leaf and I need to return the data directly.
I came up with this function to return the user picture, which does its job but I am trying to improve it.
func getProfilePictureHandler(_ req: Request) throws -> EventLoopFuture<ImageData> {
return User.find(req.parameters.get("userID"), on: req.db)
.unwrap(or: Abort(.notFound))
.flatMap { user in
// To do: throw error (flatMapThrowing?)
let filename = user.profilePicture!
let path = req.application.directory.workingDirectory
+ imageFolder
+ filename
// Improvement: Do I need this?
var data = Data()
return req.fileio.readFile(at: path) { buffer -> EventLoopFuture<Void> in
let additionalData = Data(buffer: buffer)
data.append(contentsOf: additionalData)
return req.eventLoop.makeSucceededVoidFuture()
}.map {
return ImageData(picture: data)
}
}
}
First:
How to implement this using flatMapThrowing? If I replace flatMap with flatMapThrowing I get this error: "Cannot convert return expression of type 'EventLoopFuture' to return type 'ImageData'". Which doesn't make sense to me considering that flatMap allows returning a future and not a value.
I didn't find any solution other than using a Data variable and appending chunks of data as more data is read. I am not sure that this is thread-safe, FIFO and I don't consider it an elegant solution. Does anybody know any better way of doing it?
The short answer is that as soon as you have the file path, Vapor can handle it all for you:
func getProfilePictureHandler(_ req: Request) throws -> EventLoopFuture<Response> {
return User.find(req.parameters.get("userID"), on: req.db)
.unwrap(or: Abort(.notFound))
.tryflatMap { user in
// To do: throw error (flatMapThrowing?)
guard let filename = user.profilePicture else {
throw Abort(.notFound)
}
let path = req.application.directory.workingDirectory
+ imageFolder
+ filename
return req.fileio.streamFile(at: path)
}
}
You can use tryFlatMap to have a flatMap that can throw and you want to return a Response. Manually messing around with Data is not usually a good idea.
However, the better answers are use async/await and the FileMiddleware as two tools to clean up your code and remove the handler altogether

Removing Swift RxAlamofire dependency

I'm trying to remove my dependency on RxAlamofire.
I currently have this function:
func requestData(_ urlRequest: URLRequestConvertible) -> Observable<(HTTPURLResponse, Data)> {
RxAlamofire.request(urlRequest).responseData()
}
How can I refactor this and use Alamofire directly to build and return an RxSwift Observable?
I suggest you look at the way the library wraps URLRequest to get an idea on how to do it...
Below is an abbreviated example from the library. In essence, you need to use Observable.create, make the network call passing in a closure that knows how to use the observer that create gives you.
Make sure you send a completed when done and make sure the disposable knows how to cancel the request.
Your Base will be something in Alamofire (I don't use Alamofire so I'm not sure what that might be.)
extension Reactive where Base: URLSession {
/**
Observable sequence of responses for URL request.
Performing of request starts after observer is subscribed and not after invoking this method.
**URL requests will be performed per subscribed observer.**
Any error during fetching of the response will cause observed sequence to terminate with error.
- parameter request: URL request.
- returns: Observable sequence of URL responses.
*/
public func response(request: URLRequest) -> Observable<(response: HTTPURLResponse, data: Data)> {
return Observable.create { observer in
let task = self.base.dataTask(with: request) { data, response, error in
guard let response = response, let data = data else {
observer.on(.error(error ?? RxCocoaURLError.unknown))
return
}
guard let httpResponse = response as? HTTPURLResponse else {
observer.on(.error(RxCocoaURLError.nonHTTPResponse(response: response)))
return
}
observer.on(.next((httpResponse, data)))
observer.on(.completed)
}
task.resume()
return Disposables.create(with: task.cancel)
}
}
}

Loading PDF Binary retreived from injected AJAX GET in WKWebView to PDFKit

I'm trying to intercept an ajax delivered pdf file from an authentication-secured website. Since ajax delivers the file, the URL does not contain a ".pdf" link for downloading but rather delivers the PDF file as content-disposition attachment.
As of now, when the decidePolicyFor navigationResponse delegate method detects an "application/pdf" mimetype, I call an evaluateJavaScript method with the following code:
func readUrlContent(url: NSURL, completionHandler: #escaping (_ result: String) -> Void) {
self.evaluateJavaScript("(function() { var result = ''; $.ajax({type: 'GET', url: '\(url)', contentType: 'application/pdf; charset=utf-8', success: function(r) {result = r}, failure: function() {result = null}, async: false }); return result })()", completionHandler: { (response, error) -> Void in
let result = response as! String
completionHandler(result)
})
}
With this injected code (which I use because I had issues with cookies and authentication on the website using WKWebView), I can successfully print the PDF Binary string returned as "result" in the completion handler to the log. The issue now is that I need to get this pdf binary data into a PDF document using PDFKit.
When I take this binary string and do:
self.data = result.data(using: .utf8)
self.performSegue(withIdentifier: "pdfSegue", sender: nil)
And pass the binary PDF string to a PDFKit ViewController to display it, all pages appear blank and I get the following errors:
"encountered unexpected object type: 7."
"missing or invalid object number."
"missing or invalid cross-reference stream."
"invalid stream length 3555; should be 6444."
"FlateDecode: decoding error: incorrect header check."
etc. etc. etc.
How/can I get this PDF binary data returned and view it using PDFKit?

Concise method of updating a model to match a request body with Swift?

Given a controller method which accepts a request body which conforms to some or all of the properties in an entity in Vapor, is there a means of updating the entity without manually assigning all of it's properties? Currently, I'm having to do this:
func update(_ req: Request) throws -> Future<Mission> {
let mission = try req.parameters.next(Mission.self)
let content = try req.content.decode(Mission.self)
return flatMap(to: Mission.self, mission, content) { (mission, content) in
mission.propertyA = content.propertyA
mission.propB = content.propB
mission.propC = content.propC
return mission.save(on: req)
}
}
This isn't very scalable as it requires me to manually assign each property. What I'm looking for is something like this:
func update(_ req: Request) throws -> Future<Mission> {
let mission = try req.parameters.next(Mission.self)
let content = try req.content.decode(Mission.self)
return mission.save(on: content)
}
However this yields the error Argument type 'EventLoopFuture<Mission>' does not conform to expected type 'DatabaseConnectable'.
What is a good solution here?
With Submissions you should be able to do:
func create(req: Request) throws -> Future<Either<Mission, SubmissionValidationError>> {
return try req.content.decode(Mission.Submission.self)
.updateValid(on: req)
.save(on: req)
.promoteErrors()
}
It takes some setting up but it's flexible and allows you to validate your input. The promoteErrors function + Either in the result help create useful error response but you could do without them.
That error that you're receiving is because you're trying to save(on: content), you need to save on the request:
return mission.save(on: req)
That being said, what you really want is this:
func update(_ req: Request) throws -> Future<Mission> {
let updatedMission = try req.content.decode(Mission.self)
return updatedMission.update(on: req)
}
This decodes a Mission object that is in the request body and then updates the Mission with the corresponding id in the database. So make sure when you're sending Mission JSON in the body that it has an id.

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.