# frozen_string_literal: truegem"google-cloud-storage","~> 1.8"require"google/cloud/storage"require"net/http"require"active_support/core_ext/object/to_query"moduleActiveStorage# Wraps the Google Cloud Storage as an Active Storage service. See ActiveStorage::Service for the generic API# documentation that applies to all services.classService::GCSService<Servicedefinitialize(**config)@config=configenddefupload(key,io,checksum: nil,content_type: nil,disposition: nil,filename: nil)instrument:upload,key: key,checksum: checksumdobegin# GCS's signed URLs don't include params such as response-content-type response-content_disposition# in the signature, which means an attacker can modify them and bypass our effort to force these to# binary and attachment when the file's content type requires it. The only way to force them is to# store them as object's metadata.content_disposition=content_disposition_with(type: disposition,filename: filename)ifdisposition&&filenamebucket.create_file(io,key,md5: checksum,content_type: content_type,content_disposition: content_disposition)rescueGoogle::Cloud::InvalidArgumentErrorraiseActiveStorage::IntegrityErrorendendenddefupdate_metadata(key,content_type:,disposition: nil,filename: nil)instrument:update_metadata,key: key,content_type: content_type,disposition: dispositiondofile_for(key).updatedo|file|file.content_type=content_typefile.content_disposition=content_disposition_with(type: disposition,filename: filename)ifdisposition&&filenameendendend# FIXME: Download in chunks when given a block.defdownload(key)instrument:download,key: keydoio=file_for(key).downloadio.rewindifblock_given?yieldio.stringelseio.stringendendenddefdownload_chunk(key,range)instrument:download_chunk,key: key,range: rangedofile=file_for(key)uri=URI(file.signed_url(expires: 30.seconds))Net::HTTP.start(uri.host,uri.port,use_ssl: uri.scheme=="https")do|client|client.get(uri,"Range"=>"bytes=#{range.begin}-#{range.exclude_end??range.end-1:range.end}").bodyendendenddefdelete(key)instrument:delete,key: keydobeginfile_for(key).deleterescueGoogle::Cloud::NotFoundError# Ignore files already deletedendendenddefdelete_prefixed(prefix)instrument:delete_prefixed,prefix: prefixdobucket.files(prefix: prefix).alldo|file|beginfile.deleterescueGoogle::Cloud::NotFoundError# Ignore concurrently-deleted filesendendendenddefexist?(key)instrument:exist,key: keydo|payload|answer=file_for(key).exists?payload[:exist]=answeranswerendenddefurl(key,expires_in:,filename:,content_type:,disposition:)instrument:url,key: keydo|payload|generated_url=file_for(key).signed_urlexpires: expires_in,query: {"response-content-disposition"=>content_disposition_with(type: disposition,filename: filename),"response-content-type"=>content_type}payload[:url]=generated_urlgenerated_urlendenddefurl_for_direct_upload(key,expires_in:,checksum:,**)instrument:url,key: keydo|payload|generated_url=bucket.signed_urlkey,method: "PUT",expires: expires_in,content_md5: checksumpayload[:url]=generated_urlgenerated_urlendenddefheaders_for_direct_upload(key,checksum:,**){"Content-MD5"=>checksum}endprivateattr_reader:configdeffile_for(key)bucket.file(key,skip_lookup: true)enddefbucket@bucket||=client.bucket(config.fetch(:bucket))enddefclient@client||=Google::Cloud::Storage.new(config.except(:bucket))endendend