asset_cloud 2.3.1 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f22a2b32e4f1e13f4743603220642baf85b8d1b3ddb30191c5851e84a0e12239
4
- data.tar.gz: 3e5aece0ed39d47752f86be2f5da8a78a388db4b5f686df6eb100059f2435d6e
3
+ metadata.gz: 929356d981cb9cbc1f805c713071b909ff594ff05d8a54231d1f0bc85672ffd0
4
+ data.tar.gz: '09b7b0e5ce21d53385a5824619fed80dc76c11c348ea9a9cda531dd33e9a12d4'
5
5
  SHA512:
6
- metadata.gz: 2f2d1ccf6ad860ab28e84aa67046bf45b0b8bd9e5a47b4c1973e8bbb23c8b686d040a85420d260e47aaa68ee22d0ced688c52383f0f02e46e97edaf3d773a3f5
7
- data.tar.gz: 0e53e2aab25cc6dbeb0b7de72d94f5a8f3c671850451948577f9f6d876229fdd0cefc3bbace06561145dde8b838f7fc8688aa484e2fdd8c89a6b19403a8fc610
6
+ metadata.gz: 998ab0f3e4795f228adfa07b01ab1369a140c9ea5be4a729fd089953670fca677e31a5cee59d7adbdde0a507df33410fa82517367d0411e8f8b94e86baee4272
7
+ data.tar.gz: ba2de4239a9778afc6416cedbad4830a5d1f56f59824772a0fdbbfaaeaea1d20a90d6dd80db615a1dde8a12fc1eb6e053d54ec05f60dce4d9b44b9514300b62e
data/Gemfile CHANGED
@@ -1,6 +1,6 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- gem 'aws-sdk', '~> 1.34.0', :require => nil
3
+ gem 'aws-sdk-s3', '>= 1.60.2', require: false
4
4
  gem 'google-cloud-storage'
5
5
 
6
6
  gemspec
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = %q{asset_cloud}
5
- s.version = "2.3.1"
5
+ s.version = "2.4.0"
6
6
 
7
7
  s.authors = %w(Shopify)
8
8
  s.summary = %q{An abstraction layer around arbitrary and diverse asset stores.}
@@ -1,46 +1,72 @@
1
- require 'aws'
1
+ require 'aws-sdk-s3'
2
2
 
3
3
  module AssetCloud
4
4
  class S3Bucket < Bucket
5
5
  def ls(key = nil)
6
6
  key = absolute_key(key)
7
7
 
8
- objects = cloud.s3_bucket(key).objects
9
- objects = objects.with_prefix(key) if key
8
+ options = {}
9
+ options[:prefix] = key if key
10
10
 
11
+ objects = cloud.s3_bucket(key).objects(options)
11
12
  objects.map { |o| cloud[relative_key(o.key)] }
12
13
  end
13
14
 
14
15
  def read(key, options = {})
15
- cloud.s3_bucket(key).objects[absolute_key(key)].read(options)
16
- rescue ::AWS::Errors::Base
16
+ options = options.dup
17
+
18
+ options[:range] = http_byte_range(options[:range]) if options[:range]
19
+
20
+ bucket = cloud.s3_bucket(key)
21
+ if encryption_key = options.delete(:encryption_key)
22
+ bucket = encrypted_bucket(bucket, encryption_key)
23
+ end
24
+
25
+ object = bucket.object(absolute_key(key)).get(options)
26
+
27
+ object.body.respond_to?(:read) ? object.body.read : object.body
28
+ rescue ::Aws::Errors::ServiceError
17
29
  raise AssetCloud::AssetNotFoundError, key
18
30
  end
19
31
 
20
32
  def write(key, data, options = {})
21
- object = cloud.s3_bucket(key).objects[absolute_key(key)]
33
+ options = options.dup
34
+
35
+ bucket = cloud.s3_bucket(key)
36
+ if encryption_key = options.delete(:encryption_key)
37
+ bucket = encrypted_bucket(bucket, encryption_key)
38
+ end
22
39
 
