s3storage/Sources/S3Storage/S3Storage.swift

117 lines
4.7 KiB
Swift

import Storage
import S3
/// A `Storage` interface for the Amazon S3 API, backed with the [LiveUI/S3](https://github.com/LiveUI/S3) library.
///
/// `S3Storage` use an `S3StorageClient` instead of `S3Client`. We conform the basic `S3` type for you, so as long as you register
/// your `S3` instance properly, everything should work as normal.
///
/// try services.register(S3(defaultBucket: bucket, signer: signer), as: S3StorageClient.self)
///
/// When uploading a file to Amazon S3, you can pass a path into the `.store(file:at:)` method. If you do, the file's localtion will be
/// `path/filename`. If no path is passed in and there is no default path, the file will be located at `filename`.
///
/// When accessing a file to read, override, or delete it, use the reletive path instead of the full URL.
///
/// storage.fetch(file: "documents/README.md")
public struct S3Storage: Storage {
/// The path that will be used if `nil` is passed into the `S3Storage.store(file:at:)` method
public let defaultPath: String?
/// The container used by the `S3Client` instance to create, read, and delete files.
internal let eventLoop: EventLoop
private let client: S3Client
private let allocator: ByteBufferAllocator
/// Creates a new `S3Storage` instance.
///
/// - Parameters:
/// - eventLoop: The event loop that the instance will live on and use for IO operations.
/// - client: The S3 client used for making the API calls for the IO operations.
/// - defaultPath: The default path that files will be stored at.
public init(eventLoop: EventLoop, client: S3Client, defaultPath: String? = nil) {
self.client = client
self.eventLoop = eventLoop
self.defaultPath = defaultPath
self.allocator = ByteBufferAllocator()
}
/// Creates a new `S3Storage` instance from a `Container`.
///
/// - Parameter container: The container the get the event loop and make the `S3Client` from.
public init(container: Container)throws {
try self.init(eventLoop: container.eventLoop, client: container.make())
}
/// See `Storage.store(file:at:)`
public func store(file: Vapor.File, at path: String? = nil) -> EventLoopFuture<String> {
let s3Path: String
if let unwrappedPath = path {
s3Path = unwrappedPath + "/" + file.filename
} else if let unwrappedPath = self.defaultPath {
s3Path = unwrappedPath + "/" + file.filename
} else {
s3Path = file.filename
}
let type = file.contentType?.description ?? HTTPMediaType.plainText.description
let data = Data(file.data.readableBytesView)
let upload = File.Upload(
data: data,
destination: s3Path,
mime: type
)
return client.put(file: upload, on: self.eventLoop).map { response in
return response.path
}
}
/// See `Storage.fetch(file:)`.
public func fetch(file: String) -> EventLoopFuture<Vapor.File> {
return client.get(file: file, on: self.eventLoop).flatMapThrowing { response in
guard let name = response.path.split(separator: "/").last.map(String.init) else {
throw StorageError(identifier: "fileName", reason: "Unable to extract file name from path `\(response.path)`")
}
var buffer = self.allocator.buffer(capacity: response.data.count)
buffer.writeBytes(response.data)
return Vapor.File(data: buffer, filename: name)
}
}
/// See `Storage.write(file:data:options:)`.
///
/// Amazon S3 does not support mutating files, so we just delete the existing one and upload a new version
/// with the data passed in. The `options` parameter is ignored.
public func write(file: String, with data: Data) -> EventLoopFuture<Vapor.File> {
do {
let path = String(file.split(separator: "/").dropLast().joined())
guard let name = file.split(separator: "/").last.map(String.init) else {
throw StorageError(identifier: "fileName", reason: "Unable to extract file name from path `\(file)`")
}
return self.delete(file: file).flatMap {
var buffer = self.allocator.buffer(capacity: data.count)
buffer.writeBytes(data)
let file = Vapor.File(data: buffer, filename: name)
return self.store(file: file, at: path).transform(to: file)
}
} catch let error {
return self.eventLoop.future(error: error)
}
}
/// See `Storage.delete(file:)`.
public func delete(file: String) -> EventLoopFuture<Void> {
return client.delete(file: file, on: self.eventLoop)
}
}