Image Upload in Vapor 3 using PostgreSQL
This uses automatic decoding of the multi-part form:
router.get("upload") {
request -> Future<View> in
return try request.view().render("upload")
}
struct ExampleUpload: Content {
let document: File
}
// this saves the file into a Question
router.post(ExampleUpload.self, at:"upload") {
request, upload -> Future<HTTPResponseStatus> in
let question = try Question()
question.imageData = upload.document.data
question.imageName = upload.document.filename
return question.save(on:request).transform(to: HTTPResponseStatus.ok)
}
The upload.leaf
file is:
<form method="POST" enctype="multipart/form-data">
<input type="file" name="document" />
<input type="submit" value="Send" />
</form>
Using the type File
enables the local filename of the uploaded file to be accessed as well as the file data. If you add in the rest of the Question fields to the ExampleUpload structure, you can use the route to capture the whole form's fields.
How to upload files for a model using vapor 3 for an iOS app
Server-side
Model
final class AppObject: Codable {
var id: Int?
var ipaFile: String // relative path to file in Public dir
var plistFile: String // relative path to file in Public dir
var imageFile: String // relative path to file in Public dir
var notes: String
var name: String
init(ipaFile: String, plistFile: String, imageFile: String, notes: String, name: String) {
self.ipaFile = ipaFile
self.plistFile = plistFile
self.imageFile = imageFile
self.notes = notes
self.name = name
}
}
extension AppObject: MySQLModel {}
extension AppObject: Content {}
extension AppObject: Migration {}
extension AppObject: Parameter {}
Controller
struct AppObjectsController: RouteCollection {
func boot(router: Router) throws {
let appObjectsRoute = router.grouped("api", "apps")
appObjectsRoute.get(use: getAllHandler)
appObjectsRoute.post(PostData.self, use: createHandler)
}
func getAllHandler(_ req: Request) throws -> Future<[AppObject]> {
return AppObject.query(on: req).all()
}
}
extension AppObjectsController {
struct PostData: Content {
let ipaFile, plistFile, imageFile: File
let name, notes: String
}
func createHandler(_ req: Request, payload: PostData) throws -> Future<AppObject> {
let ipaFile = ServerFile(ext: "ipa", folder: .ipa)
let plistFile = ServerFile(ext: "plist", folder: .plist)
let imageFile = ServerFile(ext: "jpg", folder: .image)
let appObject = AppObject(ipaFile: ipaFile.relativePath, plistFile: plistFile.relativePath, imageFile: imageFile.relativePath, notes: payload.notes, name: payload.name)
/// we have to wrap it in transaction
/// to rollback object creating
/// in case if file saving fails
return req.transaction(on: .mysql) { conn in
return appObject.create(on: conn).map { appObject in
try ipaFile.save(with: payload.ipaFile.data)
try plistFile.save(with: payload.plistFile.data)
try imageFile.save(with: payload.imageFile.data)
}
}
}
}
ServerFile struct
struct ServerFile {
enum Folder: String {
case ipa = "ipa"
case plist = "plists"
case image = "images"
case root = ""
}
let file, ext: String
let folder: Folder
init (file: String? = UUID().uuidString, ext: String, folder: Folder? = .root) {
self.file = file
self.ext = ext
self.folder = folder
}
var relativePath: String {
guard folder != .root else { return fileWithExt }
return folder.rawValue + "/" + fileWithExt
}
var fileWithExt: String { return file + "." + ext }
func save(with data: Data) throws {
/// Get path to project's dir
let workDir = DirectoryConfig.detect().workDir
/// Build path to Public folder
let publicDir = workDir.appending("Public")
/// Build path to file folder
let fileFolder = publicDir + "/" + folder.rawValue
/// Create file folder if needed
var isDir : ObjCBool = true
if !FileManager.default.fileExists(atPath: fileFolder, isDirectory: &isDir) {
try FileManager.default.createDirectory(atPath: fileFolder, withIntermediateDirectories: true)
}
let filePath = publicDir + "/" + relativePath
/// Save data into file
try data.write(to: URL(fileURLWithPath: filePath))
}
}
iOS
Declare AppObject
model
struct AppObject: Codable {
var id: Int
var ipaFile, plistFile, imageFile: String
var name, notes: String
}
With CodyFire library multipart requests are really easy
Declare you endpoint
import CodyFire
struct AppController: EndpointController {
static var server: ServerURL? = nil
static var endpoint: String = "apps"
}
/// Usually separate file like App+Create.swift
extension AppController {
struct CreateAppRequest: MultipartPayload {
var ipaFile, plistFile, imageFile: Attachment
var name, note: String
public init (ipaFile: Attachment, plistFile: Attachment, imageFile: Attachment, name: String, note: String) {
self.ipaFile = ipaFile
self.plistFile = plistFile
self.imageFile = imageFile
self.name = name
self.note = note
}
}
static func create(_ payload: CreateAppRequest) -> APIRequest<AppObject> {
return request(payload: payload).method(.post)
}
}
Then in some view controller try to create an app on the server
/// Replace _ with file data
let ipaFile = Attachment(data: _, fileName: "", mimeType: "ipa")
let plistFile = Attachment(data: _, fileName: "", mimeType: "plist")
let imageFile = Attachment(data: _, fileName: "", mimeType: .jpg)
let payload = AppController.CreateAppRequest(ipaFile: ipaFile,
plistFile: plistFile,
imageFile: imageFile,
name: "something",
note: "something")
AppController.create(payload).onRequestStarted {
/// it calls only if request started properly
/// start showing loading bar
}.onError { error in
let alert = UIAlertController(title: nil, message: error.description, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .cancel))
self.present(alert, animated: true)
}.onProgress { progress in
/// show progress
}.onSuccess { appObject in
/// show success
/// here you received just created `appObject`
}
And that's it, it just works :)
Next example for getting list of AppObject
s
/// Separate file like App+List.swift
extension AppController {
static func list() -> APIRequest<[AppObject]> {
return request()
}
}
then somewhere in view controller
AppController.list().onSuccess { appObjects in
/// `appObjects` is `[AppObject]`
}
Hope it helps.
Creating and consuming a cursor with Vapor 3
Ok, so as you know, the problem lies in these lines:
while ... {
...
try connection.raw("...").all(decoding: RecRegAction.self).wait()
...
}
you want to wait for a number of results and therefore you use a while
loop and .wait()
for all the intermediate results. Essentially, this is turning asynchronous code into synchronous code on the event loop. That is likely leading to deadlocks and will for sure stall other connections which is why SwiftNIO tries to detect that and give you that error. I won't go into the details why it's stalling other connections or why this is likely to lead to deadlocks in this answer.
Let's see what options we have to fix this issue:
- as you say, we could just have this
.wait()
on another thread that isn't one of the event loop threads. For this any non-EventLoop
thread would do: Either aDispatchQueue
or you could use theBlockingIOThreadPool
(which does not run on anEventLoop
) - we could rewrite your code to be asynchronous
Both solutions will work but (1) is really not advisable as you would burn a whole (kernel) thread just to wait for the results. And both Dispatch
and BlockingIOThreadPool
have a finite number of threads they're willing to spawn so if you do that often enough you might run out of threads so it'll take even longer.
So let's look into how we can call an asynchronous function multiple times whilst accumulating the intermediate results. And then if we have accumulated all the intermediate results continue with all the results.
To make things easier let's look at a function that is very similar to yours. We assume this function to be provided just like in your code
/// delivers partial results (integers) and `nil` if no further elements are available
func deliverPartialResult() -> EventLoopFuture<Int?> {
...
}
what we would like now is a new function
func deliverFullResult() -> EventLoopFuture<[Int]>
please note how the deliverPartialResult
returns one integer each time and deliverFullResult
delivers an array of integers (ie. all the integers). Ok, so how do we write deliverFullResult
without calling deliverPartialResult().wait()
?
What about this:
func accumulateResults(eventLoop: EventLoop,
partialResultsSoFar: [Int],
getPartial: @escaping () -> EventLoopFuture<Int?>) -> EventLoopFuture<[Int]> {
// let's run getPartial once
return getPartial().then { partialResult in
// we got a partial result, let's check what it is
if let partialResult = partialResult {
// another intermediate results, let's accumulate and call getPartial again
return accumulateResults(eventLoop: eventLoop,
partialResultsSoFar: partialResultsSoFar + [partialResult],
getPartial: getPartial)
} else {
// we've got all the partial results, yay, let's fulfill the overall future
return eventLoop.newSucceededFuture(result: partialResultsSoFar)
}
}
}
Given accumulateResults
, implementing deliverFullResult
is not too hard anymore:
func deliverFullResult() -> EventLoopFuture<[Int]> {
return accumulateResults(eventLoop: myCurrentEventLoop,
partialResultsSoFar: [],
getPartial: deliverPartialResult)
}
But let's look more into what accumulateResults
does:
- it invokes
getPartial
once, then when it calls back it - checks if we have
- a partial result in which case we remember it alongside the other
partialResultsSoFar
and go back to (1) nil
which meanspartialResultsSoFar
is all we get and we return a new succeeded future with everything we have collected so far
- a partial result in which case we remember it alongside the other
that's already it really. What we did here is to turn the synchronous loop into asynchronous recursion.
Ok, we looked at a lot of code but how does this relate to your function now?
Believe it or not but this should actually work (untested):
accumulateResults(eventLoop: el, partialResultsSoFar: []) {
connection.raw("FETCH \(chunkSize) FROM \(cursorName)")
.all(decoding: RecRegAction.self)
.map { results -> Int? in
if results.count > 0 {
return results.count
} else {
return nil
}
}
}.map { allResults in
return allResults.reduce(0, +)
}
The result of all this will be an EventLoopFuture<Int>
which carries the sum of all the intermediate result.count
.
Sure, we first collect all your counts into an array to then sum it up (allResults.reduce(0, +)
) at the end which is a bit wasteful but also not the end of the world. I left it this way because that makes accumulateResults
be usable in other cases where you want to accumulate partial results in an array.
Now one last thing, a real accumulateResults
function would probably be generic over the element type and also we can eliminate the partialResultsSoFar
parameter for the outer function. What about this?
func accumulateResults<T>(eventLoop: EventLoop,
getPartial: @escaping () -> EventLoopFuture<T?>) -> EventLoopFuture<[T]> {
// this is an inner function just to hide it from the outside which carries the accumulator
func accumulateResults<T>(eventLoop: EventLoop,
partialResultsSoFar: [T] /* our accumulator */,
getPartial: @escaping () -> EventLoopFuture<T?>) -> EventLoopFuture<[T]> {
// let's run getPartial once
return getPartial().then { partialResult in
// we got a partial result, let's check what it is
if let partialResult = partialResult {
// another intermediate results, let's accumulate and call getPartial again
return accumulateResults(eventLoop: eventLoop,
partialResultsSoFar: partialResultsSoFar + [partialResult],
getPartial: getPartial)
} else {
// we've got all the partial results, yay, let's fulfill the overall future
return eventLoop.newSucceededFuture(result: partialResultsSoFar)
}
}
}
return accumulateResults(eventLoop: eventLoop, partialResultsSoFar: [], getPartial: getPartial)
}
EDIT: After your edit your question suggests that you do not actually want to accumulate the intermediate results. So my guess is that instead, you want to do some processing after every intermediate result has been received. If that's what you want to do, maybe try this:
func processPartialResults<T, V>(eventLoop: EventLoop,
process: @escaping (T) -> EventLoopFuture<V>,
getPartial: @escaping () -> EventLoopFuture<T?>) -> EventLoopFuture<V?> {
func processPartialResults<T, V>(eventLoop: EventLoop,
soFar: V?,
process: @escaping (T) -> EventLoopFuture<V>,
getPartial: @escaping () -> EventLoopFuture<T?>) -> EventLoopFuture<V?> {
// let's run getPartial once
return getPartial().then { partialResult in
// we got a partial result, let's check what it is
if let partialResult = partialResult {
// another intermediate results, let's call the process function and move on
return process(partialResult).then { v in
return processPartialResults(eventLoop: eventLoop, soFar: v, process: process, getPartial: getPartial)
}
} else {
// we've got all the partial results, yay, let's fulfill the overall future
return eventLoop.newSucceededFuture(result: soFar)
}
}
}
return processPartialResults(eventLoop: eventLoop, soFar: nil, process: process, getPartial: getPartial)
}
This will (as before) run getPartial
until it returns nil
but instead of accumulating all of getPartial
's results, it calls process
which gets the partial result and can do some further processing. The next getPartial
call will happen when the EventLoopFuture
process
returns is fulfilled.
Is that closer to what you would like?
Notes: I used SwiftNIO's EventLoopFuture
type here, in Vapor you would just use Future
instead but the remainder of the code should be the same.
Swift Vapor 3 + PostgreSQL + Docker-Compose Correct configuration?
The theory is, a well-behaved container should be able to gracefully handle not having its dependencies running, because despite the best efforts of your container scheduler, containers may come and go. So if your app needs a DB, but at any given moment the DB is unavailable, it should respond rationally. For example, returning a 503 for an HTTP request, or trying again after a delay for a scheduled task.
That’s theory though, and not always applicable. In your situation, maybe you really do just need your Vapor app to wait for Postgres to come available, in which case you could use a wrapper script that polls your DB and only starts your main app after the DB is ready.
See this suggested wrapper script from the Docker docs:
#!/bin/sh
# wait-for-postgres.sh
set -e
host="$1"
shift
cmd="$@"
until PGPASSWORD=$POSTGRES_PASSWORD psql -h "$host" -U "postgres" -c '\q'; do
>&2 echo "Postgres is unavailable - sleeping"
sleep 1
done
>&2 echo "Postgres is up - executing command"
exec $cmd
command: ["./wait-for-postgres.sh", "db", "vapor-app", "run"]
Related Topics
iOS Swift: Video Thumbnail Error
Alamofire, Objectmapper, Realm: Nested Objects
What Is the Backslash(\) Used for in Swiftui
What's the Difference Between a View and a Viewcontroller
Separation of Function Declaration and Definition in Swift
Typecasting or Initialization, Which Is Better in Swift
Subclass of Gkgraphnode Costtonode Method Never Getting Called
In Swift, How to Remove a Uiview from Memory Completely
At Runtime, How Does Swift Know Which Implementation to Use
What Are the Precedence Levels of the Swift Operators
Image Upload in Vapor 3 Using Postgresql
Detail View Is Not Updated When the Model Is Updated (Using List) Swiftui
How to Rotate Object in a Scene with Pan Gesture - Scenekit
Nsnumberformatter:Show 'K' Instead of ',000' in Large Numbers
Swift Why Strcmp of Backspace Returns -92
How to Have a Swift Protocol Without Functions