diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b1268cf..2652a6b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -17,18 +17,12 @@ jobs: key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }} restore-keys: | ${{ runner.os }}-spm- - - name: Build ARM64 + - name: Build run: | - swift build -c release --arch arm64 - mv `swift build -c release --arch arm64 --show-bin-path`/torino torino-arm64 - - name: Build AMD64 - run: | - swift build -c release --arch x86_64 - mv `swift build -c release --arch x86_64 --show-bin-path`/torino torino-x86_64 + swift build -c release --arch x86_64 --arch arm64 + mv `swift build -c release --arch x86_64 --arch arm64 --show-bin-path`/torino torino - name: Release uses: softprops/action-gh-release@v2 with: generate_release_notes: true - files: | - torino-arm64 - torino-x86_64 \ No newline at end of file + files: torino \ No newline at end of file diff --git a/.github/xcode-version b/.github/xcode-version index 0d57595..67eb807 100644 --- a/.github/xcode-version +++ b/.github/xcode-version @@ -1 +1 @@ -15.2 \ No newline at end of file +16.1 \ No newline at end of file diff --git a/Package.resolved b/Package.resolved index cd48292..3c445a1 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,52 +1,78 @@ { - "object": { - "pins": [ - { - "package": "jwt-kit", - "repositoryURL": "https://github.com/vapor/jwt-kit", - "state": { - "branch": null, - "revision": "87ce13a1df913ba4d51cf00606df7ef24d455571", - "version": "4.7.0" - } - }, - { - "package": "swift-argument-parser", - "repositoryURL": "https://github.com/apple/swift-argument-parser", - "state": { - "branch": null, - "revision": "fddd1c00396eed152c45a46bea9f47b98e59301d", - "version": "1.2.0" - } - }, - { - "package": "swift-crypto", - "repositoryURL": "https://github.com/apple/swift-crypto.git", - "state": { - "branch": null, - "revision": "71ae6adf89ba5346a209ec7f48dbb571a7e8ad1e", - "version": "2.2.1" - } - }, - { - "package": "swift-system", - "repositoryURL": "https://github.com/apple/swift-system.git", - "state": { - "branch": null, - "revision": "836bc4557b74fe6d2660218d56e3ce96aff76574", - "version": "1.1.1" - } - }, - { - "package": "swift-tools-support-core", - "repositoryURL": "https://github.com/apple/swift-tools-support-core", - "state": { - "branch": null, - "revision": "0b77e67c484e532444ceeab60119b8536f8cd648", - "version": "0.3.0" - } - } - ] - }, - "version": 1 + "originHash" : "767a949f0d5e38ecc331a58fdd65d4ef898f3f3c46f43ab0e770b5530dd50607", + "pins" : [ + { + "identity" : "google-auth-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/olejnjak/google-auth-swift", + "state" : { + "revision" : "630ebdd31375b62e71565cfea97cf322ba79a2a6", + "version" : "0.1.0" + } + }, + { + "identity" : "jwt-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/jwt-kit", + "state" : { + "revision" : "02a0fa600eee1bdc892013d62fc795fc623a5cc3", + "version" : "5.1.0" + } + }, + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser", + "state" : { + "revision" : "41982a3656a71c768319979febd796c6fd111d5c", + "version" : "1.5.0" + } + }, + { + "identity" : "swift-asn1", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-asn1.git", + "state" : { + "revision" : "7faebca1ea4f9aaf0cda1cef7c43aecd2311ddf6", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-certificates", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-certificates.git", + "state" : { + "revision" : "1fbb6ef21f1525ed5faf4c95207b9c11bea27e94", + "version" : "1.6.1" + } + }, + { + "identity" : "swift-crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-crypto.git", + "state" : { + "revision" : "06dc63c6d8da54ee11ceb268cde1fa68161afc96", + "version" : "3.9.1" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "9cb486020ebf03bfa5b5df985387a14a98744537", + "version" : "1.6.1" + } + }, + { + "identity" : "swift-tools-support-core", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-tools-support-core", + "state" : { + "revision" : "5b130e04cc939373c4713b91704b0c47ceb36170", + "version" : "0.7.1" + } + } + ], + "version" : 3 } diff --git a/Package.swift b/Package.swift index ce288b2..9657037 100644 --- a/Package.swift +++ b/Package.swift @@ -1,11 +1,11 @@ -// swift-tools-version:5.5 +// swift-tools-version:6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "Torino", - platforms: [.macOS(.v11)], + platforms: [.macOS(.v13)], products: [ .executable( name: "torino", @@ -17,9 +17,18 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/apple/swift-argument-parser", .upToNextMajor(from: "1.0.1")), - .package(url: "https://github.com/apple/swift-tools-support-core", .upToNextMajor(from: "0.2.0")), - .package(url: "https://github.com/vapor/jwt-kit", .upToNextMajor(from: "4.2.6")), + .package( + url: "https://github.com/apple/swift-argument-parser", + from: "1.0.1" + ), + .package( + url: "https://github.com/apple/swift-tools-support-core", + from: "0.2.0" + ), + .package( + url: "https://github.com/olejnjak/google-auth-swift", + from: "0.1.0" + ) ], targets: [ .executableTarget( @@ -31,8 +40,14 @@ let package = Package( dependencies: [ "GCP_Remote", "Logger", - .product(name: "ArgumentParser", package: "swift-argument-parser"), - .product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"), + .product( + name: "ArgumentParser", + package: "swift-argument-parser" + ), + .product( + name: "SwiftToolsSupport-auto", + package: "swift-tools-support-core" + ), ] ), .testTarget( @@ -43,15 +58,25 @@ let package = Package( name: "GCP_Remote", dependencies: [ "Logger", - .product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"), - .product(name: "JWTKit", package: "jwt-kit") + .product( + name: "SwiftToolsSupport-auto", + package: "swift-tools-support-core" + ), + .product( + name: "GoogleAuth", + package: "google-auth-swift" + ) ] ), .target( name: "Logger", dependencies: [ - .product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"), + .product( + name: "SwiftToolsSupport-auto", + package: "swift-tools-support-core" + ), ] ), - ] + ], + swiftLanguageModes: [.v5] ) diff --git a/README.md b/README.md index 5f9b183..a9da51b 100644 --- a/README.md +++ b/README.md @@ -34,9 +34,11 @@ Torino download --prefix "Swift-5.5" ### Remote caching -Torino currently supports remote cache stored in GCP buckets. To support that you need to provide two environment variables: +Torino currently supports remote cache stored in GCP buckets. To support that you need to provide an environment variables: `TORINO_GCP_BUCKET` - name of bucket that will be used for storage
+ +For authorization you can either use `TORINO_GCP_SERVICE_ACCOUNT_PATH` or [Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials) `TORINO_GCP_SERVICE_ACCOUNT_PATH` - location of service account that will be used for access to specified bucket ### Environment configuration diff --git a/Sources/GCP_Remote/Extensions/TokenExtensions.swift b/Sources/GCP_Remote/Extensions/TokenExtensions.swift new file mode 100644 index 0000000..0603f9f --- /dev/null +++ b/Sources/GCP_Remote/Extensions/TokenExtensions.swift @@ -0,0 +1,8 @@ +import Foundation +import GoogleAuth + +extension Token { + func addToRequest(_ request: inout URLRequest) { + request.setValue(tokenType + " " + accessToken, forHTTPHeaderField: "Authorization") + } +} diff --git a/Sources/GCP_Remote/Extensions/URLSessionExtensions.swift b/Sources/GCP_Remote/Extensions/URLSessionExtensions.swift index f317515..938d66a 100644 --- a/Sources/GCP_Remote/Extensions/URLSessionExtensions.swift +++ b/Sources/GCP_Remote/Extensions/URLSessionExtensions.swift @@ -7,91 +7,4 @@ struct RequestError: Error { extension URLSession { static let torino = URLSession(configuration: .ephemeral) - - @available(*, deprecated, renamed: "data(request:)") - func syncDataTask(for request: URLRequest) throws -> (Data?, URLResponse?) { - let semaphore = DispatchSemaphore(value: 0) - - var resultData: Data? - var resultResponse: URLResponse? - var resultError: Error? - - let task = dataTask(with: request) { data, response, error in - resultData = data - resultResponse = response - resultError = error - semaphore.signal() - } - - task.resume() - semaphore.wait() - - if let error = resultError { - throw error - } - - if let httpResponse = (resultResponse as? HTTPURLResponse), - (200...299).contains(httpResponse.statusCode) { - return (resultData, resultResponse) - } - - throw RequestError(response: resultResponse, data: resultData) - } -} - -// Swift Concurrency API is available from macOS 12, -// to support lower deployment target we need following extensions -@available(macOS, deprecated: 12.0, message: "Use the built-in API instead") -extension URLSession { - @discardableResult - func data(request: URLRequest) async throws -> (Data, URLResponse) { - try await withUnsafeThrowingContinuation { continuation in - let task = self.dataTask( - with: request, - completionHandler: Self.taskCompletion(continuation: continuation) - ) - - task.resume() - } - } - - @discardableResult - func data(url: URL) async throws -> (Data, URLResponse) { - try await data(request: URLRequest(url: url)) - } - - @discardableResult - func upload(request: URLRequest, fromFile file: URL) async throws -> (Data, URLResponse) { - try await withUnsafeThrowingContinuation { continuation in - let task = self.uploadTask( - with: request, - fromFile: file, - completionHandler: Self.taskCompletion(continuation: continuation) - ) - - task.resume() - } - } - - private static func taskCompletion(continuation: UnsafeContinuation<(Data, URLResponse), Error>) -> (Data?, URLResponse?, Error?) -> () { - { data, response, error in - guard let data = data, let response = response else { - let error = error ?? URLError(.badServerResponse) - return continuation.resume(throwing: error) - } - - if let httpResponse = (response as? HTTPURLResponse), - (200...299).contains(httpResponse.statusCode) { - continuation.resume(returning: (data, response)) - } else { - continuation.resume( - throwing: - URLError( - .cannotParseResponse, - userInfo: [:] - ) - ) - } - } - } } diff --git a/Sources/GCP_Remote/Model/AccessToken.swift b/Sources/GCP_Remote/Model/AccessToken.swift deleted file mode 100644 index 0f35d63..0000000 --- a/Sources/GCP_Remote/Model/AccessToken.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// AccessToken.swift -// -// -// Created by Jakub Olejník on 11/12/2019. -// - -import Foundation - -/// Struct holding response of auth token request -public struct AccessToken: Decodable { - enum CodingKeys: String, CodingKey { - case accessToken = "access_token" - case expiration = "expires_in" - case type = "token_type" - } - - let accessToken: String - let expiration: TimeInterval - let type: String - - /// Value that can be used in HTTP request header - private var headerValue: String { type + " " + accessToken } -} - -internal struct AccessTokenRequest: Encodable { - enum CodingKeys: String, CodingKey { - case assertion - case grantType = "grant_type" - } - - /// Previously generated JWT token - let assertion: String - - /// Requested grant type - let grantType = "urn:ietf:params:oauth:grant-type:jwt-bearer" -} - -extension AccessToken { - /// Adds `Authorization` header to given `request` - public func addToRequest(_ request: inout URLRequest) { - request.addValue(headerValue, forHTTPHeaderField: "Authorization") - } -} diff --git a/Sources/GCP_Remote/Model/GCPConfig.swift b/Sources/GCP_Remote/Model/GCPConfig.swift index bdf6dd7..8ca5adf 100644 --- a/Sources/GCP_Remote/Model/GCPConfig.swift +++ b/Sources/GCP_Remote/Model/GCPConfig.swift @@ -7,9 +7,9 @@ public struct GCPConfig: Decodable { } public let bucket: String - public let serviceAccountPath: String - - public init(bucket: String, serviceAccountPath: String) { + public let serviceAccountPath: String? + + public init(bucket: String, serviceAccountPath: String?) { self.bucket = bucket self.serviceAccountPath = serviceAccountPath } diff --git a/Sources/GCP_Remote/Model/GoogleClaims.swift b/Sources/GCP_Remote/Model/GoogleClaims.swift deleted file mode 100644 index 7aae0a4..0000000 --- a/Sources/GCP_Remote/Model/GoogleClaims.swift +++ /dev/null @@ -1,35 +0,0 @@ -import Foundation -import JWTKit - -/// Struct that is used for generating second part of JWT token -struct GoogleClaims: JWTPayload { - enum Scope: String, Codable { - case readOnly = "https://www.googleapis.com/auth/devstorage.read_only" - case readWrite = "https://www.googleapis.com/auth/devstorage.full_control" - } - - /// Service account email - let iss: String - - /// Required scope - let scope: Scope - - /// Desired auth endpoint - let aud: URL - - /// Date of expiration timestamp - let exp: Int - - /// Issued at date timestamp - let iat: Int -} - -extension GoogleClaims { - init(serviceAccount: ServiceAccount, scope: Scope, exp: Int, iat: Int) { - self.init(iss: serviceAccount.clientEmail, scope: scope, aud: serviceAccount.tokenURL, exp: exp, iat: iat) - } - - func verify(using signer: JWTSigner) throws { - try ExpirationClaim(value: Date(timeIntervalSince1970: TimeInterval(exp))).verifyNotExpired() - } -} diff --git a/Sources/GCP_Remote/Model/ServiceAccount.swift b/Sources/GCP_Remote/Model/ServiceAccount.swift deleted file mode 100644 index 144d556..0000000 --- a/Sources/GCP_Remote/Model/ServiceAccount.swift +++ /dev/null @@ -1,19 +0,0 @@ -import Foundation - -/// Struct holding necessary information about service account which should access spreadsheet -public struct ServiceAccount: Decodable { - enum CodingKeys: String, CodingKey { - case clientEmail = "client_email" - case privateKey = "private_key" - case tokenURL = "token_uri" - } - - /// Email associated with the service account - let clientEmail: String - - /// Private key used to generate JWT token - let privateKey: String - - /// URL for fetching OAuth token - let tokenURL: URL -} diff --git a/Sources/GCP_Remote/Services/AuthAPIService.swift b/Sources/GCP_Remote/Services/AuthAPIService.swift deleted file mode 100644 index 8b05c54..0000000 --- a/Sources/GCP_Remote/Services/AuthAPIService.swift +++ /dev/null @@ -1,65 +0,0 @@ -import Foundation -import JWTKit - -/// Protocol wrapping a service that fetches an access token from further communication -public protocol AuthAPIServicing { - /// Fetch access token for given `serviceAccount` - func fetchAccessToken( - serviceAccount: ServiceAccount, - validFor interval: TimeInterval, - readOnly: Bool - ) async throws -> AccessToken -} - -/// Service that fetches an access token from further communication -public struct AuthAPIService: AuthAPIServicing { - private let session: URLSession - private let encoder = JSONEncoder() - private let decoder = JSONDecoder() - - // MARK: - Initializers - - public init() { - self.init(session: .torino) - } - - public init(session: URLSession) { - self.session = session - } - - // MARK: - API calls - - /// Fetch access token for given `serviceAccount` - public func fetchAccessToken( - serviceAccount: ServiceAccount, - validFor interval: TimeInterval, - readOnly: Bool - ) async throws -> AccessToken { - let claims = self.claims(serviceAccount: serviceAccount, validFor: interval, readOnly: readOnly) - let jwt = try self.jwt(for: serviceAccount, claims: claims) - let requestData = AccessTokenRequest(assertion: jwt) - var request = URLRequest(url: claims.aud) - request.httpMethod = "POST" - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - request.httpBody = try encoder.encode(requestData) - return try await decoder.decode( - AccessToken.self, - from: session.data(request: request).0 - ) - } - - // MARK: - Private helpers - - /// Create JWT token that will be sent to retrieve access token - private func jwt(for serviceAccount: ServiceAccount, claims: GoogleClaims) throws -> String { - let signers = JWTSigners() - try signers.use(.rs256(key: .private(pem: serviceAccount.privateKey))) - return try signers.sign(claims) - } - - private func claims(serviceAccount sa: ServiceAccount, validFor interval: TimeInterval, readOnly: Bool) -> GoogleClaims { - let now = Int(Date().timeIntervalSince1970) - - return .init(serviceAccount: sa, scope: readOnly ? .readOnly : .readWrite, exp: now + Int(interval), iat: now) - } -} diff --git a/Sources/GCP_Remote/Services/ConfigLoader.swift b/Sources/GCP_Remote/Services/ConfigLoader.swift index 3a4d6ee..56ce593 100644 --- a/Sources/GCP_Remote/Services/ConfigLoader.swift +++ b/Sources/GCP_Remote/Services/ConfigLoader.swift @@ -1,12 +1,5 @@ import Foundation -func loadServiceAccount(path: String) throws -> ServiceAccount { - try JSONDecoder().decode( - ServiceAccount.self, - from: try Data(contentsOf: URL(fileURLExpandingTildeInPath: path)) - ) -} - private extension URL { init(fileURLExpandingTildeInPath path: String) { self.init(fileURLWithPath: (path as NSString).expandingTildeInPath) diff --git a/Sources/GCP_Remote/Services/GCPAPIService.swift b/Sources/GCP_Remote/Services/GCPAPIService.swift index 6e7177c..9912f6a 100644 --- a/Sources/GCP_Remote/Services/GCPAPIService.swift +++ b/Sources/GCP_Remote/Services/GCPAPIService.swift @@ -1,30 +1,31 @@ import Foundation +import GoogleAuth public protocol GCPAPIServicing { func downloadObject( _ object: String, bucket: String, - token: AccessToken + token: Token ) async throws -> Data func upload( file: URL, object: String, bucket: String, - token: AccessToken + token: Token ) async throws func metadata( object: String, bucket: String, - token: AccessToken + token: Token ) async throws -> Metadata func updateMetadata( _ metadata: Metadata, object: String, bucket: String, - token: AccessToken + token: Token ) async throws } @@ -50,7 +51,7 @@ public final class GCPAPIService: GCPAPIServicing { public func downloadObject( _ object: String, bucket: String, - token: AccessToken + token: Token ) async throws -> Data { var urlComponents = URLComponents(string: url( action: .download, @@ -64,14 +65,14 @@ public final class GCPAPIService: GCPAPIServicing { token.addToRequest(&request) request.httpMethod = "GET" - return try await session.data(request: request).0 + return try await session.data(for: request).0 } public func upload( file: URL, object: String, bucket: String, - token: AccessToken + token: Token ) async throws { var urlComponents = URLComponents(string: url( action: .upload, @@ -86,14 +87,14 @@ public final class GCPAPIService: GCPAPIServicing { token.addToRequest(&request) request.setValue("application/zip", forHTTPHeaderField: "Content-Type") request.httpMethod = "POST" - - try await session.upload(request: request, fromFile: file) + + _ = try await session.upload(for: request, fromFile: file) } public func metadata( object: String, bucket: String, - token: AccessToken + token: Token ) async throws -> Metadata { var request = URLRequest(url: url( action: .get, @@ -104,7 +105,7 @@ public final class GCPAPIService: GCPAPIServicing { request.httpMethod = "GET" return try await JSONDecoder().decode( Metadata.self, - from: session.data(request: request).0 + from: session.data(for: request).0 ) } @@ -112,7 +113,7 @@ public final class GCPAPIService: GCPAPIServicing { _ metadata: Metadata, object: String, bucket: String, - token: AccessToken + token: Token ) async throws { var request = URLRequest(url: .init(string: "https://storage.googleapis.com/storage/v1/b/\(bucket)/o/\(object.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)!)")!) token.addToRequest(&request) @@ -120,7 +121,7 @@ public final class GCPAPIService: GCPAPIServicing { request.addValue("application/json", forHTTPHeaderField: "Content-Type") request.httpBody = try JSONEncoder().encode(metadata) - try await session.data(request: request) + _ = try await session.data(for: request) } // MARK: - Private helpers diff --git a/Sources/GCP_Remote/Services/GCPDownloader.swift b/Sources/GCP_Remote/Services/GCPDownloader.swift index 48b7b05..d7b6123 100644 --- a/Sources/GCP_Remote/Services/GCPDownloader.swift +++ b/Sources/GCP_Remote/Services/GCPDownloader.swift @@ -1,4 +1,5 @@ import Foundation +import GoogleAuth import TSCBasic import Logger @@ -9,7 +10,6 @@ public protocol GCPDownloading { } public struct GCPDownloader: GCPDownloading { - private let authAPI: AuthAPIServicing private let gcpAPI: GCPAPIServicing private let fileSystem: FileSystem private let logger: Logging @@ -18,13 +18,11 @@ public struct GCPDownloader: GCPDownloading { // MARK: - Initializers public init( - authAPI: AuthAPIServicing = AuthAPIService(), gcpAPI: GCPAPIServicing = GCPAPIService(), fileSystem: FileSystem = localFileSystem, logger: Logging = Logger.shared, config: GCPConfig ) { - self.authAPI = authAPI self.gcpAPI = gcpAPI self.fileSystem = fileSystem self.logger = logger @@ -38,14 +36,24 @@ public struct GCPDownloader: GCPDownloading { logger.info("Nothing to download") return } - - let sa = try loadServiceAccount(path: config.serviceAccountPath) - let token = try await authAPI.fetchAccessToken( - serviceAccount: sa, - validFor: 60, - readOnly: false - ) - + + let tokenProvider: TokenProvider + let scopes = ["https://www.googleapis.com/auth/devstorage.full_control"] + + if let saPath = config.serviceAccountPath { + tokenProvider = try await ServiceAccountTokenProvider( + serviceAccountPath: saPath, + scopes: scopes + ) + } else if let provider = await DefaultCredentialsTokenProvider(scopes: scopes) { + tokenProvider = provider + } else { + struct CannotCreateProvider: Error { } + throw CannotCreateProvider() + } + + let token = try await tokenProvider.token() + await items.asyncForEach { object, localPath in let name = localPath.basenameWithoutExt diff --git a/Sources/GCP_Remote/Services/GCPUploader.swift b/Sources/GCP_Remote/Services/GCPUploader.swift index ebf198a..b2bb388 100644 --- a/Sources/GCP_Remote/Services/GCPUploader.swift +++ b/Sources/GCP_Remote/Services/GCPUploader.swift @@ -1,4 +1,5 @@ import Foundation +import GoogleAuth import Logger import CryptoKit @@ -7,7 +8,6 @@ public protocol GCPUploading { } public struct GCPUploader: GCPUploading { - private let authAPI: AuthAPIServicing private let gcpAPI: GCPAPIServicing private let logger: Logging private let config: GCPConfig @@ -15,12 +15,10 @@ public struct GCPUploader: GCPUploading { // MARK: - Initializers public init( - authAPI: AuthAPIServicing = AuthAPIService(), gcpAPI: GCPAPIServicing = GCPAPIService(), logger: Logging = Logger.shared, config: GCPConfig ) { - self.authAPI = authAPI self.gcpAPI = gcpAPI self.logger = logger self.config = config @@ -34,13 +32,23 @@ public struct GCPUploader: GCPUploading { return } - let sa = try loadServiceAccount(path: config.serviceAccountPath) - let token = try await authAPI.fetchAccessToken( - serviceAccount: sa, - validFor: 60, - readOnly: false - ) - + let tokenProvider: TokenProvider + let scopes = ["https://www.googleapis.com/auth/devstorage.full_control"] + + if let saPath = config.serviceAccountPath { + tokenProvider = try await ServiceAccountTokenProvider( + serviceAccountPath: saPath, + scopes: scopes + ) + } else if let provider = await DefaultCredentialsTokenProvider(scopes: scopes) { + tokenProvider = provider + } else { + struct CannotCreateProvider: Error { } + throw CannotCreateProvider() + } + + let token = try await tokenProvider.token() + try await items.asyncForEach { let localPath = $0.localFile let remotePath = $0.remotePath