asset_cloud 2.3.1 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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