23
- object.write(data, options)
40
+ object = bucket.object(absolute_key(key))
41
+ object.put(options.merge(body: data))
24
42
  end
25
43
 
26
44
  def delete(key)
27
- object = cloud.s3_bucket(key).objects[absolute_key(key)]
28
-
45
+ object = cloud.s3_bucket(key).object(absolute_key(key))
29
46
  object.delete
30
-
31
47
  true
32
48
  end
33
49
 
34
50
  def stat(key)
35
- object = cloud.s3_bucket(key).objects[absolute_key(key)]
36
- metadata = object.head
51
+ bucket = cloud.s3_bucket(key)
52
+ metadata = bucket.client.head_object(bucket: bucket.name, key: absolute_key(key))
37
53
 
38
54
  AssetCloud::Metadata.new(true, metadata[:content_length], nil, metadata[:last_modified])
39
- rescue AWS::S3::Errors::NoSuchKey
55
+ rescue Aws::S3::Errors::NoSuchKey, Aws::S3::Errors::NotFound
40
56
  AssetCloud::Metadata.new(false)
41
57
  end
42
58
 
43
- protected
59
+ private
60
+
61
+ def encrypted_bucket(source_bucket, key)
62
+ Aws::S3::Resource.new(
63
+ client: Aws::S3::Encryption::Client.new(
64
+ client: source_bucket.client,
65
+ encryption_key: key,
66
+ )
67
+ ).bucket(source_bucket.name)
68
+ end
69
+
44
70
  def path_prefix
45
71
  @path_prefix ||= @cloud.url
46
72
  end
@@ -58,5 +84,11 @@ module AssetCloud
58
84
  def relative_key(key)
59
85
  key =~ /^#{path_prefix}\/(.+)/ ? $1 : key
60
86
  end
87
+
88
+ def http_byte_range(range)
89
+ # follows https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35
90
+ return "bytes=#{[range.begin, range.max].join('-')}" if range.is_a?(Range)
91
+ range
92
+ end
61
93
  end
62
94
  end
@@ -1,73 +1,71 @@
1
+ require 'ostruct'
2
+
1
3
  class MockS3Interface
4
+ VALID_ACLS = %w(
5
+ private public-read public-read-write authenticated-read aws-exec-read bucket-owner-read bucket-owner-full-control
6
+ )
7
+
2
8
  attr_reader :bucket_storage
3
9
 
4
- def initialize(aws_access_key_id=nil, aws_secret_access_key=nil, params={})
5
- @bucket_storage = {}
10
+ def initialize(aws_access_key_id = nil, aws_secret_access_key = nil, params = {})
11
+ @buckets = {}
6
12
  end
7
13
 
8
- def buckets
9
- @bucket_collection ||= BucketCollection.new(self)
14
+ def buckets(**options)
15
+ buckets = @buckets.values
16
+ buckets = buckets.select { |v| v.start_with?(options[:prefix]) } if options[:prefix]
17
+ buckets
10
18
  end
11
19
 
12
- def client
13
- @client ||= Client.new
20
+ def bucket(name)
21
+ @buckets[name] ||= Bucket.new(self, name)
14
22
  end
15
23
 
16
- class Client
24
+ def client
25
+ self
17
26
  end
18
27
 
19
- class BucketCollection
20
- def initialize(interface)
21
- @interface = interface
22
- end
28
+ def head_object(options = {})
29
+ options = bucket(options[:bucket])
30
+ .object(options[:key])
31
+ .get
23
32
 
24
- def [](name)
25
- @interface.bucket_storage[name] ||= Bucket.new(name)
26
- end
33
+ {
34
+ content_length: options.body.size,
35
+ last_modified: Time.parse("Mon Aug 27 17:37:51 UTC 2007")
36
+ }
27
37
  end
28
38
 
29
39
  class Bucket
30
- attr_reader :name
31
- def initialize(name)
40
+ attr_reader :name, :client
41
+ def initialize(client, name)
42
+ @client = client
32
43
  @name = name
