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 +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
|