S3Adapter partial implementation

This commit is contained in:
Zdenek Topic 2018-10-16 22:12:44 +02:00
parent dcbe2a191e
commit 72bc2fad90
No known key found for this signature in database
GPG Key ID: 41897C1A6A09DEF5
10 changed files with 404 additions and 4 deletions

View File

@ -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"
}
}
]
},

View File

@ -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",

View File

@ -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)
}
}

View File

@ -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 }
}
}
}

View File

@ -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()
}
}

View File

@ -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
}
}

View File

@ -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()

View File

@ -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
}
}
}

View File

@ -4,7 +4,7 @@ import Vapor
extension XCTestCase {
func container() -> Container {
func createContainer() -> Container {
return BasicContainer(
config: Config.default(),
environment: Environment.testing,

View File

@ -4,6 +4,7 @@ import XCTest
public func allTests() -> [XCTestCaseEntry] {
return [
testCase(FilesystemManagerTests.allTests),
testCsse(S3AdapterTests.allTests),
]
}
#endif