oss-ruby 1.0.0.beta
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 +7 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +55 -0
- data/LICENSE +13 -0
- data/README.md +92 -0
- data/Rakefile +7 -0
- data/docs/bucket_api.md +100 -0
- data/docs/cors.md +56 -0
- data/docs/multipart_upload.md +116 -0
- data/docs/object_api.md +159 -0
- data/docs/quick_start.md +57 -0
- data/lib/oss.rb +21 -0
- data/lib/oss/base.rb +65 -0
- data/lib/oss/bucket.rb +202 -0
- data/lib/oss/client.rb +97 -0
- data/lib/oss/config.rb +12 -0
- data/lib/oss/error.rb +23 -0
- data/lib/oss/object.rb +141 -0
- data/lib/oss/response.rb +37 -0
- data/lib/oss/service.rb +11 -0
- data/lib/oss/utils.rb +14 -0
- data/lib/oss/version.rb +3 -0
- data/oss-ruby.gemspec +27 -0
- data/spec/fixtures/complete_multi_upload.xml +7 -0
- data/spec/fixtures/get_bucket.xml +33 -0
- data/spec/fixtures/get_bucket_acl.xml +10 -0
- data/spec/fixtures/get_object_acl.xml +10 -0
- data/spec/fixtures/get_service.xml +19 -0
- data/spec/fixtures/init_multi_upload.xml +6 -0
- data/spec/fixtures/list_multi_upload.xml +27 -0
- data/spec/fixtures/list_object_parts.xml +27 -0
- data/spec/fixtures/test.txt +1 -0
- data/spec/fixtures/upload_object_part_copy.xml +5 -0
- data/spec/lib/base_spec.rb +66 -0
- data/spec/lib/bucket_spec.rb +324 -0
- data/spec/lib/client_spec.rb +66 -0
- data/spec/lib/config_spec.rb +23 -0
- data/spec/lib/object_spec.rb +268 -0
- data/spec/lib/oss_spec.rb +13 -0
- data/spec/lib/response_spec.rb +42 -0
- data/spec/spec_helper.rb +4 -0
- metadata +186 -0
data/lib/oss/client.rb
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
module OSS
|
5
|
+
class Client
|
6
|
+
OSS_HEADER_PREFIX = 'x-oss-'
|
7
|
+
attr_reader :config, :options
|
8
|
+
|
9
|
+
def initialize(config, options = {})
|
10
|
+
@config = config
|
11
|
+
@options = options
|
12
|
+
end
|
13
|
+
|
14
|
+
def run(verb, url, params = {}, headers = {})
|
15
|
+
headers['Date'] = Time.now.httpdate
|
16
|
+
if !headers['Content-Type'] && (verb == :put || verb == :post)
|
17
|
+
headers['Content-Type'] = "application/x-www-form-urlencoded"
|
18
|
+
end
|
19
|
+
headers['Authorization'] = authorization_string(verb, headers)
|
20
|
+
response = connection.send verb do |conn|
|
21
|
+
conn.url url
|
22
|
+
conn.headers = headers
|
23
|
+
if verb == :get && params
|
24
|
+
conn.params = params
|
25
|
+
elsif (verb == :put || verb == :post) && params
|
26
|
+
conn.body = params
|
27
|
+
end
|
28
|
+
end
|
29
|
+
OSS::Response.new(response)
|
30
|
+
rescue Faraday::ClientError => e
|
31
|
+
raise OSS::HTTPClientError.new(e)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# "Authorization: OSS " + Access Key Id + ":" + Signature
|
37
|
+
def authorization_string(verb, headers)
|
38
|
+
data = [
|
39
|
+
verb.to_s.upcase,
|
40
|
+
headers['Content-MD5'],
|
41
|
+
headers['Content-Type'],
|
42
|
+
headers['Date']
|
43
|
+
]
|
44
|
+
|
45
|
+
# Calculate OSS Headers
|
46
|
+
if headers.keys.any? { |key| key.to_s.downcase.start_with?(OSS_HEADER_PREFIX) }
|
47
|
+
oss_headers = headers
|
48
|
+
.select { |k, _| k.to_s.downcase.start_with?(OSS_HEADER_PREFIX) }
|
49
|
+
.map { |k, v| k.to_s.downcase.strip + ':' + v.strip }
|
50
|
+
.sort
|
51
|
+
.join("\n")
|
52
|
+
data.push oss_headers
|
53
|
+
end
|
54
|
+
|
55
|
+
# Calculate Canonicalized Resource
|
56
|
+
canonicalized_resource = options[:resource] ? options[:resource] : '/'
|
57
|
+
if options[:sub_resource]
|
58
|
+
unless options[:sub_resource].is_a?(Array)
|
59
|
+
options[:sub_resource] = [options[:sub_resource]]
|
60
|
+
end
|
61
|
+
query = options[:sub_resource].map(&:to_s).sort.join("&")
|
62
|
+
canonicalized_resource += "?#{query}"
|
63
|
+
end
|
64
|
+
data.push canonicalized_resource
|
65
|
+
|
66
|
+
"OSS " + config.access_key_id + ":" + sign(data.join("\n"))
|
67
|
+
end
|
68
|
+
|
69
|
+
# Signature = base64(hmac-sha1(AccessKeySecret,
|
70
|
+
# VERB + "\n"
|
71
|
+
# + CONTENT-MD5 + "\n"
|
72
|
+
# + CONTENT-TYPE + "\n"
|
73
|
+
# + DATE + "\n"
|
74
|
+
# + CanonicalizedOSSHeaders
|
75
|
+
# + CanonicalizedResource))
|
76
|
+
def sign(data)
|
77
|
+
key = config.access_key_secret
|
78
|
+
digest = OpenSSL::Digest.new('sha1')
|
79
|
+
hmac = OpenSSL::HMAC.digest(digest, key, data)
|
80
|
+
Base64.encode64(hmac).strip
|
81
|
+
end
|
82
|
+
|
83
|
+
def connection
|
84
|
+
endpoint =
|
85
|
+
if options[:subdomain]
|
86
|
+
"#{options[:subdomain]}.#{config.endpoint}"
|
87
|
+
else
|
88
|
+
config.endpoint
|
89
|
+
end
|
90
|
+
@connection = Faraday.new(url: 'http://' + endpoint) do |conn|
|
91
|
+
conn.request :url_encoded
|
92
|
+
conn.response :logger
|
93
|
+
conn.adapter Faraday.default_adapter
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
data/lib/oss/config.rb
ADDED
data/lib/oss/error.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
module OSS
|
2
|
+
class Error < StandardError
|
3
|
+
def inspect
|
4
|
+
%(#<#{self.class} message="#{self.message}">)
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
# Wrap Faraday client error
|
9
|
+
class HTTPClientError < Error
|
10
|
+
def initialize(exception)
|
11
|
+
@exception = exception
|
12
|
+
super(exception.message)
|
13
|
+
end
|
14
|
+
|
15
|
+
def backtrace
|
16
|
+
if @exception
|
17
|
+
@exception.backtrace
|
18
|
+
else
|
19
|
+
super
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/oss/object.rb
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
require 'mime/types'
|
2
|
+
|
3
|
+
module OSS
|
4
|
+
class Object < Service
|
5
|
+
def put_object(bucket, object, body, headers = {})
|
6
|
+
headers['Content-Type'] ||= 'binary/octet-stream'
|
7
|
+
headers['Content-Length'] = body.length.to_s
|
8
|
+
options = setup_options(bucket, object)
|
9
|
+
client(options).run :put, object, body, headers
|
10
|
+
end
|
11
|
+
|
12
|
+
def put_object_from_file(bucket, object, file_name, headers = {})
|
13
|
+
body = File.read(file_name)
|
14
|
+
if mime_type = MIME::Types.type_for(file_name).first
|
15
|
+
headers['Content-Type'] = mime_type.content_type
|
16
|
+
end
|
17
|
+
put_object(bucket, object, body, headers)
|
18
|
+
end
|
19
|
+
|
20
|
+
def copy_object(source_bucket, source_object, target_bucket, target_object, headers = {})
|
21
|
+
headers['x-oss-copy-source'] = "/#{source_bucket}/#{source_object}"
|
22
|
+
options = setup_options(target_bucket, target_object)
|
23
|
+
client(options).run :put, target_object, nil, headers
|
24
|
+
end
|
25
|
+
|
26
|
+
def get_object(bucket, object, headers = {})
|
27
|
+
options = setup_options(bucket, object)
|
28
|
+
client(options).run :get, object, nil, headers
|
29
|
+
end
|
30
|
+
|
31
|
+
def append_object(bucket, object, body, position = 0, headers = {})
|
32
|
+
headers['Content-Type'] ||= 'binary/octet-stream'
|
33
|
+
headers['Content-Length'] = body.length.to_s
|
34
|
+
options = setup_options(bucket, object, ['append', "position=#{position}"])
|
35
|
+
client(options).run :post, "#{object}?append&position=#{position}", body, headers
|
36
|
+
end
|
37
|
+
|
38
|
+
def delete_object(bucket, object)
|
39
|
+
options = setup_options(bucket, object)
|
40
|
+
client(options).run :delete, object
|
41
|
+
end
|
42
|
+
|
43
|
+
def delete_multiple_objects(bucket, objects, quiet = false)
|
44
|
+
body = '<?xml version="1.0" encoding="UTF-8"?><Delete>' \
|
45
|
+
"<Quiet>#{quiet}</Quiet>"
|
46
|
+
objects.each do |object|
|
47
|
+
body += "<Object><Key>#{object}</Key></Object>"
|
48
|
+
end
|
49
|
+
body += '</Delete>'
|
50
|
+
|
51
|
+
headers = {
|
52
|
+
'Content-Length' => body.length.to_s,
|
53
|
+
'Content-MD5' => OSS::Utils.content_md5_header(body),
|
54
|
+
'Content-Type' => 'application/xml'
|
55
|
+
}
|
56
|
+
|
57
|
+
options = setup_options(bucket, nil, 'delete')
|
58
|
+
client(options).run :post, '?delete', body, headers
|
59
|
+
end
|
60
|
+
|
61
|
+
def head_object(bucket, object, headers = {})
|
62
|
+
options = setup_options(bucket, object)
|
63
|
+
client(options).run :head, object, nil, headers
|
64
|
+
end
|
65
|
+
|
66
|
+
# permission: public-read-write,public-read, private
|
67
|
+
def put_object_acl(bucket, object, permission)
|
68
|
+
headers = {
|
69
|
+
'x-oss-object-acl' => permission
|
70
|
+
}
|
71
|
+
options = setup_options(bucket, object, 'acl')
|
72
|
+
client(options).run :put, "#{object}?acl", nil, headers
|
73
|
+
end
|
74
|
+
|
75
|
+
def get_object_acl(bucket, object)
|
76
|
+
options = setup_options(bucket, object, 'acl')
|
77
|
+
client(options).run :get, object, 'acl' => nil
|
78
|
+
end
|
79
|
+
|
80
|
+
def init_multi_upload(bucket, object, headers = {})
|
81
|
+
options = setup_options(bucket, object, 'uploads')
|
82
|
+
client(options).run :post, "#{object}?uploads", nil, headers
|
83
|
+
end
|
84
|
+
|
85
|
+
def upload_object_part(bucket, object, body, upload_id, part = 1)
|
86
|
+
headers = {
|
87
|
+
'Content-Length' => body.length.to_s,
|
88
|
+
'Content-MD5' => OSS::Utils.content_md5_header(body),
|
89
|
+
'Content-Type' => 'binary/octet-stream'
|
90
|
+
}
|
91
|
+
options = setup_options(bucket, object, ["partNumber=#{part}", "uploadId=#{upload_id}"])
|
92
|
+
client(options).run :put, "#{object}?partNumber=#{part}&uploadId=#{upload_id}", body, headers
|
93
|
+
end
|
94
|
+
|
95
|
+
def upload_object_part_copy(source_bucket, source_object, target_bucket, target_object, upload_id, part = 1, headers = {})
|
96
|
+
headers['x-oss-copy-source'] = "/#{source_bucket}/#{source_object}"
|
97
|
+
options = setup_options(target_bucket, target_object, ["partNumber=#{part}", "uploadId=#{upload_id}"])
|
98
|
+
client(options).run :put, "#{target_object}?partNumber=#{part}&uploadId=#{upload_id}", nil, headers
|
99
|
+
end
|
100
|
+
|
101
|
+
def complete_multi_upload(bucket, object, upload_id, parts = [])
|
102
|
+
body = '<?xml version="1.0" encoding="UTF-8"?><CompleteMultipartUpload>'
|
103
|
+
parts.each do |part|
|
104
|
+
body += "<Part><PartNumber>#{part[:part_number]}</PartNumber><ETag>#{part[:etag]}</ETag></Part>"
|
105
|
+
end
|
106
|
+
body += '</CompleteMultipartUpload>'
|
107
|
+
|
108
|
+
headers = {
|
109
|
+
'Content-Type' => 'application/xml'
|
110
|
+
}
|
111
|
+
|
112
|
+
options = setup_options(bucket, object, "uploadId=#{upload_id}")
|
113
|
+
client(options).run :post, "#{object}?uploadId=#{upload_id}", body, headers
|
114
|
+
end
|
115
|
+
|
116
|
+
def abort_multi_upload(bucket, object, upload_id)
|
117
|
+
options = setup_options(bucket, object, "uploadId=#{upload_id}")
|
118
|
+
client(options).run :delete, "#{object}?uploadId=#{upload_id}"
|
119
|
+
end
|
120
|
+
|
121
|
+
def list_multi_upload(bucket, headers = {})
|
122
|
+
options = setup_options(bucket, nil, 'uploads')
|
123
|
+
client(options).run :get, '/', { 'uploads' => nil }, headers
|
124
|
+
end
|
125
|
+
|
126
|
+
def list_object_parts(bucket, object, upload_id, headers = {})
|
127
|
+
options = setup_options(bucket, object, "uploadId=#{upload_id}")
|
128
|
+
client(options).run :get, object, { 'uploadId' => upload_id }, headers
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
def setup_options(bucket, object = nil, sub_resource = nil)
|
134
|
+
{
|
135
|
+
subdomain: bucket,
|
136
|
+
resource: "/#{bucket}/#{object}",
|
137
|
+
sub_resource: sub_resource
|
138
|
+
}
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
data/lib/oss/response.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
module OSS
|
2
|
+
class APIError
|
3
|
+
attr_reader :code, :message, :request_id, :host_id
|
4
|
+
|
5
|
+
def inspect
|
6
|
+
%(#<#{self.class} message="#{self.message}">)
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(doc)
|
10
|
+
@code = doc.xpath('//Code').text
|
11
|
+
@message = doc.xpath('//Message').text
|
12
|
+
@request_id = doc.xpath('//RequestId').text
|
13
|
+
@host_id = doc.xpath('//HostId').text
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class Response
|
18
|
+
extend Forwardable
|
19
|
+
attr_reader :response
|
20
|
+
|
21
|
+
def initialize(response)
|
22
|
+
@response = response
|
23
|
+
end
|
24
|
+
|
25
|
+
def doc
|
26
|
+
@doc ||= Nokogiri::XML(@response.body)
|
27
|
+
end
|
28
|
+
|
29
|
+
def error
|
30
|
+
return nil if status.to_s.start_with?('2')
|
31
|
+
@error ||= OSS::APIError.new(doc)
|
32
|
+
end
|
33
|
+
|
34
|
+
def_delegators :@response, :body, :headers, :status
|
35
|
+
def_delegators :doc, :xpath
|
36
|
+
end
|
37
|
+
end
|
data/lib/oss/service.rb
ADDED
data/lib/oss/utils.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
module OSS
|
2
|
+
module Utils
|
3
|
+
# turn symbol keys to string keys, shallow mode
|
4
|
+
def indifferent_hash(hash)
|
5
|
+
Hash[hash.map {|k, v| Symbol === k ? [k.to_s, v] : [k, v] }]
|
6
|
+
end
|
7
|
+
|
8
|
+
def content_md5_header(content)
|
9
|
+
Digest::MD5.base64digest(content)
|
10
|
+
end
|
11
|
+
|
12
|
+
extend self
|
13
|
+
end
|
14
|
+
end
|
data/lib/oss/version.rb
ADDED
data/oss-ruby.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'oss/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "oss-ruby"
|
8
|
+
spec.version = OSS::VERSION
|
9
|
+
spec.authors = ["Xiaoguang Chen"]
|
10
|
+
spec.email = ["xg.chen87@gmail.com"]
|
11
|
+
spec.summary = "Ruby SDK for Aliyun OSS"
|
12
|
+
spec.description = %q{Ruby SDK for Aliyun OSS.}
|
13
|
+
spec.homepage = "https://github.com/serco-chen/oss-ruby"
|
14
|
+
spec.license = "Apache-2.0"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0") - %w[.gitignore]
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency 'faraday', '~> 0.9.0'
|
22
|
+
spec.add_dependency 'nokogiri', '~> 1.6.6'
|
23
|
+
spec.add_dependency 'mime-types', '~> 2.6'
|
24
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
25
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
26
|
+
spec.add_development_dependency "rspec"
|
27
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<CompleteMultipartUploadResult>
|
3
|
+
<Location>http://oss-ruby-sdk-test-bucket.oss-cn-hangzhou.aliyuncs.com/oss-ruby-sdk-test-object</Location>
|
4
|
+
<Bucket>oss-ruby-sdk-test-bucket</Bucket>
|
5
|
+
<Key>oss-ruby-sdk-test-object</Key>
|
6
|
+
<ETag>B864DB6A936D376F9F8D3ED3BBE540DD-3</ETag>
|
7
|
+
</CompleteMultipartUploadResult>
|
@@ -0,0 +1,33 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<ListBucketResult>
|
3
|
+
<Name>oss-ruby-sdk-test-bucket</Name>
|
4
|
+
<Prefix></Prefix>
|
5
|
+
<Marker></Marker>
|
6
|
+
<MaxKeys>100</MaxKeys>
|
7
|
+
<Delimiter></Delimiter>
|
8
|
+
<IsTruncated>false</IsTruncated>
|
9
|
+
<Contents>
|
10
|
+
<Key>aaa.data</Key>
|
11
|
+
<LastModified>2015-11-12T09:10:02.000Z</LastModified>
|
12
|
+
<ETag>"4E672110E546ED7595087D10F8397678-2"</ETag>
|
13
|
+
<Type>Multipart</Type>
|
14
|
+
<Size>3384620</Size>
|
15
|
+
<StorageClass>Standard</StorageClass>
|
16
|
+
<Owner>
|
17
|
+
<ID>1306909297023357</ID>
|
18
|
+
<DisplayName>1306909297023357</DisplayName>
|
19
|
+
</Owner>
|
20
|
+
</Contents>
|
21
|
+
<Contents>
|
22
|
+
<Key>abc.pdf</Key>
|
23
|
+
<LastModified>2015-11-12T07:28:23.000Z</LastModified>
|
24
|
+
<ETag>"172F5D31191B965FE8CCB5A0F5BECD9A"</ETag>
|
25
|
+
<Type>Normal</Type>
|
26
|
+
<Size>1692310</Size>
|
27
|
+
<StorageClass>Standard</StorageClass>
|
28
|
+
<Owner>
|
29
|
+
<ID>1306909297023357</ID>
|
30
|
+
<DisplayName>1306909297023357</DisplayName>
|
31
|
+
</Owner>
|
32
|
+
</Contents>
|
33
|
+
</ListBucketResult>
|