Home iOS & Swift Books Server-Side Swift with Vapor

26
Adding Profile Pictures Written by Tim Condon

In previous chapters, you learned how to send data to your Vapor application in POST requests. You used JSON bodies and forms to transmit the data, but the data was always simple text. In this chapter, you’ll learn how to send files in requests and handle that in your Vapor application. You’ll use this knowledge to allow users to upload profile pictures in the web application.

Note: This chapter teaches you how to upload files to the server where your Vapor application runs. For a real application, you should consider forwarding the file to a storage service, such as AWS S3. Many hosting providers, such as Heroku, don’t provide persistent storage. This means that you’ll lose your uploaded files when redeploying the application. You’ll also lose files if the hosting provider restarts your application. Additionally, uploading the files to the same server means you can’t scale your application to more than one instance because the files won’t exist across all application instances.

Adding a picture to the model

As in previous chapters, you need to change the model so you can associate an image with a User. Open the Vapor TIL application in Xcode and open User.swift. Add the following below var email: String:

@OptionalField(key: "profilePicture")
var profilePicture: String?

This stores an optional String for the image. It will contain the filename of the user’s profile picture on disk. The filename is optional as you’re not enforcing that a user has a profile picture — and they won’t have one when they register. Replace the initializer to account for the new property with the following:

init(
  name: String,
  username: String,
  password: String,
  siwaIdentifier: String? = nil,
  email: String,
  profilePicture: String? = nil
) {
  self.name = name
  self.username = username
  self.password = password
  self.siwaIdentifier = siwaIdentifier
  self.email = email
  self.profilePicture = profilePicture
}

Providing a default value of nil for profilePicture allows your app to continue to compile and operate without further source changes.

Note: You could use the user APIs from Google and GitHub to get a URL to the user’s profile picture. This would allow you to download the image and store it along side regular users’ pictures or save the link. However, this is left as an exercise for the reader.

You could make uploading a profile picture part of the registration experience, but this chapter does it in a separate step. Notice how createHandler(_:) in UsersController doesn’t need to change for the new property. This is because the route handler uses Codable and sets the property to nil if the data isn’t present in the POST request.

Next, open CreateUser.swift and below:

.field("email", .string, .required)`:

add the following:

.field("profilePicture", .string)

This adds a new column in the database for the profile picture. Note that you haven’t added the .required constraint as the property is optional.

Reset the database

As in the past, since you’ve added a property to User, you must reset the database. In Terminal, run:

docker rm -f postgres
docker rm -f postgres-test
docker run --name postgres -e POSTGRES_DB=vapor_database \
  -e POSTGRES_USER=vapor_username \
  -e POSTGRES_PASSWORD=vapor_password \
  -p 5432:5432 -d postgres
docker run --name postgres-test -e POSTGRES_DB=vapor-test \
  -e POSTGRES_USER=vapor_username \
  -e POSTGRES_PASSWORD=vapor_password \
  -p 5433:5432 -d postgres
docker ps -a

Verify the tests

In Xcode, type Command+U to run all the tests. They should all pass.

Creating the form

With the model changed, you can now create a page to allow users to submit a picture. In Xcode, open WebsiteController.swift. Next, add the following below resetPasswordPostHandler(_:data:):

func addProfilePictureHandler(_ req: Request) 
  -> EventLoopFuture<View> {
    User.find(req.parameters.get("userID"), on: req.db)
      .unwrap(or: Abort(.notFound)).flatMap { user in
        req.view.render(
          "addProfilePicture", 
          [
            "title": "Add Profile Picture", 
            "username": user.name
          ]
        )
    }
}
protectedRoutes.get(
  "users", 
  ":userID", 
  "addProfilePicture", 
  use: addProfilePictureHandler)
<!-- 1 -->
#extend("base"):
  <!-- 2 -->
  #export("content"):
    <!-- 3 -->
    <h1>#(title)</h1>
    
    <!-- 4 -->
    <form method="post" enctype="multipart/form-data">
      <!-- 5 -->
      <div class="form-group">
        <label for="picture">
            Select Picture for #(username)
        </label>
        <input type="file" name="picture"
        class="form-control-file" id="picture"/>
      </div>

      <!-- 6 -->
      <button type="submit" class="btn btn-primary">
        Upload
      </button>
    </form>
  #endexport
#endextend
let authenticatedUser: User?
// 1
let loggedInUser = req.auth.get(User.self)
// 2
let context = UserContext(
  title: user.name,
  user: user,
  acronyms: acronyms,
  authenticatedUser: loggedInUser)
#if(authenticatedUser):
  <a href="/users/#(user.id)/addProfilePicture">
    #if(user.profilePicture): 
      Update 
    #else: 
      Add 
    #endif 
    Profile Picture
  </a>
#endif

Accepting file uploads

Next, implement the necessary code to handle the POST request from the form. In Terminal, enter the following in the TILApp directory:

# 1
mkdir ProfilePictures
# 2
touch ProfilePictures/.keep
struct ImageUploadData: Content {
  var picture: Data
}
let imageFolder = "ProfilePictures/"
func addProfilePicturePostHandler(_ req: Request) 
  throws -> EventLoopFuture<Response> {
    // 1
    let data = try req.content.decode(ImageUploadData.self)
    // 2
    return User.find(req.parameters.get("userID"), on: req.db)
      .unwrap(or: Abort(.notFound))
      .flatMap { user in
        // 3
        let userID: UUID
        do {
          userID = try user.requireID()
        } catch {
          return req.eventLoop.future(error: error)
        }
        // 4
        let name = "\(userID)-\(UUID()).jpg"
        // 5
        let path = 
          req.application.directory.workingDirectory + 
            imageFolder + name
        // 6
        return req.fileio
          .writeFile(.init(data: data.picture), at: path)
          .flatMap {
            // 7
            user.profilePicture = name
            // 8
            let redirect = req.redirect(to: "/users/\(userID)")
            return user.save(on: req.db).transform(to: redirect)
        }
    }
}
protectedRoutes.on(
  .POST, 
  "users", 
  ":userID", 
  "addProfilePicture", 
  body: .collect(maxSize: "10mb"), 
  use: addProfilePicturePostHandler)

Displaying the picture

Now that a user can upload a profile picture, you need to be able to serve the image back to the browser. Normally, you would use the FileMiddleware. However, as you’re storing the images in a different directory, this chapter teaches you how to serve them manually.

func getUsersProfilePictureHandler(_ req: Request)
  -> EventLoopFuture<Response> {
    // 1
    User.find(req.parameters.get("userID"), on: req.db)
      .unwrap(or: Abort(.notFound))
      .flatMapThrowing { user in
      // 2
      guard let filename = user.profilePicture else {
        throw Abort(.notFound)
      }
      // 3
      let path = req.application.directory
        .workingDirectory + imageFolder + filename
      // 4
      return req.fileio.streamFile(at: path)
    }
}
authSessionsRoutes.get(
  "users", 
  ":userID", 
  "profilePicture", 
  use: getUsersProfilePictureHandler)
#if(user.profilePicture):
  <img src="/users/#(user.id)/profilePicture"
   alt="#(user.name)">
#endif

Where to go from here?

In this chapter, you learned how to deal with files in Vapor. You saw how to handle file uploads and save them to disk. You also learned how to serve files from disk in a route handler.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2021 Razeware LLC

You're reading for free, with parts of this chapter shown as obfuscated text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.