aliyun-sdk 0.1.1

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,282 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'rest-client'
4
+ require 'fiber'
5
+
6
+ module Aliyun
7
+ module OSS
8
+
9
+ ##
10
+ # HTTP wraps the HTTP functionalities for accessing OSS RESTful
11
+ # API. It handles the OSS-specific protocol elements, and
12
+ # rest-client details for the user, which includes:
13
+ # * automatically generate signature for every request
14
+ # * parse response headers/body
15
+ # * raise exceptions and capture the request id
16
+ # * encapsulates streaming upload/download
17
+ # @example simple get
18
+ # headers, body = http.get({:bucket => 'bucket'})
19
+ # @example streaming download
20
+ # http.get({:bucket => 'bucket', :object => 'object'}) do |chunk|
21
+ # # handle chunk
22
+ # end
23
+ # @example streaming upload
24
+ # def streaming_upload(&block)
25
+ # http.put({:bucket => 'bucket', :object => 'object'},
26
+ # {:body => HTTP::StreamPlayload.new(block)})
27
+ # end
28
+ #
29
+ # streaming_upload do |stream|
30
+ # stream << "hello world"
31
+ # end
32
+ class HTTP
33
+
34
+ DEFAULT_CONTENT_TYPE = 'application/octet-stream'
35
+
36
+ ##
37
+ # A stream implementation
38
+ # A stream is any class that responds to :read(bytes, outbuf)
39
+ #
40
+ class StreamWriter
41
+ def initialize
42
+ @chunks = []
43
+ @producer = Fiber.new { yield self if block_given? }
44
+ @producer.resume
45
+ end
46
+
47
+ # FIXME: it may return more than bytes, not sure if that's a problem
48
+ def read(bytes = nil, outbuf = nil)
49
+ ret = ""
50
+ loop do
51
+ c = @chunks.shift
52
+ ret << c if c && !c.empty?
53
+ break if bytes && ret.size >= bytes
54
+ if @producer.alive?
55
+ @producer.resume
56
+ else
57
+ break
58
+ end
59
+ end
60
+
61
+ if outbuf
62
+ # WARNING: Using outbuf = '' here DOES NOT work!
63
+ outbuf.clear
64
+ outbuf << ret
65
+ end
66
+
67
+ ret.empty? ? nil : ret
68
+ end
69
+
70
+ def write(chunk)
71
+ @chunks << chunk
72
+ Fiber.yield
73
+ self
74
+ end
75
+
76
+ alias << write
77
+
78
+ def closed?
79
+ false
80
+ end
81
+
82
+ def inspect
83
+ "@chunks: " + @chunks.map { |c| c[0, 100] }.join(';')
84
+ end
85
+ end
86
+
87
+ # RestClient requires the payload to respones to :read(bytes)
88
+ # and return a stream.
89
+ # We are not doing the real read here, just return a
90
+ # readable stream for RestClient playload.rb treats it as:
91
+ # def read(bytes=nil)
92
+ # @stream.read(bytes)
93
+ # end
94
+ # alias :to_s :read
95
+ # net_http_do_request(http, req, payload ? payload.to_s : nil,
96
+ # &@block_response)
97
+ class StreamPayload
98
+ def initialize(&block)
99
+ @stream = StreamWriter.new(&block)
100
+ end
101
+
102
+ def read(bytes = nil)
103
+ @stream
104
+ end
105
+
106
+ def close
107
+ end
108
+
109
+ def closed?
110
+ false
111
+ end
112
+
113
+ end
114
+
115
+ include Logging
116
+
117
+ def initialize(config)
118
+ @config = config
119
+ end
120
+
121
+ def get_request_url(bucket, object)
122
+ url = ""
123
+ url += "#{@config.endpoint.scheme}://"
124
+ url += "#{bucket}." if bucket and not @config.cname
125
+ url += @config.endpoint.host
126
+ url += "/#{CGI.escape(object)}" if object
127
+
128
+ url
129
+ end
130
+
131
+ def get_resource_path(bucket, object)
132
+ if bucket
133
+ res = "/#{bucket}/"
134
+ res += "#{object}" if object
135
+ res
136
+ end
137
+ end
138
+
139
+ # Handle Net::HTTPRespoonse
140
+ def handle_response(r, &block)
141
+ # read all body on error
142
+ if r.code.to_i >= 300
143
+ r.read_body
144
+ else
145
+ # streaming read body on success
146
+ r.read_body do |chunk|
147
+ yield RestClient::Request.decode(r['content-encoding'], chunk)
148
+ end
149
+ end
150
+ end
151
+
152
+ ##
153
+ # helper methods
154
+ #
155
+ def get(resources = {}, http_options = {}, &block)
156
+ do_request('GET', resources, http_options, &block)
157
+ end
158
+
159
+ def put(resources = {}, http_options = {}, &block)
160
+ do_request('PUT', resources, http_options, &block)
161
+ end
162
+
163
+ def post(resources = {}, http_options = {}, &block)
164
+ do_request('POST', resources, http_options, &block)
165
+ end
166
+
167
+ def delete(resources = {}, http_options = {}, &block)
168
+ do_request('DELETE', resources, http_options, &block)
169
+ end
170
+
171
+ def head(resources = {}, http_options = {}, &block)
172
+ do_request('HEAD', resources, http_options, &block)
173
+ end
174
+
175
+ def options(resources = {}, http_options = {}, &block)
176
+ do_request('OPTIONS', resources, http_options, &block)
177
+ end
178
+
179
+ private
180
+ # Do HTTP reqeust
181
+ # @param verb [String] HTTP Verb: GET/PUT/POST/DELETE/HEAD/OPTIONS
182
+ # @param resources [Hash] OSS related resources
183
+ # @option resources [String] :bucket the bucket name
184
+ # @option resources [String] :object the object name
185
+ # @option resources [Hash] :sub_res sub-resources
186
+ # @param http_options [Hash] HTTP options
187
+ # @option http_options [Hash] :headers HTTP headers
188
+ # @option http_options [Hash] :query HTTP queries
189
+ # @option http_options [Object] :body HTTP body, may be String
190
+ # or Stream
191
+ def do_request(verb, resources = {}, http_options = {}, &block)
192
+ bucket = resources[:bucket]
193
+ object = resources[:object]
194
+ sub_res = resources[:sub_res]
195
+
196
+ headers = http_options[:headers] || {}
197
+ headers['User-Agent'] = get_user_agent
198
+ headers['Date'] = Time.now.httpdate
199
+ headers['Content-Type'] ||= DEFAULT_CONTENT_TYPE
200
+
201
+ if body = http_options[:body] and body.respond_to?(:read)
202
+ headers['Transfer-Encoding'] = 'chunked'
203
+ end
204
+
205
+ res = {
206
+ :path => get_resource_path(bucket, object),
207
+ :sub_res => sub_res,
208
+ }
209
+
210
+ if @config.access_key_id and @config.access_key_secret
211
+ sig = Util.get_signature(@config.access_key_secret, verb, headers, res)
212
+ headers['Authorization'] = "OSS #{@config.access_key_id}:#{sig}"
213
+ end
214
+
215
+ logger.debug("Send HTTP request, verb: #{verb}, resources: " \
216
+ "#{resources}, http options: #{http_options}")
217
+
218
+ # From rest-client:
219
+ # "Due to unfortunate choices in the original API, the params
220
+ # used to populate the query string are actually taken out of
221
+ # the headers hash."
222
+ headers[:params] = (sub_res || {}).merge(http_options[:query] || {})
223
+
224
+ block_response = ->(r) { handle_response(r, &block) } if block
225
+ r = RestClient::Request.execute(
226
+ :method => verb,
227
+ :url => get_request_url(bucket, object),
228
+ :headers => headers,
229
+ :payload => http_options[:body],
230
+ :block_response => block_response
231
+ ) do |response, request, result, &blk|
232
+
233
+ if response.code >= 300
234
+ e = ServerError.new(response)
235
+ logger.error(e.to_s)
236
+ raise e
237
+ else
238
+ response.return!(request, result, &blk)
239
+ end
240
+ end
241
+
242
+ # If streaming read_body is used, we need to create the
243
+ # RestClient::Response ourselves
244
+ unless r.is_a?(RestClient::Response)
245
+ if r.code.to_i >= 300
246
+ r = RestClient::Response.create(
247
+ RestClient::Request.decode(r['content-encoding'], r.body),
248
+ r, nil, nil)
249
+ e = ServerError.new(r)
250
+ logger.error(e.to_s)
251
+ raise e
252
+ end
253
+ r = RestClient::Response.create(nil, r, nil, nil)
254
+ r.return!
255
+ end
256
+
257
+ logger.debug("Received HTTP response, code: #{r.code}, headers: " \
258
+ "#{r.headers}, body: #{r.body}")
259
+
260
+ [r.headers, r.body]
261
+ end
262
+
263
+ def get_user_agent
264
+ "aliyun-sdk-ruby/#{VERSION}"
265
+ end
266
+
267
+ end # HTTP
268
+ end # OSS
269
+ end # Aliyun
270
+
271
+ # Monkey patch rest-client to exclude the 'Content-Length' header when
272
+ # 'Transfer-Encoding' is set to 'chuncked'. This may be a problem for
273
+ # some http servers like tengine.
274
+ module RestClient
275
+ module Payload
276
+ class Base
277
+ def headers
278
+ ({'Content-Length' => size.to_s} if size) || {}
279
+ end
280
+ end
281
+ end
282
+ end
@@ -0,0 +1,74 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ module Aliyun
4
+ module OSS
5
+ ##
6
+ # Iterator structs that wrap the multiple communications with the
7
+ # server to provide an iterable result.
8
+ #
9
+ module Iterator
10
+
11
+ ##
12
+ # Iterator base that stores fetched results and fetch more if needed.
13
+ #
14
+ class Base
15
+ def initialize(protocol, opts = {})
16
+ @protocol = protocol
17
+ @results, @more = [], opts
18
+ end
19
+
20
+ def next
21
+ loop do
22
+ # Communicate with the server to get more results
23
+ fetch_more if @results.empty?
24
+
25
+ # Return the first result
26
+ r = @results.shift
27
+ break unless r
28
+
29
+ yield r
30
+ end
31
+ end
32
+
33
+ def to_enum
34
+ self.enum_for(:next)
35
+ end
36
+
37
+ private
38
+ def fetch_more
39
+ return if @more[:truncated] == false
40
+ fetch(@more)
41
+ end
42
+ end # Base
43
+
44
+ ##
45
+ # Buckets iterator
46
+ #
47
+ class Buckets < Base
48
+ def fetch(more)
49
+ @results, cont = @protocol.list_buckets(more)
50
+ @more[:marker] = cont[:next_marker]
51
+ @more[:truncated] = cont[:truncated] || false
52
+ end
53
+ end # Buckets
54
+
55
+ ##
56
+ # Objects iterator
57
+ #
58
+ class Objects < Base
59
+ def initialize(protocol, bucket_name, opts = {})
60
+ super(protocol, opts)
61
+ @bucket = bucket_name
62
+ end
63
+
64
+ def fetch(more)
65
+ @results, cont = @protocol.list_objects(@bucket, more)
66
+ @results = cont[:common_prefixes] + @results if cont[:common_prefixes]
67
+ @more[:marker] = cont[:next_marker]
68
+ @more[:truncated] = cont[:truncated] || false
69
+ end
70
+ end # Objects
71
+
72
+ end # Iterator
73
+ end # OSS
74
+ end # Aliyun
@@ -0,0 +1,43 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'logger'
4
+
5
+ module Aliyun
6
+ module OSS
7
+ ##
8
+ # Logging support
9
+ # @example
10
+ # include Logging
11
+ # logger.info(xxx)
12
+ module Logging
13
+
14
+ DEFAULT_LOG_FILE = "./oss_sdk.log"
15
+
16
+ # level = Logger::DEBUG | Logger::INFO | Logger::ERROR | Logger::FATAL
17
+ def self.set_log_level(level)
18
+ Logging.logger.level = level
19
+ end
20
+
21
+ # 设置日志输出的文件
22
+ def self.set_log_file(file)
23
+ @@log_file = file
24
+ end
25
+
26
+ # 获取logger
27
+ def logger
28
+ Logging.logger
29
+ end
30
+
31
+ private
32
+
33
+ def self.logger
34
+ unless @logger
35
+ @logger = Logger.new(@@log_file ||= DEFAULT_LOG_FILE)
36
+ @logger.level = Logger::INFO
37
+ end
38
+ @logger
39
+ end
40
+
41
+ end # logging
42
+ end # OSS
43
+ end # Aliyun
@@ -0,0 +1,60 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'json'
4
+
5
+ module Aliyun
6
+ module OSS
7
+
8
+ ##
9
+ # Multipart upload/download structures
10
+ #
11
+ module Multipart
12
+
13
+ ##
14
+ # A multipart transaction. Provide the basic checkpoint methods.
15
+ #
16
+ class Transaction < Struct::Base
17
+
18
+ include Logging
19
+
20
+ attrs :id, :object, :bucket, :creation_time, :options
21
+
22
+ private
23
+ # Persist transaction states to file
24
+ def write_checkpoint(states, file)
25
+ states[:md5] = Util.get_content_md5(states.to_json)
26
+ File.open(file, 'w'){ |f| f.write(states.to_json) }
27
+ end
28
+
29
+ # Load transaction states from file
30
+ def load_checkpoint(file)
31
+ states = JSON.load(File.read(file))
32
+ states.symbolize_keys!
33
+ md5 = states.delete(:md5)
34
+
35
+ fail CheckpointBrokenError, "Missing MD5 in checkpoint." unless md5
36
+ unless md5 == Util.get_content_md5(states.to_json)
37
+ fail CheckpointBrokenError, "Unmatched checkpoint MD5."
38
+ end
39
+
40
+ states
41
+ end
42
+
43
+ def get_file_md5(file)
44
+ Digest::MD5.file(file).to_s
45
+ end
46
+
47
+ end # Transaction
48
+
49
+ ##
50
+ # A part in a multipart uploading transaction
51
+ #
52
+ class Part < Struct::Base
53
+
54
+ attrs :number, :etag, :size, :last_modified
55
+
56
+ end # Part
57
+
58
+ end # Multipart
59
+ end # OSS
60
+ end # Aliyun