33
44
  @storage = {}
34
- @storage_options = {}
35
45
  end
36
46
 
37
- def with_prefix(prefix)
38
- keys = @storage
39
- keys = keys.select {|k,v| k.starts_with?(prefix)}
40
- keys.map {|k,v| S3Object.new(self, k, v)}
47
+ def objects(**options)
48
+ objects = @storage.values
49
+ objects = objects.select { |v| v.start_with?(options[:prefix]) } if options[:prefix]
50
+ objects
41
51
  end
42
52
 
43
- def objects
44
- Collection.new(self, @storage.keys)
53
+ def object(key)
54
+ @storage[key] ||= NullS3Object.new(self, key)
45
55
  end
46
56
 
47
- def get(key)
48
- if @storage.key?(key)
49
- @storage[key]
50
- else
51
- raise AWS::S3::Errors::NoSuchKey.new(nil, nil)
52
- end
53
- end
57
+ def put_object(options = {})
58
+ options = options.dup
54
59
 
55
- def get_options(key)
56
- if @storage_options.key?(key)
57
- @storage_options[key]
58
- else
59
- raise AWS::S3::Errors::NoSuchKey.new(nil, nil)
60
+ if options[:acl] && !VALID_ACLS.include?(options[:acl])
61
+ raise "Invalid ACL `#{options[:acl].inspect}`, must be one of: #{VALID_ACLS.inspect}"
60
62
  end
61
- end
63
+
64
+ options[:body] = options[:body].force_encoding(Encoding::BINARY)
62
65
 
63
- def put(key, data, options={})
64
- @storage[key] = data.dup.force_encoding(Encoding::BINARY)
65
- @storage_options[key] = options.dup
66
- true
67
- end
66
+ key = options.delete(:key)
68
67
 
69
- def delete(key)
70
- @storage.delete(key)
68
+ @storage[key] = S3Object.new(self, key, options)
71
69
  true
72
70
  end
73
71
 
@@ -80,88 +78,49 @@ class MockS3Interface
80
78
  end
81
79
  end
82
80
 
83
- class Collection
84
- include Enumerable
85
-
86
- def initialize(bucket, objects)
81
+ class NullS3Object
82
+ attr_reader :key
83
+ def initialize(bucket, key)
87
84
  @bucket = bucket
88
- @objects = objects
85
+ @key = key
89
86
  end
90
87
 
91
- def with_prefix(prefix)
92
- self.class.new(@bucket, @objects.select {|k| k.start_with?(prefix)})
88
+ def get(*)
89
+ raise Aws::S3::Errors::NoSuchKey.new(nil, nil)
93
90
  end
94
91
 
95
- def [](name)
96
- S3Object.new(@bucket, name)
92
+ def delete(*)
97
93
  end
98
94
 
99
- def each
100
- @objects.each { |e| yield S3Object.new(@bucket, e) }
95
+ def put(options = {})
96
+ @bucket.put_object(options.merge(key: @key))
101
97
  end
102
98
  end
103
99
 
104
100
  class S3Object
105
- attr_reader :key
101
+ attr_reader :key, :options
106
102
 
107
- def initialize(bucket, key, data=nil)
103
+ def initialize(bucket, key, options = {})
108
104
  @bucket = bucket
109
105
  @key = key
106
+ @options = options
110
107
  end
111
108
 
112
109
  def delete
113
110
  @bucket.delete(@key)
111
+ true
114
112
  end
115
113
 
116
- def read(headers={})
117
- @bucket.get(@key)
118
- end
119
-
120
- def options()
121
- @bucket.get_options(@key)
122
- end
123
-
124
- def write(data, options={})
125
- @bucket.put(@key, data, options)
126
- end
127
-
128
- def multipart_upload(options = {})
129
- MockMultipartUpload.new(@bucket, @key)
114
+ def get(*)
115
+ OpenStruct.new(options)
130
116
  end
131
117
 
