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,100 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ module Aliyun
4
+ module OSS
5
+
6
+ ##
7
+ # OSS服务的客户端,用于获取bucket列表,创建/删除bucket。Object相关
8
+ # 的操作请使用{OSS::Bucket}。
9
+ # @example 创建Client
10
+ # endpoint = 'oss-cn-hangzhou.oss.aliyuncs.com'
11
+ # client = Client.new(
12
+ # :endpoint => endpoint,
13
+ # :access_key_id => 'access_key_id',
14
+ # :access_key_secret => 'access_key_secret')
15
+ # buckets = client.list_buckets
16
+ # client.create_bucket('my-bucket')
17
+ # client.delete_bucket('my-bucket')
18
+ # bucket = client.get_bucket('my-bucket')
19
+ class Client
20
+
21
+ include Logging
22
+
23
+ # 构造OSS client,用于操作buckets。
24
+ # @param opts [Hash] 构造Client时的参数选项
25
+ # @option opts [String] :endpoint [必填]OSS服务的地址,可以是以
26
+ # oss.aliyuncs.com的标准域名,也可以是用户绑定的域名
27
+ # @option opts [String] :access_key_id [可选]用户的ACCESS KEY ID,
28
+ # 如果不填则会尝试匿名访问
29
+ # @option opts [String] :access_key_secret [可选]用户的ACCESS
30
+ # KEY SECRET,如果不填则会尝试匿名访问
31
+ # @option opts [Boolean] :cname [可选] 指定endpoint是否是用户绑
32
+ # 定的域名
33
+ # @example 标准endpoint
34
+ # oss-cn-hangzhou.aliyuncs.com
35
+ # oss-cn-beijing.aliyuncs.com
36
+ # @example 用户绑定的域名
37
+ # my-domain.com
38
+ # foo.bar.com
39
+ def initialize(opts)
40
+ fail ClientError, "Endpoint must be provided" unless opts[:endpoint]
41
+
42
+ @config = Config.new(opts)
43
+ @protocol = Protocol.new(@config)
44
+ end
45
+
46
+ # 列出当前所有的bucket
47
+ # @param opts [Hash] 查询选项
48
+ # @option opts [String] :prefix 如果设置,则只返回以它为前缀的bucket
49
+ # @return [Enumerator<Bucket>] Bucket的迭代器
50
+ def list_buckets(opts = {})
51
+ if @config.cname
52
+ fail ClientError, "Cannot list buckets for a CNAME endpoint."
53
+ end
54
+
55
+ Iterator::Buckets.new(@protocol, opts).to_enum
56
+ end
57
+
58
+ # 创建一个bucket
59
+ # @param name [String] Bucket名字
60
+ # @param opts [Hash] 创建Bucket的属性(可选)
61
+ # @option opts [:location] [String] 指定bucket所在的区域,默认为oss-cn-hangzhou
62
+ def create_bucket(name, opts = {})
63
+ @protocol.create_bucket(name, opts)
64
+ end
65
+
66
+ # 删除一个bucket
67
+ # @param name [String] Bucket名字
68
+ # @note 如果要删除的Bucket不为空(包含有object),则删除会失败
69
+ def delete_bucket(name)
70
+ @protocol.delete_bucket(name)
71
+ end
72
+
73
+ # 判断一个bucket是否存在
74
+ # @param name [String] Bucket名字
75
+ # @return [Boolean] 如果Bucket存在则返回true,否则返回false
76
+ def bucket_exists?(name)
77
+ exist = false
78
+
79
+ begin
80
+ @protocol.get_bucket_acl(name)
81
+ exist = true
82
+ rescue ServerError => e
83
+ raise unless e.http_code == 404
84
+ end
85
+
86
+ exist
87
+ end
88
+
89
+ alias :bucket_exist? :bucket_exists?
90
+
91
+ # 获取一个Bucket对象,用于操作bucket中的objects。
92
+ # @param name [String] Bucket名字
93
+ # @return [Bucket] Bucket对象
94
+ def get_bucket(name)
95
+ Bucket.new({:name => name}, @protocol)
96
+ end
97
+
98
+ end # Client
99
+ end # OSS
100
+ end # Aliyun
@@ -0,0 +1,34 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ module Aliyun
4
+ module OSS
5
+
6
+ ##
7
+ # A place to store various configurations: credentials, api
8
+ # timeout, retry mechanism, etc
9
+ #
10
+ class Config < Struct::Base
11
+
12
+ attrs :endpoint, :cname, :access_key_id, :access_key_secret
13
+
14
+ def initialize(opts = {})
15
+ super(opts)
16
+ normalize_endpoint if endpoint
17
+ end
18
+
19
+ private
20
+
21
+ def normalize_endpoint
22
+ uri = URI.parse(endpoint)
23
+ uri = URI.parse("http://#{endpoint}") unless uri.scheme
24
+
25
+ if uri.scheme != 'http' and uri.scheme != 'https'
26
+ fail ClientError, "Only HTTP and HTTPS endpoint are accepted."
27
+ end
28
+
29
+ @endpoint = uri
30
+ end
31
+
32
+ end # Config
33
+ end # OSS
34
+ end # Aliyun
@@ -0,0 +1,216 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ module Aliyun
4
+ module OSS
5
+ module Multipart
6
+ ##
7
+ # A multipart download transaction
8
+ #
9
+ class Download < Transaction
10
+ PART_SIZE = 10 * 1024 * 1024
11
+ READ_SIZE = 16 * 1024
12
+
13
+ def initialize(protocol, opts)
14
+ args = opts.dup
15
+ @protocol = protocol
16
+ @progress = args.delete(:progress)
17
+ @file = args.delete(:file)
18
+ @checkpoint_file = args.delete(:cpt_file)
19
+ @object_meta = {}
20
+ @parts = []
21
+ super(args)
22
+ end
23
+
24
+ # Run the download transaction, which includes 3 stages:
25
+ # * 1a. initiate(new downlaod) and divide parts
26
+ # * 1b. rebuild states(resumed download)
27
+ # * 2. download each unfinished part
28
+ # * 3. combine the downloaded parts into the final file
29
+ def run
30
+ logger.info("Begin download, file: #{@file}, checkpoint file: "\
31
+ "#{@checkpoint_file}")
32
+
33
+ # Rebuild transaction states from checkpoint file
34
+ # Or initiate new transaction states
35
+ rebuild
36
+
37
+ # Divide the target object into parts to download by ranges
38
+ divide_parts if @parts.empty?
39
+
40
+ # Download each part(object range)
41
+ @parts.reject { |p| p[:done]}.each { |p| download_part(p) }
42
+
43
+ # Combine the parts into the final file
44
+ commit
45
+
46
+ logger.info("Done download, file: #{@file}")
47
+ end
48
+
49
+ # Checkpoint structures:
50
+ # @example
51
+ # states = {
52
+ # :id => 'download_id',
53
+ # :file => 'file',
54
+ # :object_meta => {
55
+ # :etag => 'xxx',
56
+ # :size => 1024
57
+ # },
58
+ # :parts => [
59
+ # {:number => 1, :range => [0, 100], :md5 => 'xxx', :done => false},
60
+ # {:number => 2, :range => [100, 200], :md5 => 'yyy', :done => true}
61
+ # ],
62
+ # :md5 => 'states_md5'
63
+ # }
64
+ def checkpoint
65
+ logger.debug("Begin make checkpoint, "\
66
+ "disable_cpt: #{options[:disable_cpt]}")
67
+
68
+ ensure_object_not_changed
69
+
70
+ states = {
71
+ :id => id,
72
+ :file => @file,
73
+ :object_meta => @object_meta,
74
+ :parts => @parts
75
+ }
76
+
77
+ # report progress
78
+ if @progress
79
+ done = @parts.count { |p| p[:done] }
80
+ @progress.call(done.to_f / @parts.size) if done > 0
81
+ end
82
+
83
+ write_checkpoint(states, @checkpoint_file) unless options[:disable_cpt]
84
+
85
+ logger.debug("Done make checkpoint, states: #{states}")
86
+ end
87
+
88
+ private
89
+ # Combine the downloaded parts into the final file
90
+ # @todo avoid copy all part files
91
+ def commit
92
+ logger.info("Begin commit transaction, id: #{id}")
93
+
94
+ # concat all part files into the target file
95
+ File.open(@file, 'w') do |w|
96
+ @parts.sort{ |x, y| x[:number] <=> y[:number] }.each do |p|
97
+ File.open(get_part_file(p[:number])) do |r|
98
+ w.write(r.read(READ_SIZE)) until r.eof?
99
+ end
100
+ end
101
+ end
102
+
103
+ File.delete(@checkpoint_file) unless options[:disable_cpt]
104
+ @parts.each{ |p| File.delete(get_part_file(p[:number])) }
105
+
106
+ logger.info("Done commit transaction, id: #{id}")
107
+ end
108
+
109
+ # Rebuild the states of the transaction from checkpoint file
110
+ def rebuild
111
+ logger.info("Begin rebuild transaction, "\
112
+ "checkpoint: #{@checkpoint_file}")
113
+
114
+ if File.exists?(@checkpoint_file) and not options[:disable_cpt]
115
+ states = load_checkpoint(@checkpoint_file)
116
+
117
+ states[:parts].select{ |p| p[:done] }.each do |p|
118
+ part_file = get_part_file(p[:number])
119
+
120
+ unless File.exist?(part_file)
121
+ fail PartMissingError, "The part file is missing: #{part_file}."
122
+ end
123
+
124
+ if p[:md5] != get_file_md5(part_file)
125
+ fail PartInconsistentError,
126
+ "The part file is changed: #{part_file}."
127
+ end
128
+ end
129
+
130
+ @id = states[:id]
131
+ @object_meta = states[:object_meta]
132
+ @parts = states[:parts]
133
+ else
134
+ initiate
135
+ end
136
+
137
+ logger.info("Done rebuild transaction, states: #{states}")
138
+ end
139
+
140
+ def initiate
141
+ logger.info("Begin initiate transaction")
142
+
143
+ @id = generate_download_id
144
+ obj = @protocol.get_object_meta(bucket, object)
145
+ @object_meta = {
146
+ :etag => obj.etag,
147
+ :size => obj.size
148
+ }
149
+ checkpoint
150
+
151
+ logger.info("Done initiate transaction, id: #{id}")
152
+ end
153
+
154
+ # Download a part
155
+ def download_part(p)
156
+ logger.debug("Begin download part: #{p}")
157
+
158
+ part_file = get_part_file(p[:number])
159
+ File.open(part_file, 'w') do |w|
160
+ @protocol.get_object(
161
+ bucket, object, :range => p[:range]) { |chunk| w.write(chunk) }
162
+ end
163
+
164
+ p[:done] = true
165
+ p[:md5] = get_file_md5(part_file)
166
+
167
+ checkpoint
168
+
169
+ logger.debug("Done download part: #{p}")
170
+ end
171
+
172
+ # Devide the object to download into parts to download
173
+ def divide_parts
174
+ logger.info("Begin divide parts, object: #{@object}")
175
+
176
+ max_parts = 100
177
+ object_size = @object_meta[:size]
178
+ part_size =
179
+ [@options[:part_size] || PART_SIZE, object_size / max_parts].max
180
+ num_parts = (object_size - 1) / part_size + 1
181
+ @parts = (1..num_parts).map do |i|
182
+ {
183
+ :number => i,
184
+ :range => [(i - 1) * part_size, [i * part_size, object_size].min],
185
+ :done => false
186
+ }
187
+ end
188
+
189
+ checkpoint
190
+
191
+ logger.info("Done divide parts, parts: #{@parts}")
192
+ end
193
+
194
+ # Ensure file not changed during uploading
195
+ def ensure_object_not_changed
196
+ obj = @protocol.get_object_meta(bucket, object)
197
+ unless obj.etag == @object_meta[:etag]
198
+ fail ObjectInconsistentError,
199
+ "The object to download is changed: #{object}."
200
+ end
201
+ end
202
+
203
+ # Generate a download id
204
+ def generate_download_id
205
+ "download_#{bucket}_#{object}_#{Time.now.to_i}"
206
+ end
207
+
208
+ # Get name for part file
209
+ def get_part_file(number)
210
+ "#{@file}.part.#{number}"
211
+ end
212
+ end # Download
213
+
214
+ end # Multipart
215
+ end # OSS
216
+ end # Aliyun
@@ -0,0 +1,116 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'nokogiri'
4
+
5
+ module Aliyun
6
+ module OSS
7
+
8
+ ##
9
+ # Base exception class
10
+ #
11
+ class Exception < RuntimeError
12
+ end
13
+
14
+ ##
15
+ # ServerError represents exceptions from the OSS
16
+ # service. i.e. Client receives a HTTP response whose status is
17
+ # NOT OK. #message provides the error message and #to_s gives
18
+ # detailed information probably including the OSS request id.
19
+ #
20
+ class ServerError < Exception
21
+ include Logging
22
+
23
+ attr_reader :http_code, :error_code, :message, :request_id
24
+
25
+ def initialize(response)
26
+ @http_code = response.code
27
+ @attrs = {'RequestId' => get_request_id(response)}
28
+
29
+ doc = Nokogiri::XML(response.body) do |config|
30
+ config.options |= Nokogiri::XML::ParseOptions::NOBLANKS
31
+ end rescue nil
32
+
33
+ if doc and doc.root
34
+ doc.root.children.each do |n|
35
+ @attrs[n.name] = n.text
36
+ end
37
+ end
38
+
39
+ @error_code = @attrs['Code']
40
+ @message = @attrs['Message']
41
+ @request_id = @attrs['RequestId']
42
+ end
43
+
44
+ def message
45
+ @attrs['Message'] || "UnknownError, HTTP Code: #{http_code}"
46
+ end
47
+
48
+ def to_s
49
+ @attrs.merge({'HTTPCode' => @http_code}).map do |k, v|
50
+ [k, v].join(": ")
51
+ end.join(", ")
52
+ end
53
+
54
+ private
55
+
56
+ def get_request_id(response)
57
+ r = response.headers[:x_oss_request_id] if response.headers
58
+ r.to_s
59
+ end
60
+
61
+ end # ServerError
62
+
63
+ ##
64
+ # ClientError represents client exceptions caused mostly by
65
+ # invalid parameters.
66
+ #
67
+ class ClientError < Exception
68
+ attr_reader :message
69
+
70
+ def initialize(message)
71
+ @message = message
72
+ end
73
+ end # ClientError
74
+
75
+ ##
76
+ # FileInconsistentError happens in a resumable upload transaction,
77
+ # when the file to upload has changed during the uploading
78
+ # process. Which means the transaction cannot go on. Or user may
79
+ # have inconsistent data uploaded to OSS.
80
+ #
81
+ class FileInconsistentError < ClientError; end
82
+
83
+ ##
84
+ # ObjectInconsistentError happens in a resumable download transaction,
85
+ # when the object to download has changed during the downloading
86
+ # process. Which means the transaction cannot go on. Or user may
87
+ # have inconsistent data downloaded to OSS.
88
+ #
89
+ class ObjectInconsistentError < ClientError; end
90
+
91
+ ##
92
+ # PartMissingError happens in a resumable download transaction,
93
+ # when a downloaded part cannot be found as the client tries to
94
+ # resume download. The process cannot go on until the part is
95
+ # restored.
96
+ #
97
+ class PartMissingError < ClientError; end
98
+
99
+ ##
100
+ # PartMissingError happens in a resumable download transaction,
101
+ # when a downloaded part has changed(MD5 mismatch) as the client
102
+ # tries to resume download. The process cannot go on until the
103
+ # part is restored.
104
+ #
105
+ class PartInconsistentError < ClientError; end
106
+
107
+ ##
108
+ # CheckpointBrokenError happens in a resumable upload/download
109
+ # transaction, when the client finds the checkpoint file has
110
+ # changed(MD5 mismatch) as it tries to resume upload/download. The
111
+ # process cannot go on until the checkpoint file is restored.
112
+ #
113
+ class CheckpointBrokenError < ClientError; end
114
+
115
+ end # OSS
116
+ end # Aliyun