File add utilizing Vapor 4


Learn to implement a fundamental HTML file add kind utilizing the Leaf template engine and Vapor, all written in Swift after all.

Vapor

Constructing a file add kind

Let’s begin with a fundamental Vapor undertaking, we’ll use Leaf (the Tau launch) for rendering our HTML recordsdata. You must notice that Tau was an experimental launch, the adjustments had been reverted from the ultimate 4.0.0 Leaf launch, however you possibly can nonetheless use Tau when you pin the precise model in your manifest file. Tau can be printed afterward in a standalone repository… 🤫


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 when you open the undertaking with Xcode, do not forget to setup a customized working listing first, as a result of we’ll 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 possibly can place it into the Assets/Views listing.


<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta identify="viewport" content material="width=device-width, initial-scale=1">
    <title>File add instance</title>
  </head>
  <physique>
    <h1>File add instance</h1>

    <kind motion="/add" methodology="put up" enctype="multipart/form-data">
        <enter kind="file" identify="file"><br><br>
        <enter kind="submit" worth="Submit">
    </kind>
  </physique>
</html>


As you possibly can see, it is a regular file add kind, whenever you wish 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 area within the kind (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. Thankfully Vapor has built-in assist for simple decoding multipart kind 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 essential 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 {

    
    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.put up("add") { req in
        "Add file..."
    }
}

You may put the snippet above into your configure.swift file then you possibly can 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 at the least we’re ready to write down our server facet Swift code to course of the incoming kind knowledge. ⬆️



File add handler in Vapor

Now that we’ve a working uploader kind 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 check if everthing works through the use of the FileMiddleware. If you do not know, the file middleware serves every part (publicly accessible) that’s positioned inside your Public folder. Let’s code.

app.put up("add") { req -> EventLoopFuture<String> 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 kind that may comprise our file knowledge. There’s a File kind in Vapor that helps us decoding multipart file add kinds. We will use the content material of the request and decode this sort. We gave the file identify to the file enter kind beforehand in our leaf template, however after all you possibly can change it, however when you accomplish that you additionally need to align the property identify contained in the Enter struct.

After we’ve an enter (please notice 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 identify (to maintain the unique identify, however you possibly can 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 is a non-blocking API, so our server can be extra performant if we use this as an alternative 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 identify as a future string. If you have not heard about futures and guarantees it’s best to examine them, as a result of they’re in all places on the server facet Swift world. Cannot anticipate async / awake assist, proper? 😅

We’ll improve the add end result web page just a bit bit. Create a brand new end result.leaf file contained in the views listing.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta identify="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’ll test if the uploaded file has a picture extension and cross 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’ll render a easy hyperlink to view the file. Contained in the put up 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 identify.

app.put up("add") { req -> EventLoopFuture<View> 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: "end result", context: [
                            "fileUrl": .string(fileName),
                            "isImage": .bool(isImage),
                        ])
                    }
            }
    }

When you run this instance it’s best to be capable of view the picture or the file straight from the end result web page.



A number of file add utilizing Vapor

By the way in which, it’s also possible to add a number of recordsdata without delay when you add the a number of attribute to the HTML file enter area and use the recordsdata[] worth as identify.

<enter kind="file" identify="recordsdata[]" a number of><br><br>

To assist this we’ve to change our add methodology, don’t fret it isn’t that difficult because it appears at first sight. 😜

app.put up("add") { req -> EventLoopFuture<View> in
    struct Enter: Content material {
        var recordsdata: [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.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"].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 { recordsdata in
        req.leaf.render(template: "end result", context: [
            "files": .array(files.map(.leafData))
        ])
    }
}

The trick is that we’ve to parse the enter as an array of recordsdata and switch each doable add right into a future add operation. We will filter the add candidates by readable byte measurement, then we map the recordsdata into futures and return an UploadedFile end result with the correct file URL and is picture flag. This construction is a LeafDataRepresentable object, as a result of we wish to cross it as a context variable to our end result template. We even have to vary that view as soon as once more.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta identify="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>

Effectively, I do know it is a lifeless easy implementation, nevertheless it’s nice if you wish to follow or discover ways to implement file uploads utilizing server facet Swift and the Vapor framework. You may also add recordsdata on to a cloud service utilizing this system, there’s a library known as Liquid, which is analogous to Fluent, however for file storages. At present you should use Liquid to add recordsdata to the native storage or you should use an AWS S3 bucket or you possibly can write your individual driver utilizing LiquidKit. The API is fairly easy to make use of, after you configure the motive force you possibly can add recordsdata with only a few traces of code.

I hope you favored this tutorial, in case you have any questions or concepts, please let me know.




Leave a Reply