aliyun-ess 0.1.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 6220299fa48afc6abb759b80526dac2889b0052c
4
+ data.tar.gz: 6c6b2757addd2ae6314f5951823c3c16b2f7a2aa
5
+ SHA512:
6
+ metadata.gz: ca60cb188e0e809e4169a58d25486f591ef59e8800fb52e0a306961017e7613734d9aa67413a1b95814be5e2c725331d503a75e602cdc19644c34b1810a09785
7
+ data.tar.gz: af36c635c32689e74a812381008171e674e5d837e653743d2a61f7726355509ea6f07d03f55dd8ec5cbc3510d1de455885861a0895318d50e759d729b6813c9b
@@ -0,0 +1,5 @@
1
+ head:
2
+
3
+ 0.1.0:
4
+
5
+ - Initial public release
data/INSTALL ADDED
@@ -0,0 +1,13 @@
1
+ == Rubygems
2
+
3
+ The easiest way to install aliyun/oss is with Rubygems:
4
+
5
+ % gem i aliyun-ess -ry
6
+
7
+ == Directly from git
8
+
9
+ % git clone git://github.com/sunrisela/aliyun-ess-sdk-for-ruby.git
10
+
11
+ == Dependencies
12
+
13
+ Aliyun::ESS requires Ruby 1.9+.
data/bin/ess ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ lib = File.dirname(__FILE__) + '/../lib/aliyun/ess'
3
+ setup = File.dirname(__FILE__) + '/setup'
4
+ irb_name = RUBY_PLATFORM =~ /mswin32/ ? 'irb.bat' : 'irb'
5
+
6
+ exec "#{irb_name} -r #{lib} -r #{setup} "
@@ -0,0 +1,11 @@
1
+ # -*- encoding : utf-8 -*-
2
+ #!/usr/bin/env ruby
3
+ if ENV['ACCESS_KEY_ID'] && ENV['SECRET_ACCESS_KEY']
4
+ Aliyun::ESS::Base.establish_connection!(
5
+ :access_key_id => ENV['ACCESS_KEY_ID'],
6
+ :secret_access_key => ENV['SECRET_ACCESS_KEY']
7
+ )
8
+ end
9
+
10
+ #require File.dirname(__FILE__) + '/../test/fixtures'
11
+ include Aliyun::ESS
@@ -0,0 +1,33 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'cgi'
3
+ require 'uri'
4
+ require 'openssl'
5
+ require 'digest/sha1'
6
+ require 'net/https'
7
+ require 'time'
8
+ require 'date'
9
+ require 'open-uri'
10
+ require 'yajl/json_gem'
11
+ require 'rack/utils'
12
+ require 'byebug'
13
+
14
+ $:.unshift(File.dirname(__FILE__))
15
+
16
+ require 'ess/version'
17
+ require 'ess/extensions' unless defined? Aliyun::OSS
18
+ require 'ess/exceptions'
19
+ require 'ess/error'
20
+ require 'ess/authentication'
21
+ require 'ess/connection'
22
+ require 'ess/parsing'
23
+ require 'ess/base'
24
+ require 'ess/service'
25
+ require 'ess/collection'
26
+ require 'ess/scaling_group'
27
+ require 'ess/scaling_rule'
28
+ require 'ess/response'
29
+
30
+
31
+ Aliyun::ESS::Base.class_eval do
32
+ include Aliyun::ESS::Connection::Management
33
+ end
@@ -0,0 +1,121 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module Aliyun
3
+ module ESS
4
+ class Authentication
5
+ class Signature < String #:nodoc:
6
+ attr_reader :params, :access_key_id, :secret_access_key, :options
7
+
8
+ def initialize(params, access_key_id, secret_access_key, options = {})
9
+ super()
10
+ @params, @access_key_id, @secret_access_key = params, access_key_id, secret_access_key
11
+ @options = options
12
+ end
13
+
14
+ private
15
+
16
+ def escape_string(str)
17
+ CGI.escape(str).gsub(/\+/,'%20').gsub(/%7E/, '~')
18
+ end
19
+
20
+ def sign_string
21
+ StringToSign.new(canonical_string)
22
+ end
23
+
24
+ def canonical_string
25
+ @canonical_string ||= CanonicalString.new(params, access_key_id, {})
26
+ end
27
+
28
+ def encoded_canonical
29
+ digest = OpenSSL::Digest::Digest.new('sha1')
30
+ b64_hmac = [OpenSSL::HMAC.digest(digest, secret_access_key+'&', sign_string)].pack("m").strip
31
+ url_encode? ? CGI.escape(b64_hmac) : b64_hmac
32
+ end
33
+
34
+ def url_encode?
35
+ !@options[:url_encode].nil?
36
+ end
37
+ end
38
+
39
+ class QueryString < Signature
40
+ def initialize(*args)
41
+ super
42
+ self << build
43
+ end
44
+
45
+ private
46
+
47
+ def build
48
+ "#{canonical_string}&Signature=#{escape_string(encoded_canonical)}"
49
+ end
50
+ end
51
+
52
+ class StringToSign < String
53
+ attr_reader :canonicalized_query_string
54
+ def initialize(canonicalized_query_string)
55
+ super()
56
+ @canonicalized_query_string = canonicalized_query_string
57
+ build
58
+ end
59
+
60
+ private
61
+
62
+ def build
63
+ self << 'GET'
64
+ self << '&%2F&'
65
+ self << escape_string(canonicalized_query_string)
66
+ end
67
+
68
+ def escape_string(str)
69
+ CGI.escape(str).gsub(/\+/,'%20').gsub(/%7E/, '~')
70
+ end
71
+ end
72
+
73
+ class CanonicalString < String
74
+ DEFAULT_PARAMS = {
75
+ 'Format' => 'JSON',
76
+ 'SignatureMethod' => 'HMAC-SHA1',
77
+ 'SignatureVersion' => '1.0',
78
+ 'Version' => '2014-08-28'
79
+ }
80
+
81
+ attr_reader :params, :query
82
+
83
+ def initialize(params, access_key_id, options = {})
84
+ super()
85
+ @params = params
86
+ @access_key_id = access_key_id
87
+ @options = options
88
+ self << build
89
+ end
90
+
91
+ private
92
+
93
+ def build
94
+ initialize_query
95
+
96
+ canonicalized_query_string = query.sort_by{|k, _| k}.map do |key, value|
97
+ value = value.to_s.strip
98
+ escaped_value = escape_string(value)
99
+ "#{key}=#{escaped_value}"
100
+ end.join('&')
101
+
102
+ canonicalized_query_string
103
+ end
104
+
105
+ def escape_string(str)
106
+ CGI.escape(str).gsub(/\+/,'%20').gsub(/%7E/, '~')
107
+ end
108
+
109
+ def initialize_query
110
+ @query = { 'AccessKeyId' => @access_key_id }
111
+ @query = params.merge(@query)
112
+ @query = DEFAULT_PARAMS.merge('SignatureNonce' => random_string, 'Timestamp' => Time.now.utc.iso8601).merge(@query)
113
+ end
114
+
115
+ def random_string(len=32)
116
+ rand(36**(len-1)..36**(len)).to_s 36
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,128 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module Aliyun
3
+ module ESS
4
+ DEFAULT_HOST = 'ess.aliyuncs.com'
5
+ class Base
6
+ class << self
7
+ # Wraps the current connection's request method and picks the appropriate response class to wrap the response in.
8
+ # If the response is an error, it will raise that error as an exception. All such exceptions can be caught by rescuing
9
+ # their superclass, the ResponseError exception class.
10
+ #
11
+ # It is unlikely that you would call this method directly. Subclasses of Base have convenience methods for each http request verb
12
+ # that wrap calls to request.
13
+ def request(verb, path, params = {}, options = {}, body = nil, attempts = 0, &block)
14
+ Service.response = nil
15
+ process_params!(params, verb)
16
+ response = response_class.new(connection.request(verb, path, params, options, body, attempts, &block))
17
+ Service.response = response
18
+
19
+ Error::Response.new(response.response).error.raise if response.error?
20
+ response
21
+ # Once in a while, a request to OSS returns an internal error. A glitch in the matrix I presume. Since these
22
+ # errors are few and far between the request method will rescue InternalErrors the first three times they encouter them
23
+ # and will retry the request again. Most of the time the second attempt will work.
24
+ rescue InternalError, RequestTimeout
25
+ if attempts == 3
26
+ raise
27
+ else
28
+ attempts += 1
29
+ retry
30
+ end
31
+ end
32
+
33
+ [:get, :post, :put, :delete, :head].each do |verb|
34
+ class_eval(<<-EVAL, __FILE__, __LINE__)
35
+ def #{verb}(path, params = {}, headers = {}, body = nil, &block)
36
+ request(:#{verb}, path, params, headers, body, &block)
37
+ end
38
+ EVAL
39
+ end
40
+
41
+ private
42
+
43
+ def response_class
44
+ FindResponseClass.for(self)
45
+ end
46
+
47
+ def process_params!(params, verb)
48
+ params.replace(RequestParams.process(params, verb))
49
+ end
50
+
51
+ # Using the conventions layed out in the <tt>response_class</tt> works for more than 80% of the time.
52
+ # There are a few edge cases though where we want a given class to wrap its responses in different
53
+ # response classes depending on which method is being called.
54
+ def respond_with(klass)
55
+ eval(<<-EVAL, binding, __FILE__, __LINE__)
56
+ def new_response_class
57
+ #{klass}
58
+ end
59
+ class << self
60
+ alias_method :old_response_class, :response_class
61
+ alias_method :response_class, :new_response_class
62
+ end
63
+ EVAL
64
+ yield
65
+ ensure
66
+ # Restore the original version
67
+ eval(<<-EVAL, binding, __FILE__, __LINE__)
68
+ class << self
69
+ alias_method :response_class, :old_response_class
70
+ end
71
+ EVAL
72
+ end
73
+
74
+ class RequestParams < Hash #:nodoc:
75
+ attr_reader :options, :verb
76
+
77
+ class << self
78
+ def process(*args, &block)
79
+ new(*args, &block).process!
80
+ end
81
+ end
82
+
83
+ def initialize(options, verb = :get)
84
+ @options = options.inject({}) {|h, (k,v)| h[k.to_s.camelize] = v.to_s; h }
85
+ @verb = verb
86
+ super()
87
+ end
88
+
89
+ def process!
90
+ replace(options)
91
+ end
92
+ end
93
+ end
94
+
95
+ def initialize(attributes = {}) #:nodoc:
96
+ @attributes = attributes
97
+ end
98
+
99
+ private
100
+
101
+ attr_reader :attributes
102
+
103
+ def connection
104
+ self.class.connection
105
+ end
106
+
107
+ def http
108
+ connection.http
109
+ end
110
+
111
+ def request(*args, &block)
112
+ self.class.request(*args, &block)
113
+ end
114
+
115
+ def method_missing(method, *args, &block)
116
+ case
117
+ when attributes.has_key?(method.to_s)
118
+ attributes[method.to_s]
119
+ when attributes.has_key?(method)
120
+ attributes[method]
121
+ else
122
+ super
123
+ end
124
+ end
125
+
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,24 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module Aliyun
3
+ module ESS
4
+ class Collection
5
+ include SelectiveAttributeProxy
6
+
7
+ attr_reader :response, :attributes
8
+
9
+ def initialize(response)
10
+ @response = response
11
+ @attributes = response.parsed.slice(*%W{page_number page_size total_count})
12
+ end
13
+
14
+ def items
15
+ @items ||= build_items!
16
+ end
17
+
18
+ def build_items!
19
+ item_class = eval response.class.name.sub(/::Response$/, '')
20
+ response.items.map{|e| item_class.new e }
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,226 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module Aliyun
3
+ module ESS
4
+ class Connection #:nodoc:
5
+ class << self
6
+ def connect(options = {})
7
+ new(options)
8
+ end
9
+
10
+ def prepare_path(path)
11
+ path = path.remove_extended unless path.valid_utf8?
12
+ URI.escape(path)
13
+ end
14
+ end
15
+
16
+ attr_reader :access_key_id, :secret_access_key, :http, :options
17
+
18
+ # Creates a new connection. Connections make the actual requests to OSS, though these requests are usually
19
+ # called from subclasses of Base.
20
+ #
21
+ # For details on establishing connections, check the Connection::Management::ClassMethods.
22
+ def initialize(options = {})
23
+ @options = Options.new(options)
24
+ connect
25
+ end
26
+
27
+ def request(verb, path, params = {}, headers = {}, body = nil, attempts = 0, &block)
28
+ body.rewind if body.respond_to?(:rewind) unless attempts.zero?
29
+
30
+ requester = Proc.new do
31
+ path = self.class.prepare_path(path) if attempts.zero? # Only escape the path once
32
+ params = ::Rack::Utils.parse_nested_query(URI(path).query).merge(params)
33
+
34
+ request_uri = "/?"+query_string_authentication(params)
35
+
36
+ puts request_uri
37
+
38
+ request = request_method(verb).new(request_uri, headers)
39
+ add_user_agent!(request)
40
+
41
+ if body
42
+ if body.respond_to?(:read)
43
+ request.body_stream = body
44
+ else
45
+ request.body = body
46
+ end
47
+ request.content_length = body.respond_to?(:lstat) ? body.stat.size : body.size
48
+ else
49
+ request.content_length = 0
50
+ end
51
+ http.request(request, &block)
52
+ end
53
+
54
+ if persistent?
55
+ http.start unless http.started?
56
+ requester.call
57
+ else
58
+ http.start(&requester)
59
+ end
60
+ rescue Errno::EPIPE, Timeout::Error, Errno::EINVAL, EOFError
61
+ @http = create_connection
62
+ attempts == 3 ? raise : (attempts += 1; retry)
63
+ end
64
+
65
+ def persistent?
66
+ options[:persistent]
67
+ end
68
+
69
+ def protocol(options = {})
70
+ # This always trumps http.use_ssl?
71
+ if options[:use_ssl] == false
72
+ 'http://'
73
+ elsif options[:use_ssl] || http.use_ssl?
74
+ 'https://'
75
+ else
76
+ 'http://'
77
+ end
78
+ end
79
+
80
+ private
81
+ def extract_keys!
82
+ missing_keys = []
83
+ extract_key = Proc.new {|key| options[key] || (missing_keys.push(key); nil)}
84
+ @access_key_id = extract_key[:access_key_id]
85
+ @secret_access_key = extract_key[:secret_access_key]
86
+ raise MissingAccessKey.new(missing_keys) unless missing_keys.empty?
87
+ end
88
+
89
+ def create_connection
90
+ http = Net::HTTP.new(options[:server], options[:port])
91
+ http.use_ssl = !options[:use_ssl].nil? || options[:port] == 443
92
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
93
+ http
94
+ end
95
+
96
+ def connect
97
+ extract_keys!
98
+ @http = create_connection
99
+ end
100
+
101
+ def port_string
102
+ default_port = options[:use_ssl] ? 443 : 80
103
+ http.port == default_port ? '' : ":#{http.port}"
104
+ end
105
+
106
+ # do authentication for now
107
+ def query_string_authentication(params)
108
+ Authentication::QueryString.new(params, access_key_id, secret_access_key)
109
+ end
110
+
111
+ def add_user_agent!(request)
112
+ request['User-Agent'] ||= "Aliyun::ESS/#{Version}"
113
+ end
114
+
115
+ def request_method(verb)
116
+ Net::HTTP.const_get(verb.to_s.capitalize)
117
+ end
118
+
119
+ def method_missing(method, *args, &block)
120
+ options[method] || super
121
+ end
122
+
123
+ module Management #:nodoc:
124
+ def self.included(base)
125
+ base.cattr_accessor :connections
126
+ base.connections = {}
127
+ base.extend ClassMethods
128
+ end
129
+
130
+ # Manage the creation and destruction of connections for Aliyun::OSS::Base and its subclasses. Connections are
131
+ # created with establish_connection!.
132
+ module ClassMethods
133
+ # Creates a new connection with which to make requests to the ESS servers for the calling class.
134
+ #
135
+ # Aliyun::ESS::Base.establish_connection!(:access_key_id => '...', :secret_access_key => '...')
136
+ #
137
+ # == Required arguments
138
+ #
139
+ # * <tt>:access_key_id</tt> - The access key id for your OSS account. Provided by Aliyun.
140
+ # * <tt>:secret_access_key</tt> - The secret access key for your OSS account. Provided by Aliyun.
141
+ #
142
+ # If any of these required arguments is missing, a MissingAccessKey exception will be raised.
143
+ #
144
+ # == Optional arguments
145
+ #
146
+ # * <tt>:server</tt> - The server to make requests to. You can use this to specify your bucket in the subdomain,
147
+ # or your own domain's cname if you are using virtual hosted buckets. Defaults to <tt>oss.aliyuncs.com</tt>.
148
+ # * <tt>:port</tt> - The port to the requests should be made on. Defaults to 80 or 443 if the <tt>:use_ssl</tt>
149
+ # argument is set.
150
+ # * <tt>:use_ssl</tt> - Whether requests should be made over SSL. If set to true, the <tt>:port</tt> argument
151
+ # will be implicitly set to 443, unless specified otherwise. Defaults to false.
152
+ # * <tt>:persistent</tt> - Whether to use a persistent connection to the server. Having this on provides around a two fold
153
+ # performance increase but for long running processes some firewalls may find the long lived connection suspicious and close the connection.
154
+ # If you run into connection errors, try setting <tt>:persistent</tt> to false. Defaults to false.
155
+ def establish_connection!(options = {})
156
+ # After you've already established the default connection, just specify
157
+ # the difference for subsequent connections
158
+ options = default_connection.options.merge(options) if connected?
159
+ connections[connection_name] = Connection.connect(options)
160
+ end
161
+
162
+ # Returns the connection for the current class, or Base's default connection if the current class does not
163
+ # have its own connection.
164
+ #
165
+ # If not connection has been established yet, NoConnectionEstablished will be raised.
166
+ def connection
167
+ if connected?
168
+ connections[connection_name] || default_connection
169
+ else
170
+ raise NoConnectionEstablished
171
+ end
172
+ end
173
+
174
+ # Returns true if a connection has been made yet.
175
+ def connected?
176
+ !connections.empty?
177
+ end
178
+
179
+ # Removes the connection for the current class. If there is no connection for the current class, the default
180
+ # connection will be removed.
181
+ def disconnect(name = connection_name)
182
+ name = default_connection unless connections.has_key?(name)
183
+ connection = connections[name]
184
+ connection.http.finish if connection.persistent?
185
+ connections.delete(name)
186
+ end
187
+
188
+ # Clears *all* connections, from all classes, with prejudice.
189
+ def disconnect!
190
+ connections.each_key {|connection| disconnect(connection)}
191
+ end
192
+
193
+ private
194
+ def connection_name
195
+ name
196
+ end
197
+
198
+ def default_connection_name
199
+ 'Aliyun::ESS::Base'
200
+ end
201
+
202
+ def default_connection
203
+ connections[default_connection_name]
204
+ end
205
+ end
206
+ end
207
+
208
+ class Options < Hash #:nodoc:
209
+ VALID_OPTIONS = [:access_key_id, :secret_access_key, :server, :port, :use_ssl, :persistent].freeze
210
+
211
+ def initialize(options = {})
212
+ super()
213
+ validate(options)
214
+ replace(:server => DEFAULT_HOST, :port => (options[:use_ssl] ? 443 : 80))
215
+ merge!(options)
216
+ end
217
+
218
+ private
219
+ def validate(options)
220
+ invalid_options = options.keys - VALID_OPTIONS
221
+ raise InvalidConnectionOption.new(invalid_options) unless invalid_options.empty?
222
+ end
223
+ end
224
+ end
225
+ end
226
+ end