Discover ways to implement a fundamental HTML file add kind utilizing the Leaf template engine and Vapor, all written in Swift after all.
Constructing a file add kind
Let’s begin with a fundamental Vapor challenge, we’re going to make use of Leaf (the Tau launch) for rendering our HTML recordsdata. It’s best to notice that Tau was an experimental launch, the adjustments had been reverted from the ultimate 4.0.0 Leaf launch, however you’ll be able to nonetheless use Tau in case you pin the precise model in your manifest file. Tau will likely be printed in a while in a standalone repository… 🤫
// swift-tools-version:5.3
import PackageDescription
let package deal = Bundle(
title: "myProject",
platforms: [
.macOS(.v10_15)
],
dependencies: [
.package(url: "https://github.com/vapor/vapor", from: "4.35.0"),
.package(url: "https://github.com/vapor/leaf", .exact("4.0.0-tau.1")),
.package(url: "https://github.com/vapor/leaf-kit", .exact("1.0.0-tau.1.1")),
],
targets: [
.target(
name: "App",
dependencies: [
.product(name: "Leaf", package: "leaf"),
.product(name: "LeafKit", package: "leaf-kit"),
.product(name: "Vapor", package: "vapor"),
],
swiftSettings: [
.unsafeFlags(["-cross-module-optimization"], .when(configuration: .launch))
]
),
.goal(title: "Run", dependencies: [.target(name: "App")]),
.testTarget(title: "AppTests", dependencies: [
.target(name: "App"),
.product(name: "XCTVapor", package: "vapor"),
])
]
)
Now in case you open the challenge with Xcode, don’t overlook to setup a customized working listing first, as a result of we’re going to create templates and Leaf will search for these view recordsdata below the present working listing by default. We’re going to construct a quite simple index.leaf
file, you’ll be able to place it into the Assets/Views
listing.
File add instance
As you’ll be able to see, it’s a regular file add kind, if you wish to add recordsdata utilizing the browser you all the time have to make use of the multipart/form-data
encryption kind. The browser will pack each area within the kind (together with the file information with the unique file title and a few meta data) utilizing a particular format and the server software can parse the contents of this. Fortuitously Vapor has built-in assist for straightforward decoding multipart kind information values. We’re going to use the POST /add route to avoid wasting the file, let’s setup the router first so we are able to render our foremost web page and we’re going to put together our add path as properly, however we are going to reply with a dummy message for now.
import Vapor
import Leaf
public func configure(_ app: Software) throws {
/// config max add file measurement
app.routes.defaultMaxBodySize = "10mb"
/// setup public file middleware (for internet hosting our uploaded recordsdata)
app.middleware.use(FileMiddleware(publicDirectory: app.listing.publicDirectory))
/// setup Leaf template engine
LeafRenderer.Possibility.caching = .bypass
app.views.use(.leaf)
/// index route
app.get { req in
req.leaf.render(template: "index")
}
/// add handler
app.publish("add") { req in
"Add file..."
}
}
You possibly can put the snippet above into your configure.swift file then you’ll be able to attempt to construct and run your server and go to http://localhost:8080
, then attempt to add any file. It gained’t truly add the file, however at the least we’re ready to jot down our server aspect Swift code to course of the incoming kind information. ⬆️
File add handler in Vapor
Now that we’ve got a working uploader kind we must always parse the incoming information, get the contents of the file and place it below our Public listing. You possibly can truly transfer the file wherever in your server, however for this instance we’re going to use the Public listing so we are able to merely take a look at if everthing works by utilizing the FileMiddleware
. When you don’t know, the file middleware serves all the things (publicly out there) that’s situated inside your Public folder. Let’s code.
app.publish("add") { req -> EventLoopFuture in
struct Enter: Content material {
var file: File
}
let enter = attempt req.content material.decode(Enter.self)
let path = app.listing.publicDirectory + enter.file.filename
return req.software.fileio.openFile(path: path,
mode: .write,
flags: .allowFileCreation(posixMode: 0x744),
eventLoop: req.eventLoop)
.flatMap { deal with in
req.software.fileio.write(fileHandle: deal with,
buffer: enter.file.information,
eventLoop: req.eventLoop)
.flatMapThrowing { _ in
attempt deal with.shut()
return enter.file.filename
}
}
}
So, let me clarify what simply occurred right here. First we outline a brand new Enter kind that may comprise our file information. There’s a File kind in Vapor that helps us decoding multipart file add kinds. We are able to use the content material of the request and decode this sort. We gave the file title to the file enter kind beforehand in our leaf template, however after all you’ll be able to change it, however in case you achieve this you additionally need to align the property title contained in the Enter struct.
After we’ve got an enter (please notice that we don’t validate the submitted request but) we are able to begin importing our file. We ask for the placement of the general public listing, we append the incoming file title (to maintain the unique title, however you’ll be able to generate a brand new title for the uploaded file as properly) and we use the non-blocking file I/O API to create a file handler and write the contents of the file into the disk. The fileio API is a part of SwiftNIO, which is nice as a result of it’s a non-blocking API, so our server will likely be extra performant if we use this as an alternative of the common FileManager
from the Basis framework. After we opened the file, we write the file information (which is a ByteBuffer
object, dangerous naming…) and eventually we shut the opened file handler and return the uploaded file title as a future string. When you haven’t heard about futures and guarantees you need to examine them, as a result of they’re in every single place on the server aspect Swift world. Can’t look ahead to async / awake assist, proper? 😅
We are going to improve the add end result web page just a bit bit. Create a brand new end result.leaf
file contained in the views listing.
File uploaded
#if(isImage):
#else:
Present me!
#endif
Add new one
So we’re going to verify if the uploaded file has a picture extension and go an isImage
parameter to the template engine, so we are able to show it if we are able to assume that the file is a picture, in any other case we’re going to render a easy hyperlink to view the file. Contained in the publish add handler technique we’re going to add a date prefix to the uploaded file so we will add a number of recordsdata even with the identical title.
app.publish("add") { req -> EventLoopFuture in
struct Enter: Content material {
var file: File
}
let enter = attempt req.content material.decode(Enter.self)
guard enter.file.information.readableBytes > 0 else {
throw Abort(.badRequest)
}
let formatter = DateFormatter()
formatter.dateFormat = "y-m-d-HH-MM-SS-"
let prefix = formatter.string(from: .init())
let fileName = prefix + enter.file.filename
let path = app.listing.publicDirectory + fileName
let isImage = ["png", "jpeg", "jpg", "gif"].incorporates(enter.file.extension?.lowercased())
return req.software.fileio.openFile(path: path,
mode: .write,
flags: .allowFileCreation(posixMode: 0x744),
eventLoop: req.eventLoop)
.flatMap { deal with in
req.software.fileio.write(fileHandle: deal with,
buffer: enter.file.information,
eventLoop: req.eventLoop)
.flatMapThrowing { _ in
attempt deal with.shut()
}
.flatMap {
req.leaf.render(template: "end result", context: [
"fileUrl": .string(fileName),
"isImage": .bool(isImage),
])
}
}
}
When you run this instance you need to have the ability to view the picture or the file straight from the end result web page.
A number of file add utilizing Vapor
By the best way, you can even add a number of recordsdata without delay in case you add the a number of attribute to the HTML file enter area and use the recordsdata[]
worth as title.
To assist this we’ve got to change our add technique, don’t fear it’s not that difficult because it appears to be like at first sight. 😜
app.publish("add") { req -> EventLoopFuture in
struct Enter: Content material {
var recordsdata: [File]
}
let enter = attempt req.content material.decode(Enter.self)
let formatter = DateFormatter()
formatter.dateFormat = "y-m-d-HH-MM-SS-"
let prefix = formatter.string(from: .init())
struct UploadedFile: LeafDataRepresentable {
let url: String
let isImage: Bool
var leafData: LeafData {
.dictionary([
"url": url,
"isImage": isImage,
])
}
}
let uploadFutures = enter.recordsdata
.filter { $0.information.readableBytes > 0 }
.map { file -> EventLoopFuture in
let fileName = prefix + file.filename
let path = app.listing.publicDirectory + fileName
let isImage = ["png", "jpeg", "jpg", "gif"].incorporates(file.extension?.lowercased())
return req.software.fileio.openFile(path: path,
mode: .write,
flags: .allowFileCreation(posixMode: 0x744),
eventLoop: req.eventLoop)
.flatMap { deal with in
req.software.fileio.write(fileHandle: deal with,
buffer: file.information,
eventLoop: req.eventLoop)
.flatMapThrowing { _ in
attempt deal with.shut()
return UploadedFile(url: fileName, isImage: isImage)
}
}
}
return req.eventLoop.flatten(uploadFutures).flatMap { recordsdata in
req.leaf.render(template: "end result", context: [
"files": .array(files.map(.leafData))
])
}
}
The trick is that we’ve got to parse the enter as an array of recordsdata and switch each doable add right into a future add operation. We are able to filter the add candidates by readable byte measurement, then we map the recordsdata into futures and return an UploadedFile
end result with the correct file URL and is picture flag. This construction is a LeafDataRepresentable object, as a result of we wish to go it as a context variable to our end result template. We even have to alter that view as soon as once more.
Information uploaded
#for(file in recordsdata):
#if(file.isImage):
#else:
#(file.url)
#endif
#endfor
Add new recordsdata
Nicely, I do know it is a lifeless easy implementation, but it surely’s nice if you wish to follow or learn to implement file uploads utilizing server aspect Swift and the Vapor framework. You can even add recordsdata on to a cloud service utilizing this method, there’s a library referred to as Liquid, which is analogous to Fluent, however for file storages. At the moment you should use Liquid to add recordsdata to the native storage or you should use an AWS S3 bucket or you’ll be able to write your individual driver utilizing LiquidKit. The API is fairly easy to make use of, after you configure the driving force you’ll be able to add recordsdata with only a few traces of code.