Skip to content
This repository has been archived by the owner on Apr 20, 2024. It is now read-only.

Commit

Permalink
Merge pull request #21 from nodes-vapor/new-s3-provider
Browse files Browse the repository at this point in the history
New s3 provider
  • Loading branch information
BrettRToomey authored Jan 3, 2017
2 parents 16e8f9a + 4ea3282 commit b0648eb
Show file tree
Hide file tree
Showing 12 changed files with 308 additions and 90 deletions.
2 changes: 2 additions & 0 deletions .codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ coverage:
range: "0...100"
ignore:
- "Sources/Storage/Utilities"
- "Sources/Storage/NetworkDriver.swift"
- "Tests/"
5 changes: 3 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ let package = Package(
name: "Storage",
dependencies: [
.Package(url: "https://github.com/vapor/vapor.git", majorVersion: 1),
.Package(url: "https://github.com/manGoweb/S3.git", majorVersion: 1),
.Package(url: "https://github.com/nodes-vapor/DataURI.git", majorVersion: 0)
.Package(url: "https://github.com/nodes-vapor/data-uri.git", majorVersion: 0),
.Package(url: "https://github.com/nodes-vapor/aws.git", majorVersion: 0),
.Package(url: "https://github.com/manGoweb/MimeLib.git", majorVersion: 1)
]
)
28 changes: 18 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@ Now, create a JSON file named `Config/storage.json` with the following contents:
```json
{
"driver": "s3",
"bucket": "mybucket",
"accessKey": "$YOUR_S3_ACCESS_KEY",
"secretKey": "$YOUR_S3_SECRET_KEY",
"template": "$folder/$file"
"cdnUrl": "$CDN_BASE_URL"
}
```
Learn about [these fields and more](#configuration-).
Expand All @@ -57,8 +58,8 @@ The aforementioned function will attempt to upload the file using your [selected

If you want to upload an image named `profile.png` your call site would look like:
```swift
let path = try Storage.upload(bytes: bytes, fileName: "profile", fileExtension: "png")
print(path) //prints `images/profile.png`
let path = try Storage.upload(bytes: bytes, fileName: "profile.png")
print(path) //prints `/profile.png`
```

#### Base64 and data URI 📡
Expand All @@ -72,13 +73,13 @@ Storage.upload(dataURI: String, //...)
To download a file that was previously uploaded you simply use the generated path.
```swift
//download image as `Foundation.Data`
let data = try Storage.get("images/profile.png")
let data = try Storage.get("/images/profile.png")
```

## Delete a file
Deleting a file using this package isn't the recommended way to handle removal, but is still possible.
```swift
try Storage.delete("images/profile.png")
try Storage.delete("/images/profile.png")
```
## Configuration
`Storage` has a variety of configurable options.
Expand All @@ -89,18 +90,19 @@ The network driver is the module responsible for interacting with your 3rd party
"driver": "s3",
"accessKey": "$YOUR_S3_ACCESS_KEY",
"secretKey": "$YOUR_S3_SECRET_KEY",
"host": "s3.amazonaws.com"
"bucket": "$YOUR_S3_BUCKET",
"region": "$YOUR_S3_REGION"
}
```
The `driver` key is optional and will default to `s3`. `accessKey` and `secretKey` are both required by the S3 driver, while `bucket` and `region` are both optional. `region` will default to `eu-west-1` if not provided.
The `driver` key is optional and will default to `s3`. `accessKey` and `secretKey` are both required by the S3 driver, while `host`, `bucket` and `region` are all optional. `region` will default to `eu-west-1` and `host` will default to `s3.amazonaws.com` if not provided.

#### Upload path 🛣
A times, you may need to upload files to a different scheme than `folder/file.ext`. You can achieve this by adding the `"template"` field to your `Config/storage.json`. If the field is omitted it will default to `$folder/$file`.
A times, you may need to upload files to a different scheme than `/file.ext`. You can achieve this by adding the `"template"` field to your `Config/storage.json`. If the field is omitted it will default to `/$file`.

The following template will upload `profile.png` from the folder `images` to `myapp/images/profile.png`
The following template will upload `profile.png` from the folder `images` to `/myapp/images/profile.png`
```json
"template": "myapp/$folder/$file"
"template": "/myapp/$folder/$file"
```

##### Aliases
Expand All @@ -109,8 +111,14 @@ There are a few aliases that will be replaced by the real metadata of the file a
* `$file`: The file's name and extension.
* `$fileName`: The file's name.
* `$fileExtension`: The file's extension.
* `$folder`: The provided folder
* `$folder`: The provided folder.
* `$mime`: The file's content type.
* `$mimeFolder`: A folder generated according to the file's mime.
* `$day`: The current day.
* `$month`: The current month.
* `$year`: The current year.
* `$timestamp`: The time of upload.
* `$uuid`: A generated UUID.

## 🏆 Credits
This package is developed and maintained by the Vapor team at [Nodes](https://www.nodes.dk).
Expand Down
39 changes: 31 additions & 8 deletions Sources/Storage/FileEntity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,25 @@ public struct FileEntity {
public enum Error : Swift.Error {
case missingFilename
case missingFileExtension
case malformedFileName
}

//TODO(Brett): considering changing all `String` fields to `Bytes`.

/// The raw bytes of the file.
var bytes: Bytes?

/// The file's name.

// The file's name with the extension.
var fullFileName: String? {
guard let fileName = fileName, let fileExtension = fileExtension else {
return nil
}

return [fileName, fileExtension].joined(separator: ".")
}

/// The file's name without the extension.
var fileName: String?

/// The file's extension.
Expand Down Expand Up @@ -47,6 +58,8 @@ public struct FileEntity {
self.fileExtension = fileExtension
self.folder = folder
self.mime = mime

sanitize()
}
}

Expand All @@ -64,16 +77,12 @@ extension FileEntity {

extension FileEntity {
func getFilePath() throws -> String {
guard let fileName = fileName else {
throw Error.missingFilename
}

guard let fileExtension = fileExtension else {
throw Error.missingFileExtension
guard let fileName = fullFileName else {
throw Error.malformedFileName
}

var path = [
"\(fileName).\(fileExtension)"
fileName
]

if let folder = folder {
Expand All @@ -85,6 +94,20 @@ extension FileEntity {
}

extension FileEntity {
mutating func sanitize() {
guard let fileName = fileName, fileName.contains(".") else { return }

let components = fileName.components(separatedBy: ".")

// don't override if a programmer provided an extension
if fileExtension == nil {
fileExtension = components.last
}

self.fileName = components.dropLast().joined(separator: ".")
}


@discardableResult
mutating func loadMimeFromFileExtension() -> Bool {
guard let fileExtension = fileExtension else { return false }
Expand Down
10 changes: 5 additions & 5 deletions Sources/Storage/NetworkDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ protocol NetworkDriver {
var pathBuilder: PathBuilder { get set }

@discardableResult func upload(entity: inout FileEntity) throws -> String
func get(path: String) throws -> Data
func get(path: String) throws -> Bytes
func delete(path: String) throws
}

Expand Down Expand Up @@ -44,16 +44,16 @@ final class S3Driver: NetworkDriver {
}

let path = try pathBuilder.build(entity: entity)
try s3.put(bytes: bytes, filePath: path, accessControl: .publicRead)
try s3.upload(bytes: bytes, path: path, access: .publicRead)

return path
}

func get(path: String) throws -> Data {
return try s3.get(fileAtPath: path)
func get(path: String) throws -> Bytes {
return try s3.get(path: path)
}

func delete(path: String) throws {
return try s3.delete(fileAtPath: path)
return try s3.delete(file: path)
}
}
4 changes: 2 additions & 2 deletions Sources/Storage/PathBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ extension PathBuilder {
}
}

final class ConfigurablePathBuilder: PathBuilder {
struct ConfigurablePathBuilder: PathBuilder {
var template: Template

init(template: String) throws {
self.template = try Template.compile(template)
}

func build(entity: FileEntity) throws -> String {
return try template.renderPath(entity: entity)
return try template.renderPath(for: entity, generateFolder)
}
}
16 changes: 12 additions & 4 deletions Sources/Storage/Provider.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import S3
import Vapor
import S3SignerAWS

import enum Driver.Region

///A provider for configuring the `Storage` package.
public final class StorageProvider: Provider {
Expand All @@ -9,6 +10,7 @@ public final class StorageProvider: Provider {
case unsupportedDriver(String)
case missingAccessKey
case missingSecretKey
case missingBucket
case unknownRegion(String)
}

Expand All @@ -23,6 +25,7 @@ public final class StorageProvider: Provider {

let networkDriver = try buildNetworkDriver(config: config)
Storage.networkDriver = networkDriver
Storage.cdnBaseURL = config["cdnUrl"]?.string
}

public func boot(_ drop: Droplet) {}
Expand All @@ -32,7 +35,7 @@ public final class StorageProvider: Provider {
public func beforeRun(_: Droplet) {}

private func buildNetworkDriver(config: Config) throws -> NetworkDriver {
let template = config["template"]?.string ?? "$folder/$file"
let template = config["template"]?.string ?? "/$file"
let networkDriver: NetworkDriver
let driver = config["driver"]?.string ?? "s3"
switch driver {
Expand All @@ -54,17 +57,22 @@ public final class StorageProvider: Provider {
throw Error.missingSecretKey
}

let bucket = config["bucket"]?.string
//TODO(Brett): add $bucket alias
guard let bucket = config["bucket"]?.string else {
throw Error.missingBucket
}

let host = config["host"]?.string ?? "s3.amazonaws.com"

let regionString = config["region"]?.string ?? "eu-west-1"
guard let region = Region(rawValue: regionString) else {
throw Error.unknownRegion(regionString)
}

let s3 = S3(
host: "\(bucket).\(host)",
accessKey: accessKey,
secretKey: secretKey,
bucketName: bucket,
region: region
)

Expand Down
15 changes: 12 additions & 3 deletions Sources/Storage/Storage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ import Foundation
public class Storage {
public enum Error: Swift.Error {
case missingNetworkDriver
case cdnBaseURLNotSet
case unsupportedMultipart(Multipart)
case missingFileName
}

static var networkDriver: NetworkDriver?

static var cdnBaseURL: String?
/**
Uploads the given `FileEntity`.

Expand Down Expand Up @@ -189,16 +190,24 @@ public class Storage {
- Parameters:
- path: The path of the file to be downloaded.

- Returns: The downloaded file as `NSData`.
- Returns: The downloaded file as `Bytes`/`[UInt8]`.
*/
public static func get(path: String) throws -> Data {
public static func get(path: String) throws -> Bytes {
guard let networkDriver = networkDriver else {
throw Error.missingNetworkDriver
}

return try networkDriver.get(path: path)
}

public static func getCDNPath(for path: String) throws -> String {
guard let cdnBaseURL = cdnBaseURL else {
throw Error.cdnBaseURLNotSet
}

return cdnBaseURL + path
}

/**
Deletes the file at `path`.

Expand Down
Loading

0 comments on commit b0648eb

Please sign in to comment.