/S3

S3 Client written in Swift

Primary LanguageSwiftMIT LicenseMIT

Build Status

S3 client for Vapor 3

Functionality

  • Signing headers for any region
  • Listing buckets
  • Create bucket
  • Delete bucket
  • Locate bucket region
  • List objects
  • Upload file
  • Get file
  • Delete file
  • Copy file
  • Move file (copy then delete old one)
  • Object info (HEAD)
  • Object info (ACL)
  • Parsing error responses

Usage

Update dependencies and targets in Package.swift

dependencies: [
    ...
    .package(url: "https://github.com/LiveUI/S3.git", from: "3.0.0-RC3.2"),
],
targets: [
        .target(name: "App", dependencies: ["Vapor", "S3"]),
        ...
]

Run vapor update

Register S3Client as a service in your configure method

try services.register(s3: S3Signer.Config(...), defaultBucket: "my-bucket")

to use a custom Minio server, use this Config/Region:

S3Signer.Config(accessKey: accessKey,
                secretKey: secretKey,
                region: Region(name: RegionName.usEast1,
                               hostName: "127.0.0.1:9000",
                               useTLS: false)

use S3Client

import S3

let s3 = try req.makeS3Client() // or req.make(S3Client.self) as? S3
s3.put(...)
s3.get(...)
s3.delete(...)

if you only want to use the signer

import S3Signer

let s3 = try req.makeS3Signer() // or req.make(S3Signer.self)
s3.headers(...)

Available methods

/// S3 client Protocol
public protocol S3Client: Service {
    
    /// Get list of objects
    func buckets(on: Container) -> EventLoopFuture<BucketsInfo>
    
    /// Create a bucket
    func create(bucket: String, region: Region?, on container: Container) -> EventLoopFuture<Void>
    
    /// Delete a bucket
    func delete(bucket: String, region: Region?, on container: Container) -> EventLoopFuture<Void>
    
    /// Get bucket location
    func location(bucket: String, on container: Container) -> EventLoopFuture<Region>
    
    /// Get list of objects
    func list(bucket: String, region: Region?, on container: Container) -> EventLoopFuture<BucketResults>
    
    /// Get list of objects
    func list(bucket: String, region: Region?, headers: [String: String], on container: Container) -> EventLoopFuture<BucketResults>
    
    /// Upload file to S3
    func put(file: File.Upload, headers: [String: String], on: Container) throws -> EventLoopEventLoopFuture<File.Response>
    
    /// Upload file to S3
    func put(file url: URL, destination: String, access: AccessControlList, on: Container) -> EventLoopFuture<File.Response>
    
    /// Upload file to S3
    func put(file url: URL, destination: String, bucket: String?, access: AccessControlList, on: Container) -> EventLoopFuture<File.Response>
    
    /// Upload file to S3
    func put(file path: String, destination: String, access: AccessControlList, on: Container) -> EventLoopFuture<File.Response>
    
    /// Upload file to S3
    func put(file path: String, destination: String, bucket: String?, access: AccessControlList, on: Container) -> EventLoopFuture<File.Response>
    
    /// Upload file to S3
    func put(string: String, destination: String, on: Container) -> EventLoopFuture<File.Response>
    
    /// Upload file to S3
    func put(string: String, destination: String, access: AccessControlList, on: Container) -> EventLoopFuture<File.Response>
    
    /// Upload file to S3
    func put(string: String, mime: MediaType, destination: String, on: Container) -> EventLoopFuture<File.Response>
    
    /// Upload file to S3
    func put(string: String, mime: MediaType, destination: String, access: AccessControlList, on: Container) -> EventLoopFuture<File.Response>
    
    /// Upload file to S3
    func put(string: String, mime: MediaType, destination: String, bucket: String?, access: AccessControlList, on: Container) -> EventLoopFuture<File.Response>
    
    /// Retrieve file data from S3
    func get(fileInfo file: LocationConvertible, on container: Container) -> EventLoopFuture<File.Info>
    
    /// Retrieve file data from S3
    func get(fileInfo file: LocationConvertible, headers: [String: String], on container: Container) -> EventLoopFuture<File.Info>
    
    /// Retrieve file data from S3
    func get(file: LocationConvertible, on: Container) -> EventLoopFuture<File.Response>
    
    /// Retrieve file data from S3
    func get(file: LocationConvertible, headers: [String: String], on: Container) -> EventLoopFuture<File.Response>
    
    /// Delete file from S3
    func delete(file: LocationConvertible, on: Container) -> EventLoopFuture<Void>
    
    /// Delete file from S3
    func delete(file: LocationConvertible, headers: [String: String], on: Container) -> EventLoopFuture<Void>
}

Example usage

public func routes(_ router: Router) throws {
    
    // Get all available buckets
    router.get("buckets")  { req -> EventLoopFuture<BucketsInfo> in
        let s3 = try req.makeS3Client()
        return try s3.buckets(on: req)
    }
    
    // Create new bucket
    router.put("bucket")  { req -> EventLoopFuture<String> in
        let s3 = try req.makeS3Client()
        return try s3.create(bucket: "api-created-bucket", region: .euCentral1, on: req).map(to: String.self) {
            return ":)"
            }.catchMap({ (error) -> (String) in
                if let error = error.s3ErrorMessage() {
                    return error.message
                }
                return ":("
            }
        )
    }
    
    // Locate bucket (get region)
    router.get("bucket/location")  { req -> EventLoopFuture<String> in
        let s3 = try req.makeS3Client()
        return try s3.location(bucket: "bucket-name", on: req).map(to: String.self) { region in
            return region.hostUrlString()
        }.catchMap({ (error) -> (String) in
                if let error = error as? S3.Error {
                    switch error {
                    case .errorResponse(_, let error):
                        return error.message
                    default:
                        return "S3 :("
                    }
                }
                return ":("
            }
        )
    }
    // Delete bucket
    router.delete("bucket")  { req -> EventLoopFuture<String> in
        let s3 = try req.makeS3Client()
        return try s3.delete(bucket: "api-created-bucket", region: .euCentral1, on: req).map(to: String.self) {
            return ":)"
            }.catchMap({ (error) -> (String) in
                if let error = error.s3ErrorMessage() {
                    return error.message
                }
                return ":("
                }
        )
    }
    
    // Get list of objects
    router.get("files")  { req -> EventLoopFuture<BucketResults> in
        let s3 = try req.makeS3Client()
        return try s3.list(bucket: "booststore", region: .usEast1, headers: [:], on: req).catchMap({ (error) -> (BucketResults) in
            if let error = error.s3ErrorMessage() {
                print(error.message)
            }
            throw error
        })
    }
    
    // Demonstrate work with files
    router.get("files/test") { req -> EventLoopFuture<String> in
        let string = "Content of my example file"
        
        let fileName = "file-hu.txt"
        
        let s3 = try req.makeS3Client()
        do {
            // Upload a file from string
            return try s3.put(string: string, destination: fileName, access: .publicRead, on: req).flatMap(to: String.self) { putResponse in
                print("PUT response:")
                print(putResponse)
                // Get the content of the newly uploaded file
                return try s3.get(file: fileName, on: req).flatMap(to: String.self) { getResponse in
                    print("GET response:")
                    print(getResponse)
                    print(String(data: getResponse.data, encoding: .utf8) ?? "Unknown content!")
                    // Get info about the file (HEAD)
                    return try s3.get(fileInfo: fileName, on: req).flatMap(to: String.self) { infoResponse in
                        print("HEAD/Info response:")
                        print(infoResponse)
                        // Delete the file
                        return try s3.delete(file: fileName, on: req).map() { response in
                            print("DELETE response:")
                            print(response)
                            let json = try JSONEncoder().encode(infoResponse)
                            return String(data: json, encoding: .utf8) ?? "Unknown content!"
                            }.catchMap({ error -> (String) in
                                if let error = error.s3ErrorMessage() {
                                    return error.message
                                }
                                return ":("
                            }
                        )
                    }
                }
            }
        } catch {
            print(error)
            fatalError()
        }
    }
}

Support

Join our Slack, channel #help-boost to ... well, get help :)

Einstore AppStore

Core package for Einstore, a completely open source enterprise AppStore written in Swift!

Other core packages

  • EinstoreCore - AppStore core module
  • ApiCore - API core module with users and team management
  • MailCore - Mailing wrapper for multiple mailing services like MailGun, SendGrig or SMTP (coming)
  • DBCore - Set of tools for work with PostgreSQL database
  • VaporTestTools - Test tools and helpers for Vapor 3

Code contributions

We love PR’s, we can’t get enough of them ... so if you have an interesting improvement, bug-fix or a new feature please don’t hesitate to get in touch. If you are not sure about something before you start the development you can always contact our dev and product team through our Slack.

Credits

Author

Ondrej Rafaj (@rafiki270 on Github, Twitter, LiveUI Slack and Vapor Slack)

Thanks

Anthoni Castelli (@anthonycastelli on Github, @anthony on Vapor Slack) for his help on updating S3Signer for Vapor3

JustinM1 (@JustinM1 on Github) for his amazing original signer package

License

See the LICENSE file for more info.