Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Google Cloud Storage uploader #240

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,10 +203,12 @@ end
```


Uploading screenshots to S3
Uploading screenshots to AWS S3 or Google Cloud Storage
--------------------------
You can configure capybara-screenshot to automatically save your screenshots to an AWS S3 bucket.
You can configure capybara-screenshot to automatically save your screenshots to either AWS S3
or Google Cloud Storage bucket but noth both.

### AWS S3
First, install the `aws-sdk-s3` gem or add it to your Gemfile:

```ruby
Expand Down Expand Up @@ -245,6 +247,36 @@ Capybara::Screenshot.s3_configuration = {
}
```

### GCS

Google Cloud Storage configuration is very simiar to that of S3.
Install the `google-cloud-storage` gem or add it to your Gemfile.

Next, configure capybara-screenshot with your GCS credentials either stored in a
[JSON file](https://cloud.google.com/iam/docs/creating-managing-service-account-keys)
or as a Hash, and the bucket to save to.
Note that one may use [environment variables to set up credentials](https://googleapis.github.io/google-cloud-ruby/docs/google-cloud-storage/latest/file.AUTHENTICATION) as well.

```ruby
Capybara::Screenshot.gcs_configuration = {
credentials: 'path-to-credentials.json',
bucket_name: 'my_screenshots',
key_prefix: "some/folder/"
}
```

It is also possible to specify object metadata.
If gzip content encoding is specified, uploaded files will be compressed for you to save on storage space.
Configure the capybara-screenshot with these options in this way:

```ruby
Capybara::Screenshot.gcs_object_configuration = {
content_encoding: 'gzip',
acl: 'public_read'
}
```


Pruning old screenshots automatically
--------------------------
By default, screenshots are saved indefinitely. If you want them to be automatically pruned on a new failure, then you can specify one of the following prune strategies as follows:
Expand Down
1 change: 1 addition & 0 deletions capybara-screenshot.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Gem::Specification.new do |s|
s.add_development_dependency 'spinach'
s.add_development_dependency 'minitest'
s.add_development_dependency 'aws-sdk-s3'
s.add_development_dependency 'google-cloud-storage'

s.files = `git ls-files`.split("\n")
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
Expand Down
9 changes: 9 additions & 0 deletions lib/capybara-screenshot.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ class << self
attr_accessor :prune_strategy
attr_accessor :s3_configuration
attr_accessor :s3_object_configuration
attr_accessor :gcs_configuration
attr_accessor :gcs_object_configuration
end

self.autosave_on_failure = true
Expand All @@ -22,6 +24,8 @@ class << self
self.prune_strategy = :keep_all
self.s3_configuration = {}
self.s3_object_configuration = {}
self.gcs_configuration = {}
self.gcs_object_configuration = {}

def self.append_screenshot_path=(value)
$stderr.puts "WARNING: Capybara::Screenshot.append_screenshot_path is deprecated. " +
Expand Down Expand Up @@ -102,6 +106,11 @@ def self.new_saver(*args)
saver = S3Saver.new_with_configuration(saver, s3_configuration, s3_object_configuration)
end

unless gcs_configuration.empty?
require 'capybara-screenshot/gcs_saver'
saver = GcsSaver.new_with_configuration(saver, gcs_configuration, gcs_object_configuration)
end

return saver
end

Expand Down
74 changes: 74 additions & 0 deletions lib/capybara-screenshot/gcs_saver.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
require 'zlib'
require 'google/cloud/storage'

module Capybara
module Screenshot
class GcsSaver
attr_accessor :html_path, :screenshot_path

def initialize(saver, bucket, object_configuration, key_prefix)
@saver = saver
@bucket = bucket
@key_prefix = key_prefix
@object_configuration = object_configuration
end

def self.new_with_configuration(saver, configuration, object_configuration)
conf = configuration.dup
bucket_name = conf.delete(:bucket_name)
key_prefix = conf.delete(:key_prefix)
storage = Google::Cloud::Storage.new conf
bucket = storage.bucket bucket_name, skip_lookup: true

new(saver, bucket, object_configuration, key_prefix)
rescue KeyError
raise "Invalid GCS Configuration #{configuration}. Please refer to the documentation for the necessary configurations."
end

def save_and_upload_screenshot
save_and do |type, local_file_path|
if object_configuration.fetch(:content_encoding, '').to_sym.eql?(:gzip)
compressed = StringIO.new ""
gz = Zlib::GzipWriter.new(compressed, Zlib::BEST_COMPRESSION)
gz.mtime = File.mtime(local_file_path)
gz.orig_name = local_file_path
gz.write IO.binread(local_file_path)
gz.close
data = StringIO.new compressed.string
else
data = local_file_path
end
gcs_upload_path = "#{@key_prefix}#{File.basename(local_file_path)}"
f = bucket.create_file data, gcs_upload_path, object_configuration
if f.acl.readers.include? 'allUsers'
url = f.public_url
else
url = "https://storage.cloud.google.com/#{bucket.name}/#{gcs_upload_path}"
end
send("#{type}_path=", url)
end
end
alias_method :save, :save_and_upload_screenshot

def method_missing(method, *args)
# Need to use @saver instead of S3Saver#saver attr_reader method because
# using the method goes into infinite loop. Maybe attr_reader implements
# its methods via method_missing?
@saver.send(method, *args)
end

private
attr_reader :saver,
:bucket,
:object_configuration
:key_prefix

def save_and
saver.save

yield(:html, saver.html_path) if block_given? && saver.html_saved?
yield(:screenshot, saver.screenshot_path) if block_given? && saver.screenshot_saved?
end
end
end
end
199 changes: 199 additions & 0 deletions spec/unit/gcs_saver_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
require 'spec_helper'
require 'capybara-screenshot/gcs_saver'

describe Capybara::Screenshot::GcsSaver do
let(:saver) { double('saver') }
let(:credentials) { double('credentials') }
let(:bucket_name) { double('bucket_name') }
let(:bucket) { double('bucket') }
let(:gcs_object_configuration) { {} }
let(:gcs_client) { double('gcs_client') }
let(:key_prefix) { 'some/path/' }
let(:gcs_file) { double('gcs_file') }

let(:gcs_saver) { described_class.new(saver, bucket, gcs_object_configuration, nil) }
let(:gcs_saver_with_key_prefix) { described_class.new(saver, bucket, gcs_object_configuration, key_prefix) }

before do
allow(gcs_client).to receive(:bucket).and_return(bucket)
allow(bucket).to receive(:create_file).and_return(gcs_file)
allow(bucket).to receive(:name).and_return(bucket_name)
allow(gcs_file).to receive_message_chain(:acl, :readers, :include?).with('allUsers').and_return(false)
end

describe '.new_with_configuration' do
before do
allow(Google::Cloud::Storage).to receive(:new).and_return(gcs_client)
allow(described_class).to receive(:new)
end

it 'destructures the configuration into its components' do
described_class.new_with_configuration(saver, {
bucket_name: bucket_name
}, gcs_object_configuration)

expect(Google::Cloud::Storage).to have_received(:new).with({})
expect(described_class).to have_received(:new).with(saver, bucket, gcs_object_configuration, nil)
end

it 'passes key_prefix option if specified' do
described_class.new_with_configuration(saver, {
credentials: credentials,
bucket_name: bucket_name,
key_prefix: key_prefix,
}, gcs_object_configuration)

expect(Google::Cloud::Storage).to have_received(:new).with(credentials: credentials)
expect(described_class).to have_received(:new).with(saver, bucket, gcs_object_configuration, key_prefix)
end

it 'stores the object configuration when passed' do
gcs_object_configuration = { acl: 'public-read' }
Capybara::Screenshot.gcs_object_configuration = { acl: 'public-read' }

described_class.new_with_configuration(saver, {
credentials: credentials,
bucket_name: bucket_name
}, gcs_object_configuration)

expect(Google::Cloud::Storage).to have_received(:new).with(credentials: credentials)
expect(described_class).to have_received(:new).with(saver, bucket, gcs_object_configuration, nil)
end
end

describe '#save' do
before do
allow(saver).to receive(:html_saved?).and_return(false)
allow(saver).to receive(:screenshot_saved?).and_return(false)
allow(saver).to receive(:save)
end

it 'calls save on the underlying saver' do
expect(saver).to receive(:save)

gcs_saver.save
end

it 'uploads the html' do
html_path = '/foo/bar.html'
expect(saver).to receive(:html_path).and_return(html_path)
expect(saver).to receive(:html_saved?).and_return(true)

expect(bucket).to receive(:create_file).with(
html_path,
File.basename(html_path),
{}
)

gcs_saver.save
end

it 'uploads the screenshot' do
screenshot_path = '/baz/bim.jpg'
expect(saver).to receive(:screenshot_path).and_return(screenshot_path)
expect(saver).to receive(:screenshot_saved?).and_return(true)

expect(bucket).to receive(:create_file).with(
screenshot_path,
File.basename(screenshot_path),
{}
)

gcs_saver.save
end

context 'with object configuration' do
let(:gcs_object_configuration) { { acl: 'public_read' } }
let(:gcs_saver) { described_class.new(saver, bucket, gcs_object_configuration, nil) }
let(:public_url) { double('public_url') }

before do
allow(gcs_file).to receive_message_chain(:acl, :readers, :include?).with('allUsers').and_return(true)
allow(gcs_file).to receive(:public_url).and_return(public_url)
end

it 'uploads the html' do
html_path = '/foo/bar.html'
expect(saver).to receive(:html_path).and_return(html_path)
expect(saver).to receive(:html_saved?).and_return(true)
expect(gcs_saver).to receive(:html_path=).with(public_url)

expect(bucket).to receive(:create_file).with(
html_path,
File.basename(html_path),
acl: 'public_read'
)

gcs_saver.save
end

it 'uploads the screenshot' do
screenshot_path = '/baz/bim.jpg'
expect(saver).to receive(:screenshot_path).and_return(screenshot_path)
expect(saver).to receive(:screenshot_saved?).and_return(true)
expect(gcs_saver).to receive(:screenshot_path=).with(public_url)

expect(bucket).to receive(:create_file).with(
screenshot_path,
File.basename(screenshot_path),
acl: 'public_read'
)

gcs_saver.save
end
end

context 'with key_prefix specified' do
it 'uploads the html with key prefix' do
html_path = '/foo/bar.html'
expect(saver).to receive(:html_path).and_return(html_path)
expect(saver).to receive(:html_saved?).and_return(true)

expect(bucket).to receive(:create_file).with(
html_path,
'some/path/' + File.basename(html_path),
{}
)

gcs_saver_with_key_prefix.save
end

it 'uploads the screenshot with key prefix' do
screenshot_path = '/baz/bim.jpg'
expect(saver).to receive(:screenshot_path).and_return(screenshot_path)
expect(saver).to receive(:screenshot_saved?).and_return(true)

expect(bucket).to receive(:create_file).with(
screenshot_path,
'some/path/' + File.basename(screenshot_path),
{}
)

gcs_saver_with_key_prefix.save
end

context 'with gzip' do
let(:gcs_object_configuration) { { content_encoding: 'gzip' } }

it 'uploads the html' do
html_path = '/foo/bar.html'
expect(saver).to receive(:html_path).and_return(html_path)
expect(saver).to receive(:html_saved?).and_return(true)

html_file = double('html_file')

expect(File).to receive(:mtime).with(html_path).and_return(Time.now)
expect(IO).to receive(:binread).with(html_path).and_return(html_file)

expect(bucket).to receive(:create_file).with(
instance_of(StringIO),
'some/path/' + File.basename(html_path),
content_encoding: 'gzip'
)

gcs_saver_with_key_prefix.save
end
end
end
end
end