aliyun-oss-ruby-sdk 0.4.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/CHANGELOG.md +95 -0
- data/README.md +423 -0
- data/examples/aliyun/oss/bucket.rb +144 -0
- data/examples/aliyun/oss/callback.rb +61 -0
- data/examples/aliyun/oss/object.rb +182 -0
- data/examples/aliyun/oss/resumable_download.rb +42 -0
- data/examples/aliyun/oss/resumable_upload.rb +49 -0
- data/examples/aliyun/oss/streaming.rb +124 -0
- data/examples/aliyun/oss/using_sts.rb +48 -0
- data/examples/aliyun/sts/assume_role.rb +59 -0
- data/lib/aliyun_sdk/common.rb +6 -0
- data/lib/aliyun_sdk/common/exception.rb +18 -0
- data/lib/aliyun_sdk/common/logging.rb +46 -0
- data/lib/aliyun_sdk/common/struct.rb +56 -0
- data/lib/aliyun_sdk/oss.rb +16 -0
- data/lib/aliyun_sdk/oss/bucket.rb +661 -0
- data/lib/aliyun_sdk/oss/client.rb +106 -0
- data/lib/aliyun_sdk/oss/config.rb +39 -0
- data/lib/aliyun_sdk/oss/download.rb +255 -0
- data/lib/aliyun_sdk/oss/exception.rb +108 -0
- data/lib/aliyun_sdk/oss/http.rb +338 -0
- data/lib/aliyun_sdk/oss/iterator.rb +92 -0
- data/lib/aliyun_sdk/oss/multipart.rb +74 -0
- data/lib/aliyun_sdk/oss/object.rb +15 -0
- data/lib/aliyun_sdk/oss/protocol.rb +1499 -0
- data/lib/aliyun_sdk/oss/struct.rb +208 -0
- data/lib/aliyun_sdk/oss/upload.rb +238 -0
- data/lib/aliyun_sdk/oss/util.rb +89 -0
- data/lib/aliyun_sdk/sts.rb +9 -0
- data/lib/aliyun_sdk/sts/client.rb +38 -0
- data/lib/aliyun_sdk/sts/config.rb +22 -0
- data/lib/aliyun_sdk/sts/exception.rb +53 -0
- data/lib/aliyun_sdk/sts/protocol.rb +130 -0
- data/lib/aliyun_sdk/sts/struct.rb +64 -0
- data/lib/aliyun_sdk/sts/util.rb +48 -0
- data/lib/aliyun_sdk/version.rb +7 -0
- data/spec/aliyun/oss/bucket_spec.rb +597 -0
- data/spec/aliyun/oss/client/bucket_spec.rb +554 -0
- data/spec/aliyun/oss/client/client_spec.rb +297 -0
- data/spec/aliyun/oss/client/resumable_download_spec.rb +220 -0
- data/spec/aliyun/oss/client/resumable_upload_spec.rb +413 -0
- data/spec/aliyun/oss/http_spec.rb +83 -0
- data/spec/aliyun/oss/multipart_spec.rb +686 -0
- data/spec/aliyun/oss/object_spec.rb +785 -0
- data/spec/aliyun/oss/service_spec.rb +142 -0
- data/spec/aliyun/oss/util_spec.rb +50 -0
- data/spec/aliyun/sts/client_spec.rb +150 -0
- data/spec/aliyun/sts/util_spec.rb +39 -0
- data/tests/config.rb +31 -0
- data/tests/test_content_encoding.rb +54 -0
- data/tests/test_content_type.rb +95 -0
- data/tests/test_custom_headers.rb +70 -0
- data/tests/test_encoding.rb +77 -0
- data/tests/test_large_file.rb +66 -0
- data/tests/test_multipart.rb +97 -0
- data/tests/test_object_acl.rb +49 -0
- data/tests/test_object_key.rb +68 -0
- data/tests/test_object_url.rb +69 -0
- data/tests/test_resumable.rb +40 -0
- metadata +240 -0
@@ -0,0 +1,338 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'rest-client'
|
4
|
+
require 'resolv'
|
5
|
+
require 'fiber'
|
6
|
+
|
7
|
+
module AliyunSDK
|
8
|
+
module OSS
|
9
|
+
|
10
|
+
##
|
11
|
+
# HTTP wraps the HTTP functionalities for accessing OSS RESTful
|
12
|
+
# API. It handles the OSS-specific protocol elements, and
|
13
|
+
# rest-client details for the user, which includes:
|
14
|
+
# * automatically generate signature for every request
|
15
|
+
# * parse response headers/body
|
16
|
+
# * raise exceptions and capture the request id
|
17
|
+
# * encapsulates streaming upload/download
|
18
|
+
# @example simple get
|
19
|
+
# headers, body = http.get({:bucket => 'bucket'})
|
20
|
+
# @example streaming download
|
21
|
+
# http.get({:bucket => 'bucket', :object => 'object'}) do |chunk|
|
22
|
+
# # handle chunk
|
23
|
+
# end
|
24
|
+
# @example streaming upload
|
25
|
+
# def streaming_upload(&block)
|
26
|
+
# http.put({:bucket => 'bucket', :object => 'object'},
|
27
|
+
# {:body => HTTP::StreamPlayload.new(block)})
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# streaming_upload do |stream|
|
31
|
+
# stream << "hello world"
|
32
|
+
# end
|
33
|
+
class HTTP
|
34
|
+
|
35
|
+
DEFAULT_CONTENT_TYPE = 'application/octet-stream'
|
36
|
+
DEFAULT_ACCEPT_ENCODING = 'identity'
|
37
|
+
STS_HEADER = 'x-oss-security-token'
|
38
|
+
OPEN_TIMEOUT = 10
|
39
|
+
READ_TIMEOUT = 120
|
40
|
+
|
41
|
+
##
|
42
|
+
# A stream implementation
|
43
|
+
# A stream is any class that responds to :read(bytes, outbuf)
|
44
|
+
#
|
45
|
+
class StreamWriter
|
46
|
+
def initialize
|
47
|
+
@buffer = ""
|
48
|
+
@producer = Fiber.new { yield self if block_given? }
|
49
|
+
@producer.resume
|
50
|
+
end
|
51
|
+
|
52
|
+
def read(bytes = nil, outbuf = nil)
|
53
|
+
ret = ""
|
54
|
+
loop do
|
55
|
+
if bytes
|
56
|
+
fail if bytes < 0
|
57
|
+
piece = @buffer.slice!(0, bytes)
|
58
|
+
if piece
|
59
|
+
ret << piece
|
60
|
+
bytes -= piece.size
|
61
|
+
break if bytes == 0
|
62
|
+
end
|
63
|
+
else
|
64
|
+
ret << @buffer
|
65
|
+
@buffer.clear
|
66
|
+
end
|
67
|
+
|
68
|
+
if @producer.alive?
|
69
|
+
@producer.resume
|
70
|
+
else
|
71
|
+
break
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
if outbuf
|
76
|
+
# WARNING: Using outbuf = '' here DOES NOT work!
|
77
|
+
outbuf.clear
|
78
|
+
outbuf << ret
|
79
|
+
end
|
80
|
+
|
81
|
+
# Conform to IO#read(length[, outbuf]):
|
82
|
+
# At end of file, it returns nil or "" depend on
|
83
|
+
# length. ios.read() and ios.read(nil) returns
|
84
|
+
# "". ios.read(positive-integer) returns nil.
|
85
|
+
return nil if ret.empty? && !bytes.nil? && bytes > 0
|
86
|
+
|
87
|
+
ret
|
88
|
+
end
|
89
|
+
|
90
|
+
def write(chunk)
|
91
|
+
@buffer << chunk.to_s.force_encoding(Encoding::ASCII_8BIT)
|
92
|
+
Fiber.yield
|
93
|
+
self
|
94
|
+
end
|
95
|
+
|
96
|
+
alias << write
|
97
|
+
|
98
|
+
def closed?
|
99
|
+
false
|
100
|
+
end
|
101
|
+
|
102
|
+
def inspect
|
103
|
+
"@buffer: " + @buffer[0, 32].inspect + "...#{@buffer.size} bytes"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# RestClient requires the payload to respones to :read(bytes)
|
108
|
+
# and return a stream.
|
109
|
+
# We are not doing the real read here, just return a
|
110
|
+
# readable stream for RestClient playload.rb treats it as:
|
111
|
+
# def read(bytes=nil)
|
112
|
+
# @stream.read(bytes)
|
113
|
+
# end
|
114
|
+
# alias :to_s :read
|
115
|
+
# net_http_do_request(http, req, payload ? payload.to_s : nil,
|
116
|
+
# &@block_response)
|
117
|
+
class StreamPayload
|
118
|
+
def initialize(&block)
|
119
|
+
@stream = StreamWriter.new(&block)
|
120
|
+
end
|
121
|
+
|
122
|
+
def read(bytes = nil)
|
123
|
+
@stream
|
124
|
+
end
|
125
|
+
|
126
|
+
def close
|
127
|
+
end
|
128
|
+
|
129
|
+
def closed?
|
130
|
+
false
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
|
135
|
+
include Common::Logging
|
136
|
+
|
137
|
+
def initialize(config)
|
138
|
+
@config = config
|
139
|
+
end
|
140
|
+
|
141
|
+
def get_request_url(bucket, object)
|
142
|
+
url = @config.endpoint.dup
|
143
|
+
isIP = !!(url.host =~ Resolv::IPv4::Regex)
|
144
|
+
url.host = "#{bucket}." + url.host if bucket && !@config.cname && !isIP
|
145
|
+
url.path = '/'
|
146
|
+
url.path << "#{bucket}/" if bucket && isIP
|
147
|
+
url.path << "#{CGI.escape(object)}" if object
|
148
|
+
|
149
|
+
url.to_s
|
150
|
+
end
|
151
|
+
|
152
|
+
def get_resource_path(bucket, object)
|
153
|
+
res = '/'
|
154
|
+
res << "#{bucket}/" if bucket
|
155
|
+
res << "#{object}" if object
|
156
|
+
|
157
|
+
res
|
158
|
+
end
|
159
|
+
|
160
|
+
# Handle Net::HTTPRespoonse
|
161
|
+
def handle_response(r, &block)
|
162
|
+
# read all body on error
|
163
|
+
if r.code.to_i >= 300
|
164
|
+
r.read_body
|
165
|
+
else
|
166
|
+
# streaming read body on success
|
167
|
+
encoding = r['content-encoding']
|
168
|
+
if encoding == 'gzip'
|
169
|
+
stream = StreamWriter.new { |s| r.read_body { |chunk| s << chunk } }
|
170
|
+
reader = Zlib::GzipReader.new(stream)
|
171
|
+
yield reader.read(16 * 1024) until reader.eof?
|
172
|
+
elsif encoding == 'deflate'
|
173
|
+
begin
|
174
|
+
stream = Zlib::Inflate.new
|
175
|
+
# 1.9.x doesn't support streaming inflate
|
176
|
+
if RUBY_VERSION < '2.0.0'
|
177
|
+
yield stream.inflate(r.read_body)
|
178
|
+
else
|
179
|
+
r.read_body { |chunk| stream << chunk }
|
180
|
+
stream.finish { |chunk| yield chunk }
|
181
|
+
end
|
182
|
+
rescue Zlib::DataError
|
183
|
+
# No luck with Zlib decompression. Let's try with raw deflate,
|
184
|
+
# like some broken web servers do.
|
185
|
+
stream = Zlib::Inflate.new(-Zlib::MAX_WBITS)
|
186
|
+
# 1.9.x doesn't support streaming inflate
|
187
|
+
if RUBY_VERSION < '2.0.0'
|
188
|
+
yield stream.inflate(r.read_body)
|
189
|
+
else
|
190
|
+
r.read_body { |chunk| stream << chunk }
|
191
|
+
stream.finish { |chunk| yield chunk }
|
192
|
+
end
|
193
|
+
end
|
194
|
+
else
|
195
|
+
r.read_body { |chunk| yield chunk }
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
##
|
201
|
+
# helper methods
|
202
|
+
#
|
203
|
+
def get(resources = {}, http_options = {}, &block)
|
204
|
+
do_request('GET', resources, http_options, &block)
|
205
|
+
end
|
206
|
+
|
207
|
+
def put(resources = {}, http_options = {}, &block)
|
208
|
+
do_request('PUT', resources, http_options, &block)
|
209
|
+
end
|
210
|
+
|
211
|
+
def post(resources = {}, http_options = {}, &block)
|
212
|
+
do_request('POST', resources, http_options, &block)
|
213
|
+
end
|
214
|
+
|
215
|
+
def delete(resources = {}, http_options = {}, &block)
|
216
|
+
do_request('DELETE', resources, http_options, &block)
|
217
|
+
end
|
218
|
+
|
219
|
+
def head(resources = {}, http_options = {}, &block)
|
220
|
+
do_request('HEAD', resources, http_options, &block)
|
221
|
+
end
|
222
|
+
|
223
|
+
def options(resources = {}, http_options = {}, &block)
|
224
|
+
do_request('OPTIONS', resources, http_options, &block)
|
225
|
+
end
|
226
|
+
|
227
|
+
private
|
228
|
+
# Do HTTP reqeust
|
229
|
+
# @param verb [String] HTTP Verb: GET/PUT/POST/DELETE/HEAD/OPTIONS
|
230
|
+
# @param resources [Hash] OSS related resources
|
231
|
+
# @option resources [String] :bucket the bucket name
|
232
|
+
# @option resources [String] :object the object name
|
233
|
+
# @option resources [Hash] :sub_res sub-resources
|
234
|
+
# @param http_options [Hash] HTTP options
|
235
|
+
# @option http_options [Hash] :headers HTTP headers
|
236
|
+
# @option http_options [Hash] :query HTTP queries
|
237
|
+
# @option http_options [Object] :body HTTP body, may be String
|
238
|
+
# or Stream
|
239
|
+
def do_request(verb, resources = {}, http_options = {}, &block)
|
240
|
+
bucket = resources[:bucket]
|
241
|
+
object = resources[:object]
|
242
|
+
sub_res = resources[:sub_res]
|
243
|
+
|
244
|
+
headers = http_options[:headers] || {}
|
245
|
+
headers['user-agent'] = get_user_agent
|
246
|
+
headers['date'] = Time.now.httpdate
|
247
|
+
headers['content-type'] ||= DEFAULT_CONTENT_TYPE
|
248
|
+
headers['accept-encoding'] ||= DEFAULT_ACCEPT_ENCODING
|
249
|
+
headers[STS_HEADER] = @config.sts_token if @config.sts_token
|
250
|
+
|
251
|
+
if body = http_options[:body]
|
252
|
+
if body.respond_to?(:read)
|
253
|
+
headers['transfer-encoding'] = 'chunked'
|
254
|
+
else
|
255
|
+
headers['content-md5'] = Util.get_content_md5(body)
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
res = {
|
260
|
+
:path => get_resource_path(bucket, object),
|
261
|
+
:sub_res => sub_res,
|
262
|
+
}
|
263
|
+
|
264
|
+
if @config.access_key_id and @config.access_key_secret
|
265
|
+
sig = Util.get_signature(@config.access_key_secret, verb, headers, res)
|
266
|
+
headers['authorization'] = "OSS #{@config.access_key_id}:#{sig}"
|
267
|
+
end
|
268
|
+
|
269
|
+
logger.debug("Send HTTP request, verb: #{verb}, resources: " \
|
270
|
+
"#{resources}, http options: #{http_options}")
|
271
|
+
|
272
|
+
# From rest-client:
|
273
|
+
# "Due to unfortunate choices in the original API, the params
|
274
|
+
# used to populate the query string are actually taken out of
|
275
|
+
# the headers hash."
|
276
|
+
headers[:params] = (sub_res || {}).merge(http_options[:query] || {})
|
277
|
+
|
278
|
+
block_response = ->(r) { handle_response(r, &block) } if block
|
279
|
+
r = RestClient::Request.execute(
|
280
|
+
:method => verb,
|
281
|
+
:url => get_request_url(bucket, object),
|
282
|
+
:headers => headers,
|
283
|
+
:payload => http_options[:body],
|
284
|
+
:block_response => block_response,
|
285
|
+
:open_timeout => @config.open_timeout || OPEN_TIMEOUT,
|
286
|
+
:timeout => @config.read_timeout || READ_TIMEOUT
|
287
|
+
) do |response, request, result, &blk|
|
288
|
+
|
289
|
+
if response.code >= 300
|
290
|
+
e = ServerError.new(response)
|
291
|
+
logger.error(e.to_s)
|
292
|
+
raise e
|
293
|
+
else
|
294
|
+
response.return!(request, result, &blk)
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
# If streaming read_body is used, we need to create the
|
299
|
+
# RestClient::Response ourselves
|
300
|
+
unless r.is_a?(RestClient::Response)
|
301
|
+
if r.code.to_i >= 300
|
302
|
+
r = RestClient::Response.create(
|
303
|
+
RestClient::Request.decode(r['content-encoding'], r.body),
|
304
|
+
r, nil, nil)
|
305
|
+
e = ServerError.new(r)
|
306
|
+
logger.error(e.to_s)
|
307
|
+
raise e
|
308
|
+
end
|
309
|
+
r = RestClient::Response.create(nil, r, nil, nil)
|
310
|
+
r.return!
|
311
|
+
end
|
312
|
+
|
313
|
+
logger.debug("Received HTTP response, code: #{r.code}, headers: " \
|
314
|
+
"#{r.headers}, body: #{r.body}")
|
315
|
+
|
316
|
+
r
|
317
|
+
end
|
318
|
+
|
319
|
+
def get_user_agent
|
320
|
+
"aliyun-sdk-ruby/#{VERSION} ruby-#{RUBY_VERSION}/#{RUBY_PLATFORM}"
|
321
|
+
end
|
322
|
+
|
323
|
+
end # HTTP
|
324
|
+
end # OSS
|
325
|
+
end # Aliyun
|
326
|
+
|
327
|
+
# Monkey patch rest-client to exclude the 'Content-Length' header when
|
328
|
+
# 'Transfer-Encoding' is set to 'chuncked'. This may be a problem for
|
329
|
+
# some http servers like tengine.
|
330
|
+
module RestClient
|
331
|
+
module Payload
|
332
|
+
class Base
|
333
|
+
def headers
|
334
|
+
({'content-length' => size.to_s} if size) || {}
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
338
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
module AliyunSDK
|
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
|
+
##
|
73
|
+
# Uploads iterator
|
74
|
+
#
|
75
|
+
class Uploads < Base
|
76
|
+
def initialize(protocol, bucket_name, opts = {})
|
77
|
+
super(protocol, opts)
|
78
|
+
@bucket = bucket_name
|
79
|
+
end
|
80
|
+
|
81
|
+
def fetch(more)
|
82
|
+
@results, cont = @protocol.list_multipart_uploads(@bucket, more)
|
83
|
+
@results = cont[:common_prefixes] + @results if cont[:common_prefixes]
|
84
|
+
@more[:id_marker] = cont[:next_id_marker]
|
85
|
+
@more[:key_marker] = cont[:next_key_marker]
|
86
|
+
@more[:truncated] = cont[:truncated] || false
|
87
|
+
end
|
88
|
+
end # Objects
|
89
|
+
|
90
|
+
end # Iterator
|
91
|
+
end # OSS
|
92
|
+
end # Aliyun
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'digest/md5'
|
5
|
+
|
6
|
+
module AliyunSDK
|
7
|
+
module OSS
|
8
|
+
|
9
|
+
##
|
10
|
+
# Multipart upload/download structures
|
11
|
+
#
|
12
|
+
module Multipart
|
13
|
+
|
14
|
+
##
|
15
|
+
# A multipart transaction. Provide the basic checkpoint methods.
|
16
|
+
#
|
17
|
+
class Transaction < Common::Struct::Base
|
18
|
+
|
19
|
+
attrs :id, :object, :bucket, :creation_time, :options
|
20
|
+
|
21
|
+
def initialize(opts = {})
|
22
|
+
super(opts)
|
23
|
+
|
24
|
+
@mutex = Mutex.new
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
# Persist transaction states to file
|
29
|
+
def write_checkpoint(states, file)
|
30
|
+
md5= Util.get_content_md5(states.to_json)
|
31
|
+
|
32
|
+
@mutex.synchronize {
|
33
|
+
File.open(file, 'w') {
|
34
|
+
|f| f.write(states.merge(md5: md5).to_json)
|
35
|
+
}
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
# Load transaction states from file
|
40
|
+
def load_checkpoint(file)
|
41
|
+
states = {}
|
42
|
+
|
43
|
+
@mutex.synchronize {
|
44
|
+
states = JSON.load(File.read(file))
|
45
|
+
}
|
46
|
+
states = Util.symbolize(states)
|
47
|
+
md5 = states.delete(:md5)
|
48
|
+
|
49
|
+
fail CheckpointBrokenError, "Missing MD5 in checkpoint." unless md5
|
50
|
+
unless md5 == Util.get_content_md5(states.to_json)
|
51
|
+
fail CheckpointBrokenError, "Unmatched checkpoint MD5."
|
52
|
+
end
|
53
|
+
|
54
|
+
states
|
55
|
+
end
|
56
|
+
|
57
|
+
def get_file_md5(file)
|
58
|
+
Digest::MD5.file(file).to_s
|
59
|
+
end
|
60
|
+
|
61
|
+
end # Transaction
|
62
|
+
|
63
|
+
##
|
64
|
+
# A part in a multipart uploading transaction
|
65
|
+
#
|
66
|
+
class Part < Common::Struct::Base
|
67
|
+
|
68
|
+
attrs :number, :etag, :size, :last_modified
|
69
|
+
|
70
|
+
end # Part
|
71
|
+
|
72
|
+
end # Multipart
|
73
|
+
end # OSS
|
74
|
+
end # Aliyun
|