Tutorial: How to implement Image-Upload
Be excited! Because at the end of this tutorial you’ll know how to upload an image for a user and save it to the disk as well as how to serve it back as a profile picture ✨ !
You can find the result of this tutorial on github here
This tutorial is a natural follow-up of How to write Controllers. You can either go for that tutorial first and come back later or be a rebel, skip it and read on 😊
Index
1. Create and generate a new project
2. Adjust Model: User
3. Create View: Upload
4. Adjust UserController: Add Upload-Routes
5. Create View: Profile
6. Adjust UserController: Add Profile-Route
7. Adjust UserController: Add Image-Route
1. Create and generate a new project
We will use the outcome of the aforementioned tutorial as a template to create our new project:
vapor new projectName --template=vaporberlin/my-first-controller --branch=vapor-2
Before we generate an Xcode project we would have to change the package name within Package.swift:
// swift-tools-version:4.0import PackageDescriptionlet package = Package(
name: "projectName", // changed
products: [
.library(name: "App", targets: ["App"]),
.executable(name: "Run", targets: ["Run"])
], dependencies: [
...
], targets: [
...
]
)
Now in the terminal at the root directory projectName/ execute:
vapor update -y
NOTE:
Sometimes you’d get an error like “backgroundExecute(code: 1, error..”
No worries - execute the command again, it sometime takes multiple attempts 😊
It may take a bit fetching the dependency, but when done you should have a project structure like this:
projectName/
├── Package.swift
├── Sources/
│ ├── App/
│ │ ├── Controllers/
│ │ │ └── UserController.swift
│ │ ├── Models/
│ │ │ └── User.swift
│ │ ├── Routes/
│ │ │ └── Routes.swift
│ │ └── Setup/
│ │ ├── Config+Setup.swift
│ │ └── Droplet+Setup.swift
│ └── Run/
├── Tests/
├── Config/
├── Resources/
├── Public/
├── Dependencies/
└── Products/
2. Adjust Model: User
The first thing we’re going to do is give our user a new property to be able to store the name to his profile image.
import Vapor
import FluentProviderfinal class User: Model {
var storage = Storage()
var username: String
var profileImage: String? init(username: String, profileImage: String? = nil) {
self.username = username
self.profileImage = profileImage
} func makeRow() throws -> Row {
var row = Row()
try row.set("username", username)
try row.set("profileImage", profileImage)
return row
} init(row: Row) throws {
self.username = try row.get("username")
self.profileImage = try row.get("profileImage")
}
}// MARK: Fluent Preparationextension User: Preparation { static func prepare(_ database: Database) throws {
try database.create(self) { builder in
builder.id()
builder.string("username")
builder.string("profileImage", optional: true)
}
} static func revert(_ database: Database) throws {
try database.delete(self)
}
}// MARK: Nodeextension User: NodeRepresentable { func makeNode(in context: Context?) throws -> Node {
var node = Node(context)
try node.set("id", id)
try node.set("username", username)
try node.set("profileImage", profileImage)
return node
}
}
That was easy and you may have seen / learned how to define optional database fields for the first time 😊
3. Create View: Upload
Let’s implement a view with a form that has a dropdown to select a user and a button to select an image to upload it for the selected user! So within Resources/Views/ create a new file with the name upload.leaf and add:
<!DOCTYPE html>
<html>
<head>
<title>Image-Upload</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="container">
<div class="row">
<div class="col-sm-6 col-sm-offset-3"> <h1 class="text-center">Image Upload</h1>
<div class="panel panel-primary">
<div class="panel-heading text-center">
Profile Picture
</div> <div class="panel-body">
<form action="/upload" method="POST" enctype="multipart/form-data" id="upload-form">
<div class="form-group">
<h4>User</h4>
<select class="form-control" name="userId" form="upload-form">
#loop(userlist, "user") {
<option value="#(user.id)">#(user.username)</option>
}
</select>
</div> <div class="form-group">
<input type="file" accept="image/png,image/jpg" name="image">
</div> <input class="btn btn-success btn-block" type="submit" value="Upload"> </form>
</div>
</div>
</div>
</div>
</body>
</html>
I have marked the important part here. I think the most interesting element is the input tag of type file. That is our button which allows us to select images from our disk. With accept we’re restricting the selection to .jpg and .png only. You can leave it empty in order to allow all files or use application/pdf for pdf only. Every element around the form are used to get a nicer look by using bootstrap classes 😊
4. Adjust UserController: Add Upload-Routes
The first route for returning our upload view is simple. Add the following within our Controllers/UserController.swift:
final class UserController {
... func list(_ req: Request) throws -> ResponseRepresentable {
...
} func create(_ req: Request) throws -> ResponseRepresentable {
...
} func getUpload(_ req: Request) throws -> ResponseRepresentable {
let list = try User.all()
return try drop.view.make("upload", ["userlist": list.makeNode(in: nil)])
}
}
And then go to Routes/Routes.swift and add the new route:
import Vaporextension Droplet {func setupRoutes() throws { let userController = UserController(drop: self)
get("register", handler: userController.getRegisterView)
post("register", handler: userController.postRegister)
get("upload", handler: userController.getUpload)
}
}
If you now cmd+r or run everything should built without any error and we should see our nice upload-form when opening /upload in our browser 😊 !
(The dropdown will be empty because you didn’t create a user under /user)
The next route we need to create is the post-route that will handle the data of our form when we submit it. This is the core of this tutorial so I will explain the code right afterwards. Within Controllers/UserController.swift add:
import Foundationfinal class UserController {
... func list(_ req: Request) throws -> ResponseRepresentable {
...
} func create(_ req: Request) throws -> ResponseRepresentable {
...
} func getUpload(_ req: Request) throws -> ResponseRepresentable {
...
} func postUpload(_ req: Request) throws -> ResponseRepresentable { guard
let userId = req.formData?["userId"]?.int,
let user = try User.find(userId),
let imageBytes = req.formData?["image"]?.bytes,
let filename = req.formData?["image"]?.filename
else {
return "whoops - something went wrong"
} /// path to directory for saving the image
let baseDir = URL(fileURLWithPath: self.drop.config.workDir).appendingPathComponent("images")
/// path to directory of user for saving the image
let userDir = baseDir.appendingPathComponent(user.username) let fileManager = FileManager() /// check whether directory already exists
if !fileManager.fileExists(atPath: userDir.path) { /// create directory and name it after the users username
try fileManager.createDirectory(at: userDir, withIntermediateDirectories: false, attributes: nil)
} let userDirWithImage = userDir.appendingPathComponent(filename) /// write image to directory
let data = Data(bytes: imageBytes)
fileManager.createFile(atPath: userDirWithImage.path, contents: data, attributes: nil) /// save image filename used as profile picture
user.profileImage = filename
try user.save() return Response(redirect: "/user/" + user.username)
}
}
Now let’s have a closer look. We added a new import. Yap! Easy to oversee but Foundation is needed 😊. Next to our function. The first guard-let statement tries to get all the information from the form we require for further operation. So we grab the userId and try to find our user by that. Next we grab the image bytes and also the filename. I love how easy it actually is!
We will store all our images within projectName/images/ in this tutorial, go and create that directory right now so we don’t forget it later 😊
Fortunately drop.config.workDir gives us the directory path of our project as a string so we just append “images” as another pathComponent. I thought it would be nice to create for each user a directory named after his username (which we can ensure to be unique in the create-user function) in order to keep things sorted and organized. That’s why we append the username as another pathComponent to our baseDir and store it in new variable userDir.
FileManager is a wonderful thing! We check whether a directory exists under the userDir path and if not, we create one.
NOTE: Since we work with an URL object - some functions of our FileManager accept that, some of them need a string. That’s why we use .path in some cases.
Next thing we append the filename to our userDir and store it in another variable userDirWithImage. The path will look similar to something like:
/Users/martinlasek/projects/projectName/images/harrypotter/wand.jpg
Here comes the magic. Next we initiate a Data-object with our image bytes. Then our FileManager creates that file at the given path we pass into with the data with also pass into. To me when I first saw a directory and a file created by a software I wrote I was amazed. It was a fantastic feeling! ✨
Okay so finally we assign the filename to our users property profileImage and save him to the database before redirecting to a route, yet to be created: /user/yourCoolUsername
Let’s add the route in Routes/Routes.swift that uses our postUpload:
import Vaporextension Droplet {func setupRoutes() throws {let userController = UserController(drop: self)
get("register", handler: userController.getRegisterView)
post("register", handler: userController.postRegister)
get("upload", handler: userController.getUpload)
post("upload", handler: userController.postUpload)
}
}
If you now cmd+r or run your project and go to /user to create a new user and then go to /upload to select this user and then select a picture and hit upload - you can go to your project with finder and look what happened inside your image/ directory ✨
5. Create View: Profile
Now it’s time to create a view in which we can see the profile image of a user! So within Resources/Views/ create a new file, name it profile.leaf and add:
<!DOCTYPE html>
<html>
<head>
<title>Profile</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="container">
<h1> Profile of #(user.username)</h1>
<div class="row">
<div class="col-xs-6 col-md-3">
<div class="thumbnail">
#if(hasImage) {
<img src="/profile-image/#(user.username)">
} ##else() {
<p> no image given for #(user.username)</p>
}
</div>
</div>
</div>
</body>
</html>
We are checking whether the variable hasImage that we will pass into the view in the next step is true and if so then please render an image from the source “/profile-image/myCoolUsername” which is a route that we will implement in step 7!
6. Adjust UserController: Add Profile-Route
All we need is a route returning the profile-view ultimately passing in some information. Therefor within our Controllers/UserController.swift add:
import Foundationfinal class UserController {
... func list(_ req: Request) throws -> ResponseRepresentable {
...
} func create(_ req: Request) throws -> ResponseRepresentable {
...
} func getUpload(_ req: Request) throws -> ResponseRepresentable {
...
} func postUpload(_ req: Request) throws -> ResponseRepresentable {
...
} func getProfile(_ req: Request) throws -> ResponseRepresentable { guard
let username = req.parameters["username"]?.string,
let user = try User.makeQuery().filter("username", username).first()
else {
return "couldn't find user"
} let hasImage = user.profileImage != nil return try self.drop.view.make("profile", ["hasImage": hasImage, "user": user.makeNode(in: nil)])
}
}
Let’s head right over to our Routes/Routes.swift and add that route:
import Vaporextension Droplet { func setupRoutes() throws { let userController = UserController(drop: self)
get("register", handler: userController.getRegisterView)
post("register", handler: userController.postRegister)
get("upload", handler: userController.getUpload)
get("user", ":username", handler: userController.getProfile)
}
}
Our function is grabbing the username from the url and trying to find a user by it. Then it stores the check whether the users profileImage is nil or not in hasImage, passes it with the user into our profile-view and returns the view.
If you now hit cmd+r or run and create a user under user/ and upload an image under upload/ you’ll be redirected to user/myCoolUsername/ where you won’t see an image, yet - let’s change that in the next step!
7. Adjust UserController: Add Image-Route
Here we are. Only you and me. And the code to be written to return a users image. Okay so within our Controllers/UserController.swift add:
import Foundationfinal class UserController {
... func list(_ req: Request) throws -> ResponseRepresentable {
...
} func create(_ req: Request) throws -> ResponseRepresentable {
...
} func getUpload(_ req: Request) throws -> ResponseRepresentable {
...
} func postUpload(_ req: Request) throws -> ResponseRepresentable {
...
} func getProfile(_ req: Request) throws -> ResponseRepresentable {
...
} func getProfileImage(_ req: Request) throws -> ResponseRepresentable { guard
let username = req.parameters["username"]?.string,
let user = try User.makeQuery().filter("username", username).first(),
let imageName = user.profileImage
else {
return "could'nt find user or user has no profile image"
} let imagePath = self.drop.config.workDir + "/images/" + user.username + "/" + imageName return try Response(filePath: imagePath)
}
}
What we do here is we once again grab the username from the url and try to find a user by it and then check whether he has a profileImage. If yes we can build the path to that image since we now how we organized our images and have it super simple to return it since vapor provides the right Response for it! Now to add the last route within out Routes/Routes.swift:
import Vaporextension Droplet { func setupRoutes() throws { let userController = UserController(drop: self)
get("register", handler: userController.getRegisterView)
post("register", handler: userController.postRegister)
get("upload", handler: userController.getUpload)
get("user", ":username", handler: userController.getProfile)
get("profile-image", ":username", handler: userController.getProfileImage)
}
}
A final cmd+r or run and that’s it! Create a new user under user/ and go to upload/ and upload a new image and you’re done! Yey! You have successfully implemented an image-upload 🌆✨ !!