aliyun-oss-rails4 0.7.0.1448446959

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/COPYING +19 -0
  3. data/INSTALL +35 -0
  4. data/README +443 -0
  5. data/Rakefile +334 -0
  6. data/bin/oss +6 -0
  7. data/bin/setup.rb +11 -0
  8. data/lib/aliyun/oss.rb +55 -0
  9. data/lib/aliyun/oss/acl.rb +132 -0
  10. data/lib/aliyun/oss/authentication.rb +222 -0
  11. data/lib/aliyun/oss/base.rb +241 -0
  12. data/lib/aliyun/oss/bucket.rb +320 -0
  13. data/lib/aliyun/oss/connection.rb +279 -0
  14. data/lib/aliyun/oss/error.rb +70 -0
  15. data/lib/aliyun/oss/exceptions.rb +134 -0
  16. data/lib/aliyun/oss/extensions.rb +364 -0
  17. data/lib/aliyun/oss/logging.rb +304 -0
  18. data/lib/aliyun/oss/object.rb +612 -0
  19. data/lib/aliyun/oss/owner.rb +45 -0
  20. data/lib/aliyun/oss/parsing.rb +100 -0
  21. data/lib/aliyun/oss/response.rb +181 -0
  22. data/lib/aliyun/oss/service.rb +52 -0
  23. data/lib/aliyun/oss/version.rb +14 -0
  24. data/support/faster-xml-simple/lib/faster_xml_simple.rb +188 -0
  25. data/support/faster-xml-simple/test/regression_test.rb +48 -0
  26. data/support/faster-xml-simple/test/test_helper.rb +18 -0
  27. data/support/faster-xml-simple/test/xml_simple_comparison_test.rb +47 -0
  28. data/support/rdoc/code_info.rb +212 -0
  29. data/test/acl_test.rb +70 -0
  30. data/test/authentication_test.rb +114 -0
  31. data/test/base_test.rb +137 -0
  32. data/test/bucket_test.rb +75 -0
  33. data/test/connection_test.rb +218 -0
  34. data/test/error_test.rb +71 -0
  35. data/test/extensions_test.rb +346 -0
  36. data/test/fixtures.rb +90 -0
  37. data/test/fixtures/buckets.yml +133 -0
  38. data/test/fixtures/errors.yml +34 -0
  39. data/test/fixtures/headers.yml +3 -0
  40. data/test/fixtures/logging.yml +15 -0
  41. data/test/fixtures/loglines.yml +5 -0
  42. data/test/fixtures/logs.yml +7 -0
  43. data/test/fixtures/policies.yml +16 -0
  44. data/test/logging_test.rb +90 -0
  45. data/test/mocks/fake_response.rb +27 -0
  46. data/test/object_test.rb +221 -0
  47. data/test/parsing_test.rb +67 -0
  48. data/test/remote/acl_test.rb +28 -0
  49. data/test/remote/bucket_test.rb +147 -0
  50. data/test/remote/logging_test.rb +86 -0
  51. data/test/remote/object_test.rb +350 -0
  52. data/test/remote/test_file.data +0 -0
  53. data/test/remote/test_helper.rb +34 -0
  54. data/test/response_test.rb +69 -0
  55. data/test/service_test.rb +24 -0
  56. data/test/test_helper.rb +110 -0
  57. metadata +177 -0
