Sources/XCMetricsBackendLib/UploadMetrics/Repository/LogFileGCSRepository.swift (92 lines of code) (raw):
// Copyright (c) 2020 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
import Core
import Storage
import Vapor
/// `LogFileRepository` that stores and fetches logs from Google Cloud Storage
struct LogFileGCSRepository: LogFileRepository {
let credentialsConfiguration: GoogleCloudCredentialsConfiguration
let cloudStorageConfiguration: GoogleCloudStorageConfiguration
let bucketName: String
let group: EventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
let logger: Logger
init(bucketName: String, credentialsConfiguration: GoogleCloudCredentialsConfiguration,
cloudStorageConfiguration: GoogleCloudStorageConfiguration, logger: Logger) {
self.bucketName = bucketName
self.credentialsConfiguration = credentialsConfiguration
self.cloudStorageConfiguration = cloudStorageConfiguration
self.logger = logger
}
init?(config: Configuration, logger: Logger) {
guard let gcProject = config.googleProject, let bucket = config.gcsBucket else {
return nil
}
let credentialsConfiguration: GoogleCloudCredentialsConfiguration?
if let credentials = config.googleCredentialsFile {
credentialsConfiguration = try? GoogleCloudCredentialsConfiguration(projectId: gcProject,
credentialsFile: credentials)
} else {
credentialsConfiguration = try? GoogleCloudCredentialsConfiguration(projectId: gcProject)
}
guard let credentialsConf = credentialsConfiguration else {
return nil
}
self.init(bucketName: bucket,
credentialsConfiguration: credentialsConf,
cloudStorageConfiguration: GoogleCloudStorageConfiguration.default(),
logger: logger)
}
func put(logFile: File) throws -> URL {
let httpClient = HTTPClient(eventLoopGroupProvider: .shared(group))
defer {
try? httpClient.syncShutdown()
}
let groupEventLoop = group.next()
let gcs = try GoogleCloudStorageClient(credentials: credentialsConfiguration,
storageConfig: cloudStorageConfiguration,
httpClient: httpClient,
eventLoop: groupEventLoop)
let mediaLink = try gcs.object.createSimpleUpload(bucket: bucketName,
body: .byteBuffer(logFile.data.xcm_onlyFileData()),
name: logFile.filename,
contentType: "application/octet-stream")
.flatMap { uploadedObject -> EventLoopFuture<String> in
if let mediaLink = uploadedObject.mediaLink {
return groupEventLoop.makeSucceededFuture(mediaLink)
} else {
return groupEventLoop.future(error: RepositoryError.unexpected(message: "No link"))
}
}.wait()
guard let fileURL = URL(string: mediaLink) else {
throw RepositoryError.unexpected(message: "Malformed URL \(mediaLink)")
}
return fileURL
}
func get(logURL: URL) throws -> LogFile {
logger.info("[LogFileGCSRepository] get \(logURL), bucket: \(bucketName), object: \(logURL.lastPathComponent)")
let httpClient = HTTPClient(eventLoopGroupProvider: .shared(group))
defer {
try? httpClient.syncShutdown()
}
let groupEventLoop = group.next()
let gcs = try GoogleCloudStorageClient(credentials: credentialsConfiguration,
storageConfig: cloudStorageConfiguration,
httpClient: httpClient,
eventLoop: groupEventLoop)
let localURL = try gcs.object.getMedia(bucket: bucketName, object: logURL.lastPathComponent).flatMap { gcsObject -> EventLoopFuture<URL> in
guard let data = gcsObject.data else {
self.logger.error("[LogFileGCSRepository] file not found")
return groupEventLoop.makeFailedFuture(RepositoryError.unexpected(message: "File not found in GCS \(logURL.lastPathComponent)"))
}
do {
self.logger.info("[LogFileGCSRepository] saving file")
let tmp = try TemporaryFile(creatingTempDirectoryForFilename: "\(UUID().uuidString).xcactivitylog")
try data.write(to: tmp.fileURL)
self.logger.info("[LogFileGCSRepository] file saved to \(tmp.fileURL)")
return groupEventLoop.makeSucceededFuture(tmp.fileURL)
} catch {
self.logger.error("[LogFileGCSRepository] error saving log locally \(error)")
return groupEventLoop.makeFailedFuture(error)
}
}.wait()
return LogFile(remoteURL: logURL, localURL: localURL)
}
}