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.
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
@@ -0,0 +1,12 @@
1
+ module OSS
2
+ class Config
3
+ OPTIONS = [:endpoint, :access_key_id, :access_key_secret]
4
+ attr_accessor *OPTIONS
5
+
6
+ def initialize(options={})
7
+ options.each do |key, val|
8
+ self.send("#{key}=", val) if self.respond_to?("#{key}=")
9
+ end
10
+ end
11
+ end
12
+ end
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
@@ -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
@@ -0,0 +1,11 @@
1
+ module OSS
2
+ class Service
3
+ def initialize(config)
4
+ @config = config
5
+ end
6
+
7
+ def client(options = {})
8
+ OSS::Client.new(@config, options)
9
+ end
10
+ end
11
+ end
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
@@ -0,0 +1,3 @@
1
+ module OSS
2
+ VERSION = '1.0.0.beta'
3
+ end
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>
@@ -0,0 +1,10 @@
1
+ <?xml version="1.0" ?>
2
+ <AccessControlPolicy>
3
+ <Owner>
4
+ <ID>00220120222</ID>
5
+ <DisplayName>user_example</DisplayName>
6
+ </Owner>
7
+ <AccessControlList>
8
+ <Grant>private</Grant>
9
+ </AccessControlList>
10
+ </AccessControlPolicy>
@@ -0,0 +1,10 @@
1
+ <?xml version="1.0" ?>
2
+ <AccessControlPolicy>
3
+ <Owner>
4
+ <ID>00220120222</ID>
5
+ <DisplayName>00220120222</DisplayName>
6
+ </Owner>
7
+ <AccessControlList>
8
+ <Grant>private</Grant>
9
+ </AccessControlList>
10
+ </AccessControlPolicy>