132
- def url_for(permission, options={})
133
- if options[:secure]
134
- URI.parse("https://www.youtube.com/watch?v=oHg5SJYRHA0")
135
- else
136
- URI.parse("http://www.youtube.com/watch?v=oHg5SJYRHA0")
118
+ def put(options = {})
119
+ if options[:acl] && !VALID_ACLS.include?(options[:acl])
120
+ raise "Invalid ACL `#{options[:acl].inspect}`, must be one of: #{VALID_ACLS.inspect}"
137
121
  end
138
- end
139
-
140
- def head
141
- {
142
- content_length: read.size,
143
- last_modified: Time.parse("Mon Aug 27 17:37:51 UTC 2007")
144
- }
145
- end
146
- end
147
-
148
- class MockMultipartUpload
149
- def initialize(bucket, key)
150
- @bucket =bucket
151
- @key = key
152
- @data = ""
153
- end
154
-
155
- def add_part(data)
156
- @data << data
157
- end
158
-
159
- def abort
160
- @bucket.delete(@key)
161
- end
162
122
 
163
- def complete(arg)
164
- @bucket.put(@key, @data, {})
123
+ @bucket.put_object(options.merge(key: @key))
165
124
  end
166
125
  end
167
126
  end
@@ -5,22 +5,24 @@ class RemoteS3Cloud < AssetCloud::Base
5
5
  bucket :tmp, AssetCloud::S3Bucket
6
6
 
7
7
  def s3_bucket(key)
8
- s3_connection.buckets[ENV['S3_BUCKET_NAME']]
8
+ s3_connection.bucket(ENV['S3_BUCKET_NAME'])
9
9
  end
10
10
  end
11
11
 
12
- describe 'Remote test for AssetCloud::S3Bucket', if: ENV['AWS_ACCESS_KEY_ID'] && ENV['AWS_SECRET_ACCESS_KEY'] && ENV['S3_BUCKET_NAME'] do
13
- require 'aws-sdk'
14
-
12
+ describe 'Remote test for AssetCloud::S3Bucket', if: ENV['AWS_ACCESS_KEY_ID'] && ENV['AWS_SECRET_ACCESS_KEY'] && ENV['S3_BUCKET_NAME'] do
15
13
  directory = File.dirname(__FILE__) + '/files'
16
14
 
17
15
  before(:all) do
18
- AWS.config({
19
- access_key_id: ENV['AWS_ACCESS_KEY_ID'],
20
- secret_access_key: ENV['AWS_SECRET_ACCESS_KEY']
21
- })
16
+ Aws.config = {
17
+ region: ENV.fetch('AWS_REGION', 'us-east-1'),
18
+ credentials: Aws::Credentials.new(
19
+ ENV['AWS_ACCESS_KEY_ID'],
20
+ ENV['AWS_SECRET_ACCESS_KEY'],
21
+ ),
22
+ }
23
+
22
24
  @cloud = RemoteS3Cloud.new(directory , 'testing/assets/files' )
23
- @cloud.s3_connection = AWS::S3.new()
25
+ @cloud.s3_connection = Aws::S3::Resource.new
24
26
  @bucket = @cloud.buckets[:tmp]
25
27
  end
26
28
 
@@ -35,9 +37,18 @@ describe 'Remote test for AssetCloud::S3Bucket', if: ENV['AWS_ACCESS_KEY_ID'] &
35
37
 
36
38
  ls = @bucket.ls('tmp')
37
39
 
38
- ls.first.class.should == AssetCloud::Asset
39
- keys = ls.map(&:key)
40
- ['tmp/test1.txt', 'tmp/test2.txt'].all? {|key| keys.include? key }
40
+ expect(ls).to all(be_an(AssetCloud::Asset))
41
+ expect(ls.map(&:key) - ['tmp/test1.txt', 'tmp/test2.txt']).to be_empty
42
+ end
43
+
44
+ it "#ls returns all assets" do
45
+ @cloud['tmp/test1.txt'] = 'test1'
46
+ @cloud['tmp/test2.txt'] = 'test2'
47
+
48
+ ls = @bucket.ls
49
+
50
+ expect(ls).to all(be_an(AssetCloud::Asset))
51
+ expect(ls.map(&:key) - ['tmp/test1.txt', 'tmp/test2.txt']).to be_empty
41
52
  end
