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 +4 -4
- data/Gemfile +1 -1
- data/asset_cloud.gemspec +1 -1
- data/lib/asset_cloud/buckets/s3_bucket.rb +46 -14
- data/spec/mock_s3_interface.rb +60 -101
- data/spec/remote_s3_bucket_spec.rb +33 -12
- data/spec/s3_bucket_spec.rb +10 -6
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 929356d981cb9cbc1f805c713071b909ff594ff05d8a54231d1f0bc85672ffd0
|
4
|
+
data.tar.gz: '09b7b0e5ce21d53385a5824619fed80dc76c11c348ea9a9cda531dd33e9a12d4'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 998ab0f3e4795f228adfa07b01ab1369a140c9ea5be4a729fd089953670fca677e31a5cee59d7adbdde0a507df33410fa82517367d0411e8f8b94e86baee4272
|
7
|
+
data.tar.gz: ba2de4239a9778afc6416cedbad4830a5d1f56f59824772a0fdbbfaaeaea1d20a90d6dd80db615a1dde8a12fc1eb6e053d54ec05f60dce4d9b44b9514300b62e
|
data/Gemfile
CHANGED
data/asset_cloud.gemspec
CHANGED
@@ -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
|
-
|
9
|
-
|
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
|
-
|
16
|
-
|
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
|
-
|
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.
|
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).
|
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
|
-
|
36
|
-
metadata =
|
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
|
55
|
+
rescue Aws::S3::Errors::NoSuchKey, Aws::S3::Errors::NotFound
|
40
56
|
AssetCloud::Metadata.new(false)
|
41
57
|
end
|
42
58
|
|
43
|
-
|
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
|
data/spec/mock_s3_interface.rb
CHANGED
@@ -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
|
-
@
|
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
|
-
|
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
|
13
|
-
@
|
20
|
+
def bucket(name)
|
21
|
+
@buckets[name] ||= Bucket.new(self, name)
|
14
22
|
end
|
15
23
|
|
16
|
-
|
24
|
+
def client
|
25
|
+
self
|
17
26
|
end
|
18
27
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
28
|
+
def head_object(options = {})
|
29
|
+
options = bucket(options[:bucket])
|
30
|
+
.object(options[:key])
|
31
|
+
.get
|
23
32
|
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
44
|
-
|
53
|
+
def object(key)
|
54
|
+
@storage[key] ||= NullS3Object.new(self, key)
|
45
55
|
end
|
46
56
|
|
47
|
-
def
|
48
|
-
|
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
|
-
|
56
|
-
|
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
|
-
|
63
|
+
|
64
|
+
options[:body] = options[:body].force_encoding(Encoding::BINARY)
|
62
65
|
|
63
|
-
|
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
|
-
|
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
|
84
|
-
|
85
|
-
|
86
|
-
def initialize(bucket, objects)
|
81
|
+
class NullS3Object
|
82
|
+
attr_reader :key
|
83
|
+
def initialize(bucket, key)
|
87
84
|
@bucket = bucket
|
88
|
-
@
|
85
|
+
@key = key
|
89
86
|
end
|
90
87
|
|
91
|
-
def
|
92
|
-
|
88
|
+
def get(*)
|
89
|
+
raise Aws::S3::Errors::NoSuchKey.new(nil, nil)
|
93
90
|
end
|
94
91
|
|
95
|
-
def
|
96
|
-
S3Object.new(@bucket, name)
|
92
|
+
def delete(*)
|
97
93
|
end
|
98
94
|
|
99
|
-
def
|
100
|
-
@
|
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,
|
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
|
117
|
-
|
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
|
133
|
-
if options[:
|
134
|
-
|
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
|
-
|
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.
|
8
|
+
s3_connection.bucket(ENV['S3_BUCKET_NAME'])
|
9
9
|
end
|
10
10
|
end
|
11
11
|
|
12
|
-
describe 'Remote test for AssetCloud::S3Bucket', if:
|
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
|
-
|
19
|
-
|
20
|
-
|
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 =
|
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.
|
39
|
-
|
40
|
-
|
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'
|
data/spec/s3_bucket_spec.rb
CHANGED
@@ -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.
|
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 =
|
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
|
-
|
34
|
-
ls.
|
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::
|
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::
|
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.
|
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-
|
11
|
+
date: 2020-03-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|