From 5afae04faa5929e7769fe04f52b1d3add1531083 Mon Sep 17 00:00:00 2001 From: Caleb Kleveter Date: Thu, 11 Jul 2019 16:27:22 -0500 Subject: [PATCH] Updated Implementation and API for Vapor 4 --- .swift-version | 2 +- .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../UserInterfaceState.xcuserstate | Bin 0 -> 66736 bytes .../xcschemes/xcschememanagement.plist | 67 ++++++++ Package.resolved | 142 ++++++----------- Package.swift | 6 +- README.md | 56 +++---- Sources/S3/Extensions/HTTPHeaders+Tools.swift | 3 +- Sources/S3/Extensions/Region+Tools.swift | 3 +- .../S3/Extensions/Response+XMLDecoding.swift | 2 +- Sources/S3/Extensions/S3+Bucket.swift | 73 +++++---- Sources/S3/Extensions/S3+Copy.swift | 48 +++--- Sources/S3/Extensions/S3+Delete.swift | 25 +-- Sources/S3/Extensions/S3+Get.swift | 28 ++-- Sources/S3/Extensions/S3+List.swift | 27 ++-- Sources/S3/Extensions/S3+Move.swift | 8 +- Sources/S3/Extensions/S3+ObjectInfo.swift | 46 +++--- Sources/S3/Extensions/S3+Private.swift | 43 ++++-- Sources/S3/Extensions/S3+Put.swift | 79 ++++++---- Sources/S3/Extensions/S3+Service.swift | 17 +- Sources/S3/Extensions/S3+Strings.swift | 22 +-- Sources/S3/Extensions/Service+S3.swift | 5 +- Sources/S3/Models/File.swift | 2 +- Sources/S3/Protocols/S3Client.swift | 58 +++---- Sources/S3/S3.swift | 37 +++-- Sources/S3/URLBuilder/S3URLBuilder.swift | 6 +- Sources/S3/URLBuilder/URLBuilder.swift | 2 +- Sources/S3DemoApp/S3DemoApp.swift | 145 ++++++++---------- Sources/S3DemoRun/main.swift | 17 +- Sources/S3Signer/HTTPMethod+Description.swift | 4 +- Sources/S3Signer/Payload.swift | 8 +- Sources/S3Signer/S3Signer+Private.swift | 29 ++-- Sources/S3Signer/S3Signer.swift | 11 +- .../Extensions/Services+S3Mock.swift | 7 +- 35 files changed, 562 insertions(+), 481 deletions(-) create mode 100644 .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata create mode 100644 .swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 .swiftpm/xcode/package.xcworkspace/xcuserdata/calebkleveter.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 .swiftpm/xcode/xcuserdata/calebkleveter.xcuserdatad/xcschemes/xcschememanagement.plist 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 0000000000000000000000000000000000000000..0100a0995394384959744397e2879a39798b562f GIT binary patch literal 66736 zcmeF42YeJo|L}KacK2?}T`mxM3xp1-^iC>-5&}u+5HFWYa->|iy8xl?AfN&^R1~oh z5CfuEK~NO1U_nF>>~1WpVY6f$^n$2o4F-{UQr+|ipe%k2vIr*w4sTm>Guo!K$qEp5c1 z758lq<(G37&dN3B!nh_}53VN{$whI|Tnrb>#c{p3-drDU7&n}Y=MuR4xXs)H+(X=> z++*AhZYTFN_YAj}+sD1gy~MrBy~e%49p>KRj&bjD?{O!%kGRv^C)`=?9QP&n756Q7 zk^7PRiTjQF9U+8~h%87!DsrGu)C4s}Em14f4uzwRs1xdjx}zu*je4Qps6QHjhM=J+ z0VSdlXe1hg#-j0P0-A^>p=oG3x(Ydw8|9%{s1SKkDGH!6RF3AO#b^n-8ZATDqU+Gz z=pJ-0x({tY6=);cgziUM&?D$k^cZ>)?La%xQ)n;RhxVfv(M#wRbP%0Hr_jgfH2MUc zL7$?}&{=dIT}0oZ@6iwF4|EB0I1D$zO>r~a9JjzNaVy*!hvP1|EAED4aUAZ2N8ph- z6_3KB@fbW7r{Qrp15d>{*oEifdAJPB;k#aJhEFcTXBC?n)AsSgmR*_rCt>iYcg={4cl5ON6@-W#>9wCpCC&@Ep z4|$e6M-Gse$t&bTa)NwBPLfmPV{)2&Le7wL-pRGwfyb;gZwuBA^u^0JO2p(DE}D$IKPvBmVb^v zz`xAD!hgu0;6LI|@~8NZ`P2L-{2Bfne}Vsr|C#?qunQrALkJZb35|s?p^4B`XeqQ8 zItbl`9zsuHxDYQS2#G?HkSwGKBZQH{7$HN*6taYg!XzPExJqyeWx^a`t}st17v>8K zgoVN)feP0O*9j|yRl+U8t-@`>7GbOKps-DNNO)M-E<7SUDm*DXBkU2L6`m6g2rmn- z2prGu-IjYS_ge0= zY_L>VHd;1WDlOYBk650v?6T~(9J0J&Ic#~;a>R1f@|NY8<+$ZN%PGq_%jcFaESIdD z6U{S$kXiSo>Q0S^HZDSch1XtYfTWt!dUA ztIL{ebzAeS`PKrf$2!YeVlA`IvCg&5vuajqU20uxz1@0;b)EH2>w4>5*1N6uSSzes ztdCkBvp#M;V13#8iuG0NYu1C-*R6-FZ&;67-?P4N{lNO6^^EmX>u1(qt-o1+xBg+h zBykdwutX$_WS2rDhtynZA+?mEq-ZHdik0G|UQ%zVkJMKhBqd0x(kN+ylp$qG1(HXa zB^63VQn6Gbd8JY*AT5v(hBKLX}xrpbhmVmbgy)uv_YznHcJml4@=vnozhd% zF6njYko1OhSb9@BA{~|9l8#C5N++dL(#O*0(ihTsnUj%>Wg_#kAd9j^w#us9SZ*b^ zmfOg&a-7^t?k)F``^x>~{_+5MsGKa1k;lqe@vQPHQbLGYI z68ReWTKPKp7WsDh4*4$mZh3=TA@7iP%1_C=#}U&|NdZ{%;~ALQTVKjcdar&tw9k(IVeJ0)CcuXIo%l#WU#rL)pS ziB@8iSY?P3uOuib$_Qn&GDb;P#w(MQ$;vdvskjxd5>VzT3l&XSuB=jSR&G<)D;tyw zWuvl7*{wXSJfrMUo>iVx_A2|7{mS#o3(AYiOUeP|W#x!+RC!A|rhKS;rkqvIDW5A} zDCdOysqx>#MJ-mTuF-mBiHZcr=Kjp`=#es#0@fLf_;QMalOs*kFV zsXNtu>VEZk^#%1s^(FPN`lfnBJ*pm8-%~$P�fP7u0Xm@715xU(`Qr*e2R!TT@#z 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 jHz9W(-+6X-&Rw&+v+#WbBcs`fH8k>L + + + + 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()) } }