aliyun-sdk 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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