Learn to implement a fundamental HTML file add type utilizing the Leaf template engine and Vapor, all written in Swift in fact.
Vapor
Constructing a file add type
Let’s begin with a fundamental Vapor venture, we will use Leaf (the Tau launch) for rendering our HTML recordsdata. It is best to word that Tau was an experimental launch, the adjustments have been reverted from the ultimate 4.0.0 Leaf launch, however you may nonetheless use Tau when you pin the precise model in your manifest file. Tau shall be revealed in a while in a standalone repository… 🤫
import PackageDescription
let package deal = Package deal(
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 when you open the venture with Xcode, do not forget to setup a customized working listing first, as a result of we will create templates and Leaf will search for these view recordsdata underneath the present working listing by default. We’re going to construct a quite simple index.leaf file, you may place it into the Assets/Views listing.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta title="viewport" content material="width=device-width, initial-scale=1">
<title>File add instance</title>
</head>
<physique>
<h1>File add instance</h1>
<type motion="/add" methodology="submit" enctype="multipart/form-data">
<enter kind="file" title="file"><br><br>
<enter kind="submit" worth="Submit">
</type>
</physique>
</html>
As you may see, it is a normal file add type, whenever you need 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 subject within the type (together with the file knowledge with the unique file title and a few meta data) utilizing a particular format and the server software can parse the contents of this. Luckily Vapor has built-in help for simple decoding multipart type knowledge values. We’re going to use the POST /add route to avoid wasting the file, let’s setup the router first so we will render our important web page and we’re going to put together our add path as properly, however we’ll reply with a dummy message for now.
import Vapor
import Leaf
public func configure(_ app: Utility) throws {
app.routes.defaultMaxBodySize = "10mb"
app.middleware.use(FileMiddleware(publicDirectory: app.listing.publicDirectory))
LeafRenderer.Possibility.caching = .bypass
app.views.use(.leaf)
app.get { req in
req.leaf.render(template: "index")
}
app.submit("add") { req in
"Add file..."
}
}
You may put the snippet above into your configure.swift file then you may attempt to construct and run your server and go to http://localhost:8080, then attempt to add any file. It will not truly add the file, however no less than we’re ready to put in writing 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 must always parse the incoming knowledge, get the contents of the file and place it underneath our Public listing. You may truly transfer the file wherever in your server, however for this instance we’re going to use the Public listing so we will merely take a look at if everthing works by utilizing the FileMiddleware. If you do not know, the file middleware serves all the things (publicly accessible) that’s situated inside your Public folder. Let’s code.
app.submit("add") { req -> EventLoopFuture<String> in
struct Enter: Content material {
var file: File
}
let enter = strive 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
strive 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 can comprise our file knowledge. There’s a File kind in Vapor that helps us decoding multipart file add varieties. We are able to use the content material of the request and decode this kind. We gave the file title to the file enter type beforehand in our leaf template, however in fact you may change it, however when you achieve this you additionally must align the property title contained in the Enter struct.
After we’ve an enter (please word that we do not 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 title (to maintain the unique title, however you may 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 is a non-blocking API, so our server shall 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, unhealthy naming…) and at last we shut the opened file handler and return the uploaded file title as a future string. If you have not heard about futures and guarantees you must examine them, as a result of they’re in every single place on the server aspect Swift world. Cannot await async / awake help, proper? 😅
We are going to improve the add outcome web page just a bit bit. Create a brand new outcome.leaf file contained in the views listing.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta title="viewport" content material="width=device-width, initial-scale=1">
<title>File uploaded</title>
</head>
<physique>
<h1>File uploaded</h1>
#if(isImage):
<img src="#(fileUrl)" width="256px"><br><br>
#else:
<a href="#(fileUrl)" goal="_blank">Present me!</a><br><br>
#endif
<a href="/">Add new one</a>
</physique>
</html>
So we will examine 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 will render a easy hyperlink to view the file. Contained in the submit add handler methodology we’re going to add a date prefix to the uploaded file so we can add a number of recordsdata even with the identical title.
app.submit("add") { req -> EventLoopFuture<View> in
struct Enter: Content material {
var file: File
}
let enter = strive 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"].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.knowledge,
eventLoop: req.eventLoop)
.flatMapThrowing { _ in
strive deal with.shut()
}
.flatMap {
req.leaf.render(template: "outcome", context: [
"fileUrl": .string(fileName),
"isImage": .bool(isImage),
])
}
}
}
In case you run this instance you must have the ability to view the picture or the file straight from the outcome web page.
A number of file add utilizing Vapor
By the way in which, you can even add a number of recordsdata directly when you add the a number of attribute to the HTML file enter subject and use the recordsdata[] worth as title.
<enter kind="file" title="recordsdata[]" a number of><br><br>
To help this we’ve to change our add methodology, don’t fret it isn’t that difficult because it appears to be like at first sight. 😜
app.submit("add") { req -> EventLoopFuture<View> in
struct Enter: Content material {
var recordsdata: [File]
}
let enter = strive 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.knowledge.readableBytes > 0 }
.map { file -> EventLoopFuture<UploadedFile> 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.knowledge,
eventLoop: req.eventLoop)
.flatMapThrowing { _ in
strive deal with.shut()
return UploadedFile(url: fileName, isImage: isImage)
}
}
}
return req.eventLoop.flatten(uploadFutures).flatMap { recordsdata in
req.leaf.render(template: "outcome", context: [
"files": .array(files.map(.leafData))
])
}
}
The trick is that we’ve to parse the enter as an array of recordsdata and switch each attainable 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 outcome 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 outcome template. We even have to alter that view as soon as once more.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta title="viewport" content material="width=device-width, initial-scale=1">
<title>Information uploaded</title>
</head>
<physique>
<h1>Information uploaded</h1>
#for(file in recordsdata):
#if(file.isImage):
<img src="#(file.url)" width="256px"><br><br>
#else:
<a href="#(file.url)" goal="_blank">#(file.url)</a><br><br>
#endif
#endfor
<a href="/">Add new recordsdata</a>
</physique>
</html>
Properly, I do know this can be a lifeless easy implementation, however it’s nice if you wish to observe or learn to implement file uploads utilizing server aspect Swift and the Vapor framework. You too can add recordsdata on to a cloud service utilizing this system, there’s a library referred to as Liquid, which has similarities to Fluent, however for file storages. At present you need to use Liquid to add recordsdata to the native storage or you need to use an AWS S3 bucket or you may write your personal driver utilizing LiquidKit. The API is fairly easy to make use of, after you configure the motive force you may add recordsdata with just some traces of code.
I hope you preferred this tutorial, when you have any questions or concepts, please let me know.