Learn to implement a fundamental HTML file add type utilizing the Leaf template engine and Vapor, all written in Swift in fact.
Constructing a file add type
Let’s begin with a fundamental Vapor mission, we’re going to make use of Leaf (the Tau launch) for rendering our HTML information. It is best to word that Tau was an experimental launch, the modifications have 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 might be printed afterward in a standalone repository… 🤫
// swift-tools-version:5.3
import PackageDescription
let bundle = Package deal(
identify: "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(identify: "Run", dependencies: [.target(name: "App")]),
.testTarget(identify: "AppTests", dependencies: [
.target(name: "App"),
.product(name: "XCTVapor", package: "vapor"),
])
]
)
Now in case you open the mission with Xcode, don’t neglect to setup a customized working listing first, as a result of we’re going to create templates and Leaf will search for these view information underneath 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 type, while you need to add information utilizing the browser you at all times have to make use of the multipart/form-data
encryption sort. The browser will pack each subject within the type (together with the file knowledge with the unique file identify and a few meta information) utilizing a particular format and the server software can parse the contents of this. Fortuitously Vapor has built-in assist for straightforward decoding multipart type knowledge values. We’re going to use the POST /add route to save lots of the file, let’s setup the router first so we will render our major 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: Utility) throws {
/// config max add file dimension
app.routes.defaultMaxBodySize = "10mb"
/// setup public file middleware (for internet hosting our uploaded information)
app.middleware.use(FileMiddleware(publicDirectory: app.listing.publicDirectory))
/// setup Leaf template engine
LeafRenderer.Choice.caching = .bypass
app.views.use(.leaf)
/// index route
app.get { req in
req.leaf.render(template: "index")
}
/// add handler
app.put up("add") { req in
"Add file..."
}
}
You’ll be able to 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 really add the file, however at the very least we’re ready to jot down our server aspect Swift code to course of the incoming type knowledge. ⬆️
File add handler in Vapor
Now that we’ve a working uploader type we should always parse the incoming knowledge, get the contents of the file and place it underneath our Public listing. You’ll be able to really transfer the file wherever in your server, however for this instance we’re going to use the Public listing so we will merely check if everthing works through the use of the FileMiddleware
. In case you don’t know, the file middleware serves all the things (publicly obtainable) that’s situated inside your Public folder. Let’s code.
app.put up("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.knowledge,
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 sort that may include our file knowledge. There’s a File sort in Vapor that helps us decoding multipart file add varieties. We will use the content material of the request and decode this sort. We gave the file identify to the file enter type beforehand in our leaf template, however in fact you’ll be able to change it, however in case you accomplish that you additionally need to align the property identify contained in the Enter struct.
After we’ve an enter (please word that we don’t validate the submitted request but) we will begin importing our file. We ask for the placement of the general public listing, we append the incoming file identify (to maintain the unique identify, however you’ll be able to generate a brand new identify 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 might be extra performant if we use this as a substitute of the common FileManager
from the Basis framework. After we opened the file, we write the file knowledge (which is a ByteBuffer
object, dangerous naming…) and eventually we shut the opened file handler and return the uploaded file identify as a future string. In case you haven’t heard about futures and guarantees it is best to examine them, as a result of they’re in all places on the server aspect Swift world. Can’t look forward to async / awake assist, proper? 😅
We are going to improve the add consequence web page just a bit bit. Create a brand new consequence.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 will show it if we will 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 put up add handler technique we’re going to add a date prefix to the uploaded file so we can add a number of information even with the identical identify.
app.put up("add") { req -> EventLoopFuture in
struct Enter: Content material {
var file: File
}
let enter = attempt req.content material.decode(Enter.self)
guard enter.file.knowledge.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"].comprises(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.knowledge,
eventLoop: req.eventLoop)
.flatMapThrowing { _ in
attempt deal with.shut()
}
.flatMap {
req.leaf.render(template: "consequence", context: [
"fileUrl": .string(fileName),
"isImage": .bool(isImage),
])
}
}
}
In case you run this instance it is best to be capable of view the picture or the file straight from the consequence web page.
A number of file add utilizing Vapor
By the way in which, you can even add a number of information without delay in case you add the a number of attribute to the HTML file enter subject and use the information[]
worth as identify.
To assist this we’ve to change our add technique, don’t fear it’s not that sophisticated because it appears to be like at first sight. 😜
app.put up("add") { req -> EventLoopFuture in
struct Enter: Content material {
var information: [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.information
.filter { $0.knowledge.readableBytes > 0 }
.map { file -> EventLoopFuture in
let fileName = prefix + file.filename
let path = app.listing.publicDirectory + fileName
let isImage = ["png", "jpeg", "jpg", "gif"].comprises(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.knowledge,
eventLoop: req.eventLoop)
.flatMapThrowing { _ in
attempt deal with.shut()
return UploadedFile(url: fileName, isImage: isImage)
}
}
}
return req.eventLoop.flatten(uploadFutures).flatMap { information in
req.leaf.render(template: "consequence", context: [
"files": .array(files.map(.leafData))
])
}
}
The trick is that we’ve to parse the enter as an array of information and switch each potential add right into a future add operation. We will filter the add candidates by readable byte dimension, then we map the information into futures and return an UploadedFile
consequence with the right file URL and is picture flag. This construction is a LeafDataRepresentable object, as a result of we need to go it as a context variable to our consequence template. We even have to vary that view as soon as once more.
Recordsdata uploaded
#for(file in information):
#if(file.isImage):
#else:
#(file.url)
#endif
#endfor
Add new information
Properly, I do know this can be a useless easy implementation, but it surely’s nice if you wish to follow or discover ways to implement file uploads utilizing server aspect Swift and the Vapor framework. You may also add information on to a cloud service utilizing this method, there’s a library referred to as Liquid, which has similarities to Fluent, however for file storages. At present you should utilize Liquid to add information to the native storage or you should utilize 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 information with just some strains of code.