# encoding: UTF-8 # frozen_string_literal: true # # Copyright (c) 2021 GoodData Corporation. All rights reserved. # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. require 'securerandom' require 'pathname' require "azure/storage/blob" module GoodData class BlobStorageClient SAS_URL_PATTERN = %r{(^https?:\/\/[^\/]*)\/.*\?(.*)} INVALID_BLOB_GENERAL_MESSAGE = "The connection string is not valid." INVALID_BLOB_SIG_WELL_FORMED_MESSAGE = "The signature format is not valid." INVALID_BLOB_CONTAINER_MESSAGE = "ContainerNotFound" INVALID_BLOB_CONTAINER_FORMED_MESSAGE = "The container with the specified name is not found." INVALID_BLOB_EXPIRED_ORIGINAL_MESSAGE = "Signature not valid in the specified time frame" INVALID_BLOB_EXPIRED_MESSAGE = "The signature expired." INVALID_BLOB_INVALID_CONNECTION_STRING_MESSAGE = "The connection string is not valid." INVALID_BLOB_PATH_MESSAGE = "BlobNotFound" INVALID_BLOB_INVALID_PATH_MESSAGE = "The path to the data is not found." attr_reader :use_sas def initialize(options = {}) raise("Data Source needs a client to Blob Storage to be able to get blob file but 'blobStorage_client' is empty.") unless options['blobStorage_client'] if options['blobStorage_client']['connectionString'] && options['blobStorage_client']['container'] @connection_string = options['blobStorage_client']['connectionString'] @container = options['blobStorage_client']['container'] @path = options['blobStorage_client']['path'] @use_sas = false build_sas(@connection_string) else raise('Missing connection info for Blob Storage client') end end def realize_blob(file, _params) GoodData.gd_logger.info("Realizing download from Blob Storage. Container #{@container}.") filename = '' begin connect filename = "#{SecureRandom.urlsafe_base64(6)}_#{Time.now.to_i}.csv" blob_name = get_blob_name(@path, file) measure = Benchmark.measure do _blob, content = @client.get_blob(@container, blob_name) File.open(filename, "wb") { |f| f.write(content) } end rescue => e raise_error(e) end GoodData.gd_logger.info("Done downloading file type=blobStorage status=finished duration=#{measure.real}") filename end def connect GoodData.logger.info "Setting up connection to Blob Storage" if use_sas @client = Azure::Storage::Blob::BlobService.create(:storage_blob_host => @host, :storage_sas_token => @sas_token) else @client = Azure::Storage::Blob::BlobService.create_from_connection_string(@connection_string) end end def build_sas(url) matches = url.scan(SAS_URL_PATTERN) return unless matches && matches[0] @use_sas = true @host = matches[0][0] @sas_token = matches[0][1] end def raise_error(e) if e.message && e.message.include?(INVALID_BLOB_EXPIRED_ORIGINAL_MESSAGE) raise INVALID_BLOB_EXPIRED_MESSAGE elsif e.message && e.message.include?(INVALID_BLOB_SIG_WELL_FORMED_MESSAGE) raise INVALID_BLOB_SIG_WELL_FORMED_MESSAGE elsif e.message && e.message.include?(INVALID_BLOB_CONTAINER_MESSAGE) raise INVALID_BLOB_CONTAINER_FORMED_MESSAGE elsif e.message && e.message.include?(INVALID_BLOB_PATH_MESSAGE) raise INVALID_BLOB_INVALID_PATH_MESSAGE else raise INVALID_BLOB_GENERAL_MESSAGE end end def get_blob_name(path, file) return file unless path path.rindex('/') == path.length - 1 ? "#{path}#{file}" : "#{path}/#{file}" end end end