File upload using Swift Vapor 3.0 - swift

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.

Related

Combine - how to proceed to decode a local json file if online fetch failed?

I have an up-to-date json file hosted online and a local json file in my Xcode workspace. I would like to proceeed to decode a locally stored file if fetching failed: MyError.fetchError e.g. for no internet connection. This is the pipeline:
func fetchAndDecode<T: Decodable>(url: URL) -> AnyPublisher<T, MyError> {
fetchURL(url: url)
.decode(type: T.self, decoder: JSONDecoder())
.mapError { error in
if let error = error as? DecodingError {
return MyError.parsingError
} else {
return MyError.fetchError //here somehow proceed to parse local json file
}
}
.eraseToAnyPublisher()
}
How to achieve this ?
.mapError is the wrong operator because it considers only the Error branch.
fetchURL returns obviously Data, so before decoding the data you have to replace the fetch error with the local data.
Before the .decode... line insert
.replaceError(with: try! Data(contentsOf: Bundle.main.url(forResource: "local", withExtension: "json")!))
and delete the .mapError operator.
local.json represents the file name of the local file in the bundle.
I can propose an alternate but similar method to download the data and handle the error, using the async functions introduced for iOS 15.
Create a function that reads the data asynchronously and returns the data from the server if the connection worked, otherwise it will return the local JSON if a problem was found:
func getData(fromURL url: URL) async -> Data {
let request = URLRequest(url: url)
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
print("HTTP response: \(response.debugDescription)")
// Found an issue: return the local JSON
return localJSON
}
// If everything is OK, return the data from the server
return data
}
Decode the data returned:
// Use the code below in an asynchronous environment -
// either an async function or inside a Task { } closure
let data = await getData(fromURL: url)
do {
let decoded = try JSONDecoder().decode(T.self, from: data)
print("Decoded JSON: \(decoded)")
return decoded
} catch {
print("Error decoding JSON: \(error), \(error.localizedDescription)")
}

How to download all files from dropbox synchronously in swift?

Using SwiftyDropbox download method to download all files. But, i found progress block showing data all together of different files, not downloading files in a way that downloading of one file start after completing the previous one.
Here, is the code used :
DropboxClientsManager.authorizedClient?.files
.download(path: entry.pathLower!, overwrite: true, destination: destination)
.response { response, error in
if let response = response {
print(response)
}
else if let error = error {
print(error)
}
}
.progress { progressData in
print(progressData)
}

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

Vapor upload multiple files at once

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

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?