File Handling Tutorial for Server-Side Swift Part 1

In this two-part file handling tutorial, we’ll take a close look at Server-side Swift file handling and distribution by building a MadLibs clone. By Brian Schick.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 4 of 4 of this article. Click here to view the first page.

Writing a File Asynchronously

The last piece of your puzzle is to make your write operations asynchronous. Here, you hit a minor hiccup.

As it turns out, Apple initially released SwiftNIO without support for asynchronous write functionality. That's less surprising than it might seem since in production, you typically do any serious file management via APIs. You likely won't store large files on a production web server.

Nonetheless, SwiftNIO added this functionality last year.

At the time of writing, the major Swift frameworks have not yet added convenience wrappers for SwiftNIO's write functionality. When that happens, using a framework's wrapper will become a best practice.

Until then, one common solution is to use Grand Central Dispatch (GCD) to wrap FileManager's synchronous write functionality within an asynchronous DispatchQueue task. This ensures that write operations are non-blocking. Many production sites currently use this method, and you'll follow suit here.

Using Grand Central Dispatch

It's easy enough to wrap a synchronous method call within an asynchronous DispatchQueue task. The challenge is ensuring that the route handler waits for the task to complete and sees its result.

Open FileController.swift, and find writeFileAsync(named:with:on:queue:overwrite:). Replace its contents with:

// 1
guard overwrite || !fileExists(filename) else {
  return req.future(false)
}
// 2
let promise = req.eventLoop.newPromise(of: Bool.self)
// 3
queue.async {
  let result = fileManager.createFile(
    atPath: workingDir + filename, contents: data)
  // 4
  promise.succeed(result: result)
}
// 5
return promise.futureResult

Here's what you've done:

  1. As with the synchronous variant, you ensure you're not about to accidentally overwrite an existing file.
  2. You register a new promise on the Request object's EventLoop. This promise must either fail or be fulfilled before the route handler completes.
  3. You then call the synchronous createFile(atPath:contents:) within a DispatchQueue task, and dispatch it to execute asynchronously.
  4. Importantly, you bind the results of the call to the promise, and call its succeed promise method.
  5. Finally, you return the promise's futureResult, effectively binding the asynchronous DispatchQueue task to the route's EventLoop. This ensures that the method returns the Future-wrapped results to its calling context.

With this in place, open WebController.swift, and locate saveTemplate(from:on:) at the bottom of the file.

You'll update its signature to return a Future, and replace the current guard clause, which relies on synchronous writes, with code that works with asynchronous writes.

Replace the entire method with this:

private func saveTemplate(from data: UploadFormData, on req: Request) 
  throws -> Future<String> {
  // 1
  guard !data.title.isEmpty, !data.content.isEmpty 
    else { throw Abort(.badRequest) }
  // 2
  let title = data.title.trimmingCharacters(in: CharacterSet.whitespaces)
  let filename = String(title.compactMap({ $0.isWhitespace ? "_" : $0 }))
  let contentStr = data.content.trimmingCharacters(in: CharacterSet.whitespaces)

  guard let data = contentStr.data(using: .utf8) 
    else { throw Abort(.internalServerError) }

  // 3
  return try FileController
    .writeFileAsync(named: filename, with: data, on: req, queue: dispatchQueue)
    .map(to: String.self) { success in
      return success ? filename : ""
  }
}

With this code you:

  1. Ensure the title is not empty: If it is, throw a 400 bad request error.
  2. Build the content string by trimming unwanted characters.
  3. Asynchronously write the file data to disk, and return a Future.

And now your app handles both read and write functionality without blocking the main thread. Bring on the web-scale, baby!

Build and run, and open your browser to localhost:8080 one final time. You haven't changed any functionality, but you can enjoy the satisfaction of knowing that your app is now ready to read and write files and to scale gracefully without blocking!

Where to Go From Here

You can download the completed project files by clicking the Download Materials button at the top or bottom of the tutorial.

Be sure to check out Part 2 of this file handling tutorial when it's published. It focuses on how to serve up files from your Server-Side Swift app, including important nuances of doing this in the Linux and Docker environments, where most Server-Side Swift apps live.

If you're curious about SwiftNIO, Futures and Promises, check out Creating an API Helper Library for SwiftNIO.

Meanwhile, what do you think about reading and writing files in Server-Side Swift? Have you run into difficulties doing this, or have you found any helpful techniques you'd like to share? Let us know in the comments section, and Happy File-ing!