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,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