From 49a0049766991fc5c8afbbdfbb7096db07c1e224 Mon Sep 17 00:00:00 2001 From: Andrew Edwards Date: Thu, 15 Aug 2019 17:43:43 -0400 Subject: [PATCH] Updated for Vapor 4 and GoogleCloudKit. --- Package.swift | 8 +- README.md | 82 ++-- .../Credentials/ApplicationDefault.swift | 32 -- .../Common/Credentials/Loaders.swift | 85 ---- .../Common/Credentials/ServiceAccount.swift | 53 --- .../Common/GoogleCloudAPIConfig.swift | 14 - .../GoogleCloud/Common/GoogleCloudError.swift | 10 - .../Common/OAuth/OAuthAccessToken.swift | 20 - .../OAuth/OAuthApplicationDefault.swift | 43 -- .../OAuthComputeEngine+AppEngineFlex.swift | 33 -- .../Common/OAuth/OAuthCredentialLoader.swift | 33 -- .../Common/OAuth/OAuthPayload.swift | 20 - .../Common/OAuth/OAuthRefreshable.swift | 26 -- .../Common/OAuth/OAuthServiceAccount.swift | 59 --- Sources/GoogleCloud/Extensions.swift | 101 ----- Sources/GoogleCloud/GoogleCloudProvider.swift | 30 ++ Sources/GoogleCloud/Provider.swift | 45 -- .../Storage/API/BucketAccessControlAPI.swift | 119 ----- .../GoogleCloud/Storage/API/ChannelsAPI.swift | 39 -- .../Storage/API/DefaultObjectACLAPI.swift | 119 ----- .../Storage/API/ObjectACLAPI.swift | 119 ----- .../Storage/API/StorageBucketAPI.swift | 429 ------------------ .../Storage/API/StorageNotificationsAPI.swift | 84 ---- .../Storage/API/StorageObjectAPI.swift | 217 --------- .../Storage/CloudStorageError.swift | 35 -- .../Storage/Models/Enums/StorageClass.swift | 19 - .../Storage/Models/Enums/StorageScope.swift | 21 - .../Storage/Models/IAMPolicy.swift | 53 --- .../Storage/Models/StorageBucket.swift | 374 --------------- .../Storage/Models/StorageBucketList.swift | 51 --- .../Storage/Models/StorageObject.swift | 269 ----------- Sources/GoogleCloud/Storage/README.md | 81 ---- .../GoogleCloud/Storage/StorageClient.swift | 90 ---- .../GoogleCloud/Storage/StorageConfig.swift | 27 -- .../GoogleCloud/Storage/StorageRequest.swift | 89 ---- Tests/GoogleCloudTests/CredentialTests.swift | 49 -- Tests/GoogleCloudTests/Fixtures/ADC.json | 6 - .../Fixtures/ServiceAccount.json | 12 - .../GoogleCloudProviderTests.swift | 15 - Tests/GoogleCloudTests/XCTestManifests.swift | 2 - Tests/LinuxMain.swift | 5 +- 41 files changed, 73 insertions(+), 2945 deletions(-) delete mode 100644 Sources/GoogleCloud/Common/Credentials/ApplicationDefault.swift delete mode 100644 Sources/GoogleCloud/Common/Credentials/Loaders.swift delete mode 100644 Sources/GoogleCloud/Common/Credentials/ServiceAccount.swift delete mode 100644 Sources/GoogleCloud/Common/GoogleCloudAPIConfig.swift delete mode 100644 Sources/GoogleCloud/Common/GoogleCloudError.swift delete mode 100644 Sources/GoogleCloud/Common/OAuth/OAuthAccessToken.swift delete mode 100644 Sources/GoogleCloud/Common/OAuth/OAuthApplicationDefault.swift delete mode 100644 Sources/GoogleCloud/Common/OAuth/OAuthComputeEngine+AppEngineFlex.swift delete mode 100644 Sources/GoogleCloud/Common/OAuth/OAuthCredentialLoader.swift delete mode 100644 Sources/GoogleCloud/Common/OAuth/OAuthPayload.swift delete mode 100644 Sources/GoogleCloud/Common/OAuth/OAuthRefreshable.swift delete mode 100644 Sources/GoogleCloud/Common/OAuth/OAuthServiceAccount.swift delete mode 100644 Sources/GoogleCloud/Extensions.swift create mode 100644 Sources/GoogleCloud/GoogleCloudProvider.swift delete mode 100644 Sources/GoogleCloud/Provider.swift delete mode 100644 Sources/GoogleCloud/Storage/API/BucketAccessControlAPI.swift delete mode 100644 Sources/GoogleCloud/Storage/API/ChannelsAPI.swift delete mode 100644 Sources/GoogleCloud/Storage/API/DefaultObjectACLAPI.swift delete mode 100644 Sources/GoogleCloud/Storage/API/ObjectACLAPI.swift delete mode 100644 Sources/GoogleCloud/Storage/API/StorageBucketAPI.swift delete mode 100644 Sources/GoogleCloud/Storage/API/StorageNotificationsAPI.swift delete mode 100644 Sources/GoogleCloud/Storage/API/StorageObjectAPI.swift delete mode 100644 Sources/GoogleCloud/Storage/CloudStorageError.swift delete mode 100644 Sources/GoogleCloud/Storage/Models/Enums/StorageClass.swift delete mode 100644 Sources/GoogleCloud/Storage/Models/Enums/StorageScope.swift delete mode 100644 Sources/GoogleCloud/Storage/Models/IAMPolicy.swift delete mode 100644 Sources/GoogleCloud/Storage/Models/StorageBucket.swift delete mode 100644 Sources/GoogleCloud/Storage/Models/StorageBucketList.swift delete mode 100644 Sources/GoogleCloud/Storage/Models/StorageObject.swift delete mode 100644 Sources/GoogleCloud/Storage/README.md delete mode 100644 Sources/GoogleCloud/Storage/StorageClient.swift delete mode 100644 Sources/GoogleCloud/Storage/StorageConfig.swift delete mode 100644 Sources/GoogleCloud/Storage/StorageRequest.swift delete mode 100644 Tests/GoogleCloudTests/CredentialTests.swift delete mode 100644 Tests/GoogleCloudTests/Fixtures/ADC.json delete mode 100644 Tests/GoogleCloudTests/Fixtures/ServiceAccount.json delete mode 100644 Tests/GoogleCloudTests/GoogleCloudProviderTests.swift diff --git a/Package.swift b/Package.swift index 231261d..e7b13ad 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:4.0 +// swift-tools-version:5.1 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -13,15 +13,15 @@ let package = Package( ], dependencies: [ // Dependencies declare other packages that this package depends on. - .package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"), - .package(url: "https://github.com/vapor/jwt.git", from: "3.0.0"), + .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0-alpha"), + .package(url: "https://github.com/vapor-community/GoogleCloudKit.git", from: "1.0.0-alpha.1") ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets can depend on other targets in this package, and on products in packages which this package depends on. .target( name: "GoogleCloud", - dependencies: ["Vapor","JWT"]), + dependencies: ["Vapor", "GoogleCloudKit"]), .testTarget( name: "GoogleCloudTests", dependencies: ["GoogleCloud"]), diff --git a/README.md b/README.md index 11beea5..4d04d5d 100644 --- a/README.md +++ b/README.md @@ -1,66 +1,58 @@ # GoogleCloudProvider -![Swift](http://img.shields.io/badge/swift-4.1-brightgreen.svg) -![Vapor](http://img.shields.io/badge/vapor-3.0-brightgreen.svg) +![Swift](http://img.shields.io/badge/swift-5.1-brightgreen.svg) +![Vapor](http://img.shields.io/badge/vapor-4.0-brightgreen.svg) -## This project aims to bring over as many [Google Cloud Platform APIs](https://cloud.google.com/products/) as possible to Vapor projects. +### GoogleCloudProvider is a Vapor wrapper around [GoogleCloudKit](https://github.com/vapor-community/GoogleCloudKit) -### Because the products and API's are so vast this will start slowly and over time add more API's as time goes on. -## Quickstart - -### Before you begin note that this package only supports using Service accounts to authenticate to the various Google Cloud Platform APIs using OAuth2. - -1. Select or create a Cloud Platform project. - -[Go to the projects page][projects] - -2. Enable billing for your project. - -[Enable billing][billing] - -3. Enable the Google Cloud Storage API. - -[Enable the API][enable_api] - -4. [Set up authentication with a service account][auth] so you can access the -API from your local workstation. - -[projects]: https://console.cloud.google.com/project -[billing]: https://support.google.com/cloud/answer/6293499#enable-billing -[enable_api]: https://console.cloud.google.com/flows/enableapi?apiid=storage-api.googleapis.com -[auth]: https://cloud.google.com/docs/authentication/getting-started - -### To begin using GogleCloudProvider in your project you'll need to setup the initial configuration +## Installation In your `Package.swift` file, add the following ```swift -.package(url: "https://github.com/vapor-community/google-cloud-provider.git", from: "0.1.0") +.package(url: "https://github.com/vapor-community/google-cloud-provider.git", from: "1.0.0-alpha.1") ``` -And In `Configure.swift` or wherever you setup your configuration in Vapor +Register the credentials configuration (required) and the provider in `Configure.swift` ```swift import GoogleCloud - let cloudConfig = GoogleCloudProviderConfig(project: "myprojectid-12345", credentialFile: "path to your service account json") - services.register(cloudConfig) - try services.register(GoogleCloudProvider()) + // register the credentials configuration which is used by all APIs + s.register(GoogleCloudCredentialsConfiguration.self) { _ in + return GoogleCloudCredentialsConfiguration(project: "myprojectid-12345", + credentialsFile: "~/path/to/service-account.json") + } + + // Register an API specific configuration. CloudStorage in this example. + s.register(GoogleCloudStorageConfiguration.self) { _ in + return GoogleCloudStorageConfiguration.defult() + } + + s.provider(GoogleCloudProvider()) ``` -Optionally, you can register an empty `GoogleCloudProviderConfig()` and configure the following environment variables: +Example usage +```swift -```shell -export PROJECT_ID=myprojectid-12345 -export GOOGLE_APPLICATION_CREDENTIALS=/path/to/your/service-account.json +struct UploadRequest: Content { + var data: Data + var filename: String +} + +func uploadImage(_ req: Request) throws { + let upload = try req.content.decode(UploadRequest.self) + + let storageClient = try container.make(GoogleCloudStorageClient.self) + storageClient.object.createSimpleUpload(bucket: "vapor-cloud-storage-demo", + data: upload.data, + name: upload.filename, + contentType: "image/jpeg").flatMap { uploadedObject in + print(uploadedObject.mediaLink) // prints the download link for the image. + } +} ``` -Additionally, you can copy and paste the contents of your Service Account JSON file as the value for the `GOOGLE_APPLICATION_CREDENTIALS` environment variable: - -```shell -export GOOGLE_APPLICATION_CREDENTIALS=[Valid Pasted JSON] -``` +### A More detailed guide can be found [here](https://github.com/vapor-community/GoogleCloudKit). -### Currently the following API's are implemented and you can follow the setup guides for each specific API to integrate with your Vapor project. -* [x] Cloud Storage [Setup guide](https://github.com/Andrewangeta/GoogleCloudProvider/tree/master/Sources/GoogleCloud/Storage/README.md) diff --git a/Sources/GoogleCloud/Common/Credentials/ApplicationDefault.swift b/Sources/GoogleCloud/Common/Credentials/ApplicationDefault.swift deleted file mode 100644 index 2b0ac65..0000000 --- a/Sources/GoogleCloud/Common/Credentials/ApplicationDefault.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// ApplicationDefault.swift -// Async -// -// Created by Brian Hatfield on 7/17/18. -// - -import Foundation - -// Loads credentials from ~/.config/gcloud/application_default_credentials.json -// -// Example JSON: -// { -// "client_id": "IDSTRING.apps.googleusercontent.com", -// "client_secret": "SECRETSTRING", -// "refresh_token": "REFRESHTOKEN", -// "type": "authorized_user" -// } - -struct GoogleApplicationDefaultCredentials: Codable { - let clientId: String - let clientSecret: String - let refreshToken: String - let type: String - - enum CodingKeys: String, CodingKey { - case clientId = "client_id" - case clientSecret = "client_secret" - case refreshToken = "refresh_token" - case type - } -} diff --git a/Sources/GoogleCloud/Common/Credentials/Loaders.swift b/Sources/GoogleCloud/Common/Credentials/Loaders.swift deleted file mode 100644 index bd97483..0000000 --- a/Sources/GoogleCloud/Common/Credentials/Loaders.swift +++ /dev/null @@ -1,85 +0,0 @@ -// -// Loaders.swift -// Async -// -// Created by Brian Hatfield on 7/17/18. -// - -import Foundation - -enum CredentialLoadError: GoogleCloudError { - - var localizedDescription: String { - switch self { - case .fileLoadError(let path): - return "Failed to load GoogleCloud credentials from the file path \(path)" - case .jsonLoadError: - return "Failed to load GoogleCloud credentials from the JSON provided in the environment variable" - } - } - var identifier: String { - switch self { - case .fileLoadError(_): - return "file-load-error" - case .jsonLoadError: - return "json-load-error" - } - } - - var reason: String { return localizedDescription } - - case fileLoadError(String) - case jsonLoadError -} - -extension GoogleApplicationDefaultCredentials { - init(contentsOfFile path: String) throws { - let decoder = JSONDecoder() - let filePath = NSString(string: path).expandingTildeInPath - - if let contents = try String(contentsOfFile: filePath).data(using: .utf8) { - self = try decoder.decode(GoogleApplicationDefaultCredentials.self, from: contents) - } else { - throw CredentialLoadError.fileLoadError(path) - } - } -} - -// Decodes JSON in String form -extension GoogleApplicationDefaultCredentials { - init(json: String) throws { - let decoder = JSONDecoder() - - if let data = json.data(using: .utf8) { - self = try decoder.decode(GoogleApplicationDefaultCredentials.self, from: data) - } else { - throw CredentialLoadError.jsonLoadError - } - } -} - -extension GoogleServiceAccountCredentials { - init(contentsOfFile path: String) throws { - let decoder = JSONDecoder() - let filePath = NSString(string: path).expandingTildeInPath - - if let contents = try String(contentsOfFile: filePath).data(using: .utf8) { - self = try decoder.decode(GoogleServiceAccountCredentials.self, from: contents) - } else { - throw CredentialLoadError.fileLoadError(path) - } - } -} - -// Decodes JSON in String form -extension GoogleServiceAccountCredentials { - init(json: String) throws { - let decoder = JSONDecoder() - - if let data = json.data(using: .utf8) { - self = try decoder.decode(GoogleServiceAccountCredentials.self, from: data) - } else { - throw CredentialLoadError.jsonLoadError - } - } -} diff --git a/Sources/GoogleCloud/Common/Credentials/ServiceAccount.swift b/Sources/GoogleCloud/Common/Credentials/ServiceAccount.swift deleted file mode 100644 index 50fd496..0000000 --- a/Sources/GoogleCloud/Common/Credentials/ServiceAccount.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// ServiceAccount.swift -// Async -// -// Created by Brian Hatfield on 7/17/18. -// - -import Foundation - -// Loads credentials from a file specified in env:GOOGLE_APPLICATION_CREDENTIALS -// -// Example JSON: -// -// { -// "type": "service_account", -// "project_id": "PROJECTID", -// "private_key_id": "PRIVATEKEYID", -// "private_key": "-----BEGIN PRIVATE KEY-----\nPEMPRIVATEKEY\n-----END PRIVATE KEY-----\n", -// "client_email": "SERVICEACCOUNTNAME@PROJECTID.iam.gserviceaccount.com", -// "client_id": "CLIENTID", -// "auth_uri": "https://accounts.google.com/o/oauth2/auth", -// "token_uri": "https://accounts.google.com/o/oauth2/token", -// "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", -// "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/SERVICEACCOUNTNAME%40PROJECTID.iam.gserviceaccount.com" -// } - -struct GoogleServiceAccountCredentials: Codable { - let type: String - let projectId: String - let privateKeyId: String - let privateKey: String - let clientEmail: String - let clientId: String - - let authUri: URL - let tokenUri: URL - let authProviderX509CertUrl: URL - let clientX509CertUrl: URL - - enum CodingKeys: String, CodingKey { - case type - case projectId = "project_id" - case privateKeyId = "private_key_id" - case privateKey = "private_key" - case clientEmail = "client_email" - case clientId = "client_id" - case authUri = "auth_uri" - case tokenUri = "token_uri" - case authProviderX509CertUrl = "auth_provider_x509_cert_url" - case clientX509CertUrl = "client_x509_cert_url" - } -} - diff --git a/Sources/GoogleCloud/Common/GoogleCloudAPIConfig.swift b/Sources/GoogleCloud/Common/GoogleCloudAPIConfig.swift deleted file mode 100644 index c2cd8df..0000000 --- a/Sources/GoogleCloud/Common/GoogleCloudAPIConfig.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// GoogleCloudAPIConfig.swift -// GoogleCloud -// -// Created by Andrew Edwards on 11/15/18. -// - -import Foundation - -public protocol GoogleCloudAPIConfig { - var scope: [String] { get } - var serviceAccount: String { get } - var project: String? { get } -} diff --git a/Sources/GoogleCloud/Common/GoogleCloudError.swift b/Sources/GoogleCloud/Common/GoogleCloudError.swift deleted file mode 100644 index a723f0f..0000000 --- a/Sources/GoogleCloud/Common/GoogleCloudError.swift +++ /dev/null @@ -1,10 +0,0 @@ -// -// GoogleCloudError.swift -// GoogleCloud -// -// Created by Andrew Edwards on 11/16/18. -// - -import Vapor - -public protocol GoogleCloudError: Debuggable {} diff --git a/Sources/GoogleCloud/Common/OAuth/OAuthAccessToken.swift b/Sources/GoogleCloud/Common/OAuth/OAuthAccessToken.swift deleted file mode 100644 index 58d269b..0000000 --- a/Sources/GoogleCloud/Common/OAuth/OAuthAccessToken.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// OAuthResponse.swift -// GoogleCloudProvider -// -// Created by Andrew Edwards on 4/15/18. -// - -import Vapor - -public struct OAuthAccessToken: Content { - var accessToken: String - var tokenType: String - var expiresIn: Int - - public enum CodingKeys: String, CodingKey { - case accessToken = "access_token" - case tokenType = "token_type" - case expiresIn = "expires_in" - } -} diff --git a/Sources/GoogleCloud/Common/OAuth/OAuthApplicationDefault.swift b/Sources/GoogleCloud/Common/OAuth/OAuthApplicationDefault.swift deleted file mode 100644 index eba945a..0000000 --- a/Sources/GoogleCloud/Common/OAuth/OAuthApplicationDefault.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// OAuthApplicationDefault.swift -// GoogleCloudProvider -// -// Created by Brian Hatfield on 7/17/18. -// - -import Vapor -import Crypto -import JWT - -public class OAuthApplicationDefault: OAuthRefreshable { - let client: Client - let credentials: GoogleApplicationDefaultCredentials - - init(credentials: GoogleApplicationDefaultCredentials, httpClient: Client) { - self.credentials = credentials - self.client = httpClient - } - - // Google Documentation for this approach: https://developers.google.com/identity/protocols/OAuth2WebServer#offline - public func refresh() throws -> Future { - let headers: HTTPHeaders = ["Content-Type": MediaType.urlEncodedForm.description] - - let encoder = URLEncodedFormEncoder() - - let bodyParts = [ - "client_id": credentials.clientId, - "client_secret": credentials.clientSecret, - "refresh_token": credentials.refreshToken, - "grant_type": "refresh_token", - ] - - let body = try encoder.encode(bodyParts) - - return client.post(GoogleOAuthTokenUrl, headers: headers, beforeSend: { $0.http.body = HTTPBody(data: body) }).flatMap(to: OAuthAccessToken.self) { (response) in - if response.http.status == .ok { - return try JSONDecoder().decode(OAuthAccessToken.self, from: response.http, maxSize: 65_536, on: response) - } - throw Abort(response.http.status, reason: "An unexpected error occured when attempting to refresh GoogleOAuth token.") - } - } -} diff --git a/Sources/GoogleCloud/Common/OAuth/OAuthComputeEngine+AppEngineFlex.swift b/Sources/GoogleCloud/Common/OAuth/OAuthComputeEngine+AppEngineFlex.swift deleted file mode 100644 index a46ab69..0000000 --- a/Sources/GoogleCloud/Common/OAuth/OAuthComputeEngine+AppEngineFlex.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// OAuthComputeEngine+AppEngineFlex.swift -// GoogleCloud -// -// Created by Andrew Edwards on 11/15/18. -// - -import Vapor - -// TODO: - Implement instance metadata API and have as separate module or class maybe? Probably a separate class/file. -// Implementation is really straight forward https://cloud.google.com/compute/docs/storing-retrieving-metadata#default -public class OAuthComputeEngineAppEngineFlex: OAuthRefreshable { - let serviceAccount: String - let client: Client - var serviceAccountTokenURL: String { - return "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/\(serviceAccount)/token" - } - - init(serviceAccount: String = "default", httpClient: Client) { - self.serviceAccount = serviceAccount - self.client = httpClient - } - - public func refresh() throws -> Future { - let headers: HTTPHeaders = ["Metadata-Flavor": "Google"] - return client.get(serviceAccountTokenURL, headers: headers, beforeSend: { _ in }).flatMap(to: OAuthAccessToken.self) { response in - if response.http.status == .ok { - return try JSONDecoder().decode(OAuthAccessToken.self, from: response.http, maxSize: 65_536, on: response) - } - throw Abort(response.http.status, reason: "An unexpected error occured when querying the metadata server.") - } - } -} diff --git a/Sources/GoogleCloud/Common/OAuth/OAuthCredentialLoader.swift b/Sources/GoogleCloud/Common/OAuth/OAuthCredentialLoader.swift deleted file mode 100644 index 2dfd85a..0000000 --- a/Sources/GoogleCloud/Common/OAuth/OAuthCredentialLoader.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// OAuthCredentialLoader.swift -// GoogleCloudProvider -// -// Created by Brian Hatfield on 7/19/18. -// - -import Vapor - -public class OAuthCredentialLoader { - public static func getRefreshableToken(credentialFilePath: String, withConfig config: GoogleCloudAPIConfig, andClient client: Client) throws -> OAuthRefreshable { - - // Tries to access file first - if let credentials = try? GoogleServiceAccountCredentials(contentsOfFile: credentialFilePath) { - return OAuthServiceAccount(credentials: credentials, scopes: config.scope, httpClient: client) - } - - // Then tries to decode as JSON - if let credentials = try? GoogleServiceAccountCredentials(json: credentialFilePath) { - return OAuthServiceAccount(credentials: credentials, scopes: config.scope, httpClient: client) - } - - if let credentials = try? GoogleApplicationDefaultCredentials(contentsOfFile: credentialFilePath) { - return OAuthApplicationDefault(credentials: credentials, httpClient: client) - } - - if let credentials = try? GoogleApplicationDefaultCredentials(json: credentialFilePath) { - return OAuthApplicationDefault(credentials: credentials, httpClient: client) - } - - return OAuthComputeEngineAppEngineFlex(serviceAccount: config.serviceAccount, httpClient: client) - } -} diff --git a/Sources/GoogleCloud/Common/OAuth/OAuthPayload.swift b/Sources/GoogleCloud/Common/OAuth/OAuthPayload.swift deleted file mode 100644 index 8a2617c..0000000 --- a/Sources/GoogleCloud/Common/OAuth/OAuthPayload.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// OAuthPayload.swift -// GoogleCloudProvider -// -// Created by Andrew Edwards on 4/15/18. -// - -import JWT - -public struct OAuthPayload: JWTPayload { - var iss: IssuerClaim - var scope: String - var aud: AudienceClaim - var iat: IssuedAtClaim - var exp: ExpirationClaim - - public func verify(using signer: JWTSigner) throws { - try exp.verifyNotExpired() - } -} diff --git a/Sources/GoogleCloud/Common/OAuth/OAuthRefreshable.swift b/Sources/GoogleCloud/Common/OAuth/OAuthRefreshable.swift deleted file mode 100644 index 97ddf59..0000000 --- a/Sources/GoogleCloud/Common/OAuth/OAuthRefreshable.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// OAuthRefreshable.swift -// GoogleCloudProvider -// -// Created by Brian Hatfield on 7/17/18. -// - -import Vapor - -// Constants for OAuth URLs. PascalCase style from this suggestion: https://stackoverflow.com/a/31893982 -let GoogleOAuthTokenUrl = "https://www.googleapis.com/oauth2/v4/token" -let GoogleOAuthTokenAudience = GoogleOAuthTokenUrl - -public protocol OAuthRefreshable { - func isFresh(token: OAuthAccessToken, created: Date) -> Bool - func refresh() throws -> Future -} - -extension OAuthRefreshable { - public func isFresh(token: OAuthAccessToken, created: Date) -> Bool { - let now = Date() - let expiration = created.addingTimeInterval(TimeInterval(token.expiresIn)) - - return expiration > now - } -} diff --git a/Sources/GoogleCloud/Common/OAuth/OAuthServiceAccount.swift b/Sources/GoogleCloud/Common/OAuth/OAuthServiceAccount.swift deleted file mode 100644 index 4318bc0..0000000 --- a/Sources/GoogleCloud/Common/OAuth/OAuthServiceAccount.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// OAuthServiceAccount.swift -// GoogleCloudProvider -// -// Created by Andrew Edwards on 4/15/18. -// - -import Vapor -import Crypto -import JWT - -public class OAuthServiceAccount: OAuthRefreshable { - let client: Client - let credentials: GoogleServiceAccountCredentials - - let scope: String - - init(credentials: GoogleServiceAccountCredentials, scopes: [String], httpClient: Client) { - self.credentials = credentials - self.scope = scopes.joined(separator: " ") - self.client = httpClient - } - - // Google Documentation for this approach: https://developers.google.com/identity/protocols/OAuth2ServiceAccount - public func refresh() throws -> Future { - let headers: HTTPHeaders = ["Content-Type": MediaType.urlEncodedForm.description] - let token = try generateJWT() - - let encoder = URLEncodedFormEncoder() - - let bodyParts = [ - "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", - "assertion": token - ] - - let body = try encoder.encode(bodyParts) - - return client.post(GoogleOAuthTokenUrl, headers: headers, beforeSend: { $0.http.body = HTTPBody(data: body) }).flatMap(to: OAuthAccessToken.self) { (response) in - if response.http.status == .ok { - return try JSONDecoder().decode(OAuthAccessToken.self, from: response.http, maxSize: 65_536, on: response) - } - throw Abort(response.http.status, reason: "An unexpected error occured when attempting to refresh GoogleOAuth token.") - } - } - - public func generateJWT() throws -> String { - let payload = OAuthPayload(iss: IssuerClaim(value: credentials.clientEmail), - scope: scope, - aud: AudienceClaim(value: GoogleOAuthTokenAudience), - iat: IssuedAtClaim(value: Date()), - exp: ExpirationClaim(value: Date().addingTimeInterval(3600))) - - let pk = try RSAKey.private(pem: credentials.privateKey) - let signer = JWTSigner.rs256(key: pk) - let jwt = JWT(payload: payload) - let jwtData = try jwt.sign(using: signer) - return String(data: jwtData, encoding: .utf8)! - } -} diff --git a/Sources/GoogleCloud/Extensions.swift b/Sources/GoogleCloud/Extensions.swift deleted file mode 100644 index 0031013..0000000 --- a/Sources/GoogleCloud/Extensions.swift +++ /dev/null @@ -1,101 +0,0 @@ -// -// Extensions.swift -// GoogleCloudProvider -// -// Created by Andrew Edwards on 4/21/18. -// - -import Vapor - -public protocol GoogleCloudModel: Content { - func toEncodedDictionary() throws -> [String: Any] -} - -extension GoogleCloudModel { - public func toEncodedDictionary() throws -> [String: Any] { - let encoded = try JSONEncoder().encode(self) - - return try JSONDecoder().decode(AnyDecodable.self, from: encoded).value as? [String: Any] ?? [:] - } -} - -public struct AnyDecodable: Decodable { - public var value: Any - - private struct CodingKeys: CodingKey { - var stringValue: String - var intValue: Int? - init?(intValue: Int) { - self.stringValue = "\(intValue)" - self.intValue = intValue - } - init?(stringValue: String) { self.stringValue = stringValue } - } - - public init(from decoder: Decoder) throws { - if let container = try? decoder.container(keyedBy: CodingKeys.self) { - var result = [String: Any]() - try container.allKeys.forEach { (key) throws in - result[key.stringValue] = try container.decode(AnyDecodable.self, forKey: key).value - } - value = result - } else if var container = try? decoder.unkeyedContainer() { - var result = [Any]() - while !container.isAtEnd { - result.append(try container.decode(AnyDecodable.self).value) - } - value = result - } else if let container = try? decoder.singleValueContainer() { - if let intVal = try? container.decode(Int.self) { - value = intVal - } else if let doubleVal = try? container.decode(Double.self) { - value = doubleVal - } else if let boolVal = try? container.decode(Bool.self) { - value = boolVal - } else if let stringVal = try? container.decode(String.self) { - value = stringVal - } else { - throw DecodingError.dataCorruptedError(in: container, debugDescription: "the container contains nothing serialisable") - } - } else { - throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Could not serialise")) - } - } -} - -extension Dictionary { - - var queryParameters: String { - guard let me = self as? [String: Any] else - { return "" } - return query(parameters: me) - } - - func query(parameters: [String: Any]) -> String { - var components: [(String, String)] = [] - - for key in parameters.keys { - let value = parameters[key]! - components += queryComponents(key: key, value) - } - return (components.map { "\($0)=\($1)" } as [String]).joined(separator: "&") - } - - public func queryComponents(key: String, _ value: Any) -> [(String, String)] { - var components: [(String, String)] = [] - - if let dictionary = value as? [String: Any] { - for (nestedKey, value) in dictionary { - components += queryComponents(key: "\(key)[\(nestedKey)]", value) - } - } else if let array = value as? [Any] { - for i in 0.., + data: <#T##Data#>, + name: <#T##String#>, + contentType: <#T##String#>, + queryParameters: <#T##[String : String]?#>) + } + } +} diff --git a/Sources/GoogleCloud/Provider.swift b/Sources/GoogleCloud/Provider.swift deleted file mode 100644 index 820ffff..0000000 --- a/Sources/GoogleCloud/Provider.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// StorageBucket.swift -// GoogleCloudProvider -// -// Created by Andrew Edwards on 4/17/18. -// - -import Vapor - -public struct GoogleCloudProviderConfig: Service { - let serviceAccountCredentialPath: String - let project: String? - - public init(project: String? = nil, credentialFile: String? = nil) { - self.project = project - - let env = ProcessInfo.processInfo.environment - // Locate the credentials to use for this client. In order of priority: - // - Environment Variable Specified Credentials (GOOGLE_APPLICATION_CREDENTIALS) - // - CredentialFile (optionally configured) - // - Application Default Credentials, located in the constant - self.serviceAccountCredentialPath = env["GOOGLE_APPLICATION_CREDENTIALS"] ?? - credentialFile ?? - "~/.config/gcloud/application_default_credentials.json" - } -} - - -public final class GoogleCloudProvider: Provider { - - public static let repositoryName = "google-cloud-provider" - - public init() {} - - public func boot(_ worker: Container) throws {} - - - public func didBoot(_ worker: Container) throws -> Future { - return .done(on: worker) - } - - public func register(_ services: inout Services) throws { - services.register(GoogleCloudStorageClient.self) - } -} diff --git a/Sources/GoogleCloud/Storage/API/BucketAccessControlAPI.swift b/Sources/GoogleCloud/Storage/API/BucketAccessControlAPI.swift deleted file mode 100644 index e6df25a..0000000 --- a/Sources/GoogleCloud/Storage/API/BucketAccessControlAPI.swift +++ /dev/null @@ -1,119 +0,0 @@ -// -// BucketAccessControlAPI.swift -// GoogleCloudProvider -// -// Created by Andrew Edwards on 5/19/18. -// - -import Vapor - -public protocol BucketAccessControlAPI { - func delete(bucket: String, entity: String, queryParameters: [String: String]?) throws -> Future - func get(bucket: String, entity: String, queryParameters: [String: String]?) throws -> Future - func create(bucket: String, entity: String, role: String, queryParameters: [String: String]?) throws -> Future - func list(bucket: String, queryParameters: [String: String]?) throws -> Future - func patch(bucket: String, entity: String, queryParameters: [String: String]?) throws -> Future - func update(bucket: String, entity: String, role: String?, queryParameters: [String: String]?) throws -> Future -} - -extension BucketAccessControlAPI { - public func delete(bucket: String, entity: String, queryParameters: [String: String]? = nil) throws -> Future { - return try delete(bucket: bucket, entity: entity, queryParameters: queryParameters) - } - - public func get(bucket: String, entity: String, queryParameters: [String: String]? = nil) throws -> Future { - return try get(bucket: bucket, entity: entity, queryParameters: queryParameters) - } - - public func create(bucket: String, entity: String, role: String, queryParameters: [String: String]? = nil) throws -> Future { - return try create(bucket: bucket, entity: entity, role: role, queryParameters: queryParameters) - } - - public func list(bucket: String, queryParameters: [String: String]? = nil) throws -> Future { - return try list(bucket: bucket, queryParameters: queryParameters) - } - - public func patch(bucket: String, entity: String, queryParameters: [String: String]? = nil) throws -> Future { - return try patch(bucket: bucket, entity: entity, queryParameters: queryParameters) - } - - public func update(bucket: String, entity: String, role: String? = nil, queryParameters: [String: String]? = nil) throws -> Future { - return try update(bucket: bucket, entity: entity, role: role, queryParameters: queryParameters) - } -} - -public final class GoogleBucketAccessControlAPI: BucketAccessControlAPI { - let endpoint = "https://www.googleapis.com/storage/v1/b" - let request: GoogleCloudStorageRequest - - init(request: GoogleCloudStorageRequest) { - self.request = request - } - - /// Permanently deletes the ACL entry for the specified entity on the specified bucket. - public func delete(bucket: String, entity: String, queryParameters: [String: String]?) throws -> Future { - var queryParams = "" - if let queryParameters = queryParameters { - queryParams = queryParameters.queryParameters - } - - return try request.send(method: .DELETE, path: "\(endpoint)/\(bucket)/acl/\(entity)", query: queryParams) - } - - /// Returns the ACL entry for the specified entity on the specified bucket. - public func get(bucket: String, entity: String, queryParameters: [String: String]?) throws -> Future { - var queryParams = "" - if let queryParameters = queryParameters { - queryParams = queryParameters.queryParameters - } - - return try request.send(method: .GET, path: "\(endpoint)/\(bucket)/acl/\(entity)", query: queryParams) - } - - /// Creates a new ACL entry on the specified bucket. - public func create(bucket: String, entity: String, role: String, queryParameters: [String: String]?) throws -> Future { - var queryParams = "" - if let queryParameters = queryParameters { - queryParams = queryParameters.queryParameters - } - - let body = try JSONEncoder().encode(["entity": entity, "role": role]).convertToHTTPBody() - - return try request.send(method: .POST, path: "\(endpoint)/\(bucket)/acl", query: queryParams, body: body) - } - - /// Retrieves ACL entries on a specified bucket. - public func list(bucket: String, queryParameters: [String: String]?) throws -> Future { - var queryParams = "" - if let queryParameters = queryParameters { - queryParams = queryParameters.queryParameters - } - - return try request.send(method: .GET, path: "\(endpoint)/\(bucket)/acl", query: queryParams) - } - - /// Updates an ACL entry on the specified bucket. This method supports patch semantics. - public func patch(bucket: String, entity: String, queryParameters: [String: String]?) throws -> Future { - var queryParams = "" - if let queryParameters = queryParameters { - queryParams = queryParameters.queryParameters - } - - return try request.send(method: .PATCH, path: "\(endpoint)/\(bucket)/acl/\(entity)", query: queryParams) - } - - /// Updates an ACL entry on the specified bucket. - public func update(bucket: String, entity: String, role: String?, queryParameters: [String: String]?) throws -> Future { - var queryParams = "" - if let queryParameters = queryParameters { - queryParams = queryParameters.queryParameters - } - var body = "" - - if let role = role { - body = try JSONEncoder().encode(["role": role]).convert(to: String.self) - } - - return try request.send(method: .POST, path: "\(endpoint)/\(bucket)/acl/\(entity)", query: queryParams, body: body.convertToHTTPBody()) - } -} diff --git a/Sources/GoogleCloud/Storage/API/ChannelsAPI.swift b/Sources/GoogleCloud/Storage/API/ChannelsAPI.swift deleted file mode 100644 index 68c1c2a..0000000 --- a/Sources/GoogleCloud/Storage/API/ChannelsAPI.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// ChannelsAPI.swift -// GoogleCloudProvider -// -// Created by Andrew Edwards on 5/19/18. -// - -import Vapor - -public protocol ChannelsAPI { - func stop(channelId: String, resourceId: String, queryParameters: [String: String]?) throws -> Future -} - -extension ChannelsAPI { - public func stop(channelId: String, resourceId: String, queryParameters: [String: String]? = nil) throws -> Future { - return try stop(channelId: channelId, resourceId: resourceId, queryParameters: queryParameters) - } -} - -public final class GoogleChannelsAPI: ChannelsAPI { - let endpoint = "https://www.googleapis.com/storage/v1/channels" - let request: GoogleCloudStorageRequest - - init(request: GoogleCloudStorageRequest) { - self.request = request - } - - /// Stop receiving object change notifications through this channel. - public func stop(channelId: String, resourceId: String, queryParameters: [String: String]?) throws -> Future { - var queryParams = "" - if let queryParameters = queryParameters { - queryParams = queryParameters.queryParameters - } - - let requestBody = try JSONEncoder().encode(["id": channelId, "resourceid": resourceId]).convertToHTTPBody() - - return try request.send(method: .POST, path: "\(endpoint)/stop)", query: queryParams, body: requestBody) - } -} diff --git a/Sources/GoogleCloud/Storage/API/DefaultObjectACLAPI.swift b/Sources/GoogleCloud/Storage/API/DefaultObjectACLAPI.swift deleted file mode 100644 index b1143b6..0000000 --- a/Sources/GoogleCloud/Storage/API/DefaultObjectACLAPI.swift +++ /dev/null @@ -1,119 +0,0 @@ -// -// DefaultObjectACLAPI.swift -// GoogleCloudProvider -// -// Created by Andrew Edwards on 5/20/18. -// - -import Vapor - -public protocol DefaultObjectACLAPI { - func delete(bucket: String, entity: String, queryParameters: [String: String]?) throws -> Future - func get(bucket: String, entity: String, queryParameters: [String: String]?) throws -> Future - func create(bucket: String, entity: String, role: String, queryParameters: [String: String]?) throws -> Future - func list(bucket: String, queryParameters: [String: String]?) throws -> Future - func patch(bucket: String, entity: String, queryParameters: [String: String]?) throws -> Future - func update(bucket: String, entity: String, defaultAccessControl: ObjectAccessControls?, queryParameters: [String: String]?) throws -> Future -} - -extension DefaultObjectACLAPI { - public func delete(bucket: String, entity: String, queryParameters: [String: String]? = nil) throws -> Future { - return try delete(bucket: bucket, entity: entity, queryParameters: queryParameters) - } - - public func get(bucket: String, entity: String, queryParameters: [String: String]? = nil) throws -> Future { - return try get(bucket: bucket, entity: entity, queryParameters: queryParameters) - } - - public func create(bucket: String, entity: String, role: String, queryParameters: [String: String]? = nil) throws -> Future { - return try create(bucket: bucket, entity: entity, role: role, queryParameters: queryParameters) - } - - public func list(bucket: String, queryParameters: [String: String]? = nil) throws -> Future { - return try list(bucket: bucket, queryParameters: queryParameters) - } - - public func patch(bucket: String, entity: String, queryParameters: [String: String]? = nil) throws -> Future { - return try patch(bucket: bucket, entity: entity, queryParameters: queryParameters) - } - - public func update(bucket: String, entity: String, defaultAccessControl: ObjectAccessControls? = nil, queryParameters: [String: String]? = nil) throws -> Future { - return try update(bucket: bucket, entity: entity, defaultAccessControl: defaultAccessControl, queryParameters: queryParameters) - } -} - -public final class GoogleDefaultObjectACLAPI: DefaultObjectACLAPI { - let endpoint = "https://www.googleapis.com/storage/v1/b" - let request: GoogleCloudStorageRequest - - init(request: GoogleCloudStorageRequest) { - self.request = request - } - - /// Permanently deletes the default object ACL entry for the specified entity on the specified bucket. - public func delete(bucket: String, entity: String, queryParameters: [String: String]?) throws -> Future { - var queryParams = "" - if let queryParameters = queryParameters { - queryParams = queryParameters.queryParameters - } - - return try request.send(method: .DELETE, path: "\(endpoint)/\(bucket)/defaultObjectAcl/\(entity)", query: queryParams) - } - - /// Returns the default object ACL entry for the specified entity on the specified bucket. - public func get(bucket: String, entity: String, queryParameters: [String: String]?) throws -> Future { - var queryParams = "" - if let queryParameters = queryParameters { - queryParams = queryParameters.queryParameters - } - - return try request.send(method: .GET, path: "\(endpoint)/\(bucket)/defaultObjectAcl/\(entity)", query: queryParams) - } - - /// Creates a new default object ACL entry on the specified bucket. - public func create(bucket: String, entity: String, role: String, queryParameters: [String: String]?) throws -> Future { - var queryParams = "" - if let queryParameters = queryParameters { - queryParams = queryParameters.queryParameters - } - - let body = try JSONEncoder().encode(["entity": entity, "role": role]).convertToHTTPBody() - - return try request.send(method: .POST, path: "\(endpoint)/\(bucket)/defaultObjectAcl", query: queryParams, body: body) - } - - /// Retrieves default object ACL entries on the specified bucket. - public func list(bucket: String, queryParameters: [String: String]?) throws -> Future { - var queryParams = "" - if let queryParameters = queryParameters { - queryParams = queryParameters.queryParameters - } - - return try request.send(method: .GET, path: "\(endpoint)/\(bucket)/defaultObjectAcl", query: queryParams) - } - - /// Updates a default object ACL entry on the specified bucket. This method supports patch semantics. - public func patch(bucket: String, entity: String, queryParameters: [String: String]?) throws -> Future { - var queryParams = "" - if let queryParameters = queryParameters { - queryParams = queryParameters.queryParameters - } - - return try request.send(method: .PATCH, path: "\(endpoint)/\(bucket)/defaultObjectAcl/\(entity)", query: queryParams) - } - - /// Updates a default object ACL entry on the specified bucket. - public func update(bucket: String, entity: String, defaultAccessControl: ObjectAccessControls?, queryParameters: [String: String]?) throws -> Future { - var queryParams = "" - if let queryParameters = queryParameters { - queryParams = queryParameters.queryParameters - } - var body = "" - - if let defaultAccessControl = defaultAccessControl { - body = try JSONSerialization.data(withJSONObject: try defaultAccessControl.toEncodedDictionary()).convert(to: String.self) - } - - return try request.send(method: .POST, path: "\(endpoint)/\(bucket)/defaultObjectAcl/\(entity)", query: queryParams, body: body.convertToHTTPBody()) - } -} diff --git a/Sources/GoogleCloud/Storage/API/ObjectACLAPI.swift b/Sources/GoogleCloud/Storage/API/ObjectACLAPI.swift deleted file mode 100644 index e853765..0000000 --- a/Sources/GoogleCloud/Storage/API/ObjectACLAPI.swift +++ /dev/null @@ -1,119 +0,0 @@ -// -// ObjectACLAPI.swift -// GoogleCloudProvider -// -// Created by Andrew Edwards on 5/20/18. -// - -import Vapor - -public protocol ObjectAccessControlsAPI { - func delete(bucket: String, entity: String, object: String, queryParameters: [String: String]?) throws -> Future - func get(bucket: String, entity: String, object: String, queryParameters: [String: String]?) throws -> Future - func create(bucket: String, object: String, entity: String, role: String, queryParameters: [String: String]?) throws -> Future - func list(bucket: String, object: String, queryParameters: [String: String]?) throws -> Future - func patch(bucket: String, object: String, entity: String, queryParameters: [String: String]?) throws -> Future - func update(bucket: String, object: String, entity: String, defaultAccessControl: ObjectAccessControls?, queryParameters: [String: String]?) throws -> Future -} - -extension ObjectAccessControlsAPI { - public func delete(bucket: String, entity: String, object: String, queryParameters: [String: String]? = nil) throws -> Future { - return try delete(bucket: bucket, entity: entity, object: object, queryParameters: queryParameters) - } - - public func get(bucket: String, entity: String, object: String, queryParameters: [String: String]? = nil) throws -> Future { - return try get(bucket: bucket, entity: entity, object: object, queryParameters: queryParameters) - } - - public func create(bucket: String, object: String, entity: String, role: String, queryParameters: [String: String]? = nil) throws -> Future { - return try create(bucket: bucket, object: object, entity: entity, role: role, queryParameters: queryParameters) - } - - public func list(bucket: String, object: String, queryParameters: [String: String]? = nil) throws -> Future { - return try list(bucket: bucket, object: object, queryParameters: queryParameters) - } - - public func patch(bucket: String, object: String, entity: String, queryParameters: [String: String]? = nil) throws -> Future { - return try patch(bucket: bucket, object: object, entity: entity, queryParameters: queryParameters) - } - - public func update(bucket: String, object: String, entity: String, defaultAccessControl: ObjectAccessControls? = nil, queryParameters: [String: String]? = nil) throws -> Future { - return try update(bucket: bucket, object: object, entity: entity, defaultAccessControl: defaultAccessControl, queryParameters: queryParameters) - } -} - -public final class GoogleObjectAccessControlsAPI: ObjectAccessControlsAPI { - let endpoint = "https://www.googleapis.com/storage/v1/b" - let request: GoogleCloudStorageRequest - - init(request: GoogleCloudStorageRequest) { - self.request = request - } - - /// Permanently deletes the ACL entry for the specified entity on the specified object. - public func delete(bucket: String, entity: String, object: String, queryParameters: [String: String]?) throws -> Future { - var queryParams = "" - if let queryParameters = queryParameters { - queryParams = queryParameters.queryParameters - } - - return try request.send(method: .DELETE, path: "\(endpoint)/\(bucket)/o/\(object)/acl/\(entity)", query: queryParams) - } - - /// Returns the ACL entry for the specified entity on the specified object. - public func get(bucket: String, entity: String, object: String, queryParameters: [String: String]?) throws -> Future { - var queryParams = "" - if let queryParameters = queryParameters { - queryParams = queryParameters.queryParameters - } - - return try request.send(method: .GET, path: "\(endpoint)/\(bucket)/o/\(object)acl/\(entity)", query: queryParams) - } - - /// Creates a new ACL entry on the specified object. - public func create(bucket: String, object: String, entity: String, role: String, queryParameters: [String: String]?) throws -> Future { - var queryParams = "" - if let queryParameters = queryParameters { - queryParams = queryParameters.queryParameters - } - - let body = try JSONEncoder().encode(["entity": entity, "role": role]).convertToHTTPBody() - - return try request.send(method: .POST, path: "\(endpoint)/\(bucket)/o/\(object)/acl", query: queryParams, body: body) - } - - /// Retrieves ACL entries on the specified object. - public func list(bucket: String, object: String, queryParameters: [String: String]?) throws -> Future { - var queryParams = "" - if let queryParameters = queryParameters { - queryParams = queryParameters.queryParameters - } - - return try request.send(method: .GET, path: "\(endpoint)/\(bucket)/o/\(object)/acl", query: queryParams) - } - - /// Updates an ACL entry on the specified object. This method supports patch semantics. - public func patch(bucket: String, object: String, entity: String, queryParameters: [String: String]?) throws -> Future { - var queryParams = "" - if let queryParameters = queryParameters { - queryParams = queryParameters.queryParameters - } - - return try request.send(method: .PATCH, path: "\(endpoint)/\(bucket)/o/\(object)/acl/\(entity)", query: queryParams) - } - - /// Updates an ACL entry on the specified object. - public func update(bucket: String, object: String, entity: String, defaultAccessControl: ObjectAccessControls?, queryParameters: [String: String]?) throws -> Future { - var queryParams = "" - if let queryParameters = queryParameters { - queryParams = queryParameters.queryParameters - } - var body = "" - - if let defaultAccessControl = defaultAccessControl { - body = try JSONSerialization.data(withJSONObject: try defaultAccessControl.toEncodedDictionary()).convert(to: String.self) - } - - return try request.send(method: .POST, path: "\(endpoint)/\(bucket)/o/\(object)/acl/\(entity)", query: queryParams, body: body.convertToHTTPBody()) - } -} diff --git a/Sources/GoogleCloud/Storage/API/StorageBucketAPI.swift b/Sources/GoogleCloud/Storage/API/StorageBucketAPI.swift deleted file mode 100644 index 3f2a316..0000000 --- a/Sources/GoogleCloud/Storage/API/StorageBucketAPI.swift +++ /dev/null @@ -1,429 +0,0 @@ -// -// StorageBucket.swift -// GoogleCloudProvider -// -// Created by Andrew Edwards on 4/17/18. -// - -import Vapor - -public protocol StorageBucketAPI { - func delete(bucket: String, queryParameters: [String: String]?) throws -> Future - func get(bucket: String, queryParameters: [String: String]?) throws -> Future - func getIAMPolicy(bucket: String, queryParameters: [String: String]?) throws -> Future - func create(queryParameters: [String: String]?, name: String, acl: [BucketAccessControls]?, billing: Billing?, cors: [Cors]?, defaultObjectAcl: [ObjectAccessControls]?, encryption: Encryption?, labels: [String: String]?, lifecycle: Lifecycle?, location: String?, logging: Logging?, storageClass: StorageClass?, versioning: Versioning?, website: Website?) throws -> Future - func list(queryParameters: [String: String]?) throws -> Future - func patch(bucket: String, queryParameters: [String: String]?, acl: [BucketAccessControls]?, billing: Billing?, cors: [Cors]?, defaultObjectAcl: [ObjectAccessControls]?, encryption: Encryption?, labels: [String: String]?, lifecycle: Lifecycle?, logging: Logging?, versioning: Versioning?, website: Website?) throws -> Future - func setIAMPolicy(bucket: String, iamPolicy: IAMPolicy, queryParameters: [String : String]?) throws -> Future - func testIAMPermissions(bucket: String, permissions: [String], queryParameters: [String : String]?) throws -> Future - func update(bucket: String, acl: [BucketAccessControls], queryParameters: [String: String]?, billing: Billing?, cors: [Cors]?, defaultObjectAcl: [ObjectAccessControls]?, encryption: Encryption?, labels: [String: String]?, lifecycle: Lifecycle?, logging: Logging?, storageClass: StorageClass?, versioning: Versioning?, website: Website?) throws -> Future -} - -extension StorageBucketAPI { - public func delete(bucket: String, queryParameters: [String: String]? = nil) throws -> Future { - return try delete(bucket: bucket, queryParameters: queryParameters) - } - - public func get(bucket: String, queryParameters: [String: String]? = nil) throws -> Future { - return try get(bucket: bucket, queryParameters: queryParameters) - } - - public func getIAMPolicy(bucket: String, queryParameters: [String: String]? = nil) throws -> Future { - return try getIAMPolicy(bucket: bucket, queryParameters: queryParameters) - } - - public func create( queryParameters: [String: String]? = nil, - name: String, - acl: [BucketAccessControls]? = nil, - billing: Billing? = nil, - cors: [Cors]? = nil, - defaultObjectAcl: [ObjectAccessControls]? = nil, - encryption: Encryption? = nil, - labels: [String: String]? = nil, - lifecycle: Lifecycle? = nil, - location: String? = nil, - logging: Logging? = nil, - storageClass: StorageClass? = nil, - versioning: Versioning? = nil, - website: Website? = nil) throws -> Future { - return try create(queryParameters: queryParameters, - name: name, - acl: acl, - billing: billing, - cors: cors, - defaultObjectAcl: defaultObjectAcl, - encryption: encryption, - labels: labels, - lifecycle: lifecycle, - location: location, - logging: logging, - storageClass: storageClass, - versioning: versioning, - website: website) - } - - public func list(queryParameters: [String: String]? = nil) throws -> Future { - return try list(queryParameters: queryParameters) - } - - public func patch(bucket: String, - queryParameters: [String: String]? = nil, - acl: [BucketAccessControls]? = nil, - billing: Billing? = nil, - cors: [Cors]? = nil, - defaultObjectAcl: [ObjectAccessControls]? = nil, - encryption: Encryption? = nil, - labels: [String: String]? = nil, - lifecycle: Lifecycle? = nil, - logging: Logging? = nil, - versioning: Versioning? = nil, - website: Website? = nil) throws -> Future { - return try patch(bucket: bucket, - queryParameters: queryParameters, - acl: acl, - billing: billing, - cors: cors, - defaultObjectAcl: defaultObjectAcl, - encryption: encryption, - labels: labels, - lifecycle: lifecycle, - logging: logging, - versioning: versioning, - website: website) - } - - public func setIAMPolicy(bucket: String, iamPolicy: IAMPolicy, queryParameters: [String: String]? = nil) throws -> Future { - return try setIAMPolicy(bucket: bucket, iamPolicy: iamPolicy, queryParameters: queryParameters) - } - - public func testIAMPermissions(bucket: String, permissions: [String], queryParameters: [String: String]? = nil) throws -> Future { - return try testIAMPermissions(bucket: bucket, permissions: permissions, queryParameters: queryParameters) - } - - public func update(bucket: String, - acl: [BucketAccessControls], - queryParameters: [String: String]? = nil, - billing: Billing? = nil, - cors: [Cors]? = nil, - defaultObjectAcl: [ObjectAccessControls]? = nil, - encryption: Encryption? = nil, - labels: [String: String]? = nil, - lifecycle: Lifecycle? = nil, - logging: Logging? = nil, - storageClass: StorageClass? = nil, - versioning: Versioning? = nil, - website: Website? = nil) throws -> Future { - return try update(bucket: bucket, - acl: acl, - queryParameters: queryParameters, - billing: billing, - cors: cors, - defaultObjectAcl: defaultObjectAcl, - encryption: encryption, - labels: labels, - lifecycle: lifecycle, - logging: logging, - storageClass: storageClass, - versioning: versioning, - website: website) - } -} - -public final class GoogleStorageBucketAPI: StorageBucketAPI { - let endpoint = "https://www.googleapis.com/storage/v1/b" - let request: GoogleCloudStorageRequest - - init(request: GoogleCloudStorageRequest) { - self.request = request - } - - /// Permanently deletes an empty bucket. - public func delete(bucket: String, queryParameters: [String: String]?) throws -> Future { - var queryParams = "" - if let queryParameters = queryParameters { - queryParams = queryParameters.queryParameters - } - - return try request.send(method: .DELETE, path: "\(endpoint)/\(bucket)", query: queryParams) - } - - /// Returns metadata for the specified bucket. - public func get(bucket: String, queryParameters: [String: String]?) throws -> Future { - var queryParams = "" - if let queryParameters = queryParameters { - queryParams = queryParameters.queryParameters - } - - return try request.send(method: .GET, path: "\(endpoint)/\(bucket)", query: queryParams) - } - - /// Returns an IAM policy for the specified bucket. - public func getIAMPolicy(bucket: String, queryParameters: [String: String]?) throws -> Future { - var queryParams = "" - if let queryParameters = queryParameters { - queryParams = queryParameters.queryParameters - } - - return try request.send(method: .GET, path: "\(endpoint)/\(bucket)/iam", query: queryParams) - } - - /// Creates a new bucket. - public func create(queryParameters: [String: String]?, - name: String, - acl: [BucketAccessControls]?, - billing: Billing?, - cors: [Cors]?, - defaultObjectAcl: [ObjectAccessControls]?, - encryption: Encryption?, - labels: [String: String]?, - lifecycle: Lifecycle?, - location: String?, - logging: Logging?, - storageClass: StorageClass?, - versioning: Versioning?, - website: Website?) throws -> Future { - var body: [String: Any] = ["name": name] - var query = "" - - if var queryParameters = queryParameters { - queryParameters["project"] = request.project - query = queryParameters.queryParameters - } - else { - query = "project=\(request.project)" - } - - if let acl = acl { - body["acl"] = try acl.map { try $0.toEncodedDictionary() } - } - - if let billing = billing { - body["billing"] = try billing.toEncodedDictionary() - } - - if let cors = cors { - body["cors"] = try cors.map { try $0.toEncodedDictionary() } - } - - if let defaultObjectAcl = defaultObjectAcl { - body["defaultObjectAcl"] = try defaultObjectAcl.map { try $0.toEncodedDictionary() } - } - - if let encryption = encryption { - body["encryption"] = try encryption.toEncodedDictionary() - } - - if let labels = labels { - body["labels"] = labels - } - - if let lifecycle = lifecycle { - body["lifecycle"] = try lifecycle.toEncodedDictionary() - } - - if let location = location { - body["location"] = location - } - - if let logging = logging { - body["logging"] = try logging.toEncodedDictionary() - } - - if let storageClass = storageClass { - body["storageClass"] = storageClass.rawValue - } - - if let versioning = versioning { - body["versioning"] = try versioning.toEncodedDictionary() - } - - if let website = website { - body["website"] = try website.toEncodedDictionary() - } - - let requestBody = try JSONSerialization.data(withJSONObject: body).convertToHTTPBody() - - return try request.send(method: .POST, path: endpoint, query: query, body: requestBody) - } - - /// Retrieves a list of buckets for a given project. - public func list(queryParameters: [String: String]?) throws -> Future { - var query = "" - - if var queryParameters = queryParameters { - queryParameters["project"] = request.project - query = queryParameters.queryParameters - } - else { - query = "project=\(request.project)" - } - - return try request.send(method: .GET, path: endpoint, query: query) - } - - /// Updates a bucket. Changes to the bucket will be readable immediately after writing, but configuration changes may take time to propagate. - public func patch(bucket: String, - queryParameters: [String: String]?, - acl: [BucketAccessControls]?, - billing: Billing?, - cors: [Cors]?, - defaultObjectAcl: [ObjectAccessControls]?, - encryption: Encryption?, - labels: [String: String]?, - lifecycle: Lifecycle?, - logging: Logging?, - versioning: Versioning?, - website: Website?) throws -> Future { - var body: [String: Any] = [:] - var query = "" - - if let queryParameters = queryParameters { - query = queryParameters.queryParameters - } - - if let acl = acl { - body["acl"] = try acl.map { try $0.toEncodedDictionary() } - } - - if let billing = billing { - body["billing"] = try billing.toEncodedDictionary() - } - - if let cors = cors { - body["cors"] = try cors.map { try $0.toEncodedDictionary() } - } - - if let defaultObjectAcl = defaultObjectAcl { - body["defaultObjectAcl"] = try defaultObjectAcl.map { try $0.toEncodedDictionary() } - } - - if let encryption = encryption { - body["encryption"] = try encryption.toEncodedDictionary() - } - - if let labels = labels { - body["labels"] = labels - } - - if let lifecycle = lifecycle { - body["lifecycle"] = try lifecycle.toEncodedDictionary() - } - - if let logging = logging { - body["logging"] = try logging.toEncodedDictionary() - } - - if let versioning = versioning { - body["versioning"] = try versioning.toEncodedDictionary() - } - - if let website = website { - body["website"] = try website.toEncodedDictionary() - } - - let requestBody = try JSONSerialization.data(withJSONObject: body).convertToHTTPBody() - - return try request.send(method: .PATCH, path: endpoint, query: query, body: requestBody) - } - - /// Updates an IAM policy for the specified bucket. - public func setIAMPolicy(bucket: String, iamPolicy: IAMPolicy, queryParameters: [String: String]?) throws -> Future { - var query = "" - - if let queryParameters = queryParameters { - query = queryParameters.queryParameters - } - - let requestBody = try JSONSerialization.data(withJSONObject: try iamPolicy.toEncodedDictionary()).convertToHTTPBody() - - return try request.send(method: .PUT, path: "\(endpoint)/\(bucket)/iam", query: query, body: requestBody) - } - - /// Tests a set of permissions on the given bucket to see which, if any, are held by the caller. - public func testIAMPermissions(bucket: String, permissions: [String], queryParameters: [String: String]?) throws -> Future { - var query = "" - - if let queryParameters = queryParameters { - query = queryParameters.queryParameters - // if there are any permissions it's safe to add an ampersand to the end of the query we currently have. - if permissions.count > 0 { - query.append("&") - } - } - - let perms = permissions.map({ "permissions=\($0)" }).joined(separator: "&") - - query.append(perms) - - return try request.send(method: .GET, path: "\(endpoint)/\(bucket)/iam/testPermissions", query: query) - } - - /// Updates a bucket. Changes to the bucket will be readable immediately after writing, but configuration changes may take time to propagate. This method sets the complete metadata of a bucket. If you want to change some of a bucket's metadata while leaving other parts unaffected, use the PATCH function instead. - public func update(bucket: String, - acl: [BucketAccessControls], - queryParameters: [String: String]?, - billing: Billing?, - cors: [Cors]?, - defaultObjectAcl: [ObjectAccessControls]?, - encryption: Encryption?, - labels: [String: String]?, - lifecycle: Lifecycle?, - logging: Logging?, - storageClass: StorageClass?, - versioning: Versioning?, - website: Website?) throws -> Future { - var body: [String: Any] = [:] - var query = "" - - body["acl"] = try acl.map { try $0.toEncodedDictionary() } - - if let queryParameters = queryParameters { - query = queryParameters.queryParameters - } - - - if let billing = billing { - body["billing"] = try billing.toEncodedDictionary() - } - - if let cors = cors { - body["cors"] = try cors.map { try $0.toEncodedDictionary() } - } - - if let defaultObjectAcl = defaultObjectAcl { - body["defaultObjectAcl"] = try defaultObjectAcl.map { try $0.toEncodedDictionary() } - } - - if let encryption = encryption { - body["encryption"] = try encryption.toEncodedDictionary() - } - - if let labels = labels { - body["labels"] = labels - } - - if let lifecycle = lifecycle { - body["lifecycle"] = try lifecycle.toEncodedDictionary() - } - - if let logging = logging { - body["logging"] = try logging.toEncodedDictionary() - } - - if let storageClass = storageClass { - body["storageClass"] = storageClass.rawValue - } - - if let versioning = versioning { - body["versioning"] = try versioning.toEncodedDictionary() - } - - if let website = website { - body["website"] = try website.toEncodedDictionary() - } - - let requestBody = try JSONSerialization.data(withJSONObject: body).convertToHTTPBody() - - return try request.send(method: .PUT, path: "\(endpoint)/\(bucket)", query: query, body: requestBody) - } -} diff --git a/Sources/GoogleCloud/Storage/API/StorageNotificationsAPI.swift b/Sources/GoogleCloud/Storage/API/StorageNotificationsAPI.swift deleted file mode 100644 index 364f3eb..0000000 --- a/Sources/GoogleCloud/Storage/API/StorageNotificationsAPI.swift +++ /dev/null @@ -1,84 +0,0 @@ -// -// StorageNotificationsAPI.swift -// GoogleCloudProvider -// -// Created by Andrew Edwards on 5/20/18. -// - -import Vapor - -public protocol StorageNotificationsAPI { - func delete(bucket: String, notification: String, queryParameters: [String: String]?) throws -> Future - func get(bucket: String, notification: String, queryParameters: [String: String]?) throws -> Future - func create(bucket: String, notification: StorageNotification, queryParameters: [String: String]?) throws -> Future - func list(bucket: String, queryParameters: [String: String]?) throws -> Future -} - -extension StorageNotificationsAPI { - public func delete(bucket: String, notification: String, queryParameters: [String: String]? = nil) throws -> Future { - return try delete(bucket: bucket, notification: notification, queryParameters: queryParameters) - } - - public func get(bucket: String, notification: String, queryParameters: [String: String]? = nil) throws -> Future { - return try get(bucket: bucket, notification: notification, queryParameters: queryParameters) - } - - public func create(bucket: String, notification: StorageNotification, queryParameters: [String: String]? = nil) throws -> Future { - return try create(bucket: bucket, notification: notification, queryParameters: queryParameters) - } - - public func list(bucket: String, queryParameters: [String: String]? = nil) throws -> Future { - return try list(bucket: bucket, queryParameters: queryParameters) - } -} - -public final class GoogleStorageNotificationsAPI: StorageNotificationsAPI { - let endpoint = "https://www.googleapis.com/storage/v1/b" - let request: GoogleCloudStorageRequest - - init(request: GoogleCloudStorageRequest) { - self.request = request - } - - /// Permanently deletes a notification subscription. - public func delete(bucket: String, notification: String, queryParameters: [String: String]?) throws -> Future { - var queryParams = "" - if let queryParameters = queryParameters { - queryParams = queryParameters.queryParameters - } - - return try request.send(method: .DELETE, path: "\(endpoint)/\(bucket)/notificationConfigs/\(notification)", query: queryParams) - } - - /// View a notification configuration. - public func get(bucket: String, notification: String, queryParameters: [String: String]?) throws -> Future { - var queryParams = "" - if let queryParameters = queryParameters { - queryParams = queryParameters.queryParameters - } - - return try request.send(method: .GET, path: "\(endpoint)/\(bucket)/notificationConfigs/\(notification)", query: queryParams) - } - - /// Creates a notification subscription for a given bucket. - public func create(bucket: String, notification: StorageNotification, queryParameters: [String: String]?) throws -> Future { - var queryParams = "" - if let queryParameters = queryParameters { - queryParams = queryParameters.queryParameters - } - - let body = try JSONSerialization.data(withJSONObject: try notification.toEncodedDictionary()).convertToHTTPBody() - - return try request.send(method: .POST, path: "\(endpoint)/\(bucket)/notificationConfigs", query: queryParams, body: body) - } - - /// Retrieves a list of notification subscriptions for a given bucket. - public func list(bucket: String, queryParameters: [String: String]?) throws -> Future { - var queryParams = "" - if let queryParameters = queryParameters { - queryParams = queryParameters.queryParameters - } - - return try request.send(method: .GET, path: "\(endpoint)/\(bucket)/notificationConfigs", query: queryParams) - } -} diff --git a/Sources/GoogleCloud/Storage/API/StorageObjectAPI.swift b/Sources/GoogleCloud/Storage/API/StorageObjectAPI.swift deleted file mode 100644 index 33511b4..0000000 --- a/Sources/GoogleCloud/Storage/API/StorageObjectAPI.swift +++ /dev/null @@ -1,217 +0,0 @@ -// -// StorageObjectAPI.swift -// GoogleCloudProvider -// -// Created by Andrew Edwards on 5/20/18. -// - -import Vapor - -public protocol StorageObjectAPI { - func compose(destinationBucket: String, destinationObject: String, composeRequest: StorageComposeRequest, queryParameters: [String: String]?) throws -> Future - func copy(destinationBucket: String, destinationObject: String, sourceBucket: String, sourceObject: String, object: GoogleStorageObject, queryParameters: [String: String]?) throws -> Future - func delete(bucket: String, objectName: String, queryParameters: [String: String]?) throws -> Future - func get(bucket: String, objectName: String, queryParameters: [String: String]?) throws -> Future - func getMedia(bucket: String, objectName: String, queryParameters: [String: String]?) throws -> Future - func createSimpleUpload(bucket: String, data: Data, name: String, mediaType: MediaType, queryParameters: [String: String]?) throws -> Future - func list(bucket: String, queryParameters: [String: String]?) throws -> Future - func patch(bucket: String, objectName: String, object: GoogleStorageObject?, queryParameters: [String: String]?) throws -> Future - func rewrite(destinationBucket: String, destinationObject: String, sourceBucket: String, sourceObject: String, object: GoogleStorageObject?, queryParameters: [String: String]?) throws -> Future - func update(bucket: String, objectName: String, object: GoogleStorageObject, queryParameters: [String: String]?) throws -> Future - func watchAll(bucket: String, notificationChannel: StorageNotificationChannel, queryParameters: [String: String]?) throws -> Future -} - -extension StorageObjectAPI { - public func compose(destinationBucket: String, destinationObject: String, composeRequest: StorageComposeRequest, queryParameters: [String: String]? = nil) throws -> Future { - return try compose(destinationBucket: destinationBucket, destinationObject: destinationObject, composeRequest: composeRequest, queryParameters: queryParameters) - } - - public func copy(destinationBucket: String, destinationObject: String, sourceBucket: String, sourceObject: String, object: GoogleStorageObject, queryParameters: [String: String]? = nil) throws -> Future { - return try copy(destinationBucket: destinationBucket, destinationObject: destinationObject, sourceBucket: sourceBucket, sourceObject: sourceObject, object: object, queryParameters: queryParameters) - } - - public func delete(bucket: String, objectName: String, queryParameters: [String: String]? = nil) throws -> Future { - return try delete(bucket: bucket, objectName: objectName, queryParameters: queryParameters) - } - - public func get(bucket: String, objectName: String, queryParameters: [String: String]? = nil) throws -> Future { - return try get(bucket: bucket, objectName: objectName, queryParameters: queryParameters) - } - - public func getMedia(bucket: String, objectName: String, queryParameters: [String: String]? = nil) throws -> Future { - return try getMedia(bucket: bucket, objectName: objectName, queryParameters: queryParameters) - } - - public func createSimpleUpload(bucket: String, data: Data, name: String, mediaType: MediaType, queryParameters: [String: String]? = nil) throws -> Future { - return try createSimpleUpload(bucket: bucket, data: data, name: name, mediaType: mediaType, queryParameters: queryParameters) - } - - public func list(bucket: String, queryParameters: [String: String]? = nil) throws -> EventLoopFuture { - return try list(bucket: bucket, queryParameters: queryParameters) - } - - public func patch(bucket: String, objectName: String, object: GoogleStorageObject? = nil, queryParameters: [String: String]? = nil) throws -> Future { - return try patch(bucket: bucket, objectName: objectName, object: object, queryParameters: queryParameters) - } - - public func rewrite(destinationBucket: String, destinationObject: String, sourceBucket: String, sourceObject: String, object: GoogleStorageObject? = nil, queryParameters: [String: String]? = nil) throws -> Future { - return try rewrite(destinationBucket: destinationBucket, destinationObject: destinationObject, sourceBucket: sourceBucket, sourceObject: sourceObject, object: object, queryParameters: queryParameters) - } - - public func update(bucket: String, objectName: String, object: GoogleStorageObject, queryParameters: [String: String]? = nil) throws -> Future { - return try update(bucket: bucket, objectName: objectName, object: object, queryParameters: queryParameters) - } - - public func watchAll(bucket: String, notificationChannel: StorageNotificationChannel, queryParameters: [String: String]? = nil) throws -> Future { - return try watchAll(bucket: bucket, notificationChannel: notificationChannel, queryParameters: queryParameters) - } -} - -public final class GoogleStorageObjectAPI: StorageObjectAPI { - let endpoint = "https://www.googleapis.com/storage/v1/b" - let request: GoogleCloudStorageRequest - - init(request: GoogleCloudStorageRequest) { - self.request = request - } - - /// Concatenates a list of existing objects into a new object in the same bucket. - public func compose(destinationBucket: String, destinationObject: String, composeRequest: StorageComposeRequest, queryParameters: [String: String]?) throws -> Future { - var queryParams = "" - if let queryParameters = queryParameters { - queryParams = queryParameters.queryParameters - } - - let body = try JSONSerialization.data(withJSONObject: try composeRequest.toEncodedDictionary()).convertToHTTPBody() - - return try request.send(method: .POST, path: "\(endpoint)/\(destinationBucket)/o/\(destinationObject)/compose", query: queryParams, body: body) - } - - /// Copies a source object to a destination object. Optionally overrides metadata. - public func copy(destinationBucket: String, destinationObject: String, sourceBucket: String, sourceObject: String, object: GoogleStorageObject, queryParameters: [String: String]?) throws -> Future { - var queryParams = "" - if let queryParameters = queryParameters { - queryParams = queryParameters.queryParameters - } - - let body = try JSONSerialization.data(withJSONObject: try object.toEncodedDictionary()).convertToHTTPBody() - - return try request.send(method: .POST, path: "\(endpoint)/\(sourceBucket)/o/\(sourceObject)/copyTo/b/\(destinationBucket)/o/\(destinationObject)", query: queryParams, body: body) - } - - /// Deletes an object and its metadata. Deletions are permanent if versioning is not enabled for the bucket, or if the generation parameter is used. - public func delete(bucket: String, objectName: String, queryParameters: [String: String]?) throws -> Future { - var queryParams = "" - if let queryParameters = queryParameters { - queryParams = queryParameters.queryParameters - } - - return try request.send(method: .DELETE, path: "\(endpoint)/\(bucket)/o/\(objectName)", query: queryParams) - } - - /// Retrieves an object's metadata. - public func get(bucket: String, objectName: String, queryParameters: [String: String]?) throws -> Future { - var queryParams = "" - if let queryParameters = queryParameters { - queryParams = queryParameters.queryParameters - } - - return try request.send(method: .GET, path: "\(endpoint)/\(bucket)/o/\(objectName)", query: queryParams) - } - - /// Retrieves an object's contents. - public func getMedia(bucket: String, objectName: String, queryParameters: [String: String]?) throws -> Future { - var queryParams = ["alt": "media"].queryParameters - - if let queryParameters = queryParameters { - queryParams = queryParameters.queryParameters - } - - return try request.send(method: .GET, path: "\(endpoint)/\(bucket)/o/\(objectName)", query: queryParams) - } - - /// Stores a new object and metadata. Upload the media only, without any metadata. - public func createSimpleUpload(bucket: String, data: Data, name: String, mediaType: MediaType, queryParameters: [String: String]?) throws -> Future { - var queryParams = "" - if var queryParameters = queryParameters { - queryParameters["name"] = name - queryParameters["uploadType"] = "media" - queryParams = queryParameters.queryParameters - } - else { - queryParams = "uploadType=media&name=\(name)" - } - - let body = data.convertToHTTPBody() - - let headers: HTTPHeaders = [HTTPHeaderName.contentType.description: mediaType.description] - - return try request.send(method: .POST, headers: headers, path: "https://www.googleapis.com/upload/storage/v1/b/\(bucket)/o", query: queryParams, body: body) - } - - /// Retrieves a list of objects matching the criteria. - public func list(bucket: String, queryParameters: [String: String]?) throws -> EventLoopFuture { - var queryParams = "" - if let queryParameters = queryParameters { - queryParams = queryParameters.queryParameters - } - - return try request.send(method: .GET, path: "\(endpoint)/\(bucket)/o)", query: queryParams) - } - - /// Updates a data blob's associated metadata. This method supports patch semantics - public func patch(bucket: String, objectName: String, object: GoogleStorageObject?, queryParameters: [String: String]?) throws -> Future { - var queryParams = "" - if let queryParameters = queryParameters { - queryParams = queryParameters.queryParameters - } - - var body = "" - - if let object = object { - body = try JSONSerialization.data(withJSONObject: try object.toEncodedDictionary()).convert(to: String.self) - } - - return try request.send(method: .PATCH, path: "\(endpoint)/\(bucket)/o)", query: queryParams, body: body.convertToHTTPBody()) - } - - /// Rewrites a source object to a destination object. Optionally overrides metadata. - public func rewrite(destinationBucket: String, destinationObject: String, sourceBucket: String, sourceObject: String, object: GoogleStorageObject?, queryParameters: [String: String]?) throws -> Future { - var queryParams = "" - if let queryParameters = queryParameters { - queryParams = queryParameters.queryParameters - } - - var body = "" - - if let object = object { - body = try JSONSerialization.data(withJSONObject: try object.toEncodedDictionary()).convert(to: String.self) - } - - return try request.send(method: .POST, path: "\(endpoint)/\(sourceBucket)/o/\(sourceObject)/rewriteTo/b/\(destinationBucket)/o/\(destinationObject)", query: queryParams, body: body.convertToHTTPBody()) - } - - /// Updates an object's metadata. - public func update(bucket: String, objectName: String, object: GoogleStorageObject, queryParameters: [String: String]?) throws -> Future { - var queryParams = "" - if let queryParameters = queryParameters { - queryParams = queryParameters.queryParameters - } - - let body = try JSONSerialization.data(withJSONObject: try object.toEncodedDictionary()).convertToHTTPBody() - - return try request.send(method: .PUT, path: "\(endpoint)/\(bucket)/o)", query: queryParams, body: body) - } - - /// Watch for changes on all objects in a bucket. - public func watchAll(bucket: String, notificationChannel: StorageNotificationChannel, queryParameters: [String: String]?) throws -> Future { - var queryParams = "" - if let queryParameters = queryParameters { - queryParams = queryParameters.queryParameters - } - - let body = try JSONSerialization.data(withJSONObject: try notificationChannel.toEncodedDictionary()).convertToHTTPBody() - - return try request.send(method: .POST, path: "\(endpoint)/\(bucket)/o/watch)", query: queryParams, body: body) - } -} diff --git a/Sources/GoogleCloud/Storage/CloudStorageError.swift b/Sources/GoogleCloud/Storage/CloudStorageError.swift deleted file mode 100644 index 2f0fab9..0000000 --- a/Sources/GoogleCloud/Storage/CloudStorageError.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// CloudStorageError.swift -// GoogleCloudProvider -// -// Created by Andrew Edwards on 4/21/18. -// - -import Vapor - -// https://cloud.google.com/storage/docs/json_api/v1/status-codes -public struct CloudStorageError: GoogleCloudError, GoogleCloudModel { - public var identifier: String { - return "\(self.error.code)-\(self.error.message)" - } - - public var reason: String { - return self.error.message - } - - public var error: CloudStorageErrorBody -} - -public struct CloudStorageErrorBody: Content { - public var errors: [CloudStorageErrors] - public var code: Int - public var message: String -} - -public struct CloudStorageErrors: Content { - public var domain: String? - public var reason: String? - public var message: String? - public var locationType: String? - public var location: String? -} diff --git a/Sources/GoogleCloud/Storage/Models/Enums/StorageClass.swift b/Sources/GoogleCloud/Storage/Models/Enums/StorageClass.swift deleted file mode 100644 index b07aa80..0000000 --- a/Sources/GoogleCloud/Storage/Models/Enums/StorageClass.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// StorageClass.swift -// GoogleCloudProvider -// -// Created by Andrew Edwards on 4/17/18. -// - -import Vapor - -public enum StorageClass: String, Content { - /// Storing data that is frequently accessed ("hot" objects) around the world, such as serving website content, streaming videos, or gaming and mobile applications. - case multiRegional = "multi_regional" - /// Storing frequently accessed in the same region as your Google Cloud DataProc or Google Compute Engine instances that use it, such as for data analytics. - case regional - /// Data you do not expect to access frequently (i.e., no more than once per month). Ideal for back-up and serving long-tail multimedia content. - case nearline - /// Data you expect to access infrequently (i.e., no more than once per year). Typically this is for disaster recovery, or data that is archived and may or may not be needed at some future time. - case coldline -} diff --git a/Sources/GoogleCloud/Storage/Models/Enums/StorageScope.swift b/Sources/GoogleCloud/Storage/Models/Enums/StorageScope.swift deleted file mode 100644 index f8f600f..0000000 --- a/Sources/GoogleCloud/Storage/Models/Enums/StorageScope.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// StorageScope.swift -// GoogleCloudProvider -// -// Created by Andrew Edwards on 4/16/18. -// - -import Vapor - -public struct StorageScope { - /// Only allows access to read data, including listing buckets. - public static let readOnly = "https://www.googleapis.com/auth/devstorage.read_only" - /// Allows access to read and change data, but not metadata like IAM policies. - public static let readWrite = "https://www.googleapis.com/auth/devstorage.read_write" - /// Allows full control over data, including the ability to modify IAM policies. - public static let fullControl = "https://www.googleapis.com/auth/devstorage.full_control" - /// View your data across Google Cloud Platform services. For Cloud Storage, this is the same as devstorage.read-only. - public static let cloudPlatformReadOnly = "https://www.googleapis.com/auth/cloud-platform.read-only" - /// View and manage data across all Google Cloud Platform services. For Cloud Storage, this is the same as devstorage.full-control. - public static let cloudPlatform = "https://www.googleapis.com/auth/cloud-platform" -} diff --git a/Sources/GoogleCloud/Storage/Models/IAMPolicy.swift b/Sources/GoogleCloud/Storage/Models/IAMPolicy.swift deleted file mode 100644 index 7eb2cd2..0000000 --- a/Sources/GoogleCloud/Storage/Models/IAMPolicy.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// IAMPolicy.swift -// GoogleCloudProvider -// -// Created by Andrew Edwards on 4/20/18. -// - -import Vapor - -public struct IAMPolicy: GoogleCloudModel { - /// The kind of item this is. For policies, this is always storage#policy. This field is ignored on input. - public var kind: String? - /// The ID of the resource to which this policy belongs. - public var resourceId: String? - /// An association between a role, which comes with a set of permissions, and members who may assume that role. - public var bindings: [Binding]? - /// HTTP 1.1 Entity tag for the policy. - public var etag: String? - - public init(kind: String? = nil, - resourceId: String? = nil, - bindings: [Binding]? = nil, - etag: String? = nil) { - self.kind = kind - self.resourceId = resourceId - self.bindings = bindings - self.etag = etag - } -} - -public struct Binding: GoogleCloudModel { - /// The role to which members belong. - public var role: String? - /// A collection of identifiers for members who may assume the provided role. - public var members: [String]? - /// Any value - public var condition: String? - - public init(role: String? = nil, - members: [String]? = nil, - condition: String? = nil) { - self.role = role - self.members = members - self.condition = condition - } -} - -public struct Permission: GoogleCloudModel { - /// The kind of item this is. - public var kind: String? - /// The permissions held by the caller. - public var permissions: [String]? -} diff --git a/Sources/GoogleCloud/Storage/Models/StorageBucket.swift b/Sources/GoogleCloud/Storage/Models/StorageBucket.swift deleted file mode 100644 index cb88f19..0000000 --- a/Sources/GoogleCloud/Storage/Models/StorageBucket.swift +++ /dev/null @@ -1,374 +0,0 @@ -// -// StorageBucket.swift -// GoogleCloudProvider -// -// Created by Andrew Edwards on 4/17/18. -// - -import Vapor -/// The Buckets resource represents a bucket in Google Cloud Storage. There is a single global namespace shared by all buckets. For more information, see Bucket Name Requirements. -public struct GoogleStorageBucket: GoogleCloudModel { - /// The kind of item this is. For buckets, this is always storage#bucket. - public var kind: String? - /// The ID of the bucket. For buckets, the id and name properties are the same. - public var id: String? - /// The URI of this bucket. - public var selfLink: String? - /// The project number of the project the bucket belongs to. - public var projectNumber: String? - /// The name of the bucket. - public var name: String? - /// The creation time of the bucket in RFC 3339 format. - public var timeCreated: Date? - /// The modification time of the bucket in RFC 3339 format. - public var updated: Date? - /// The metadata generation of this bucket. - public var metageneration: String? - /// Access controls on the bucket, containing one or more bucketAccessControls Resources. - public var acl: [BucketAccessControls]? - /// Default access controls to apply to new objects when no ACL is provided. - public var defaultObjectAcl: [ObjectAccessControls]? - /// The owner of the bucket. This is always the project team's owner group. - public var owner: Owner? - /// The location of the bucket. Object data for objects in the bucket resides in physical storage within this region. Defaults to US. - public var location: String? - /// The bucket's website configuration, controlling how the service behaves when accessing bucket contents as a web site. - public var website: Website? - /// The bucket's logging configuration, which defines the destination bucket and optional name prefix for the current bucket's logs. - public var logging: Logging? - /// The bucket's versioning configuration. - public var versioning: Versioning? - /// The bucket's Cross-Origin Resource Sharing (CORS) configuration. - public var cors: [Cors]? - /// The bucket's lifecycle configuration. See lifecycle management for more information. - public var lifecycle: Lifecycle? - /// User-provided labels, in key/value pairs. - public var labels: [String: String]? - /// The bucket's default storage class, used whenever no storageClass is specified for a newly-created object. This defines how objects in the bucket are stored and determines the SLA and the cost of storage. Values include MULTI_REGIONAL, REGIONAL, STANDARD, NEARLINE, COLDLINE, and DURABLE_REDUCED_AVAILABILITY. If this value is not specified when the bucket is created, it will default to STANDARD. - public var storageClass: String? - /// The bucket's billing configuration. - public var billing: Billing? - /// HTTP 1.1 Entity tag for the bucket. - public var etag: String? -} - -public struct BucketAccessControls: GoogleCloudModel { - /// The kind of item this is. For bucket access control entries, this is always storage#bucketAccessControl. - public var kind: String? - /// The ID of the access-control entry. - public var id: String? - /// The link to this access-control entry. - public var selfLink: String? - /// The name of the bucket. - public var bucket: String? - /// The entity holding the permission. - public var entity: String? - /// The access permission for the entity. - public var role: String? - /// The email address associated with the entity, if any. - public var email: String? - /// The ID for the entity, if any. - public var entityId: String? - /// The domain associated with the entity, if any. - public var domain: String? - /// The project team associated with the entity, if any. - public var projectTeam: ProjectTeam? - /// HTTP 1.1 Entity tag for the access-control entry. - public var etag: String? - - public init(kind: String? = nil, - id: String? = nil, - selfLink: String? = nil, - bucket: String? = nil, - entity: String? = nil, - role: String? = nil, - email: String? = nil, - entityId: String? = nil, - domain: String? = nil, - projectTeam: ProjectTeam? = nil, - etag: String? = nil) { - self.kind = kind - self.id = id - self.selfLink = selfLink - self.bucket = bucket - self.entity = entity - self.role = role - self.email = email - self.entityId = entityId - self.domain = domain - self.projectTeam = projectTeam - self.etag = etag - } -} - -public struct StorageNotification: GoogleCloudModel { - /// The kind of item this is. For notifications, this is always storage#notification. - public var kind: String? - /// The ID of the notification. - public var id: String? - /// The canonical URL of this notification. - public var selfLink: String? - /// The Cloud PubSub topic to which this subscription publishes. Formatted as: '//pubsub.googleapis.com/projects/{project-identifier}/topics/{my-topic}' - public var topic: String? - /// If present, only send notifications about listed event types. If empty, sent notifications for all event types. - public var eventTypes: [String]? - /// An optional list of additional attributes to attach to each Cloud PubSub message published for this notification subscription. - public var customAttributes: [String: String]? - /// The desired content of the Payload. Acceptable values are: "JSON_API_V1" and "NONE". - public var payloadFormat: String? - /// If present, only apply this notification configuration to object names that begin with this prefix. - public var objectNamePrefix: String? - /// HTTP 1.1 Entity tag for this subscription notification. - public var etag: String? - - public init(kind: String? = nil, - id: String? = nil, - selfLink: String? = nil, - topic: String? = nil, - eventTypes: [String]? = nil, - customAttributes: [String: String]? = nil, - payloadFormat: String? = nil, - objectNamePrefix: String? = nil, - etag: String? = nil) { - self.kind = kind - self.id = id - self.selfLink = selfLink - self.topic = topic - self.eventTypes = eventTypes - self.customAttributes = customAttributes - self.payloadFormat = payloadFormat - self.objectNamePrefix = objectNamePrefix - self.etag = etag - } - - public enum CodingKeys: String, CodingKey { - case kind - case id - case selfLink - case topic - case eventTypes = "event_types" - case customAttributes = "custom_attributes" - case payloadFormat = "payload_format" - case objectNamePrefix = "object_name_prefix" - case etag - } -} - -public struct ObjectAccessControls: GoogleCloudModel { - /// The kind of item this is. For object access control entries, this is always storage#objectAccessControl. - public var kind: String? - /// The ID of the access-control entry. - public var id: String? - /// The link to this access-control entry. - public var selfLink: String? - /// The name of the bucket. - public var bucket: String? - /// The name of the object, if applied to an object. - public var object: String? - /// The content generation of the object, if applied to an object. - public var generation: String? - /// The entity holding the permission. - public var entity: String? - /// The access permission for the entity. Acceptable values are: "OWNER", "READER" - public var role: String? - /// The email address associated with the entity, if any. - public var email: String? - /// The ID for the entity, if any. - public var entityId: String? - /// The domain associated with the entity, if any. - public var domain: String? - /// The project team associated with the entity, if any. - public var projectTeam: ProjectTeam? - /// HTTP 1.1 Entity tag for the access-control entry. - public var etag: String? - - public init(kind: String? = nil, - id: String? = nil, - selfLink: String? = nil, - bucket: String? = nil, - object: String? = nil, - generation: String? = nil, - entity: String? = nil, - role: String? = nil, - email: String? = nil, - entityId: String? = nil, - domain: String? = nil, - projectTeam: ProjectTeam? = nil, - etag: String? = nil) { - self.kind = kind - self.id = id - self.selfLink = selfLink - self.bucket = bucket - self.object = object - self.generation = generation - self.entity = entity - self.role = role - self.email = email - self.entityId = entityId - self.domain = domain - self.projectTeam = projectTeam - self.etag = etag - } -} - -public struct ProjectTeam: GoogleCloudModel { - /// The project number. - public var projectNumber: String? - /// The team. Acceptable values are: "editors", "owners", "viewers" - public var team: String? - - public init(projectNumber: String? = nil, - team: String? = nil) { - self.projectNumber = projectNumber - self.team = team - } -} - -public struct Owner: GoogleCloudModel { - /// The entity, in the form project-owner-projectId. - public var entity: String? - /// The ID for the entity. - public var entityId: String? - - public init(entity: String? = nil, - entityId: String? = nil) { - self.entity = entity - self.entityId = entityId - } -} - -public struct Website: GoogleCloudModel { - /// If the requested object path is missing, the service will ensure the path has a trailing '/', append this suffix, and attempt to retrieve the resulting object. This allows the creation of index.html objects to represent directory pages. - public var mainPageSuffix: String? - /// If the requested object path is missing, and any mainPageSuffix object is missing, if applicable, the service will return the named object from this bucket as the content for a 404 Not Found result. - public var notFoundPage: String? - - public init(mainPageSuffix: String? = nil, - notFoundPage: String? = nil) { - self.mainPageSuffix = mainPageSuffix - self.notFoundPage = notFoundPage - } -} - -public struct Logging: GoogleCloudModel { - /// The destination bucket where the current bucket's logs should be placed. - public var logBucket: String? - /// A prefix for log object names. - public var logObjectPrefix: String? - - public init(logBucket: String? = nil, - logObjectPrefix: String? = nil) { - self.logBucket = logBucket - self.logObjectPrefix = logObjectPrefix - } -} - -public struct Versioning: GoogleCloudModel { - /// While set to true, versioning is fully enabled for this bucket. - public var enabled: Bool? - - public init(enabled: Bool? = nil) { - self.enabled = enabled - } -} - -public struct Cors: GoogleCloudModel { - /// The list of Origins eligible to receive CORS response headers. Note: "*" is permitted in the list of origins, and means "any Origin". - public var origin: [String]? - /// The list of HTTP methods on which to include CORS response headers, (GET, OPTIONS, POST, etc) Note: "*" is permitted in the list of methods, and means "any method". - public var method: [String]? - /// The list of HTTP headers other than the simple response headers to give permission for the user-agent to share across domains. - public var responseHeader: [String]? - /// The value, in seconds, to return in the Access-Control-Max-Age header used in preflight responses. - public var maxAgeSeconds: Int? - - public init(origin: [String]? = nil, - method: [String]? = nil, - responseHeader: [String]? = nil, - maxAgeSeconds: Int? = nil) { - self.origin = origin - self.method = method - self.responseHeader = responseHeader - self.maxAgeSeconds = maxAgeSeconds - } -} - -public struct Lifecycle: GoogleCloudModel { - /// A lifecycle management rule, which is made of an action to take and the condition(s) under which the action will be taken. - public var rule: [Rule]? - - public init(rule: [Rule]? = nil) { - self.rule = rule - } -} - -public struct Rule: GoogleCloudModel { - /// The action to take. - public var action: Action? - /// The condition(s) under which the action will be taken. - public var condition: Condition? - - public init(action: Action? = nil, - condition: Condition? = nil) { - self.action = action - self.condition = condition - } -} - -public struct Action: GoogleCloudModel { - /// Type of the action. Currently, only Delete and SetStorageClass are supported. Acceptable values are: "Delete", "SetStorageClass" - public var type: String? - /// Target storage class. Required iff the type of the action is SetStorageClass. - public var storageClass: String? - - public init(type: String? = nil, - storageClass: String? = nil) { - self.type = type - self.storageClass = storageClass - } -} - -public struct Condition: GoogleCloudModel { - /// Age of an object (in days). This condition is satisfied when an object reaches the specified age. - public var age: Int? - /// A date in RFC 3339 format with only the date part (for instance, "2013-01-15"). This condition is satisfied when an object is created before midnight of the specified date in UTC. - public var createdBefore: String? - /// Relevant only for versioned objects. If the value is true, this condition matches live objects; if the value is false, it matches archived objects. - public var isLive: Bool? - /// Objects having any of the storage classes specified by this condition will be matched. Values include MULTI_REGIONAL, REGIONAL, NEARLINE, COLDLINE, STANDARD, and DURABLE_REDUCED_AVAILABILITY. - public var matchesStorageClass: [String]? - /// Relevant only for versioned objects. If the value is N, this condition is satisfied when there are at least N versions (including the live version) newer than this version of the object. - public var numNewerVersions: Int? - - public init(age: Int? = nil, - createdBefore: String? = nil, - isLive: Bool? = nil, - matchesStorageClass: [String]? = nil, - numNewerVersions: Int? = nil) { - self.age = age - self.createdBefore = createdBefore - self.isLive = isLive - self.matchesStorageClass = matchesStorageClass - self.numNewerVersions = numNewerVersions - } -} - -public struct Billing: GoogleCloudModel { - /// When set to true, bucket is requester pays. - public var requesterPays: Bool? - - public init(requesterPays: Bool? = nil) { - self.requesterPays = requesterPays - } -} - -public struct Encryption: GoogleCloudModel { - /// When set to true, bucket is requester pays. - public var defaultKmsKeyName: String? - - public init(defaultKmsKeyName: String? = nil) { - self.defaultKmsKeyName = defaultKmsKeyName - } -} - -public struct EmptyResponse: GoogleCloudModel {} diff --git a/Sources/GoogleCloud/Storage/Models/StorageBucketList.swift b/Sources/GoogleCloud/Storage/Models/StorageBucketList.swift deleted file mode 100644 index 3a63af9..0000000 --- a/Sources/GoogleCloud/Storage/Models/StorageBucketList.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// StorageBucketList.swift -// GoogleCloudProvider -// -// Created by Andrew Edwards on 4/18/18. -// - -import Vapor - -public struct GoogleStorageBucketList: GoogleCloudModel { - /// The kind of item this is. For lists of buckets, this is always storage#buckets. - public var kind: String? - /// The continuation token, used to page through large result sets. Provide this value in a subsequent request to return the next page of results. - public var nextPageToken: String? - /// The list of items. - public var items: [GoogleStorageBucket]? -} - -public struct BucketAccessControlList: GoogleCloudModel { - /// The kind of item this is. For lists of bucket access control entries, this is always storage#bucketAccessControls. - public var kind: String? - /// The list of items. - public var items: [BucketAccessControls]? -} - -public struct ObjectAccessControlsList: GoogleCloudModel { - /// The kind of item this is. For lists of object access control entries, this is always storage#objectAccessControls - public var kind: String? - /// The list of items. - public var items: [ObjectAccessControls]? -} - -public struct StorageNotificationsList: GoogleCloudModel { - /// The kind of item this is. For lists of notifications, this is always storage#notifications. - public var kind: String? - /// The list of items. - public var items: [StorageNotification]? -} - -public struct StorageObjectList: GoogleCloudModel { - /// The kind of item this is. For lists of objects, this is always storage#objects. - public var kind: String? - /// The continuation token, used to page through large result sets. Provide this value in a subsequent request to return the next page of results. - public var nextPageToken: String? - - /// The list of prefixes of objects matching-but-not-listed up to and including the requested delimiter. - public var prefixes: [String]? - - /// The list of items. - public var items: [GoogleStorageObject]? -} diff --git a/Sources/GoogleCloud/Storage/Models/StorageObject.swift b/Sources/GoogleCloud/Storage/Models/StorageObject.swift deleted file mode 100644 index b226eb5..0000000 --- a/Sources/GoogleCloud/Storage/Models/StorageObject.swift +++ /dev/null @@ -1,269 +0,0 @@ -// -// StorageObject.swift -// GoogleCloudProvider -// -// Created by Andrew Edwards on 5/20/18. -// - -import Vapor - -public struct GoogleStorageObject: GoogleCloudModel { - /// The kind of item this is. For objects, this is always storage#object. - public var kind: String? - /// The ID of the object, including the bucket name, object name, and generation number. - public var id: String? - /// The link to this object. - public var selfLink: String? - /// The name of the object. Required if not specified by URL parameter. - public var name: String? - /// The name of the bucket containing this object. - public var bucket: String? - /// The content generation of this object. Used for object versioning. - public var generation: String? - /// The version of the metadata for this object at this generation. Used for preconditions and for detecting changes in metadata. A metageneration number is only meaningful in the context of a particular generation of a particular object. - public var metageneration: String? - /// Content-Type of the object data. If an object is stored without a Content-Type, it is served as application/octet-stream. - public var contentType: String? - /// The creation time of the object in RFC 3339 format. - public var timeCreated: Date? - /// The modification time of the object metadata in RFC 3339 format. - public var updated: Date? - /// The deletion time of the object in RFC 3339 format. Will be returned if and only if this version of the object has been deleted. - public var timeDeleted: Date? - /// Storage class of the object. - public var storageClass: String? - /// The time at which the object's storage class was last changed. When the object is initially created, it will be set to timeCreated. - public var timeStorageClassUpdated: Date? - /// Content-Length of the data in bytes. - public var size: String? - /// MD5 hash of the data; encoded using base64. - public var md5Hash: String? - /// Media download link. - public var mediaLink: String? - /// Content-Encoding of the object data. - public var contentEncoding: String? - /// Content-Disposition of the object data. - public var contentDisposition: String? - /// Content-Language of the object data. - public var contentLanguage: String? - /// Cache-Control directive for the object data. If omitted, and the object is accessible to all anonymous users, the default will be public, max-age=3600. - public var cacheControl: String? - /// User-provided metadata, in key/value pairs. - public var metadata: [String: String]? - /// Access controls on the object, containing one or more objectAccessControls - public var acl: [ObjectAccessControls]? - /// The owner of the object. This will always be the uploader of the object. - public var owner: Owner? - /// CRC32c checksum, as described in RFC 4960, Appendix B; encoded using base64 in big-endian byte order. - public var crc32c: String? - /// Number of underlying components that make up this object. Components are accumulated by compose operations and are limited to a count of 1024, counting 1 for each non-composite component object and componentCount for each composite component object. Note: componentCount is included in the metadata for composite objects only. - public var componentCount: String? - /// HTTP 1.1 Entity tag for the object. - public var etag: String? - /// Metadata of customer-supplied encryption key, if the object is encrypted by such a key. - public var customerEncryption: CustomerEncryption? - /// Cloud KMS Key used to encrypt this object, if the object is encrypted by such a key. - public var kmsKeyName: String? - - public init(kind: String? = nil, - id: String? = nil, - selfLink: String? = nil, - name: String? = nil, - bucket: String? = nil, - generation: String? = nil, - metageneration: String? = nil, - contentType: String? = nil, - timeCreated: Date? = nil, - updated: Date? = nil, - timeDeleted: Date? = nil, - storageClass: String? = nil, - timeStorageClassUpdated: Date? = nil, - size: String? = nil, - md5Hash: String? = nil, - mediaLink: String? = nil, - contentEncoding: String? = nil, - contentDisposition: String? = nil, - contentLanguage: String? = nil, - cacheControl: String? = nil, - metadata: [String: String]? = nil, - acl: [ObjectAccessControls]? = nil, - owner: Owner? = nil, - crc32c: String? = nil, - componentCount: String? = nil, - etag: String? = nil, - customerEncryption: CustomerEncryption? = nil, - kmsKeyName: String? = nil) { - self.kind = kind - self.id = id - self.selfLink = selfLink - self.name = name - self.bucket = bucket - self.generation = generation - self.metageneration = metageneration - self.contentType = contentType - self.timeCreated = timeCreated - self.updated = updated - self.timeDeleted = timeDeleted - self.storageClass = storageClass - self.timeStorageClassUpdated = timeStorageClassUpdated - self.size = size - self.md5Hash = md5Hash - self.mediaLink = mediaLink - self.contentEncoding = contentEncoding - self.contentDisposition = contentDisposition - self.contentLanguage = contentLanguage - self.cacheControl = cacheControl - self.metadata = metadata - self.acl = acl - self.owner = owner - self.crc32c = crc32c - self.componentCount = componentCount - self.etag = etag - self.customerEncryption = customerEncryption - self.kmsKeyName = kmsKeyName - } -} - -public struct CustomerEncryption: GoogleCloudModel { - /// The encryption algorithm. - public var encryptionAlgorithm: String? - /// SHA256 hash value of the encryption key. - public var keySha256: String? - - public init(encryptionAlgorithm: String? = nil, - keySha256: String? = nil) { - self.encryptionAlgorithm = encryptionAlgorithm - self.keySha256 = keySha256 - } -} - -public struct StorageComposeRequest: GoogleCloudModel { - /// The kind of item this is. - public var kind: String? = "storage#composeRequest" - /// The list of source objects that will be concatenated into a single object. - public var sourceObjects: [StorageSourcObject]? - /// Properties of the resulting object. - public var destination: GoogleStorageObject? - - public init(kind: String? = nil, - sourceObjects: [StorageSourcObject]? = nil, - destination: GoogleStorageObject? = nil) { - self.kind = kind - self.sourceObjects = sourceObjects - self.destination = destination - } -} - -public struct StorageSourcObject: GoogleCloudModel { - /// The source object's name. The source object's bucket is implicitly the destination bucket. - public var name: String? - /// The generation of this object to use as the source. - public var generation: String? - /// Conditions that must be met for this operation to execute. - public var objectPreconditions: StorageObjectPreconditions? - - public init(name: String? = nil, - generation: String? = nil, - objectPreconditions: StorageObjectPreconditions? = nil) { - self.name = name - self.generation = generation - self.objectPreconditions = objectPreconditions - } -} - -public struct StorageObjectPreconditions: GoogleCloudModel { - /// Only perform the composition if the generation of the source object that would be used matches this value. If this value and a generation are both specified, they must be the same value or the call will fail. - public var ifGenerationMatch: String? - - public init(ifGenerationMatch: String? = nil) { - self.ifGenerationMatch = ifGenerationMatch - } -} - -public struct StorageRewriteObject: GoogleCloudModel { - /// The kind of item this is. - public var kind: String? - - ///The total bytes written so far, which can be used to provide a waiting user with a progress indicator. This property is always present in the response. - public var totalBytesRewritten: String? - - /// The total size of the object being copied in bytes. This property is always present in the response. - public var objectSize: String? - - /// true if the copy is finished; otherwise, false if the copy is in progress. This property is always present in the response. - public var done: Bool? - - /// A token to use in subsequent requests to continue copying data. This token is present in the response only when there is more data to copy. - public var rewriteToken: String? - - /// A resource containing the metadata for the copied-to object. This property is present in the response only when copying completes. - public var resource: GoogleStorageObject? - - public init(kind: String? = nil, - totalBytesRewritten: String? = nil, - objectSize: String? = nil, - done: Bool? = nil, - rewriteToken: String? = nil, - resource: GoogleStorageObject? = nil) { - self.kind = kind - self.totalBytesRewritten = totalBytesRewritten - self.objectSize = objectSize - self.done = done - self.rewriteToken = rewriteToken - self.resource = resource - } -} - -public struct StorageNotificationChannel: GoogleCloudModel { - /// Identifies this as a notification channel used to watch for changes to a resource. Value: the fixed string "api#channel". - public var kind: String? - - /// A UUID or similar unique string that identifies this channel. - public var id: String? - - /// An opaque ID that identifies the resource being watched on this channel. Stable across different API versions. - public var resourceId: String? - - /// A version-specific identifier for the watched resource. - public var resourceUri: String? - - /// An arbitrary string delivered to the target address with each notification delivered over this channel. Optional. - public var token: String? - - /// Date and time of notification channel expiration, expressed as a Unix timestamp, in milliseconds. Optional. - public var expiration: String? - - /// The type of delivery mechanism used for this channel. Value: the fixed string "WEBHOOK". - public var type: String? - - /// The address where notifications are delivered for this channel. - public var address: String? - - /// Additional parameters controlling delivery channel behavior. Optional. - public var params: [String: String]? - - /// A Boolean value to indicate whether payload is wanted. Optional. - public var payload: Bool? - - public init(kind: String? = nil, - id: String? = nil, - resourceId: String? = nil, - resourceUri: String? = nil, - token: String? = nil, - expiration: String? = nil, - type: String? = nil, - address: String? = nil, - params: [String: String]? = nil, - payload: Bool? = nil) { - self.kind = kind - self.id = id - self.resourceId = resourceId - self.resourceUri = resourceUri - self.token = token - self.expiration = expiration - self.type = type - self.address = address - self.params = params - self.payload = payload - } -} diff --git a/Sources/GoogleCloud/Storage/README.md b/Sources/GoogleCloud/Storage/README.md deleted file mode 100644 index f583e93..0000000 --- a/Sources/GoogleCloud/Storage/README.md +++ /dev/null @@ -1,81 +0,0 @@ -# Google Cloud Storage - -## Using the Storage API - -### Setting up StorageConfig - -To make the GoogleCloudProvider as flexible as possible to work with different API's and projects, -you can configure each API with their own configuration if the default `GoogleCloudProviderConfig` doesn't satisfy your needs. - -For example the `GoogleCloudProviderConfig` can be configured with a `ProjectID`, but you might -want to use this API with a different project. Additionally every API has their own scope and you might want to configure it. -You must register a `GoogleCloudStorageConfig` in one of 2 ways. - -In `configure.swift` -```swift -let cloudStorageConfig = GoogleCloudStorageConfig(scope: [StorageScope.fullControl], serviceAccount: "default", project: "myproject-000000") -services.register(cloudStorageConfig) -// OR -services.register(GoogleCloudStorageConfig.default()) -``` -The priority of which projectID to use is as follows: -`$PROJECT_ID` environment variable -Service Account projectID -`GoogleCloudStorageConfig`'s `.project` (the config for this API) -`GoogleCloudProviderConfig`'s `.project` -and will throw an error if no projectID is set. - -### Creating a storage bucket - -```swift -func create(_ req: Request) throws -> Future { - let cloudStorage = try req.make(GoogleCloudStorageClient.self) - - return try cloudStorage.buckets.create(name: "vapor-cloud-storage-demo") -} -``` - -```json -// Results in the following response -{ - "updated": "2018-05-27T20:21:19Z", - "selfLink": "https://www.googleapis.com/storage/v1/b/vapor-cloud-storage-demo", - "projectNumber": "0000000011", - "id": "vapor-cloud-storage-demo", - "location": "US", - "storageClass": "STANDARD", - "metageneration": "1", - "kind": "storage#bucket", - "timeCreated": "2018-05-27T20:21:19Z", - "name": "vapor-cloud-storage-demo", - "etag": "CAE=" -} -``` - -### Uploading an object to cloud storage - -```swift -func upload(_ req: Request) throws -> Future { - let cloudStorage = try req.make(GoogleCloudStorageClient.self) - - return try cloudStorage.object.createSimpleUpload(bucket: "vapor-cloud-storage-demo", - data: Data(imagedata), - name: "hello", - mediaType: .jpeg) -} -``` - -There are other API's available which are well [documented](https://cloud.google.com/storage/docs/json_api/v1/). -This is just a basic example of creating a bucket and uploading an object. - -### What's implemented - -* [x] BucketAccessControls -* [x] Buckets -* [x] Channels -* [x] DefaultObjectAccessControls -* [x] Notifications -* [x] ObjectAccessControls -* [x] Simple Object upload -* [ ] Multipart Object upload -* [ ] Resumable Object upload diff --git a/Sources/GoogleCloud/Storage/StorageClient.swift b/Sources/GoogleCloud/Storage/StorageClient.swift deleted file mode 100644 index 8278eb3..0000000 --- a/Sources/GoogleCloud/Storage/StorageClient.swift +++ /dev/null @@ -1,90 +0,0 @@ -// -// StorageClient.swift -// GoogleCloudProvider -// -// Created by Andrew Edwards on 4/16/18. -// - -import Vapor - -public enum GoogleCloudStorageError: GoogleCloudError { - case projectIdMissing - case unknownError(String) - - var localizedDescription: String { - switch self { - case .projectIdMissing: - return "Missing project id for GoogleCloudStorage API. Did you forget to set your project id?" - case .unknownError(let reason): - return "An unknown error occured: \(reason)" - } - } - - public var identifier: String { - switch self { - case .projectIdMissing: - return "missing-project-id" - case .unknownError(_): - return "unknown" - } - } - - public var reason: String { return localizedDescription } -} - -public protocol StorageClient: ServiceType { - var bucketAccessControl: BucketAccessControlAPI { get set } - var buckets: StorageBucketAPI { get set } - var channels: ChannelsAPI { get set } - var defaultObjectAccessControl: DefaultObjectACLAPI { get set } - var objectAccessControl: ObjectAccessControlsAPI { get set } - var notifications: StorageNotificationsAPI { get set } - var object: StorageObjectAPI { get set } -} - -public final class GoogleCloudStorageClient: StorageClient { - public var bucketAccessControl: BucketAccessControlAPI - public var buckets: StorageBucketAPI - public var channels: ChannelsAPI - public var defaultObjectAccessControl: DefaultObjectACLAPI - public var objectAccessControl: ObjectAccessControlsAPI - public var notifications: StorageNotificationsAPI - public var object: StorageObjectAPI - - init(providerconfig: GoogleCloudProviderConfig, storageConfig: GoogleCloudStorageConfig, client: Client) throws { - // A token implementing OAuthRefreshable. Loaded from credentials from the provider config. - let refreshableToken = try OAuthCredentialLoader.getRefreshableToken(credentialFilePath: providerconfig.serviceAccountCredentialPath, - withConfig: storageConfig, - andClient: client) - - // Set the projectId to use for this client. In order of priority: - // - Environment Variable (PROJECT_ID) - // - Service Account's projectID - // - GoogleCloudStorageConfig's .project (optionally configured) - // - GoogleCloudProviderConfig's .project (optionally configured) - guard let projectId = ProcessInfo.processInfo.environment["PROJECT_ID"] ?? - (refreshableToken as? OAuthServiceAccount)?.credentials.projectId ?? - storageConfig.project ?? providerconfig.project else { - throw GoogleCloudStorageError.projectIdMissing - } - - let storageRequest = GoogleCloudStorageRequest(httpClient: client, oauth: refreshableToken, project: projectId) - - bucketAccessControl = GoogleBucketAccessControlAPI(request: storageRequest) - buckets = GoogleStorageBucketAPI(request: storageRequest) - channels = GoogleChannelsAPI(request: storageRequest) - defaultObjectAccessControl = GoogleDefaultObjectACLAPI(request: storageRequest) - objectAccessControl = GoogleObjectAccessControlsAPI(request: storageRequest) - notifications = GoogleStorageNotificationsAPI(request: storageRequest) - object = GoogleStorageObjectAPI(request: storageRequest) - } - - public static var serviceSupports: [Any.Type] { return [StorageClient.self] } - - public static func makeService(for worker: Container) throws -> GoogleCloudStorageClient { - let client = try worker.make(Client.self) - let providerConfig = try worker.make(GoogleCloudProviderConfig.self) - let storageConfig = try worker.make(GoogleCloudStorageConfig.self) - return try GoogleCloudStorageClient(providerconfig: providerConfig, storageConfig: storageConfig, client: client) - } -} diff --git a/Sources/GoogleCloud/Storage/StorageConfig.swift b/Sources/GoogleCloud/Storage/StorageConfig.swift deleted file mode 100644 index a834156..0000000 --- a/Sources/GoogleCloud/Storage/StorageConfig.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// StorageConfig.swift -// GoogleCloudProvider -// -// Created by Andrew Edwards on 4/21/18. -// - -import Vapor - -public struct GoogleCloudStorageConfig: Service, GoogleCloudAPIConfig { - - public let scope: [String] - public let serviceAccount: String - public let project: String? - - public init(scope: [String], serviceAccount: String, project: String?) { - self.scope = scope - self.serviceAccount = serviceAccount - self.project = project - } - - /// Create a new `GoogleCloudStorageConfig` with full control scope and the default service account. - public static func `default`() -> GoogleCloudStorageConfig { - return GoogleCloudStorageConfig(scope: [StorageScope.fullControl], serviceAccount: "default", project: nil) - } -} - diff --git a/Sources/GoogleCloud/Storage/StorageRequest.swift b/Sources/GoogleCloud/Storage/StorageRequest.swift deleted file mode 100644 index a836acf..0000000 --- a/Sources/GoogleCloud/Storage/StorageRequest.swift +++ /dev/null @@ -1,89 +0,0 @@ -// -// StorageRequest.swift -// GoogleCloudProvider -// -// Created by Andrew Edwards on 5/19/18. -// - -import Vapor - -extension HTTPHeaders { - public static var gcsDefault: HTTPHeaders { - var headers: HTTPHeaders = [:] - headers.replaceOrAdd(name: .contentType, value: MediaType.json.description) - return headers - } -} - -public final class GoogleCloudStorageRequest { - let refreshableToken: OAuthRefreshable - let project: String - - let httpClient: Client - let responseDecoder: JSONDecoder - let dateFormatter: DateFormatter - - var currentToken: OAuthAccessToken? - var tokenCreatedTime: Date? - - init(httpClient: Client, oauth: OAuthRefreshable, project: String) { - self.refreshableToken = oauth - self.httpClient = httpClient - self.project = project - self.responseDecoder = JSONDecoder() - self.dateFormatter = DateFormatter() - - self.dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" - self.responseDecoder.dateDecodingStrategy = .formatted(self.dateFormatter) - } - - func send(method: HTTPMethod, headers: HTTPHeaders = [:], path: String, query: String, body: HTTPBody = HTTPBody()) throws -> Future { - return try withToken({ token in - return try self._send(method: method, headers: headers, path: path, query: query, body: body, accessToken: token.accessToken).flatMap({ response in - if GCM.self is EmptyResponse.Type && response.http.body.data == Data() { - response.http.body = HTTPBody(staticString: "{}") - } - - return try self.responseDecoder.decode(GCM.self, from: response.http, maxSize: 65_536, on: response) - }) - }) - } - - func send(method: HTTPMethod = .GET, headers: HTTPHeaders = [:], path: String, query: String, body: HTTPBody = HTTPBody()) throws -> Future { - return try withToken({ token in - return try self._send(method: method, headers: headers, path: path, query: query, body: body, accessToken: token.accessToken).flatMap({ response in - return response.http.body.consumeData(on: response) - }) - }) - } - - private func withToken(_ closure: @escaping (OAuthAccessToken) throws -> Future) throws -> Future{ - guard let token = currentToken, let created = tokenCreatedTime, refreshableToken.isFresh(token: token, created: created) else { - return try refreshableToken.refresh().flatMap({ newToken in - self.currentToken = newToken - self.tokenCreatedTime = Date() - - return try closure(newToken) - }) - } - - return try closure(token) - } - - private func _send(method: HTTPMethod, headers: HTTPHeaders, path: String, query: String, body: HTTPBody, accessToken: String) throws -> Future { - var finalHeaders: HTTPHeaders = HTTPHeaders.gcsDefault - finalHeaders.add(name: .authorization, value: "Bearer \(accessToken)") - headers.forEach { finalHeaders.replaceOrAdd(name: $0.name, value: $0.value) } - - return httpClient.send(method, headers: finalHeaders, to: "\(path)?\(query)", beforeSend: { $0.http.body = body }).flatMap({ response in - guard (200...299).contains(response.http.status.code) else { - return try self.responseDecoder.decode(CloudStorageError.self, from: response.http, maxSize: 65_536, on: self.httpClient.container).map { error in - throw error - }.catchMap { error -> Response in - throw GoogleCloudStorageError.unknownError(error.localizedDescription) - } - } - return response.future(response) - }) - } -} diff --git a/Tests/GoogleCloudTests/CredentialTests.swift b/Tests/GoogleCloudTests/CredentialTests.swift deleted file mode 100644 index e065574..0000000 --- a/Tests/GoogleCloudTests/CredentialTests.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// CredentialTests.swift -// Async -// -// Created by Brian Hatfield on 7/17/18. -// - -import Foundation -import XCTest - -@testable import GoogleCloud - -final class CredentialTests: XCTestCase { - var checkoutPath: String { - if let path = ProcessInfo.processInfo.environment["PROJECT_PATH"] { - return path - } - - XCTFail("PROJECT_PATH environment variable not set; cannot load fixtures") - return "" - } - - func testLoadApplicationDefaultCredentials() throws { - let credentialFile = checkoutPath + "/Tests/GoogleCloudProviderTests/Fixtures/ADC.json" - - XCTAssertNoThrow(try GoogleApplicationDefaultCredentials(contentsOfFile: credentialFile)) - - let creds = try GoogleApplicationDefaultCredentials(contentsOfFile: credentialFile) - - XCTAssert(creds.clientId == "IDSTRING.apps.googleusercontent.com") - XCTAssert(creds.type == "authorized_user") - } - - func testLoadServiceAccountCredentials() throws { - let credentialFile = checkoutPath + "/Tests/GoogleCloudProviderTests/Fixtures/ServiceAccount.json" - - XCTAssertNoThrow(try GoogleServiceAccountCredentials(contentsOfFile: credentialFile)) - - let creds = try GoogleServiceAccountCredentials(contentsOfFile: credentialFile) - - XCTAssert(creds.clientId == "CLIENTID") - XCTAssert(creds.type == "service_account") - } - - static var allTests = [ - ("testLoadApplicationDefaultCredentials", testLoadApplicationDefaultCredentials), - ("testLoadServiceAccount", testLoadServiceAccountCredentials) - ] -} diff --git a/Tests/GoogleCloudTests/Fixtures/ADC.json b/Tests/GoogleCloudTests/Fixtures/ADC.json deleted file mode 100644 index a043ad5..0000000 --- a/Tests/GoogleCloudTests/Fixtures/ADC.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "client_id": "IDSTRING.apps.googleusercontent.com", - "client_secret": "SECRETSTRING", - "refresh_token": "REFRESHTOKEN", - "type": "authorized_user" -} diff --git a/Tests/GoogleCloudTests/Fixtures/ServiceAccount.json b/Tests/GoogleCloudTests/Fixtures/ServiceAccount.json deleted file mode 100644 index db3a322..0000000 --- a/Tests/GoogleCloudTests/Fixtures/ServiceAccount.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "type": "service_account", - "project_id": "PROJECTID", - "private_key_id": "PRIVATEKEYID", - "private_key": "-----BEGIN PRIVATE KEY-----\nPEMPRIVATEKEY\n-----END PRIVATE KEY-----\n", - "client_email": "SERVICEACCOUNTNAME@PROJECTID.iam.gserviceaccount.com", - "client_id": "CLIENTID", - "auth_uri": "https://accounts.google.com/o/oauth2/auth", - "token_uri": "https://accounts.google.com/o/oauth2/token", - "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", - "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/SERVICEACCOUNTNAME%40PROJECTID.iam.gserviceaccount.com" -} diff --git a/Tests/GoogleCloudTests/GoogleCloudProviderTests.swift b/Tests/GoogleCloudTests/GoogleCloudProviderTests.swift deleted file mode 100644 index e366994..0000000 --- a/Tests/GoogleCloudTests/GoogleCloudProviderTests.swift +++ /dev/null @@ -1,15 +0,0 @@ -import XCTest -@testable import GoogleCloud - -final class GoogleCloudProviderTests: XCTestCase { - func testExample() { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct - // results. - } - - - static var allTests = [ - ("testExample", testExample), - ] -} diff --git a/Tests/GoogleCloudTests/XCTestManifests.swift b/Tests/GoogleCloudTests/XCTestManifests.swift index a9b6649..f9ce505 100644 --- a/Tests/GoogleCloudTests/XCTestManifests.swift +++ b/Tests/GoogleCloudTests/XCTestManifests.swift @@ -3,8 +3,6 @@ import XCTest #if !os(macOS) public func allTests() -> [XCTestCaseEntry] { return [ - testCase(GoogleCloudProviderTests.allTests), - testCase(CredentialTests.allTests), ] } #endif diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 4a34246..3c6f89c 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -1,7 +1,6 @@ import XCTest -import GoogleCloudProviderTests var tests = [XCTestCaseEntry]() -tests += GoogleCloudProviderTests.allTests() -XCTMain(tests) \ No newline at end of file + +XCTMain(tests)