diff --git a/.swift-version b/.swift-version index 7d5c902..a75b92f 100644 --- a/.swift-version +++ b/.swift-version @@ -1 +1 @@ -4.1 +5.1 diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/.swiftpm/xcode/package.xcworkspace/xcuserdata/calebkleveter.xcuserdatad/UserInterfaceState.xcuserstate b/.swiftpm/xcode/package.xcworkspace/xcuserdata/calebkleveter.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000..0100a09 Binary files /dev/null and b/.swiftpm/xcode/package.xcworkspace/xcuserdata/calebkleveter.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/.swiftpm/xcode/xcuserdata/calebkleveter.xcuserdatad/xcschemes/xcschememanagement.plist b/.swiftpm/xcode/xcuserdata/calebkleveter.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..5bb8aa3 --- /dev/null +++ b/.swiftpm/xcode/xcuserdata/calebkleveter.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,67 @@ + + + + + SchemeUserState + + S3-Package.xcscheme_^#shared#^_ + + orderHint + 0 + + S3.xcscheme_^#shared#^_ + + orderHint + 1 + + S3DemoRun.xcscheme_^#shared#^_ + + orderHint + 4 + + S3Signer.xcscheme_^#shared#^_ + + orderHint + 2 + + S3TestTools.xcscheme_^#shared#^_ + + orderHint + 3 + + + SuppressBuildableAutocreation + + S3 + + primary + + + S3DemoApp + + primary + + + S3DemoRun + + primary + + + S3Signer + + primary + + + S3TestTools + + primary + + + S3Tests + + primary + + + + + diff --git a/Package.resolved b/Package.resolved index 126f8d3..55aeeb5 100644 --- a/Package.resolved +++ b/Package.resolved @@ -2,75 +2,57 @@ "object": { "pins": [ { - "package": "Console", - "repositoryURL": "https://github.com/vapor/console.git", + "package": "async-kit", + "repositoryURL": "https://github.com/vapor/async-kit.git", "state": { "branch": null, - "revision": "74cfbea629d4aac34a97cead2447a6870af1950b", - "version": "3.1.1" + "revision": "b5742bfbbe2d60f3b77465a0907777261e418f23", + "version": "1.0.0-alpha.1" } }, { - "package": "Core", - "repositoryURL": "https://github.com/vapor/core.git", + "package": "console-kit", + "repositoryURL": "https://github.com/vapor/console-kit.git", "state": { "branch": null, - "revision": "727ae2905ebdca162c5da5bfe187c406555859e2", - "version": "3.7.3" + "revision": "252a8b8e22cc70ebe8a073b241b7275a3b3880d9", + "version": "4.0.0-alpha.1" } }, { - "package": "Crypto", - "repositoryURL": "https://github.com/vapor/crypto.git", + "package": "crypto-kit", + "repositoryURL": "https://github.com/vapor/crypto-kit.git", "state": { "branch": null, - "revision": "45bb12d13cdec80dbd1cc0685ea002e51ab83439", - "version": "3.3.2" + "revision": "9d5e780ad8f621e14226f0c2824648a9c062d37c", + "version": "4.0.0-alpha.1" } }, { - "package": "DatabaseKit", - "repositoryURL": "https://github.com/vapor/database-kit.git", + "package": "nio-websocket-client", + "repositoryURL": "https://github.com/vapor/nio-websocket-client.git", "state": { "branch": null, - "revision": "8f352c8e66dab301ab9bfef912a01ce1361ba1e4", - "version": "1.3.3" + "revision": "3e515024c4c9eab6adb1ab516be15777a28a233e", + "version": "1.0.0-alpha.1" } }, { - "package": "HTTP", - "repositoryURL": "https://github.com/vapor/http.git", + "package": "routing-kit", + "repositoryURL": "https://github.com/vapor/routing-kit.git", "state": { "branch": null, - "revision": "8da7d475a1a060714766d9ad4b24eb0dae243aab", - "version": "3.1.11" + "revision": "6c7f4b471f9662d05045d82e64e22d5572a16a82", + "version": "4.0.0-alpha.1" } }, { - "package": "Multipart", - "repositoryURL": "https://github.com/vapor/multipart.git", + "package": "swift-log", + "repositoryURL": "https://github.com/apple/swift-log.git", "state": { "branch": null, - "revision": "bd7736c5f28e48ed8b683dcc9df3dcd346064c2b", - "version": "3.0.3" - } - }, - { - "package": "Routing", - "repositoryURL": "https://github.com/vapor/routing.git", - "state": { - "branch": null, - "revision": "626190ddd2bd9f967743b60ba6adaf90bbd2651c", - "version": "3.0.2" - } - }, - { - "package": "Service", - "repositoryURL": "https://github.com/vapor/service.git", - "state": { - "branch": null, - "revision": "fa5b5de62bd68bcde9a69933f31319e46c7275fb", - "version": "1.0.2" + "revision": "f4240bf022a69815241a883c03645444b58ac553", + "version": "1.1.0" } }, { @@ -78,89 +60,53 @@ "repositoryURL": "https://github.com/apple/swift-nio.git", "state": { "branch": null, - "revision": "29a9f2aca71c8afb07e291336f1789337ce235dd", - "version": "1.13.2" - } - }, - { - "package": "swift-nio-ssl", - "repositoryURL": "https://github.com/apple/swift-nio-ssl.git", - "state": { - "branch": null, - "revision": "0f3999f3e3c359cc74480c292644c3419e44a12f", - "version": "1.4.0" + "revision": "7f20464df31e85f86bedf4a8afdd1785e0cb5059", + "version": "2.3.0" } }, { - "package": "swift-nio-ssl-support", - "repositoryURL": "https://github.com/apple/swift-nio-ssl-support.git", + "package": "swift-nio-extras", + "repositoryURL": "https://github.com/apple/swift-nio-extras.git", "state": { "branch": null, - "revision": "c02eec4e0e6d351cd092938cf44195a8e669f555", - "version": "1.0.0" + "revision": "96e8335180ca61c7187efe47058a1c99eadaf265", + "version": "1.1.0" } }, { - "package": "swift-nio-zlib-support", - "repositoryURL": "https://github.com/apple/swift-nio-zlib-support.git", + "package": "swift-nio-http-client", + "repositoryURL": "https://github.com/vapor/swift-nio-http-client.git", "state": { "branch": null, - "revision": "37760e9a52030bb9011972c5213c3350fa9d41fd", - "version": "1.0.0" + "revision": "93682abeb568b3df47f0744558c4436bb325f860", + "version": "0.0.0" } }, { - "package": "TemplateKit", - "repositoryURL": "https://github.com/vapor/template-kit.git", + "package": "swift-nio-http2", + "repositoryURL": "https://github.com/apple/swift-nio-http2.git", "state": { "branch": null, - "revision": "4b1073d74be9f5c6a5bc63a07a84e83efec26229", - "version": "1.1.2" + "revision": "06880b0abdc5d2aa68b96d6b3b29b089cb423922", + "version": "1.3.0" } }, { - "package": "URLEncodedForm", - "repositoryURL": "https://github.com/vapor/url-encoded-form.git", - "state": { - "branch": null, - "revision": "82d8d63bdb76b6dd8febe916c639ab8608dbbaed", - "version": "1.0.6" - } - }, - { - "package": "Validation", - "repositoryURL": "https://github.com/vapor/validation.git", + "package": "swift-nio-ssl", + "repositoryURL": "https://github.com/apple/swift-nio-ssl.git", "state": { "branch": null, - "revision": "4de213cf319b694e4ce19e5339592601d4dd3ff6", + "revision": "77173ac9038e8e9b92f3efe8780923447e3c890b", "version": "2.1.1" } }, { - "package": "Vapor", + "package": "vapor", "repositoryURL": "https://github.com/vapor/vapor.git", "state": { "branch": null, - "revision": "c86ada59b31c69f08a6abd4f776537cba48d5df6", - "version": "3.3.0" - } - }, - { - "package": "VaporTestTools", - "repositoryURL": "https://github.com/LiveUI/VaporTestTools.git", - "state": { - "branch": null, - "revision": "135d02e2e2a632c134567754d65ce2afe262bab3", - "version": "0.1.7" - } - }, - { - "package": "WebSocket", - "repositoryURL": "https://github.com/vapor/websocket.git", - "state": { - "branch": null, - "revision": "d85e5b6dce4d04065865f77385fc3324f98178f6", - "version": "1.1.2" + "revision": "e415cbcacc14a5f8d02e3b6e1aea36f86064b98e", + "version": "4.0.0-alpha.1.5" } }, { diff --git a/Package.swift b/Package.swift index 0f6e3ea..100c593 100644 --- a/Package.swift +++ b/Package.swift @@ -9,9 +9,8 @@ let package = Package( .library(name: "S3TestTools", targets: ["S3TestTools"]) ], dependencies: [ - .package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"), - .package(url: "https://github.com/LiveUI/XMLCoding.git", from: "0.1.0"), - .package(url: "https://github.com/LiveUI/VaporTestTools.git", from: "0.1.5") + .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0-alpha"), + .package(url: "https://github.com/LiveUI/XMLCoding.git", from: "0.1.0") ], targets: [ .target(name: "S3", dependencies: [ @@ -36,7 +35,6 @@ let package = Package( ), .target(name: "S3TestTools", dependencies: [ "Vapor", - "VaporTestTools", "S3" ] ), diff --git a/README.md b/README.md index 5eef124..9297b7b 100644 --- a/README.md +++ b/README.md @@ -79,70 +79,70 @@ s3.headers(...) public protocol S3Client: Service { /// Get list of objects - func buckets(on: Container) throws -> Future + func buckets(on: Container) -> EventLoopFuture /// Create a bucket - func create(bucket: String, region: Region?, on container: Container) throws -> Future + func create(bucket: String, region: Region?, on container: Container) -> EventLoopFuture /// Delete a bucket - func delete(bucket: String, region: Region?, on container: Container) throws -> Future + func delete(bucket: String, region: Region?, on container: Container) -> EventLoopFuture /// Get bucket location - func location(bucket: String, on container: Container) throws -> Future + func location(bucket: String, on container: Container) -> EventLoopFuture /// Get list of objects - func list(bucket: String, region: Region?, on container: Container) throws -> Future + func list(bucket: String, region: Region?, on container: Container) -> EventLoopFuture /// Get list of objects - func list(bucket: String, region: Region?, headers: [String: String], on container: Container) throws -> Future + func list(bucket: String, region: Region?, headers: [String: String], on container: Container) -> EventLoopFuture /// Upload file to S3 - func put(file: File.Upload, headers: [String: String], on: Container) throws -> EventLoopFuture + func put(file: File.Upload, headers: [String: String], on: Container) throws -> EventLoopEventLoopFuture /// Upload file to S3 - func put(file url: URL, destination: String, access: AccessControlList, on: Container) throws -> Future + func put(file url: URL, destination: String, access: AccessControlList, on: Container) -> EventLoopFuture /// Upload file to S3 - func put(file url: URL, destination: String, bucket: String?, access: AccessControlList, on: Container) throws -> Future + func put(file url: URL, destination: String, bucket: String?, access: AccessControlList, on: Container) -> EventLoopFuture /// Upload file to S3 - func put(file path: String, destination: String, access: AccessControlList, on: Container) throws -> Future + func put(file path: String, destination: String, access: AccessControlList, on: Container) -> EventLoopFuture /// Upload file to S3 - func put(file path: String, destination: String, bucket: String?, access: AccessControlList, on: Container) throws -> Future + func put(file path: String, destination: String, bucket: String?, access: AccessControlList, on: Container) -> EventLoopFuture /// Upload file to S3 - func put(string: String, destination: String, on: Container) throws -> Future + func put(string: String, destination: String, on: Container) -> EventLoopFuture /// Upload file to S3 - func put(string: String, destination: String, access: AccessControlList, on: Container) throws -> Future + func put(string: String, destination: String, access: AccessControlList, on: Container) -> EventLoopFuture /// Upload file to S3 - func put(string: String, mime: MediaType, destination: String, on: Container) throws -> Future + func put(string: String, mime: MediaType, destination: String, on: Container) -> EventLoopFuture /// Upload file to S3 - func put(string: String, mime: MediaType, destination: String, access: AccessControlList, on: Container) throws -> Future + func put(string: String, mime: MediaType, destination: String, access: AccessControlList, on: Container) -> EventLoopFuture /// Upload file to S3 - func put(string: String, mime: MediaType, destination: String, bucket: String?, access: AccessControlList, on: Container) throws -> Future + func put(string: String, mime: MediaType, destination: String, bucket: String?, access: AccessControlList, on: Container) -> EventLoopFuture /// Retrieve file data from S3 - func get(fileInfo file: LocationConvertible, on container: Container) throws -> Future + func get(fileInfo file: LocationConvertible, on container: Container) -> EventLoopFuture /// Retrieve file data from S3 - func get(fileInfo file: LocationConvertible, headers: [String: String], on container: Container) throws -> Future + func get(fileInfo file: LocationConvertible, headers: [String: String], on container: Container) -> EventLoopFuture /// Retrieve file data from S3 - func get(file: LocationConvertible, on: Container) throws -> Future + func get(file: LocationConvertible, on: Container) -> EventLoopFuture /// Retrieve file data from S3 - func get(file: LocationConvertible, headers: [String: String], on: Container) throws -> Future + func get(file: LocationConvertible, headers: [String: String], on: Container) -> EventLoopFuture /// Delete file from S3 - func delete(file: LocationConvertible, on: Container) throws -> Future + func delete(file: LocationConvertible, on: Container) -> EventLoopFuture /// Delete file from S3 - func delete(file: LocationConvertible, headers: [String: String], on: Container) throws -> Future + func delete(file: LocationConvertible, headers: [String: String], on: Container) -> EventLoopFuture } ``` @@ -152,13 +152,13 @@ public protocol S3Client: Service { public func routes(_ router: Router) throws { // Get all available buckets - router.get("buckets") { req -> Future in + router.get("buckets") { req -> EventLoopFuture in let s3 = try req.makeS3Client() return try s3.buckets(on: req) } // Create new bucket - router.put("bucket") { req -> Future in + router.put("bucket") { req -> EventLoopFuture in let s3 = try req.makeS3Client() return try s3.create(bucket: "api-created-bucket", region: .euCentral1, on: req).map(to: String.self) { return ":)" @@ -172,7 +172,7 @@ public func routes(_ router: Router) throws { } // Locate bucket (get region) - router.get("bucket/location") { req -> Future in + router.get("bucket/location") { req -> EventLoopFuture in let s3 = try req.makeS3Client() return try s3.location(bucket: "bucket-name", on: req).map(to: String.self) { region in return region.hostUrlString() @@ -190,7 +190,7 @@ public func routes(_ router: Router) throws { ) } // Delete bucket - router.delete("bucket") { req -> Future in + router.delete("bucket") { req -> EventLoopFuture in let s3 = try req.makeS3Client() return try s3.delete(bucket: "api-created-bucket", region: .euCentral1, on: req).map(to: String.self) { return ":)" @@ -204,7 +204,7 @@ public func routes(_ router: Router) throws { } // Get list of objects - router.get("files") { req -> Future in + router.get("files") { req -> EventLoopFuture 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() { @@ -215,7 +215,7 @@ public func routes(_ router: Router) throws { } // Demonstrate work with files - router.get("files/test") { req -> Future in + router.get("files/test") { req -> EventLoopFuture in let string = "Content of my example file" let fileName = "file-hu.txt" diff --git a/Sources/S3/Extensions/HTTPHeaders+Tools.swift b/Sources/S3/Extensions/HTTPHeaders+Tools.swift index 0534ab7..a6053ff 100644 --- a/Sources/S3/Extensions/HTTPHeaders+Tools.swift +++ b/Sources/S3/Extensions/HTTPHeaders+Tools.swift @@ -12,8 +12,7 @@ import Vapor extension HTTPHeaders { func string(_ name: String) -> String? { - let header = HTTPHeaderName(name) - return self[header].first + return self[name].first } func int(_ name: String) -> Int? { diff --git a/Sources/S3/Extensions/Region+Tools.swift b/Sources/S3/Extensions/Region+Tools.swift index 6034f46..31faebb 100644 --- a/Sources/S3/Extensions/Region+Tools.swift +++ b/Sources/S3/Extensions/Region+Tools.swift @@ -5,9 +5,8 @@ // Created by Ondrej Rafaj on 19/04/2018. // -import Foundation -import Core @_exported import S3Signer +import Foundation extension Region { diff --git a/Sources/S3/Extensions/Response+XMLDecoding.swift b/Sources/S3/Extensions/Response+XMLDecoding.swift index de3384c..3ab119d 100644 --- a/Sources/S3/Extensions/Response+XMLDecoding.swift +++ b/Sources/S3/Extensions/Response+XMLDecoding.swift @@ -31,7 +31,7 @@ extension Response { }() func decode(to: T.Type) throws -> T where T: Decodable { - guard let data = http.body.data else { + guard let data = self.body.data else { throw S3.Error.badResponse(self) } diff --git a/Sources/S3/Extensions/S3+Bucket.swift b/Sources/S3/Extensions/S3+Bucket.swift index b9f17f5..1c861ea 100644 --- a/Sources/S3/Extensions/S3+Bucket.swift +++ b/Sources/S3/Extensions/S3+Bucket.swift @@ -16,17 +16,23 @@ extension S3 { // MARK: Buckets /// Get bucket location - public func location(bucket: String, on container: Container) throws -> Future { - let builder = urlBuilder(for: container) + public func location(bucket: String, on eventLoop: EventLoop) -> EventLoopFuture { + let url: URL + let awsHeaders: HTTPHeaders let region = Region.euWest2 - let url = try builder.url(region: region, bucket: bucket, path: nil) - - let awsHeaders = try signer.headers(for: .GET, urlString: url.absoluteString, region: region, bucket: bucket, payload: .none) - return try make(request: url, method: .GET, headers: awsHeaders, data: emptyData(), on: container).map(to: Region.self) { response in - if response.http.status == .notFound { + + do { + url = try makeURLBuilder().url(region: region, bucket: bucket, path: nil) + awsHeaders = try signer.headers(for: .GET, urlString: url.absoluteString, region: region, bucket: bucket, payload: .none) + } catch let error { + return eventLoop.future(error: error) + } + + return make(request: url, method: .GET, headers: awsHeaders, data: emptyData(), on: eventLoop).flatMapThrowing { response in + if response.status == .notFound { throw Error.notFound } - if response.http.status == .ok { + if response.status == .ok { return region } else { if let error = try? response.decode(to: ErrorMessage.self), error.code == "PermanentRedirect", let endpoint = error.endpoint { @@ -49,36 +55,41 @@ extension S3 { } /// Delete bucket - public func delete(bucket: String, region: Region? = nil, on container: Container) throws -> Future { - let builder = urlBuilder(for: container) - let url = try builder.url(region: region, bucket: bucket, path: nil) - - let awsHeaders = try signer.headers(for: .DELETE, urlString: url.absoluteString, region: region, bucket: bucket, payload: .none) - return try make(request: url, method: .DELETE, headers: awsHeaders, data: emptyData(), on: container).map(to: Void.self) { response in - try self.check(response) - return Void() + public func delete(bucket: String, region: Region? = nil, on eventLoop: EventLoop) -> EventLoopFuture { + let url: URL + let awsHeaders: HTTPHeaders + + do { + url = try makeURLBuilder().url(region: region, bucket: bucket, path: nil) + awsHeaders = try signer.headers(for: .DELETE, urlString: url.absoluteString, region: region, bucket: bucket, payload: .none) + } catch let error { + return eventLoop.future(error: error) } + + return make(request: url, method: .DELETE, headers: awsHeaders, data: emptyData(), on: eventLoop).flatMapThrowing(self.check).transform(to: ()) } /// Create a bucket - public func create(bucket: String, region: Region? = nil, on container: Container) throws -> Future { + public func create(bucket: String, region: Region? = nil, on eventLoop: EventLoop) -> EventLoopFuture { let region = region ?? signer.config.region - - let builder = urlBuilder(for: container) - let url = try builder.url(region: region, bucket: bucket, path: nil) - let content = """ - - \(region.name) - - """ - - let data = content.convertToData() - let awsHeaders = try signer.headers(for: .PUT, urlString: url.absoluteString, region: region, bucket: bucket, payload: .bytes(data)) - return try make(request: url, method: .PUT, headers: awsHeaders, data: data, on: container).map(to: Void.self) { response in - try self.check(response) - return Void() + + \(region.name) + + """ + let data = Data(content.utf8) + + let awsHeaders: HTTPHeaders + let url: URL + + do { + url = try makeURLBuilder().url(region: region, bucket: bucket, path: nil) + awsHeaders = try signer.headers(for: .PUT, urlString: url.absoluteString, region: region, bucket: bucket, payload: .bytes(data)) + } catch let error { + return eventLoop.future(error: error) } + + return make(request: url, method: .PUT, headers: awsHeaders, data: data, on: eventLoop).flatMapThrowing(self.check).transform(to: ()) } } diff --git a/Sources/S3/Extensions/S3+Copy.swift b/Sources/S3/Extensions/S3+Copy.swift index 1fefdf4..c8505ef 100644 --- a/Sources/S3/Extensions/S3+Copy.swift +++ b/Sources/S3/Extensions/S3+Copy.swift @@ -14,30 +14,32 @@ extension S3 { // MARK: Copy /// Copy file on S3 - public func copy(file: LocationConvertible, to: LocationConvertible, headers: [String: String], on container: Container) throws -> Future { - let builder = urlBuilder(for: container) - let originPath = "\(file.bucket ?? defaultBucket)/\(file.path)" - let destinationUrl = try builder.url(file: to) - - var awsHeaders: [String: String] = headers - awsHeaders["x-amz-copy-source"] = originPath - let headers = try signer.headers( - for: .PUT, - urlString: destinationUrl.absoluteString, - headers: awsHeaders, - payload: .none - ) - - let request = Request(using: container) - request.http.method = .PUT - request.http.headers = headers - request.http.body = .empty - request.http.url = destinationUrl + public func copy(file: LocationConvertible, to: LocationConvertible, headers strHeaders: [String: String], on eventLoop: EventLoop) -> EventLoopFuture { + let headers: HTTPHeaders + let destinationUrl: URL + + do { + destinationUrl = try makeURLBuilder().url(file: to) + + var awsHeaders: [String: String] = strHeaders + awsHeaders["x-amz-copy-source"] = "\(file.bucket ?? defaultBucket)/\(file.path)" + headers = try signer.headers( + for: .PUT, + urlString: destinationUrl.absoluteString, + headers: awsHeaders, + payload: .none + ) + } catch let error { + return eventLoop.future(error: error) + } - let client = try container.make(Client.self) - return client.send(request).map { - try self.check($0) - return try $0.decode(to: File.CopyResponse.self) + var request = ClientRequest() + request.method = .PUT + request.headers = headers + request.url = URI(string: destinationUrl.description) + + return self.execute(request, on: eventLoop).flatMapThrowing { response in + return try self.check(response).content.decode(File.CopyResponse.self) } } diff --git a/Sources/S3/Extensions/S3+Delete.swift b/Sources/S3/Extensions/S3+Delete.swift index c7bf11d..10ca393 100644 --- a/Sources/S3/Extensions/S3+Delete.swift +++ b/Sources/S3/Extensions/S3+Delete.swift @@ -15,21 +15,24 @@ extension S3 { // MARK: Delete /// Delete file from S3 - public func delete(file: LocationConvertible, headers: [String: String], on container: Container) throws -> Future { - let builder = urlBuilder(for: container) - let url = try builder.url(file: file) - - let headers = try signer.headers(for: .DELETE, urlString: url.absoluteString, headers: headers, payload: .none) - return try make(request: url, method: .DELETE, headers: headers, data: emptyData(), on: container).map(to: Void.self) { response in - try self.check(response) - - return Void() + public func delete(file: LocationConvertible, headers strHeaders: [String: String], on eventLoop: EventLoop) -> EventLoopFuture { + let headers: HTTPHeaders + let url: URL + + do { + url = try makeURLBuilder().url(file: file) + headers = try signer.headers(for: .DELETE, urlString: url.absoluteString, headers: strHeaders, payload: .none) + } catch let error { + return eventLoop.future(error: error) } + + return make(request: url, method: .DELETE, headers: headers, data: emptyData(), on: eventLoop).flatMapThrowing(self.check).transform(to: ()) } /// Delete file from S3 - public func delete(file: LocationConvertible, on container: Container) throws -> Future { - return try delete(file: file, headers: [:], on: container) + public func delete(file: LocationConvertible, on eventLoop: EventLoop) -> EventLoopFuture { + return + delete(file: file, headers: [:], on: eventLoop) } } diff --git a/Sources/S3/Extensions/S3+Get.swift b/Sources/S3/Extensions/S3+Get.swift index 6375e5e..7c4f933 100644 --- a/Sources/S3/Extensions/S3+Get.swift +++ b/Sources/S3/Extensions/S3+Get.swift @@ -15,8 +15,8 @@ extension S3 { // MARK: URL /// File URL - public func url(fileInfo file: LocationConvertible, on container: Container) throws -> URL { - let builder = urlBuilder(for: container) + public func url(fileInfo file: LocationConvertible) throws -> URL { + let builder = makeURLBuilder() let url = try builder.url(file: file) return url } @@ -24,15 +24,21 @@ extension S3 { // MARK: Get /// Retrieve file data from S3 - public func get(file: LocationConvertible, headers: [String: String], on container: Container) throws -> Future { - let builder = urlBuilder(for: container) - let url = try builder.url(file: file) - - let headers = try signer.headers(for: .GET, urlString: url.absoluteString, headers: headers, payload: .none) - return try make(request: url, method: .GET, headers: headers, on: container).map(to: File.Response.self) { response in + public func get(file: LocationConvertible, headers strHeaders: [String: String], on eventLoop: EventLoop) -> EventLoopFuture { + let url: URL + let headers: HTTPHeaders + + do { + url = try makeURLBuilder().url(file: file) + headers = try signer.headers(for: .GET, urlString: url.absoluteString, headers: strHeaders, payload: .none) + } catch let error { + return eventLoop.future(error: error) + } + + return make(request: url, method: .GET, headers: headers, on: eventLoop).flatMapThrowing { response in try self.check(response) - guard let data = response.http.body.data else { + guard let data = response.body.data else { throw Error.missingData } @@ -42,8 +48,8 @@ extension S3 { } /// Retrieve file data from S3 - public func get(file: LocationConvertible, on container: Container) throws -> EventLoopFuture { - return try get(file: file, headers: [:], on: container) + public func get(file: LocationConvertible, on eventLoop: EventLoop) -> EventLoopFuture { + return get(file: file, headers: [:], on: eventLoop) } } diff --git a/Sources/S3/Extensions/S3+List.swift b/Sources/S3/Extensions/S3+List.swift index a88a766..e72b530 100644 --- a/Sources/S3/Extensions/S3+List.swift +++ b/Sources/S3/Extensions/S3+List.swift @@ -6,36 +6,45 @@ // import Foundation +import NIOHTTP1 // Helper S3 extension for getting file indexes extension S3 { /// Get list of objects - public func list(bucket: String, region: Region? = nil, headers: [String: String], on container: Container) throws -> Future { + public func list(bucket: String, region: Region? = nil, headers: [String: String], on eventLoop: EventLoop) -> EventLoopFuture { let region = region ?? signer.config.region guard let baseUrl = URL(string: region.hostUrlString(bucket: bucket)), let host = baseUrl.host, var components = URLComponents(string: baseUrl.absoluteString) else { - throw S3.Error.invalidUrl + return eventLoop.future(error: S3.Error.invalidUrl) } components.queryItems = [ URLQueryItem(name: "list-type", value: "2") ] guard let url = components.url else { - throw S3.Error.invalidUrl + return eventLoop.future(error: S3.Error.invalidUrl) } - var headers = headers - headers["host"] = host - let awsHeaders = try signer.headers(for: .GET, urlString: url.absoluteString, region: region, bucket: bucket, headers: headers, payload: .none) - return try make(request: url, method: .GET, headers: awsHeaders, data: emptyData(), on: container).map(to: BucketResults.self) { response in + + let awsHeaders: HTTPHeaders + + do { + var headers = headers + headers["host"] = host + awsHeaders = try signer.headers(for: .GET, urlString: url.absoluteString, region: region, bucket: bucket, headers: headers, payload: .none) + } catch let error { + return eventLoop.future(error: error) + } + + return make(request: url, method: .GET, headers: awsHeaders, data: emptyData(), on: eventLoop).flatMapThrowing { response in try self.check(response) return try response.decode(to: BucketResults.self) } } /// Get list of objects - public func list(bucket: String, region: Region? = nil, on container: Container) throws -> Future { - return try list(bucket: bucket, region: region, headers: [:], on: container) + public func list(bucket: String, region: Region? = nil, on eventLoop: EventLoop) -> EventLoopFuture { + return list(bucket: bucket, region: region, headers: [:], on: eventLoop) } } diff --git a/Sources/S3/Extensions/S3+Move.swift b/Sources/S3/Extensions/S3+Move.swift index de67935..0fbaaf3 100644 --- a/Sources/S3/Extensions/S3+Move.swift +++ b/Sources/S3/Extensions/S3+Move.swift @@ -14,11 +14,9 @@ extension S3 { // MARK: Move /// Copy file on S3 - public func move(file: LocationConvertible, to destination: LocationConvertible, headers: [String: String], on container: Container) throws -> EventLoopFuture { - return try copy(file: file, to: destination, headers: headers, on: container).flatMap(to: File.CopyResponse.self) { copyResult in - return try self.delete(file: file, on: container).map(to: File.CopyResponse.self) { _ in - return copyResult - } + public func move(file: LocationConvertible, to destination: LocationConvertible, headers: [String: String], on eventLoop: EventLoop) -> EventLoopFuture { + return copy(file: file, to: destination, headers: headers, on: eventLoop).flatMap { copyResult in + return self.delete(file: file, on: eventLoop).transform(to: copyResult) } } diff --git a/Sources/S3/Extensions/S3+ObjectInfo.swift b/Sources/S3/Extensions/S3+ObjectInfo.swift index 9cfd05b..ad677d6 100644 --- a/Sources/S3/Extensions/S3+ObjectInfo.swift +++ b/Sources/S3/Extensions/S3+ObjectInfo.swift @@ -17,37 +17,43 @@ extension S3 { /// Get acl file information (ACL) /// https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectGETacl.html - public func get(acl file: LocationConvertible, headers: [String: String], on container: Container) throws -> Future { + public func get(acl file: LocationConvertible, headers: [String: String], on eventLoop: EventLoop) -> EventLoopFuture { fatalError("Not implemented") } /// Get acl file information /// https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectGETacl.html - func get(acl file: LocationConvertible, on container: Container) throws -> Future { - return try get(fileInfo: file, headers: [:], on: container) + func get(acl file: LocationConvertible, on eventLoop: EventLoop) -> EventLoopFuture { + return get(fileInfo: file, headers: [:], on: eventLoop) } /// Get file information (HEAD) /// https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectHEAD.html - public func get(fileInfo file: LocationConvertible, headers: [String: String], on container: Container) throws -> Future { - let builder = urlBuilder(for: container) - let url = try builder.url(file: file) - - let headers = try signer.headers(for: .HEAD, urlString: url.absoluteString, headers: headers, payload: .none) - return try make(request: url, method: .HEAD, headers: headers, data: emptyData(), on: container).map(to: File.Info.self) { response in + public func get(fileInfo file: LocationConvertible, headers strHeaders: [String: String], on eventLoop: EventLoop) -> EventLoopFuture { + let url: URL + let headers: HTTPHeaders + + do { + url = try makeURLBuilder().url(file: file) + headers = try signer.headers(for: .HEAD, urlString: url.absoluteString, headers: strHeaders, payload: .none) + } catch let error { + return eventLoop.future(error: error) + } + + return make(request: url, method: .HEAD, headers: headers, data: emptyData(), on: eventLoop).flatMapThrowing { response in try self.check(response) let bucket = file.bucket ?? self.defaultBucket let region = file.region ?? self.signer.config.region - let mime = response.http.headers.string(File.Info.CodingKeys.mime.rawValue) - let size = response.http.headers.int(File.Info.CodingKeys.size.rawValue) - let server = response.http.headers.string(File.Info.CodingKeys.server.rawValue) - let etag = response.http.headers.string(File.Info.CodingKeys.etag.rawValue) - let expiration = response.http.headers.date(File.Info.CodingKeys.expiration.rawValue) - let created = response.http.headers.date(File.Info.CodingKeys.created.rawValue) - let modified = response.http.headers.date(File.Info.CodingKeys.modified.rawValue) - let versionId = response.http.headers.string(File.Info.CodingKeys.versionId.rawValue) - let storageClass = response.http.headers.string(File.Info.CodingKeys.storageClass.rawValue) + let mime = response.headers.string(File.Info.CodingKeys.mime.rawValue) + let size = response.headers.int(File.Info.CodingKeys.size.rawValue) + let server = response.headers.string(File.Info.CodingKeys.server.rawValue) + let etag = response.headers.string(File.Info.CodingKeys.etag.rawValue) + let expiration = response.headers.date(File.Info.CodingKeys.expiration.rawValue) + let created = response.headers.date(File.Info.CodingKeys.created.rawValue) + let modified = response.headers.date(File.Info.CodingKeys.modified.rawValue) + let versionId = response.headers.string(File.Info.CodingKeys.versionId.rawValue) + let storageClass = response.headers.string(File.Info.CodingKeys.storageClass.rawValue) let info = File.Info(bucket: bucket, region: region, path: file.path, access: .authenticatedRead, mime: mime, size: size, server: server, etag: etag, expiration: expiration, created: created, modified: modified, versionId: versionId, storageClass: storageClass) return info @@ -56,8 +62,8 @@ extension S3 { /// Get file information (HEAD) /// https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectHEAD.html - public func get(fileInfo file: LocationConvertible, on container: Container) throws -> Future { - return try get(fileInfo: file, headers: [:], on: container) + public func get(fileInfo file: LocationConvertible, on eventLoop: EventLoop) -> EventLoopFuture { + return get(fileInfo: file, headers: [:], on: eventLoop) } } diff --git a/Sources/S3/Extensions/S3+Private.swift b/Sources/S3/Extensions/S3+Private.swift index deaafde..201685d 100644 --- a/Sources/S3/Extensions/S3+Private.swift +++ b/Sources/S3/Extensions/S3+Private.swift @@ -7,12 +7,11 @@ import Foundation import Vapor -import HTTP extension S3 { /// Make an S3 request - func make(request url: URL, method: HTTPMethod, headers: HTTPHeaders, data: Data? = nil, cachePolicy: URLRequest.CachePolicy = .useProtocolCachePolicy, on container: Container) throws -> Future { + func make(request url: URL, method: HTTPMethod, headers: HTTPHeaders, data: Data? = nil, cachePolicy: URLRequest.CachePolicy = .useProtocolCachePolicy, on eventLoop: EventLoop) -> EventLoopFuture { var request = URLRequest(url: url, cachePolicy: cachePolicy) request.httpMethod = method.string request.httpBody = data @@ -20,38 +19,50 @@ extension S3 { request.addValue(val, forHTTPHeaderField: key.description) } - return execute(request, on: container) + return execute(request, on: eventLoop) } - + + func execute(_ request: ClientRequest, on eventLoop: EventLoop) -> EventLoopFuture { + guard let url = URL(string: request.url.string) else { + return eventLoop.future(error: Abort(.internalServerError, reason: "Found an invalid URL")) + } + + var urlRequest = URLRequest(url: url, cachePolicy: .useProtocolCachePolicy) + urlRequest.httpMethod = request.method.rawValue + urlRequest.httpBody = Data(request.body!.readableBytesView) + request.headers.forEach { key, value in + urlRequest.addValue(value, forHTTPHeaderField: key) + } + + return self.execute(urlRequest, on: eventLoop) + } + /// Execute given request with URLSession.shared - func execute(_ request: URLRequest, on container: Container) -> Future { - let promise = container.eventLoop.newPromise(Response.self) + func execute(_ request: URLRequest, on eventLoop: EventLoop) -> EventLoopFuture { + let promise = eventLoop.makePromise(of: Response.self) URLSession.shared.dataTask(with: request, completionHandler: { (data, urlResponse, error) in if let error = error { - promise.fail(error: error) + promise.fail(error) return } guard let httpResponse = urlResponse as? HTTPURLResponse else { - let error = VaporError(identifier: "httpURLResponse", reason: "URLResponse was not a HTTPURLResponse.") - promise.fail(error: error) + promise.fail(Abort(.internalServerError, reason: "URLResponse was not a HTTPURLResponse.")) return } - let response = S3.convert(foundationResponse: httpResponse, data: data, on: container) - - promise.succeed(result: Response(http: response, using: container)) + promise.succeed(S3.convert(foundationResponse: httpResponse, data: data)) }).resume() return promise.futureResult } - + /// Convert given response and data to HTTPResponse from Vapors HTTP package - static func convert(foundationResponse httpResponse: HTTPURLResponse, data: Data?, on worker: Worker) -> HTTPResponse { - var response = HTTPResponse(status: .init(statusCode: httpResponse.statusCode)) + static func convert(foundationResponse httpResponse: HTTPURLResponse, data: Data?) -> Response { + let response = Response(status: .init(statusCode: httpResponse.statusCode)) if let data = data { - response.body = HTTPBody(data: data) + response.body = Response.Body(data: data) } for (key, value) in httpResponse.allHeaderFields { response.headers.replaceOrAdd(name: "\(key)", value: "\(value)") diff --git a/Sources/S3/Extensions/S3+Put.swift b/Sources/S3/Extensions/S3+Put.swift index 6fa4641..d9c9ef0 100755 --- a/Sources/S3/Extensions/S3+Put.swift +++ b/Sources/S3/Extensions/S3+Put.swift @@ -16,24 +16,31 @@ extension S3 { // MARK: Upload /// Upload file to S3 - public func put(file: File.Upload, headers: [String: String], on container: Container) throws -> EventLoopFuture { - let builder = urlBuilder(for: container) - let url = try builder.url(file: file) - -// let url = URL(string: "https://s3.eu-west-2.amazonaws.com/s3-liveui-test/file-hu.txt")! - - var awsHeaders: [String: String] = headers - awsHeaders["content-type"] = file.mime.description - awsHeaders["x-amz-acl"] = file.access.rawValue - let headers = try signer.headers(for: .PUT, urlString: url.absoluteString, headers: awsHeaders, payload: Payload.bytes(file.data)) - - let request = Request(using: container) - request.http.method = .PUT - request.http.headers = headers - request.http.body = HTTPBody(data: file.data) - request.http.url = url - let client = try container.make(Client.self) - return client.send(request).map(to: File.Response.self) { response in + public func put(file: File.Upload, headers strHeaders: [String: String], on eventLoop: EventLoop) -> EventLoopFuture { + let headers: HTTPHeaders + let url: URL + + do { + url = try makeURLBuilder().url(file: file) + + var awsHeaders: [String: String] = strHeaders + awsHeaders["content-type"] = file.mime.description + awsHeaders["x-amz-acl"] = file.access.rawValue + headers = try signer.headers(for: .PUT, urlString: url.absoluteString, headers: awsHeaders, payload: .bytes(file.data)) + } catch let error { + return eventLoop.future(error: error) + } + + var buffer = ByteBufferAllocator().buffer(capacity: file.data.count) + buffer.writeBytes(file.data) + + var request = ClientRequest() + request.method = .PUT + request.headers = headers + request.body = buffer + request.url = URI(string: url.description) + + return self.execute(request, on: eventLoop).flatMapThrowing { response in try self.check(response) let res = File.Response(data: file.data, bucket: file.bucket ?? self.defaultBucket, path: file.path, access: file.access, mime: file.mime) return res @@ -41,34 +48,46 @@ extension S3 { } /// Upload file to S3 - public func put(file: File.Upload, on container: Container) throws -> EventLoopFuture { - return try put(file: file, headers: [:], on: container) + public func put(file: File.Upload, on eventLoop: EventLoop) -> EventLoopFuture { + return put(file: file, headers: [:], on: eventLoop) } /// Upload file by it's URL to S3 - public func put(file url: URL, destination: String, access: AccessControlList = .privateAccess, on container: Container) throws -> Future { - let data: Data = try Data(contentsOf: url) + public func put(file url: URL, destination: String, access: AccessControlList = .privateAccess, on eventLoop: EventLoop) -> EventLoopFuture { + let data: Data + do { + data = try Data(contentsOf: url) + } catch let error { + return eventLoop.future(error: error) + } + let file = File.Upload(data: data, bucket: nil, destination: destination, access: access, mime: mimeType(forFileAtUrl: url)) - return try put(file: file, on: container) + return put(file: file, on: eventLoop) } /// Upload file by it's path to S3 - public func put(file path: String, destination: String, access: AccessControlList = .privateAccess, on container: Container) throws -> Future { + public func put(file path: String, destination: String, access: AccessControlList = .privateAccess, on eventLoop: EventLoop) -> EventLoopFuture { let url: URL = URL(fileURLWithPath: path) - return try put(file: url, destination: destination, bucket: nil, access: access, on: container) + return put(file: url, destination: destination, bucket: nil, access: access, on: eventLoop) } /// Upload file by it's URL to S3, full set - public func put(file url: URL, destination: String, bucket: String?, access: AccessControlList = .privateAccess, on container: Container) throws -> Future { - let data: Data = try Data(contentsOf: url) + public func put(file url: URL, destination: String, bucket: String?, access: AccessControlList = .privateAccess, on eventLoop: EventLoop) -> EventLoopFuture { + let data: Data + do { + data = try Data(contentsOf: url) + } catch let error { + return eventLoop.future(error: error) + } + let file = File.Upload(data: data, bucket: bucket, destination: destination, access: access, mime: mimeType(forFileAtUrl: url)) - return try put(file: file, on: container) + return put(file: file, on: eventLoop) } /// Upload file by it's path to S3, full set - public func put(file path: String, destination: String, bucket: String?, access: AccessControlList = .privateAccess, on container: Container) throws -> Future { + public func put(file path: String, destination: String, bucket: String?, access: AccessControlList = .privateAccess, on eventLoop: EventLoop) -> EventLoopFuture { let url: URL = URL(fileURLWithPath: path) - return try put(file: url, destination: destination, bucket: bucket, access: access, on: container) + return put(file: url, destination: destination, bucket: bucket, access: access, on: eventLoop) } } diff --git a/Sources/S3/Extensions/S3+Service.swift b/Sources/S3/Extensions/S3+Service.swift index 634c99b..7578db9 100644 --- a/Sources/S3/Extensions/S3+Service.swift +++ b/Sources/S3/Extensions/S3+Service.swift @@ -16,11 +16,18 @@ extension S3 { // MARK: Buckets /// Get list of buckets - public func buckets(on container: Container) throws -> Future { - let builder = urlBuilder(for: container) - let url = try builder.plain(region: nil) - let headers = try signer.headers(for: .GET, urlString: url.absoluteString, payload: .none) - return try make(request: url, method: .GET, headers: headers, data: emptyData(), on: container).map(to: BucketsInfo.self) { response in + public func buckets(on eventLoop: EventLoop) -> EventLoopFuture { + let headers: HTTPHeaders + let url: URL + + do { + url = try makeURLBuilder().plain(region: nil) + headers = try signer.headers(for: .GET, urlString: url.absoluteString, payload: .none) + } catch let error { + return eventLoop.future(error: error) + } + + return make(request: url, method: .GET, headers: headers, data: emptyData(), on: eventLoop).flatMapThrowing { response in try self.check(response) return try response.decode(to: BucketsInfo.self) } diff --git a/Sources/S3/Extensions/S3+Strings.swift b/Sources/S3/Extensions/S3+Strings.swift index 4c69440..3eb0a1c 100755 --- a/Sources/S3/Extensions/S3+Strings.swift +++ b/Sources/S3/Extensions/S3+Strings.swift @@ -13,32 +13,32 @@ import Vapor extension S3 { /// Upload file content to S3, full set - public func put(string: String, mime: MediaType, destination: String, bucket: String?, access: AccessControlList, on container: Container) throws -> Future { + public func put(string: String, mime: HTTPMediaType, destination: String, bucket: String?, access: AccessControlList, on eventLoop: EventLoop) -> EventLoopFuture { guard let data: Data = string.data(using: String.Encoding.utf8) else { - throw Error.badStringData + return eventLoop.future(error: Error.badStringData) } let file = File.Upload(data: data, bucket: bucket, destination: destination, access: access, mime: mime.description) - return try put(file: file, on: container) + return put(file: file, on: eventLoop) } /// Upload file content to S3 - public func put(string: String, mime: MediaType, destination: String, access: AccessControlList, on container: Container) throws -> Future { - return try put(string: string, mime: mime, destination: destination, bucket: nil, access: access, on: container) + public func put(string: String, mime: HTTPMediaType, destination: String, access: AccessControlList, on eventLoop: EventLoop) -> EventLoopFuture { + return put(string: string, mime: mime, destination: destination, bucket: nil, access: access, on: eventLoop) } /// Upload file content to S3 - public func put(string: String, destination: String, access: AccessControlList, on container: Container) throws -> Future { - return try put(string: string, mime: .plainText, destination: destination, bucket: nil, access: access, on: container) + public func put(string: String, destination: String, access: AccessControlList, on eventLoop: EventLoop) -> EventLoopFuture { + return put(string: string, mime: .plainText, destination: destination, bucket: nil, access: access, on: eventLoop) } /// Upload file content to S3 - public func put(string: String, mime: MediaType, destination: String, on container: Container) throws -> Future { - return try put(string: string, mime: mime, destination: destination, access: .privateAccess, on: container) + public func put(string: String, mime: HTTPMediaType, destination: String, on eventLoop: EventLoop) -> EventLoopFuture { + return put(string: string, mime: mime, destination: destination, access: .privateAccess, on: eventLoop) } /// Upload file content to S3 - public func put(string: String, destination: String, on container: Container) throws -> Future { - return try put(string: string, mime: .plainText, destination: destination, bucket: nil, access: .privateAccess, on: container) + public func put(string: String, destination: String, on eventLoop: EventLoop) -> EventLoopFuture { + return put(string: string, mime: .plainText, destination: destination, bucket: nil, access: .privateAccess, on: eventLoop) } } diff --git a/Sources/S3/Extensions/Service+S3.swift b/Sources/S3/Extensions/Service+S3.swift index c99efa6..2a33eef 100644 --- a/Sources/S3/Extensions/Service+S3.swift +++ b/Sources/S3/Extensions/Service+S3.swift @@ -5,10 +5,9 @@ // Created by Ondrej Rafaj on 19/04/2018. // -import Foundation -import Service @_exported import S3Signer - +import Foundation +import Vapor extension Services { diff --git a/Sources/S3/Models/File.swift b/Sources/S3/Models/File.swift index ed7e1b1..312a798 100644 --- a/Sources/S3/Models/File.swift +++ b/Sources/S3/Models/File.swift @@ -36,7 +36,7 @@ public struct File { // MARK: Initialization /// File data to be uploaded - public init(data: Data, bucket: String? = nil, destination: String, access: AccessControlList = .privateAccess, mime: String = MediaType.plainText.description) { + public init(data: Data, bucket: String? = nil, destination: String, access: AccessControlList = .privateAccess, mime: String = HTTPMediaType.plainText.description) { self.data = data self.bucket = bucket self.path = destination diff --git a/Sources/S3/Protocols/S3Client.swift b/Sources/S3/Protocols/S3Client.swift index 0d61637..fd146d2 100644 --- a/Sources/S3/Protocols/S3Client.swift +++ b/Sources/S3/Protocols/S3Client.swift @@ -10,92 +10,92 @@ import Vapor /// S3 client Protocol -public protocol S3Client: Service { +public protocol S3Client { /// Get list of objects - func buckets(on: Container) throws -> Future + func buckets(on eventLoop: EventLoop) -> EventLoopFuture /// Create a bucket - func create(bucket: String, region: Region?, on container: Container) throws -> Future + func create(bucket: String, region: Region?, on eventLoop: EventLoop) -> EventLoopFuture /// Delete a bucket wherever it is -// func delete(bucket: String, on container: Container) throws -> Future +// func delete(bucket: String, on container: Container) -> EventLoopFuture /// Delete a bucket - func delete(bucket: String, region: Region?, on container: Container) throws -> Future + func delete(bucket: String, region: Region?, on eventLoop: EventLoop) -> EventLoopFuture /// Get bucket location - func location(bucket: String, on container: Container) throws -> Future + func location(bucket: String, on eventLoop: EventLoop) -> EventLoopFuture /// Get list of objects - func list(bucket: String, region: Region?, on container: Container) throws -> Future + func list(bucket: String, region: Region?, on eventLoop: EventLoop) -> EventLoopFuture /// Get list of objects - func list(bucket: String, region: Region?, headers: [String: String], on container: Container) throws -> Future + func list(bucket: String, region: Region?, headers: [String: String], on eventLoop: EventLoop) -> EventLoopFuture /// Upload file to S3 - func put(file: File.Upload, on container: Container) throws -> EventLoopFuture + func put(file: File.Upload, on eventLoop: EventLoop) -> EventLoopFuture /// Upload file to S3 - func put(file: File.Upload, headers: [String: String], on: Container) throws -> EventLoopFuture + func put(file: File.Upload, headers: [String: String], on eventLoop: EventLoop) -> EventLoopFuture /// Upload file to S3 - func put(file url: URL, destination: String, access: AccessControlList, on: Container) throws -> Future + func put(file url: URL, destination: String, access: AccessControlList, on eventLoop: EventLoop) -> EventLoopFuture /// Upload file to S3 - func put(file url: URL, destination: String, bucket: String?, access: AccessControlList, on: Container) throws -> Future + func put(file url: URL, destination: String, bucket: String?, access: AccessControlList, on eventLoop: EventLoop) -> EventLoopFuture /// Upload file to S3 - func put(file path: String, destination: String, access: AccessControlList, on: Container) throws -> Future + func put(file path: String, destination: String, access: AccessControlList, on eventLoop: EventLoop) -> EventLoopFuture /// Upload file to S3 - func put(file path: String, destination: String, bucket: String?, access: AccessControlList, on: Container) throws -> Future + func put(file path: String, destination: String, bucket: String?, access: AccessControlList, on eventLoop: EventLoop) -> EventLoopFuture /// Upload file to S3 - func put(string: String, destination: String, on: Container) throws -> Future + func put(string: String, destination: String, on eventLoop: EventLoop) -> EventLoopFuture /// Upload file to S3 - func put(string: String, destination: String, access: AccessControlList, on: Container) throws -> Future + func put(string: String, destination: String, access: AccessControlList, on eventLoop: EventLoop) -> EventLoopFuture /// Upload file to S3 - func put(string: String, mime: MediaType, destination: String, on: Container) throws -> Future + func put(string: String, mime: HTTPMediaType, destination: String, on eventLoop: EventLoop) -> EventLoopFuture /// Upload file to S3 - func put(string: String, mime: MediaType, destination: String, access: AccessControlList, on: Container) throws -> Future + func put(string: String, mime: HTTPMediaType, destination: String, access: AccessControlList, on eventLoop: EventLoop) -> EventLoopFuture /// Upload file to S3 - func put(string: String, mime: MediaType, destination: String, bucket: String?, access: AccessControlList, on: Container) throws -> Future + func put(string: String, mime: HTTPMediaType, destination: String, bucket: String?, access: AccessControlList, on eventLoop: EventLoop) -> EventLoopFuture /// File URL - func url(fileInfo file: LocationConvertible, on container: Container) throws -> URL + func url(fileInfo file: LocationConvertible) throws -> URL /// Retrieve file data from S3 - func get(fileInfo file: LocationConvertible, on container: Container) throws -> Future + func get(fileInfo file: LocationConvertible, on eventLoop: EventLoop) -> EventLoopFuture /// Retrieve file data from S3 - func get(fileInfo file: LocationConvertible, headers: [String: String], on container: Container) throws -> Future + func get(fileInfo file: LocationConvertible, headers: [String: String], on eventLoop: EventLoop) -> EventLoopFuture /// Retrieve file data from S3 - func get(file: LocationConvertible, on: Container) throws -> Future + func get(file: LocationConvertible, on eventLoop: EventLoop) -> EventLoopFuture /// Retrieve file data from S3 - func get(file: LocationConvertible, headers: [String: String], on: Container) throws -> Future + func get(file: LocationConvertible, headers: [String: String], on eventLoop: EventLoop) -> EventLoopFuture /// Delete file from S3 - func delete(file: LocationConvertible, on: Container) throws -> Future + func delete(file: LocationConvertible, on eventLoop: EventLoop) -> EventLoopFuture /// Delete file from S3 - func delete(file: LocationConvertible, headers: [String: String], on: Container) throws -> Future + func delete(file: LocationConvertible, headers: [String: String], on eventLoop: EventLoop) -> EventLoopFuture /// Copy file on S3 - func copy(file: LocationConvertible, to: LocationConvertible, headers: [String: String], on: Container) throws -> Future + func copy(file: LocationConvertible, to: LocationConvertible, headers: [String: String], on eventLoop: EventLoop) -> EventLoopFuture } extension S3Client { /// Copy file on S3 - public func copy(file: LocationConvertible, to: LocationConvertible, on container: Container) throws -> Future { - return try self.copy(file: file, to: to, headers: [:], on: container) + public func copy(file: LocationConvertible, to: LocationConvertible, on eventLoop: EventLoop) -> EventLoopFuture { + return self.copy(file: file, to: to, headers: [:], on: eventLoop) } } diff --git a/Sources/S3/S3.swift b/Sources/S3/S3.swift index 9a27f83..43f8d74 100755 --- a/Sources/S3/S3.swift +++ b/Sources/S3/S3.swift @@ -6,11 +6,9 @@ // Copyright © 2016 manGoweb UK Ltd. All rights reserved. // +@_exported import S3Signer import Foundation import Vapor -import HTTP -@_exported import S3Signer - /// Main S3 class public class S3: S3Client { @@ -19,6 +17,7 @@ public class S3: S3Client { public enum Error: Swift.Error { case invalidUrl case errorResponse(HTTPResponseStatus, ErrorMessage) + case badClientResponse(ClientResponse) case badResponse(Response) case badStringData case missingData @@ -40,9 +39,9 @@ public class S3: S3Client { @discardableResult public convenience init(defaultBucket: String, config: S3Signer.Config, services: inout Services) throws { let signer = try S3Signer(config) try self.init(defaultBucket: defaultBucket, signer: signer) - - services.register(signer) - services.register(self, as: S3Client.self) + + services.instance(signer) + services.instance(S3Client.self, self) } /// Basic initialization method @@ -68,25 +67,37 @@ extension S3 { // QUESTION: Can we replace this with just Data()? /// Serve empty data func emptyData() -> Data { - return "".convertToData() + return Data("".utf8) } /// Check response for error @discardableResult func check(_ response: Response) throws -> Response { - guard response.http.status == .ok || response.http.status == .noContent else { + guard response.status == .ok || response.status == .noContent else { if let error = try? response.decode(to: ErrorMessage.self) { - throw Error.errorResponse(response.http.status, error) + throw Error.errorResponse(response.status, error) } else { throw Error.badResponse(response) } } return response } + + /// Check response for error + @discardableResult func check(_ response: ClientResponse) throws -> ClientResponse { + guard response.status == .ok || response.status == .noContent else { + if let error = try? response.content.decode(ErrorMessage.self) { + throw Error.errorResponse(response.status, error) + } else { + throw Error.badClientResponse(response) + } + } + return response + } /// Get mime type for file static func mimeType(forFileAtUrl url: URL) -> String { - guard let mediaType = MediaType.fileExtension(url.pathExtension) else { - return MediaType(type: "application", subType: "octet-stream").description + guard let mediaType = HTTPMediaType.fileExtension(url.pathExtension) else { + return HTTPMediaType(type: "application", subType: "octet-stream").description } return mediaType.description } @@ -97,8 +108,8 @@ extension S3 { } /// Create URL builder - func urlBuilder(for container: Container) -> URLBuilder { - return urlBuilder ?? S3URLBuilder(container, defaultBucket: defaultBucket, config: signer.config) + func makeURLBuilder() -> URLBuilder { + return urlBuilder ?? S3URLBuilder(defaultBucket: defaultBucket, config: signer.config) } } diff --git a/Sources/S3/URLBuilder/S3URLBuilder.swift b/Sources/S3/URLBuilder/S3URLBuilder.swift index 9fcbf1d..4e3bcbc 100644 --- a/Sources/S3/URLBuilder/S3URLBuilder.swift +++ b/Sources/S3/URLBuilder/S3URLBuilder.swift @@ -13,9 +13,6 @@ import S3Signer /// URL builder public final class S3URLBuilder: URLBuilder { - /// Container - let container: Container - /// Default bucket let defaultBucket: String @@ -23,8 +20,7 @@ public final class S3URLBuilder: URLBuilder { let config: S3Signer.Config /// Initializer - public init(_ container: Container, defaultBucket: String, config: S3Signer.Config) { - self.container = container + public init(defaultBucket: String, config: S3Signer.Config) { self.defaultBucket = defaultBucket self.config = config } diff --git a/Sources/S3/URLBuilder/URLBuilder.swift b/Sources/S3/URLBuilder/URLBuilder.swift index da5e206..c474a83 100644 --- a/Sources/S3/URLBuilder/URLBuilder.swift +++ b/Sources/S3/URLBuilder/URLBuilder.swift @@ -31,7 +31,7 @@ extension Region { public protocol URLBuilder { /// Initializer - init(_ container: Container, defaultBucket: String, config: S3Signer.Config) + init(defaultBucket: String, config: S3Signer.Config) /// Plain Base URL with no bucket specified /// *Format: https://s3.eu-west-2.amazonaws.com/ diff --git a/Sources/S3DemoApp/S3DemoApp.swift b/Sources/S3DemoApp/S3DemoApp.swift index 0d78d6c..140c402 100644 --- a/Sources/S3DemoApp/S3DemoApp.swift +++ b/Sources/S3DemoApp/S3DemoApp.swift @@ -3,122 +3,109 @@ import Vapor @testable import S3 -public func routes(_ router: Router) throws { +public func routes(_ router: RoutesBuilder, s3: S3Client) throws { // Get all available buckets - router.get("buckets") { req -> Future in - let s3 = try req.makeS3Client() - return try s3.buckets(on: req) + router.get("buckets") { req -> EventLoopFuture in + return s3.buckets(on: req.eventLoop) } // Create new bucket - router.put("bucket") { req -> Future in - let s3 = try req.makeS3Client() - return try s3.create(bucket: "api-created-bucket", region: .euCentral1, on: req).map(to: String.self) { + router.put("bucket") { req -> EventLoopFuture in + return s3.create(bucket: "api-created-bucket", region: .euCentral1, on: req.eventLoop).map { return ":)" - }.catchMap({ (error) -> (String) in - if let error = error.s3ErrorMessage() { - return error.message - } - return ":(" + }.recover { error in + if let error = error.s3ErrorMessage() { + return error.message } - ) + return ":(" + } } // Delete bucket - router.delete("bucket") { req -> Future in - let s3 = try req.makeS3Client() - return try s3.delete(bucket: "api-created-bucket", region: .euCentral1, on: req).map(to: String.self) { + router.delete("bucket") { req -> EventLoopFuture in + return s3.delete(bucket: "api-created-bucket", region: .euCentral1, on: req.eventLoop).map { return ":)" - }.catchMap({ (error) -> (String) in - if let error = error.s3ErrorMessage() { - return error.message - } - return ":(" - } - ) + }.recover { error in + if let error = error.s3ErrorMessage() { + return error.message + } + return ":(" + } } // Delete bucket - router.get("files") { req -> Future in - let s3 = try req.makeS3Client() - return try s3.list(bucket: "booststore", region: .usEast1, headers: [:], on: req).catchMap({ (error) -> (BucketResults) in + router.get("files") { req -> EventLoopFuture in + return s3.list(bucket: "booststore", region: .usEast1, headers: [:], on: req.eventLoop).flatMapErrorThrowing { error in if let error = error.s3ErrorMessage() { print(error.message) } + throw error - }) + } } // Bucket location - router.get("bucket/location") { req -> Future in - let s3 = try req.makeS3Client() - return try s3.location(bucket: "adfasdfasdfasdf", on: req).map(to: String.self) { region in + router.get("bucket/location") { req -> EventLoopFuture in + return s3.location(bucket: "adfasdfasdfasdf", on: req.eventLoop).map { 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 :(" - } + }.recover { error -> String in + if let error = error as? S3.Error { + switch error { + case .errorResponse(_, let error): + return error.message + default: + return "S3 :(" } - return ":(" } - ) + return ":(" + } } // Demonstrate work with files - router.get("files/test") { req -> Future in + router.get("files/test") { req -> EventLoopFuture in let string = "Content of my example file" let fileName = "file-hu.txt" - - let s3 = try req.makeS3Client() - do { - return try s3.put(string: string, destination: fileName, access: .publicRead, on: req).flatMap(to: String.self) { putResponse in - print("PUT response:") - print(putResponse) - 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!") - return try s3.get(fileInfo: fileName, on: req).flatMap(to: String.self) { infoResponse in - print("HEAD/Info response:") - print(infoResponse) - 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 ":(" - } - ) + return s3.put(string: string, destination: fileName, access: .publicRead, on: req.eventLoop).flatMap { putResponse -> EventLoopFuture in + print("PUT response:") + print(putResponse) + return s3.get(file: fileName, on: req.eventLoop).flatMap { getResponse in + print("GET response:") + print(getResponse) + print(String(data: getResponse.data, encoding: .utf8) ?? "Unknown content!") + + return s3.get(fileInfo: fileName, on: req.eventLoop).flatMap { infoResponse in + print("HEAD/Info response:") + print(infoResponse) + + return s3.delete(file: fileName, on: req.eventLoop).flatMapThrowing { response in + print("DELETE response:") + print(response) + let json = try JSONEncoder().encode(infoResponse) + return String(data: json, encoding: .utf8) ?? "Unknown content!" + }.recover { error -> (String) in + if let error = error.s3ErrorMessage() { + return error.message + } + return ":(" } } - }.catchMap({ error -> (String) in - if let error = error.s3ErrorMessage() { - return error.message - } - return ":(" - }) - } catch { - print(error) - fatalError() + } + }.recover { error -> (String) in + if let error = error.s3ErrorMessage() { + return error.message + } + return ":(" } } } -public func configure(_ config: inout Config, _ env: inout Vapor.Environment, _ services: inout Services) throws { - let router = EngineRouter.default() - try routes(router) - services.register(router, as: Router.self) +public func configure(env: inout Vapor.Environment, _ services: inout Services) throws { + services.extend(RoutesBuilder.self) { router, container in + try routes(router, s3: container.makeS3Client()) + } // Get API key and secret from environmental variables guard let key = Environment.get("S3_ACCESS_KEY"), let secret = Environment.get("S3_SECRET") else { diff --git a/Sources/S3DemoRun/main.swift b/Sources/S3DemoRun/main.swift index 36cd445..791108e 100644 --- a/Sources/S3DemoRun/main.swift +++ b/Sources/S3DemoRun/main.swift @@ -3,18 +3,11 @@ import Service import Vapor do { - var config = Config.default() - var env = try Environment.detect() - var services = Services.default() - - try S3DemoApp.configure(&config, &env, &services) - - let app = try Application( - config: config, - environment: env, - services: services - ) - + var env: Vapor.Environment = .testing + let app = Application(environment: env, configure: { services in + try S3DemoApp.configure(env: &env, &services) + }) + try app.run() } catch { print("Top-level failure: \(error)") diff --git a/Sources/S3Signer/HTTPMethod+Description.swift b/Sources/S3Signer/HTTPMethod+Description.swift index b645684..a736cb4 100755 --- a/Sources/S3Signer/HTTPMethod+Description.swift +++ b/Sources/S3Signer/HTTPMethod+Description.swift @@ -1,5 +1,5 @@ import Foundation -import HTTP +import NIOHTTP1 extension HTTPMethod { @@ -38,6 +38,8 @@ extension HTTPMethod { return "PURGE" case .NOTIFY: return "NOTIFY" + case .SOURCE: + return "SOURCE" case .SEARCH: return "SEARCH" case .UNLOCK: diff --git a/Sources/S3Signer/Payload.swift b/Sources/S3Signer/Payload.swift index 72652fa..f43f5c3 100755 --- a/Sources/S3Signer/Payload.swift +++ b/Sources/S3Signer/Payload.swift @@ -1,5 +1,5 @@ import Vapor -import Crypto +import CryptoKit /// Payload object @@ -21,16 +21,16 @@ extension Payload { case .bytes(let bytes): return bytes default: - return "".convertToData() + return Data("".utf8) } } func hashed() throws -> String { switch self { case .bytes(let bytes): - return try SHA256.hash(bytes).hexEncodedString() + return try SHA256.hash(.data(bytes)).hexEncodedString() case .none: - return try SHA256.hash(Data()).hexEncodedString() + return try SHA256.hash(.data(Data())).hexEncodedString() case .unsigned: return "UNSIGNED-PAYLOAD" } diff --git a/Sources/S3Signer/S3Signer+Private.swift b/Sources/S3Signer/S3Signer+Private.swift index 39c522f..e53e351 100755 --- a/Sources/S3Signer/S3Signer+Private.swift +++ b/Sources/S3Signer/S3Signer+Private.swift @@ -1,7 +1,6 @@ import Foundation -import HTTP -import Crypto - +import CryptoKit +import Vapor /// Private interface @@ -50,16 +49,16 @@ extension S3Signer { } func createSignature(_ stringToSign: String, timeStampShort: String, region: Region) throws -> String { - let dateKey = try HMAC.SHA256.authenticate(timeStampShort.convertToData(), key: "AWS4\(config.secretKey)".convertToData()) - let dateRegionKey = try HMAC.SHA256.authenticate(region.name.description.convertToData(), key: dateKey) - let dateRegionServiceKey = try HMAC.SHA256.authenticate(config.service.convertToData(), key: dateRegionKey) - let signingKey = try HMAC.SHA256.authenticate("aws4_request".convertToData(), key: dateRegionServiceKey) - let signature = try HMAC.SHA256.authenticate(stringToSign.convertToData(), key: signingKey) + let dateKey = try HMAC.SHA256.authenticate(.string(timeStampShort), key: .string("AWS4\(config.secretKey)")) + let dateRegionKey = try HMAC.SHA256.authenticate(.string(region.name.description), key: dateKey) + let dateRegionServiceKey = try HMAC.SHA256.authenticate(.string(config.service), key: dateRegionKey) + let signingKey = try HMAC.SHA256.authenticate(.string("aws4_request"), key: dateRegionServiceKey) + let signature = try HMAC.SHA256.authenticate(.string(stringToSign), key: signingKey) return signature.hexEncodedString() } func createStringToSign(_ canonicalRequest: String, dates: Dates, region: Region) throws -> String { - let canonRequestHash = try SHA256.hash(canonicalRequest.convertToData()).hexEncodedString() + let canonRequestHash = try SHA256.hash(.string(canonicalRequest)).hexEncodedString() return ["AWS4-HMAC-SHA256", dates.long, credentialScope(dates.short, region: region), canonRequestHash].joined(separator: "\n") } @@ -122,7 +121,7 @@ extension S3Signer { let canonicalizedAmzHeaders = canonicalHeadersV2(headers) let canonicalizedResource = canonicalResourceV2(url: url, region: region, bucket: bucket) let stringToSign = "\(method)\n\(contentMD5)\n\(contentType)\n\(date)\n\(canonicalizedAmzHeaders)\n\(canonicalizedResource)" - let signature = try HMAC.SHA1.authenticate(stringToSign.convertToData(), key: config.secretKey.convertToData()).base64EncodedString() + let signature = try Data(HMAC.SHA1.authenticate(.string(stringToSign), key: .string(config.secretKey)).bytes()).base64EncodedString() let authHeader = "AWS \(config.accessKey):\(signature)" return authHeader } @@ -217,8 +216,8 @@ extension S3Signer { return presignedURL } - func headers(for httpMethod: HTTPMethod, urlString: URLRepresentable, region: Region? = nil, bucket: String? = nil, headers: [String: String] = [:], payload: Payload, dates: Dates) throws -> HTTPHeaders { - guard let url = urlString.convertToURL() else { + func headers(for httpMethod: HTTPMethod, urlString: String, region: Region? = nil, bucket: String? = nil, headers: [String: String] = [:], payload: Payload, dates: Dates) throws -> HTTPHeaders { + guard let url = URL(string: urlString) else { throw Error.badURL("\(urlString)") } @@ -227,13 +226,13 @@ extension S3Signer { var updatedHeaders = update(headers: headers, url: url, longDate: dates.long, bodyDigest: bodyDigest, region: region) if httpMethod == .PUT && payload.isBytes { - updatedHeaders["content-md5"] = try MD5.hash(payload.bytes).base64EncodedString() + updatedHeaders["content-md5"] = try Data(MD5.hash(.data(payload.bytes)).bytes()).base64EncodedString() } - + if httpMethod == .PUT || httpMethod == .DELETE { updatedHeaders["content-length"] = payload.size() if httpMethod == .PUT && url.pathExtension != "" { - updatedHeaders["content-type"] = (MediaType.fileExtension(url.pathExtension) ?? .plainText).description + updatedHeaders["content-type"] = (HTTPMediaType.fileExtension(url.pathExtension) ?? .plainText).description } } diff --git a/Sources/S3Signer/S3Signer.swift b/Sources/S3Signer/S3Signer.swift index 77180ce..66407cf 100755 --- a/Sources/S3Signer/S3Signer.swift +++ b/Sources/S3Signer/S3Signer.swift @@ -1,11 +1,10 @@ import Foundation -import Service -import HTTP -import Crypto +import CryptoKit +import Vapor /// S3 Client: All network calls to and from AWS' S3 servers -public final class S3Signer: Service { +public final class S3Signer { /// Errors public enum Error: Swift.Error { @@ -21,7 +20,7 @@ public final class S3Signer: Service { } /// S3 Configuration - public struct Config: Service { + public struct Config { /// AWS authentication version let authVersion: Version @@ -65,7 +64,7 @@ public final class S3Signer: Service { extension S3Signer { /// Generates auth headers for Simple Storage Services - public func headers(for httpMethod: HTTPMethod, urlString: URLRepresentable, region: Region? = nil, bucket: String? = nil, headers: [String: String] = [:], payload: Payload) throws -> HTTPHeaders { + public func headers(for httpMethod: HTTPMethod, urlString: String, region: Region? = nil, bucket: String? = nil, headers: [String: String] = [:], payload: Payload) throws -> HTTPHeaders { return try self.headers(for: httpMethod, urlString: urlString, region: region, bucket: bucket, headers: headers, payload: payload, dates: Dates(Date())) } diff --git a/Sources/S3TestTools/Extensions/Services+S3Mock.swift b/Sources/S3TestTools/Extensions/Services+S3Mock.swift index 1528205..3ccc6ec 100644 --- a/Sources/S3TestTools/Extensions/Services+S3Mock.swift +++ b/Sources/S3TestTools/Extensions/Services+S3Mock.swift @@ -6,15 +6,14 @@ // import Foundation -import Service +import Vapor import S3Signer import S3 -extension Services { - +extension Vapor.Services { public mutating func registerS3Mock() throws { - register(try! S3Mock(), as: S3Client.self) + try self.instance(S3Client.self, S3Mock()) } }