S3Adapter partial implementation
This commit is contained in:
parent
dcbe2a191e
commit
72bc2fad90
|
@ -64,6 +64,15 @@
|
|||
"version": "3.0.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "S3",
|
||||
"repositoryURL": "https://github.com/LiveUI/S3.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "9a7e7aa5486396665a792eed6489499ef33c344b",
|
||||
"version": "3.0.0-rc2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Service",
|
||||
"repositoryURL": "https://github.com/vapor/service.git",
|
||||
|
@ -145,6 +154,15 @@
|
|||
"version": "3.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "VaporTestTools",
|
||||
"repositoryURL": "https://github.com/LiveUI/VaporTestTools.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "e07ce263257463f2f2af9e640f81eb95a72760e4",
|
||||
"version": "0.1.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "WebSocket",
|
||||
"repositoryURL": "https://github.com/vapor/websocket.git",
|
||||
|
@ -153,6 +171,15 @@
|
|||
"revision": "149af03348f60ac8b84defdf277112d62fd8c704",
|
||||
"version": "1.0.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "XMLCoding",
|
||||
"repositoryURL": "https://github.com/LiveUI/XMLCoding.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "65d576be8093150c5e7fe5c41df1b608ea6e2c6b",
|
||||
"version": "0.3.0"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -12,11 +12,13 @@ let package = Package(
|
|||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"),
|
||||
|
||||
.package(url: "https://github.com/LiveUI/S3.git", from: "3.0.0-RC3.1"),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "VaporFilesystem",
|
||||
dependencies: ["Vapor"]
|
||||
dependencies: ["Vapor", "S3"]
|
||||
),
|
||||
.testTarget(
|
||||
name: "VaporFilesystemTests",
|
||||
|
|
|
@ -15,7 +15,10 @@ public protocol FilesystemReading {
|
|||
func read(file: String, on: Container, options: FileOptions?) -> Future<Data>
|
||||
func metadata(of: String, on: Container, options: FileOptions?) -> Future<FileMetadata>
|
||||
func size(of: String, on: Container, options: FileOptions?) -> Future<Int>
|
||||
|
||||
#warning("FIXME: creationDate and modificationDate")
|
||||
func timestamp(of: String, on: Container, options: FileOptions?) -> Future<Date>
|
||||
func mediaType(of: String, on: Container, options: FileOptions?) -> Future<MediaType>
|
||||
|
||||
}
|
||||
|
||||
|
@ -32,3 +35,34 @@ public protocol FilesystemWriting {
|
|||
}
|
||||
|
||||
public typealias FilesystemAdapter = FilesystemReading & FilesystemWriting
|
||||
|
||||
|
||||
extension FilesystemReading {
|
||||
|
||||
public func ext(of file: String) -> String? {
|
||||
return file.split(separator: ".").last.map(String.init)
|
||||
}
|
||||
|
||||
public func mediaType(of file: String) -> MediaType? {
|
||||
guard let ext = self.ext(of: file) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return MediaType.fileExtension(ext)
|
||||
}
|
||||
|
||||
public func mediaType(of file: String, on worker: Container, options: FileOptions?) -> Future<MediaType> {
|
||||
guard let ext = file.split(separator: ".").last.map(String.init) else {
|
||||
#warning("FIXME: proper error")
|
||||
return worker.eventLoop.newFailedFuture(error: FilesystemError.creationFailed)
|
||||
}
|
||||
|
||||
guard let mediaType = MediaType.fileExtension(ext) else {
|
||||
#warning("FIXME: proper error")
|
||||
return worker.eventLoop.newFailedFuture(error: FilesystemError.creationFailed)
|
||||
}
|
||||
|
||||
return worker.eventLoop.newSucceededFuture(result: mediaType)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
import Foundation
|
||||
import Vapor
|
||||
import S3
|
||||
|
||||
extension S3Adapter: FilesystemReading {
|
||||
|
||||
|
||||
public func has(file: String, on worker: Container, options: FileOptions?) -> EventLoopFuture<Bool> {
|
||||
return run(file: file, on: worker) {
|
||||
return try self.client.get(fileInfo: $0, on: worker)
|
||||
.transform(to: true)
|
||||
}.catchMap{ (error) -> (Bool) in
|
||||
if case FilesystemError.fileNotFound(_) = error {
|
||||
return false
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
public func metadata(of: String, on: Container, options: FileOptions?) -> EventLoopFuture<FileMetadata> {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
public func size(of file: String, on worker: Container, options: FileOptions?) -> EventLoopFuture<Int> {
|
||||
return run(file: file, on: worker) {
|
||||
return try self.client.get(fileInfo: $0, on: worker)
|
||||
.map { $0.size }
|
||||
.unwrap(or: FilesystemError.fileSizeNotAvailable)
|
||||
}
|
||||
}
|
||||
|
||||
public func timestamp(of file: String, on worker: Container, options: FileOptions?) -> EventLoopFuture<Date> {
|
||||
return run(file: file, on: worker) {
|
||||
return try self.client.get(fileInfo: $0, on: worker)
|
||||
.map { $0.created }
|
||||
.unwrap(or: FilesystemError.timestampNotAvailable)
|
||||
}
|
||||
}
|
||||
|
||||
public func read(file: String, on worker: Container, options: FileOptions?) -> EventLoopFuture<Data> {
|
||||
return run(file: file, on: worker) {
|
||||
return try self.client
|
||||
.get(file: $0, on: worker)
|
||||
.map { $0.data }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
import Foundation
|
||||
import Vapor
|
||||
|
||||
extension S3Adapter: FilesystemWriting {
|
||||
|
||||
public func write(data: Data, to: String, on worker: Container, options: FileOptions?) -> EventLoopFuture<()> {
|
||||
return run(file: to, on: worker) {
|
||||
guard let mediaType = self.mediaType(of: to) else {
|
||||
#warning("FIXME: proper error")
|
||||
return worker.eventLoop.newFailedFuture(error: FilesystemError.creationFailed)
|
||||
}
|
||||
|
||||
#warning("FIXME: access")
|
||||
#warning("FIXME: Region is not passed anywhere to upload!")
|
||||
let upload = File.Upload(
|
||||
data: data,
|
||||
bucket: $0.bucket,
|
||||
destination: $0.path,
|
||||
access: .privateAccess,
|
||||
mime: mediaType.description
|
||||
)
|
||||
|
||||
return self.has(file: to, on: worker, options: options)
|
||||
.flatMap { has -> Future<()> in
|
||||
guard !has else {
|
||||
#warning("FIXME: Use proper error")
|
||||
throw FilesystemError.creationFailed
|
||||
}
|
||||
|
||||
return try self.client.put(file: upload, on: worker)
|
||||
.transform(to: ())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func update(data: Data, to: String, on: Container, options: FileOptions?) -> EventLoopFuture<()> {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
public func rename(file: String, to: String, on: Container, options: FileOptions?) -> EventLoopFuture<()> {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
public func copy(file: String, to: String, on: Container, options: FileOptions?) -> EventLoopFuture<()> {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
public func delete(file: String, on worker: Container, options: FileOptions?) -> EventLoopFuture<()> {
|
||||
return run(file: file, on: worker) {
|
||||
return try self.client.delete(file: $0, on: worker)
|
||||
.transform(to: ())
|
||||
}
|
||||
}
|
||||
|
||||
public func delete(directory: String, on: Container, options: FileOptions?) -> EventLoopFuture<()> {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
public func create(directory: String, on: Container, options: FileOptions?) -> EventLoopFuture<()> {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
import Foundation
|
||||
@_exported import S3
|
||||
|
||||
|
||||
open class S3Adapter {
|
||||
|
||||
public let bucket: String
|
||||
public let client: S3
|
||||
public let signer: S3Signer
|
||||
public let region: Region
|
||||
|
||||
#warning("TODO: abstract away S3Signer.Config?")
|
||||
#warning("TODO: default config for access and other properties and metadata")
|
||||
public init(bucket: String, config: S3Signer.Config) throws {
|
||||
self.bucket = bucket
|
||||
self.region = config.region
|
||||
|
||||
self.signer = try S3Signer(config)
|
||||
self.client = try S3(defaultBucket: bucket, signer: self.signer)
|
||||
}
|
||||
|
||||
open func fileLocation(for path: String) -> File.Location {
|
||||
return File.Location(
|
||||
path: path,
|
||||
bucket: bucket,
|
||||
region: region
|
||||
)
|
||||
}
|
||||
|
||||
internal func run<F>(file: String, on worker: Container, _ block: (LocationConvertible) throws -> Future<F>) -> Future<F> {
|
||||
do {
|
||||
return try block(fileLocation(for: file))
|
||||
.catchMap { (error) -> F in
|
||||
throw self.map(error: error, file: file)
|
||||
}
|
||||
} catch {
|
||||
return worker.eventLoop.newFailedFuture(error: error)
|
||||
}
|
||||
}
|
||||
|
||||
internal func run<F>(directory: String, on worker: Container, _ block: () throws -> Future<F>) -> Future<F> {
|
||||
do {
|
||||
return try block()
|
||||
.catchMap { (error) -> F in
|
||||
throw self.map(error: error, directory: directory)
|
||||
}
|
||||
} catch {
|
||||
return worker.eventLoop.newFailedFuture(error: error)
|
||||
}
|
||||
}
|
||||
|
||||
internal func map(error: Error, file: String) -> Error {
|
||||
guard case S3.Error.badResponse(let response) = error else {
|
||||
return error
|
||||
}
|
||||
|
||||
if response.http.status == .notFound {
|
||||
return FilesystemError.fileNotFound(file)
|
||||
}
|
||||
|
||||
return error
|
||||
}
|
||||
|
||||
internal func map(error: Error, directory: String) -> Error {
|
||||
guard case S3.Error.badResponse(let response) = error else {
|
||||
return error
|
||||
}
|
||||
|
||||
if response.http.status == .notFound {
|
||||
return FilesystemError.directoryNotFound(directory)
|
||||
}
|
||||
|
||||
return error
|
||||
}
|
||||
|
||||
}
|
|
@ -15,7 +15,7 @@ final class FilesystemManagerTests: XCTestCase {
|
|||
let manager = try FilesystemManager(
|
||||
disks: [.potatoes: DummyAdapter(), .images: local],
|
||||
default: .images,
|
||||
on: container()
|
||||
on: createContainer()
|
||||
)
|
||||
|
||||
XCTAssertNoThrow(try manager.use(.images))
|
||||
|
@ -30,7 +30,7 @@ final class FilesystemManagerTests: XCTestCase {
|
|||
let manager = try FilesystemManager(
|
||||
disks: [.images: local, .potatoes: dummy],
|
||||
default: .potatoes,
|
||||
on: container()
|
||||
on: createContainer()
|
||||
)
|
||||
|
||||
let result = try manager.has(file: "irelevant").wait()
|
||||
|
|
|
@ -0,0 +1,145 @@
|
|||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@testable import VaporFilesystem
|
||||
|
||||
final class S3AdapterTests: XCTestCase {
|
||||
|
||||
static var allTests = [
|
||||
("testReadIntegration", testReadIntegration),
|
||||
("testSizeIntegration", testSizeIntegration),
|
||||
("testPositiveHasIntegration", testPositiveHasIntegration),
|
||||
("testNegativeHasIntegration", testNegativeHasIntegration),
|
||||
("testWriteIntegration", testWriteIntegration),
|
||||
("testDeleteIntegration", testDeleteIntegration),
|
||||
]
|
||||
|
||||
func testReadIntegration() throws {
|
||||
let adapter = try createAdapter()
|
||||
let container = createContainer()
|
||||
|
||||
try useTestFile("read_test1.txt") {
|
||||
let data = try adapter.read(file: "read_test.txt", on: container, options: nil).wait()
|
||||
XCTAssertEqual(data.count, testFileSize)
|
||||
}
|
||||
}
|
||||
|
||||
func testSizeIntegration() throws {
|
||||
let adapter = try createAdapter()
|
||||
let container = createContainer()
|
||||
|
||||
try useTestFile("size_test.txt") {
|
||||
let size = try adapter.size(of: "size_test.txt", on: container, options: nil).wait()
|
||||
XCTAssertEqual(size, testFileSize)
|
||||
}
|
||||
}
|
||||
|
||||
func testPositiveHasIntegration() throws {
|
||||
let adapter = try createAdapter()
|
||||
let container = createContainer()
|
||||
|
||||
try useTestFile("positive_has_test.txt") {
|
||||
let has = try adapter.has(file: "positive_has_test.txt", on: container, options: nil).wait()
|
||||
XCTAssertTrue(has)
|
||||
}
|
||||
}
|
||||
|
||||
func testNegativeHasIntegration() throws {
|
||||
let adapter = try createAdapter()
|
||||
let container = createContainer()
|
||||
|
||||
let has = try adapter.has(file: "some/fake/file.png", on: container, options: nil).wait()
|
||||
XCTAssertFalse(has)
|
||||
}
|
||||
|
||||
func testWriteIntegration() throws {
|
||||
let adapter = try createAdapter()
|
||||
let container = createContainer()
|
||||
let file = "write_test.txt"
|
||||
|
||||
try useTestFile(file) {
|
||||
let hasAfter = try adapter.has(file: file, on: container, options: nil).wait()
|
||||
XCTAssertTrue(hasAfter)
|
||||
}
|
||||
}
|
||||
|
||||
func testDeleteIntegration() throws {
|
||||
let adapter = try createAdapter()
|
||||
let container = createContainer()
|
||||
let file = "delete_test.txt"
|
||||
|
||||
try useTestFile(file) {
|
||||
try adapter.delete(file: file, on: container, options: nil).wait()
|
||||
let has = try adapter.has(file: file, on: container, options: nil).wait()
|
||||
XCTAssertFalse(has)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fileprivate extension S3AdapterTests {
|
||||
|
||||
var testFileSize: Int {
|
||||
return testFileData.count
|
||||
}
|
||||
|
||||
var testFileData: Data {
|
||||
return "hello world".data(using: .utf8)!
|
||||
}
|
||||
|
||||
func createAdapter() throws -> S3Adapter {
|
||||
return try S3Adapter(
|
||||
bucket: "<your-bucket>",
|
||||
config: S3Signer.Config(
|
||||
accessKey: "<your-accessKey>",
|
||||
secretKey: "<your-secretKey>",
|
||||
region: .euCentral1 // your region
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fileprivate func writeTestFile(_ path: String) throws {
|
||||
let adapter = try createAdapter()
|
||||
let container = createContainer()
|
||||
|
||||
try adapter.write(data: testFileData, to: path, on: container, options: nil).wait()
|
||||
}
|
||||
|
||||
fileprivate func deleteTestFile(_ path: String) throws {
|
||||
let adapter = try createAdapter()
|
||||
let container = createContainer()
|
||||
|
||||
let has = try adapter.has(file: path, on: container, options: nil).wait()
|
||||
if has {
|
||||
try adapter.delete(file: path, on: container, options: nil).wait()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func useTestFile(_ path: String, _ closure: () throws -> ()) throws {
|
||||
try? deleteTestFile(path)
|
||||
|
||||
do {
|
||||
try writeTestFile(path)
|
||||
}
|
||||
catch {
|
||||
try deleteTestFile(path)
|
||||
XCTFail("Failed preparing file \(path): \(error)")
|
||||
throw error
|
||||
}
|
||||
|
||||
do {
|
||||
try closure()
|
||||
} catch {
|
||||
XCTFail("Failed: \(error)")
|
||||
throw error
|
||||
}
|
||||
|
||||
do {
|
||||
try deleteTestFile(path)
|
||||
} catch {
|
||||
XCTFail("Failed removing file \(path): \(error)")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -4,7 +4,7 @@ import Vapor
|
|||
|
||||
extension XCTestCase {
|
||||
|
||||
func container() -> Container {
|
||||
func createContainer() -> Container {
|
||||
return BasicContainer(
|
||||
config: Config.default(),
|
||||
environment: Environment.testing,
|
||||
|
|
|
@ -4,6 +4,7 @@ import XCTest
|
|||
public func allTests() -> [XCTestCaseEntry] {
|
||||
return [
|
||||
testCase(FilesystemManagerTests.allTests),
|
||||
testCsse(S3AdapterTests.allTests),
|
||||
]
|
||||
}
|
||||
#endif
|
||||
|
|
Loading…
Reference in New Issue