42
53
 
43
54
  it "#delete should ignore errors when deleting" do
@@ -59,6 +70,12 @@ describe 'Remote test for AssetCloud::S3Bucket', if: ENV['AWS_ACCESS_KEY_ID'] &
59
70
  metadata.updated_at.should >= start_time
60
71
  end
61
72
 
73
+ it "#stat a missing asset" do
74
+ metadata = @bucket.stat('i_do_not_exist_and_never_will.test')
75
+ expect(metadata).to be_an(AssetCloud::Metadata)
76
+ expect(metadata.exist).to be false
77
+ end
78
+
62
79
  it "#read " do
63
80
  value = 'hello world'
64
81
  key = 'tmp/new_file.txt'
@@ -67,6 +84,10 @@ describe 'Remote test for AssetCloud::S3Bucket', if: ENV['AWS_ACCESS_KEY_ID'] &
67
84
  data.should == value
68
85
  end
69
86
 
87
+ it "#read a missing asset" do
88
+ expect { @bucket.read("i_do_not_exist_and_never_will.test") }.to raise_error(AssetCloud::AssetNotFoundError)
89
+ end
90
+
70
91
  it "#reads first bytes when passed options" do
71
92
  value = 'hello world'
72
93
  key = 'tmp/new_file.txt'
@@ -6,7 +6,7 @@ class S3Cloud < AssetCloud::Base
6
6
  attr_accessor :s3_connection, :s3_bucket_name
7
7
 
8
8
  def s3_bucket(key)
9
- s3_connection.buckets[s3_bucket_name]
9
+ s3_connection.bucket(s3_bucket_name)
10
10
  end
11
11
  end
12
12
 
@@ -27,21 +27,25 @@ describe AssetCloud::S3Bucket do
27
27
  end
28
28
 
29
29
  it "#ls should return assets with proper keys" do
30
- collection = MockS3Interface::Collection.new(nil, ["#{@cloud.url}/tmp/blah.gif", "#{@cloud.url}/tmp/add_to_cart.gif"])
30
+ collection = ["#{@cloud.url}/tmp/blah.gif", "#{@cloud.url}/tmp/add_to_cart.gif"].map do |key|
31
+ MockS3Interface::S3Object.new(nil, key)
32
+ end
31
33
  expect_any_instance_of(MockS3Interface::Bucket).to receive(:objects).and_return(collection)
34
+
32
35
  ls = @bucket.ls('tmp')
33
- ls.first.class.should == AssetCloud::Asset
34
- ls.map(&:key).should == ['tmp/blah.gif', 'tmp/add_to_cart.gif']
36
+
37
+ expect(ls).to all(be_an(AssetCloud::Asset))
38
+ expect(ls.map(&:key) - ['tmp/blah.gif', 'tmp/add_to_cart.gif']).to be_empty
35
39
  end
36
40
 
37
41
  it "#delete should not ignore errors when deleting" do
38
- expect_any_instance_of(MockS3Interface::Bucket).to receive(:delete).and_raise(StandardError)
42
+ expect_any_instance_of(MockS3Interface::NullS3Object).to receive(:delete).and_raise(StandardError)
39
43
 
40
44
  expect { @bucket.delete('assets/fail.gif') }.to raise_error(StandardError)
41
45
  end
42
46
 
43
47
  it "#delete should always return true" do
44
- expect_any_instance_of(MockS3Interface::Bucket).to receive(:delete).and_return(nil)
48
+ expect_any_instance_of(MockS3Interface::NullS3Object).to receive(:delete).and_return(nil)
45
49
 
46
50
  @bucket.delete('assets/fail.gif').should == true
47
51
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: asset_cloud
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.1
4
+ version: 2.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-02-24 00:00:00.000000000 Z
11
+ date: 2020-03-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport