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.
- checksums.yaml +7 -0
- data/README.md +364 -0
- data/lib/aliyun/oss.rb +17 -0
- data/lib/aliyun/oss/bucket.rb +555 -0
- data/lib/aliyun/oss/client.rb +100 -0
- data/lib/aliyun/oss/config.rb +34 -0
- data/lib/aliyun/oss/download.rb +216 -0
- data/lib/aliyun/oss/exception.rb +116 -0
- data/lib/aliyun/oss/http.rb +282 -0
- data/lib/aliyun/oss/iterator.rb +74 -0
- data/lib/aliyun/oss/logging.rb +43 -0
- data/lib/aliyun/oss/multipart.rb +60 -0
- data/lib/aliyun/oss/object.rb +15 -0
- data/lib/aliyun/oss/protocol.rb +1432 -0
- data/lib/aliyun/oss/struct.rb +199 -0
- data/lib/aliyun/oss/upload.rb +195 -0
- data/lib/aliyun/oss/util.rb +88 -0
- data/lib/aliyun/oss/version.rb +9 -0
- data/spec/aliyun/oss/bucket_spec.rb +595 -0
- data/spec/aliyun/oss/client/bucket_spec.rb +338 -0
- data/spec/aliyun/oss/client/client_spec.rb +228 -0
- data/spec/aliyun/oss/client/resumable_download_spec.rb +217 -0
- data/spec/aliyun/oss/client/resumable_upload_spec.rb +318 -0
- data/spec/aliyun/oss/http_spec.rb +26 -0
- data/spec/aliyun/oss/multipart_spec.rb +675 -0
- data/spec/aliyun/oss/object_spec.rb +741 -0
- data/spec/aliyun/oss/service_spec.rb +142 -0
- data/spec/aliyun/oss/util_spec.rb +50 -0
- metadata +181 -0
@@ -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
|