@@ -0,0 +1,222 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module Aliyun
3
+ module OSS
4
+ # All authentication is taken care of for you by the Aliyun::OSS library. None the less, some details of the two types
5
+ # of authentication and when they are used may be of interest to some.
6
+ #
7
+ # === Header based authentication
8
+ #
9
+ # Header based authentication is achieved by setting a special <tt>Authorization</tt> header whose value
10
+ # is formatted like so:
11
+ #
12
+ # "OSS #{access_key_id}:#{encoded_canonical}"
13
+ #
14
+ # The <tt>access_key_id</tt> is the public key that is assigned by Aliyun for a given account which you use when
15
+ # establishing your initial connection. The <tt>encoded_canonical</tt> is computed according to rules layed out
16
+ # by Aliyun which we will describe presently.
17
+ #
18
+ # ==== Generating the encoded canonical string
19
+ #
20
+ # The "canonical string", generated by the CanonicalString class, is computed by collecting the current request method,
21
+ # a set of significant headers of the current request, and the current request path into a string.
22
+ # That canonical string is then encrypted with the <tt>secret_access_key</tt> assigned by Aliyun. The resulting encrypted canonical
23
+ # string is then base 64 encoded.
24
+ #
25
+ # === Query string based authentication
26
+ #
27
+ # When accessing a restricted object from the browser, you can authenticate via the query string, by setting the following parameters:
28
+ #
29
+ # "AliyunAccessKeyId=#{access_key_id}&Expires=#{expires}&Signature=#{encoded_canonical}"
30
+ #
31
+ # The QueryString class is responsible for generating the appropriate parameters for authentication via the
32
+ # query string.
33
+ #
34
+ # The <tt>access_key_id</tt> and <tt>encoded_canonical</tt> are the same as described in the Header based authentication section.
35
+ # The <tt>expires</tt> value dictates for how long the current url is valid (by default, it will expire in 5 minutes). Expiration can be specified
36
+ # either by an absolute time (expressed in seconds since the epoch), or in relative time (in number of seconds from now).
37
+ # Details of how to customize the expiration of the url are provided in the documentation for the QueryString class.
38
+ #
39
+ # All requests made by this library use header authentication. When a query string authenticated url is needed,
40
+ # the OSSObject#url method will include the appropriate query string parameters.
41
+ #
42
+ # === Full authentication specification
43
+ #
44
+ # The full specification of the authentication protocol can be found at
45
+ # http://docs.aliyunwebservices.com/AliyunOSS/2006-03-01/RESTAuthentication.html
46
+ class Authentication
47
+ constant :OSS_HEADER_PREFIX, 'x-oss-'
48
+
49
+ # Signature is the abstract super class for the Header and QueryString authentication methods. It does the job
50
+ # of computing the canonical_string using the CanonicalString class as well as encoding the canonical string. The subclasses
51
+ # parameterize these computations and arrange them in a string form appropriate to how they are used, in one case a http request
52
+ # header value, and in the other case key/value query string parameter pairs.
53
+ class Signature < String #:nodoc:
54
+ attr_reader :request, :access_key_id, :secret_access_key, :options
55
+
56
+ def initialize(request, access_key_id, secret_access_key, options = {})
57
+ super()
58
+ @request, @access_key_id, @secret_access_key = request, access_key_id, secret_access_key
59
+ @options = options
60
+ end
61
+
62
+ private
63
+
64
+ def canonical_string
65
+ options = {}
66
+ options[:expires] = expires if expires?
67
+ CanonicalString.new(request, options)
68
+ end
69
+ memoized :canonical_string
70
+
71
+ def encoded_canonical
72
+ digest = OpenSSL::Digest.new('sha1')
73
+ b64_hmac = [OpenSSL::HMAC.digest(digest, secret_access_key, canonical_string)].pack("m").strip
74
+ url_encode? ? CGI.escape(b64_hmac) : b64_hmac
75
+ end
76
+
77
+ def url_encode?
78
+ !@options[:url_encode].nil?
79
+ end
80
+
81
+ def expires?
82
+ is_a? QueryString
83
+ end
84
+
85
+ def date
86
+ request['date'].to_s.strip.empty? ? Time.now : Time.parse(request['date'])
87
+ end
88
+ end
89
+
90
+ # Provides header authentication by computing the value of the Authorization header. More details about the
91
+ # various authentication schemes can be found in the docs for its containing module, Authentication.
92
+ class Header < Signature #:nodoc:
93
+ def initialize(*args)
94
+ super
95
+ self << "OSS #{access_key_id}:#{encoded_canonical}"
96
+ end
97
+ end
98
+
99
+ # Provides query string authentication by computing the three authorization parameters: AliyunAccessKeyId, Expires and Signature.
100
+ # More details about the various authentication schemes can be found in the docs for its containing module, Authentication.
101
+ class QueryString < Signature #:nodoc:
102
+ constant :DEFAULT_EXPIRY, 300 # 5 minutes
103
+ def initialize(*args)
104
+ super
105
+ options[:url_encode] = true
106
+ self << build
107
+ end
108
+
109
+ private
110
+
111
+ # Will return one of three values, in the following order of precedence:
112
+ #
113
+ # 1) Seconds since the epoch explicitly passed in the +:expires+ option
114
+ # 2) The current time in seconds since the epoch plus the number of seconds passed in
115
+ # the +:expires_in+ option
116
+ # 3) The current time in seconds since the epoch plus the default number of seconds (60 seconds)
117
+ def expires
118
+ return options[:expires] if options[:expires]
119
+ date.to_i + expires_in
120
+ end
121
+
122
+ def expires_in
123
+ options.has_key?(:expires_in) ? Integer(options[:expires_in]) : DEFAULT_EXPIRY
124
+ end
125
+
126
+ # Keep in alphabetical order
127
+ def build
128
+ "OSSAccessKeyId=#{access_key_id}&Expires=#{expires}&Signature=#{encoded_canonical}"
129
+ end
130
+ end
131
+
132
+ # The CanonicalString is used to generate an encrypted signature, signed with your secrect access key. It is composed of
133
+ # data related to the given request for which it provides authentication. This data includes the request method, request headers,
134
+ # and the request path. Both Header and QueryString use it to generate their signature.
135
+ class CanonicalString < String #:nodoc:
136
+ class << self
137
+ def default_headers
138
+ %w(content-type content-md5)
139
+ end
140
+
141
+ def interesting_headers
142
+ ['content-md5', 'content-type', 'date', aliyun_header_prefix]
143
+ end
144
+
145
+ def aliyun_header_prefix
146
+ /^#{OSS_HEADER_PREFIX}/io
147
+ end
148
+ end
149
+
150
+ attr_reader :request, :headers
151
+
152
+ def initialize(request, options = {})
153
+ super()
154
+ @request = request
155
+ @headers = {}
156
+ @options = options
157
+ # "For non-authenticated or anonymous requests. A NotImplemented error result code will be returned if
158
+ # an authenticated (signed) request specifies a Host: header other than 'oss.aliyuncs.com'"
159
+ # (from http://docs.aliyunwebservices.com/AliyunOSS/2006-03-01/VirtualHosting.html)
160
+ request['Host'] = DEFAULT_HOST
161
+ build
162
+ end
163
+
164
+ private
165
+ def build
166
+ self << "#{request.method}\n"
167
+ ensure_date_is_valid
168
+
169
+ initialize_headers
170
+ set_expiry!
171
+
172
+ headers.sort_by {|k, _| k}.each do |key, value|
173
+ value = value.to_s.strip
174
+ self << (key =~ self.class.aliyun_header_prefix ? "#{key}:#{value}" : value)
175
+ self << "\n"
176
+ end
177
+ self << URI.unescape(path)
178
+ end
179
+
180
+ def initialize_headers
181
+ identify_interesting_headers
182
+ set_default_headers
183
+ end
184
+
185
+ def set_expiry!
186
+ self.headers['date'] = @options[:expires] if @options[:expires]
187
+ end
188
+
189
+ def ensure_date_is_valid
190
+ request['Date'] ||= Time.now.httpdate
191
+ end
192
+
193
+ def identify_interesting_headers
194
+ request.each do |key, value|
195
+ key = key.downcase # Can't modify frozen string so no bang
196
+ if self.class.interesting_headers.any? {|header| header === key}
197
+ self.headers[key] = value.to_s.strip
198
+ end
199
+ end
200
+ end
201
+
202
+ def set_default_headers
203
+ self.class.default_headers.each do |header|
204
+ self.headers[header] ||= ''
205
+ end
206
+ end
207
+
208
+ def path
209
+ [only_path, extract_significant_parameter].compact.join('?')
210
+ end
211
+
212
+ def extract_significant_parameter
213
+ request.path[/[&?](acl|logging)(?:&|=|$)/, 1]
214
+ end
215
+
216
+ def only_path
217
+ request.path[/^[^?]*/]
218
+ end
219
+ end
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,241 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module Aliyun #:nodoc:
3
+ # Aliyun::OSS is a Ruby library for Aliyun's Open Storage Service's REST API (http://aliyun.aliyun.com/oss).
4
+ # Full documentation of the currently supported API can be found at http://docs.aliyunwebservices.com/AliyunOSS/2006-03-01.
5
+ #
6
+ # == Getting started
7
+ #
8
+ # To get started you need to require 'aliyun/oss':
9
+ #
10
+ # % irb -rubygems
11
+ # irb(main):001:0> require 'aliyun/oss'
12
+ # # => true
13
+ #
14
+ # The Aliyun::OSS library ships with an interactive shell called <tt>osssh</tt>. From within it, you have access to all the operations the library exposes from the command line.
15
+ #
16
+ # % osssh
17
+ # >> Version
18
+ #
19
+ # Before you can do anything, you must establish a connection using Base.establish_connection!. A basic connection would look something like this:
20
+ #
21
+ # Aliyun::OSS::Base.establish_connection!(
22
+ # :access_key_id => 'abc',
23
+ # :secret_access_key => '123'
24
+ # )
25
+ #
26
+ # The minimum connection options that you must specify are your access key id and your secret access key.
27
+ #
28
+ # (If you don't already have your access keys, all you need to sign up for the OSS service is an account at Aliyun. You can sign up for OSS and get access keys by visiting http://aliyun.aliyun.com/oss.)
29
+ #
30
+ # For convenience, if you set two special environment variables with the value of your access keys, the console will automatically create a default connection for you. For example:
31
+ #
32
+ # % cat .aliyun_keys
33
+ # export OSS_ACCESS_KEY_ID='abcdefghijklmnop'
34
+ # export OSS_SECRET_ACCESS_KEY='1234567891012345'
35
+ #
36
+ # Then load it in your shell's rc file.
37
+ #
38
+ # % cat .zshrc
39
+ # if [[ -f "$HOME/.aliyun_keys" ]]; then
40
+ # source "$HOME/.aliyun_keys";
41
+ # fi
42
+ #
43
+ # See more connection details at Aliyun::OSS::Connection::Management::ClassMethods.
44
+ module OSS
45
+ constant :DEFAULT_HOST, 'oss.aliyuncs.com'
46
+
47
+ # Aliyun::OSS::Base is the abstract super class of all classes who make requests against OSS, such as the built in
48
+ # Service, Bucket and OSSObject classes. It provides methods for making requests, inferring or setting response classes,
49
+ # processing request options, and accessing attributes from OSS's response data.
50
+ #
51
+ # Establishing a connection with the Base class is the entry point to using the library:
52
+ #
53
+ # Aliyun::OSS::Base.establish_connection!(:access_key_id => '...', :secret_access_key => '...')
54
+ #
55
+ # The <tt>:access_key_id</tt> and <tt>:secret_access_key</tt> are the two required connection options. More
56
+ # details can be found in the docs for Connection::Management::ClassMethods.
57
+ #
58
+ # Extensive examples can be found in the README[link:files/README.html].
59
+ class Base
60
+ class << self
61
+ # Wraps the current connection's request method and picks the appropriate response class to wrap the response in.
62
+ # If the response is an error, it will raise that error as an exception. All such exceptions can be caught by rescuing
63
+ # their superclass, the ResponseError exception class.
64
+ #
65
+ # It is unlikely that you would call this method directly. Subclasses of Base have convenience methods for each http request verb
66
+ # that wrap calls to request.
67
+ def request(verb, path, options = {}, body = nil, attempts = 0, &block)
68
+ Service.response = nil
69
+ process_options!(options, verb)
70
+ response = response_class.new(connection.request(verb, path, options, body, attempts, &block))
71
+ Service.response = response
72
+
73
+ Error::Response.new(response.response).error.raise if response.error?
74
+ response
75
+ # Once in a while, a request to OSS returns an internal error. A glitch in the matrix I presume. Since these
76
+ # errors are few and far between the request method will rescue InternalErrors the first three times they encouter them
77
+ # and will retry the request again. Most of the time the second attempt will work.
78
+ rescue InternalError, RequestTimeout
79
+ if attempts == 3
80
+ raise
81
+ else
82
+ attempts += 1
83
+ retry
84
+ end
85
+ end
86
+
87
+ [:get, :post, :put, :delete, :head].each do |verb|
88
+ class_eval(<<-EVAL, __FILE__, __LINE__)
89
+ def #{verb}(path, headers = {}, body = nil, &block)
90
+ request(:#{verb}, path, headers, body, &block)
91
+ end
92
+ EVAL
93
+ end
94
+
95
+ # Called when a method which requires a bucket name is called without that bucket name specified. It will try to
96
+ # infer the current bucket by looking for it as the subdomain of the current connection's address. If no subdomain
97
+ # is found, CurrentBucketNotSpecified will be raised.
98
+ #
99
+ # MusicBucket.establish_connection! :server => 'jukeboxzero.oss.aliyuncs.com'
100
+ # MusicBucket.connection.server
101
+ # => 'jukeboxzero.oss.aliyuncs.com'
102
+ # MusicBucket.current_bucket
103
+ # => 'jukeboxzero'
104
+ #
105
+ # Rather than infering the current bucket from the subdomain, the current class' bucket can be explicitly set with
106
+ # set_current_bucket_to.
107
+ def current_bucket
108
+ connection.subdomain or raise CurrentBucketNotSpecified.new(connection.http.address)
109
+ end
110
+
111
+ # If you plan on always using a specific bucket for certain files, you can skip always having to specify the bucket by creating
112
+ # a subclass of Bucket or OSSObject and telling it what bucket to use:
113
+ #
114
+ # class JukeBoxSong < Aliyun::OSS::OSSObject
115
+ # set_current_bucket_to 'jukebox'
116
+ # end
117
+ #
118
+ # For all methods that take a bucket name as an argument, the current bucket will be used if the bucket name argument is omitted.
119
+ #
120
+ # other_song = 'baby-please-come-home.mp3'
121
+ # JukeBoxSong.store(other_song, open(other_song))
122
+ #
123
+ # This time we didn't have to explicitly pass in the bucket name, as the JukeBoxSong class knows that it will
124
+ # always use the 'jukebox' bucket.
125
+ #
126
+ # "Astute readers", as they say, may have noticed that we used the third parameter to pass in the content type,
127
+ # rather than the fourth parameter as we had the last time we created an object. If the bucket can be inferred, or
128
+ # is explicitly set, as we've done in the JukeBoxSong class, then the third argument can be used to pass in
129
+ # options.
130
+ #
131
+ # Now all operations that would have required a bucket name no longer do.
132
+ #
133
+ # other_song = JukeBoxSong.find('baby-please-come-home.mp3')
134
+ def set_current_bucket_to(name)
135
+ raise ArgumentError, "`#{__method__}' must be called on a subclass of #{self.name}" if self == Aliyun::OSS::Base
136
+ instance_eval(<<-EVAL)
137
+ def current_bucket
138
+ '#{name}'
139
+ end
140
+ EVAL
141
+ end
142
+ alias_method :current_bucket=, :set_current_bucket_to
143
+
144
+ private
145
+
146
+ def response_class
147
+ FindResponseClass.for(self)
148
+ end
149
+
150
+ def process_options!(options, verb)
151
+ options.replace(RequestOptions.process(options, verb))
152
+ end
153
+
154
+ # Using the conventions layed out in the <tt>response_class</tt> works for more than 80% of the time.
155
+ # There are a few edge cases though where we want a given class to wrap its responses in different
156
+ # response classes depending on which method is being called.
157
+ def respond_with(klass)
158
+ eval(<<-EVAL, binding, __FILE__, __LINE__)
159
+ def new_response_class
160
+ #{klass}
161
+ end
162
+
163
+ class << self
164
+ alias_method :old_response_class, :response_class
165
+ alias_method :response_class, :new_response_class
166
+ end
167
+ EVAL
168
+
169
+ yield
170
+ ensure
171
+ # Restore the original version
172
+ eval(<<-EVAL, binding, __FILE__, __LINE__)
173
+ class << self
174
+ alias_method :response_class, :old_response_class
175
+ end
176
+ EVAL
177
+ end
178
+
179
+ def bucket_name(name)
180
+ name || current_bucket
181
+ end
182
+
183
+ class RequestOptions < Hash #:nodoc:
184
+ attr_reader :options, :verb
185
+
186
+ class << self
187
+ def process(*args, &block)
188
+ new(*args, &block).process!
189
+ end
190
+ end
191
+
192
+ def initialize(options, verb = :get)
193
+ @options = options.to_normalized_options
194
+ @verb = verb
195
+ super()
196
+ end
197
+
198
+ def process!
199
+ set_access_controls! if verb == :put
200
+ replace(options)
201
+ end
202
+
203
+ private
204
+ def set_access_controls!
205
+ ACL::OptionProcessor.process!(options)
206
+ end
207
+ end
208
+ end
209
+
210
+ def initialize(attributes = {}) #:nodoc:
211
+ @attributes = attributes
212
+ end
213
+
214
+ private
215
+ attr_reader :attributes
216
+
217
+ def connection
218
+ self.class.connection
219
+ end
220
+
221
+ def http
222
+ connection.http
223
+ end
224
+
225
+ def request(*args, &block)
226
+ self.class.request(*args, &block)
227
+ end
228
+
229
+ def method_missing(method, *args, &block)
230
+ case
231
+ when attributes.has_key?(method.to_s)
232
+ attributes[method.to_s]
233
+ when attributes.has_key?(method)
234
+ attributes[method]
235
+ else
236
+ super
237
+ end
238
+ end
239
+ end
240
+ end
241
+ end