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,199 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ module Aliyun
4
+ module OSS
5
+
6
+ ##
7
+ # Access Control List, it controls how the bucket/object can be
8
+ # accessed.
9
+ # * public-read-write: allow access(read&write) anonymously
10
+ # * public-read: allow read anonymously
11
+ # * private: access must be signatured
12
+ #
13
+ module ACL
14
+ PUBLIC_READ_WRITE = "public-read-write"
15
+ PUBLIC_READ = "public-read"
16
+ PRIVATE = "private"
17
+ end # ACL
18
+
19
+ ##
20
+ # A OSS object may carry some metas(String key-value pairs) with
21
+ # it. MetaDirective specifies what to do with the metas in the
22
+ # copy process.
23
+ # * COPY: metas are copied from the source object to the dest
24
+ # object
25
+ # * REPLACE: source object's metas are NOT copied, use user
26
+ # provided metas for the dest object
27
+ #
28
+ module MetaDirective
29
+ COPY = "COPY"
30
+ REPLACE = "REPLACE"
31
+ end # MetaDirective
32
+
33
+ ##
34
+ # The object key may contains unicode charactors which cannot be
35
+ # encoded in the request/response body(XML). KeyEncoding specifies
36
+ # the encoding type for the object key.
37
+ # * url: the object key is url-encoded
38
+ # @note url-encoding is the only supported KeyEncoding type
39
+ #
40
+ module KeyEncoding
41
+ URL = "url"
42
+
43
+ @@all = [URL]
44
+
45
+ def self.include?(enc)
46
+ all.include?(enc)
47
+ end
48
+
49
+ def self.all
50
+ @@all
51
+ end
52
+ end # KeyEncoding
53
+
54
+ ##
55
+ # Common structs used. It provides a 'attrs' helper method for
56
+ # subclass to define its attributes. 'attrs' is based on
57
+ # attr_reader and provide additional functionalities for classes
58
+ # that inherits Struct::Base :
59
+ # * the constuctor is provided to accept options and set the
60
+ # corresponding attibute automatically
61
+ # * the #to_s method is rewrite to concatenate the defined
62
+ # attributes keys and values
63
+ # @example
64
+ # class X < Struct::Base
65
+ # attrs :foo, :bar
66
+ # end
67
+ #
68
+ # x.new(:foo => 'hello', :bar => 'world')
69
+ # x.foo # == "hello"
70
+ # x.bar # == "world"
71
+ # x.to_s # == "foo: hello, bar: world"
72
+ module Struct
73
+ class Base
74
+ module AttrHelper
75
+ def attrs(*s)
76
+ define_method(:attrs) {s}
77
+ attr_reader(*s)
78
+ end
79
+ end
80
+
81
+ extend AttrHelper
82
+
83
+ def initialize(opts = {})
84
+ extra_keys = opts.keys - attrs
85
+ unless extra_keys.empty?
86
+ fail ClientError, "Unexpected extra keys: #{extra_keys.join(', ')}"
87
+ end
88
+
89
+ attrs.each do |attr|
90
+ instance_variable_set("@#{attr}", opts[attr])
91
+ end
92
+ end
93
+
94
+ def to_s
95
+ attrs.map do |attr|
96
+ v = instance_variable_get("@#{attr}")
97
+ "#{attr.to_s}: #{v}"
98
+ end.join(", ")
99
+ end
100
+ end # Base
101
+ end # Struct
102
+
103
+ ##
104
+ # Bucket Logging setting. See: {http://help.aliyun.com/document_detail/oss/product-documentation/function/logging.html OSS Bucket logging}
105
+ # Attributes:
106
+ # * enable [Boolean] whether to enable bucket logging
107
+ # * target_bucket [String] the target bucket to store access logs
108
+ # * target_prefix [String] the target object prefix to store access logs
109
+ # @example Enable bucket logging
110
+ # bucket.logging = BucketLogging.new(
111
+ # :enable => true, :target_bucket => 'log_bucket', :target_prefix => 'my-log')
112
+ # @example Disable bucket logging
113
+ # bucket.logging = BucketLogging.new(:enable => false)
114
+ class BucketLogging < Struct::Base
115
+ attrs :enable, :target_bucket, :target_prefix
116
+
117
+ def enabled?
118
+ enable == true
119
+ end
120
+ end
121
+
122
+ ##
123
+ # Bucket website setting. See: {http://help.aliyun.com/document_detail/oss/product-documentation/function/host-static-website.html OSS Website hosting}
124
+ # Attributes:
125
+ # * enable [Boolean] whether to enable website hosting for the bucket
126
+ # * index [String] the index object as the index page for the website
127
+ # * error [String] the error object as the error page for the website
128
+ class BucketWebsite < Struct::Base
129
+ attrs :enable, :index, :error
130
+
131
+ def enabled?
132
+ enable == true
133
+ end
134
+ end
135
+
136
+ ##
137
+ # Bucket referer setting. See: {http://help.aliyun.com/document_detail/oss/product-documentation/function/referer-white-list.html OSS Website hosting}
138
+ # Attributes:
139
+ # * allow_empty [Boolean] whether to allow requests with empty "Referer"
140
+ # * whitelist [Array<String>] the allowed origins for requests
141
+ class BucketReferer < Struct::Base
142
+ attrs :allow_empty, :whitelist
143
+
144
+ def allow_empty?
145
+ allow_empty == true
146
+ end
147
+ end
148
+
149
+ ##
150
+ # LifeCycle rule for bucket. See: {http://help.aliyun.com/document_detail/oss/product-documentation/function/lifecycle.html OSS Bucket LifeCycle}
151
+ # Attributes:
152
+ # * id [String] the unique id of a rule
153
+ # * enabled [Boolean] whether to enable this rule
154
+ # * prefix [String] the prefix objects to apply this rule
155
+ # * expiry [Date] or [Fixnum] the expire time of objects
156
+ # * if expiry is a Date, it specifies the absolute date to
157
+ # expire objects
158
+ # * if expiry is a Fixnum, it specifies the relative date to
159
+ # expire objects: how many days after the object's last
160
+ # modification time to expire the object
161
+ # @example Specify expiry as Date
162
+ # LifeCycleRule.new(
163
+ # :id => 'rule1',
164
+ # :enabled => true,
165
+ # :prefix => 'foo/',
166
+ # :expiry => Date.new(2016, 1, 1))
167
+ # @example Specify expiry as days
168
+ # LifeCycleRule.new(
169
+ # :id => 'rule1',
170
+ # :enabled => true,
171
+ # :prefix => 'foo/',
172
+ # :expiry => 15)
173
+ # @note the expiry date is treated as UTC time
174
+ class LifeCycleRule < Struct::Base
175
+
176
+ attrs :id, :enable, :prefix, :expiry
177
+
178
+ def enabled?
179
+ enable == true
180
+ end
181
+ end # LifeCycleRule
182
+
183
+ ##
184
+ # CORS rule for bucket. See: {http://help.aliyun.com/document_detail/oss/product-documentation/function/referer-white-list.html OSS CORS}
185
+ # Attributes:
186
+ # * allowed_origins [Array<String>] the allowed origins
187
+ # * allowed_methods [Array<String>] the allowed methods
188
+ # * allowed_headers [Array<String>] the allowed headers
189
+ # * expose_headers [Array<String>] the expose headers
190
+ # * max_age_seconds [Integer] the max age seconds
191
+ class CORSRule < Struct::Base
192
+
193
+ attrs :allowed_origins, :allowed_methods, :allowed_headers,
194
+ :expose_headers, :max_age_seconds
195
+
196
+ end # CORSRule
197
+
198
+ end # OSS
199
+ end # Aliyun
@@ -0,0 +1,195 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ module Aliyun
4
+ module OSS
5
+ module Multipart
6
+ ##
7
+ # A multipart upload transaction
8
+ #
9
+ class Upload < Transaction
10
+ PART_SIZE = 4 * 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
+ @file_meta = {}
20
+ @parts = []
21
+ super(args)
22
+ end
23
+
24
+ # Run the upload transaction, which includes 3 stages:
25
+ # * 1a. initiate(new upload) and divide parts
26
+ # * 1b. rebuild states(resumed upload)
27
+ # * 2. upload each unfinished part
28
+ # * 3. commit the multipart upload transaction
29
+ def run
30
+ logger.info("Begin upload, 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 file to upload into parts to upload separately
38
+ divide_parts if @parts.empty?
39
+
40
+ # Upload each part
41
+ @parts.reject { |p| p[:done] }.each { |p| upload_part(p) }
42
+
43
+ # Commit the multipart upload transaction
44
+ commit
45
+
46
+ logger.info("Done upload, file: #{@file}")
47
+ end
48
+
49
+ # Checkpoint structures:
50
+ # @example
51
+ # states = {
52
+ # :id => 'upload_id',
53
+ # :file => 'file',
54
+ # :file_meta => {
55
+ # :mtime => Time.now,
56
+ # :md5 => 1024
57
+ # },
58
+ # :parts => [
59
+ # {:number => 1, :range => [0, 100], :done => false},
60
+ # {:number => 2, :range => [100, 200], :done => true}
61
+ # ],
62
+ # :md5 => 'states_md5'
63
+ # }
64
+ def checkpoint
65
+ logger.debug("Begin make checkpoint, disable_cpt: #{options[:disable_cpt]}")
66
+
67
+ ensure_file_not_changed
68
+
69
+ states = {
70
+ :id => id,
71
+ :file => @file,
72
+ :file_meta => @file_meta,
73
+ :parts => @parts
74
+ }
75
+
76
+ # report progress
77
+ if @progress
78
+ done = @parts.count { |p| p[:done] }
79
+ @progress.call(done.to_f / @parts.size) if done > 0
80
+ end
81
+
82
+ write_checkpoint(states, @checkpoint_file) unless options[:disable_cpt]
83
+
84
+ logger.debug("Done make checkpoint, states: #{states}")
85
+ end
86
+
87
+ private
88
+ # Commit the transaction when all parts are succefully uploaded
89
+ # @todo handle undefined behaviors: commit succeeds in server
90
+ # but return error in client
91
+ def commit
92
+ logger.info("Begin commit transaction, id: #{id}")
93
+
94
+ parts = @parts.map{ |p| Part.new(:number => p[:number], :etag => p[:etag])}
95
+ @protocol.complete_multipart_upload(bucket, object, id, parts)
96
+
97
+ File.delete(@checkpoint_file) unless options[:disable_cpt]
98
+
99
+ logger.info("Done commit transaction, id: #{id}")
100
+ end
101
+
102
+ # Rebuild the states of the transaction from checkpoint file
103
+ def rebuild
104
+ logger.info("Begin rebuild transaction, checkpoint: #{@checkpoint_file}")
105
+
106
+ if File.exists?(@checkpoint_file) and not options[:disable_cpt]
107
+ states = load_checkpoint(@checkpoint_file)
108
+
109
+ if states[:file_md5] != @file_meta[:md5]
110
+ fail FileInconsistentError.new("The file to upload is changed.")
111
+ end
112
+
113
+ @id = states[:id]
114
+ @file_meta = states[:file_meta]
115
+ @parts = states[:parts]
116
+ else
117
+ initiate
118
+ end
119
+
120
+ logger.info("Done rebuild transaction, states: #{states}")
121
+ end
122
+
123
+ def initiate
124
+ logger.info("Begin initiate transaction")
125
+
126
+ @id = @protocol.initiate_multipart_upload(bucket, object, options)
127
+ @file_meta = {
128
+ :mtime => File.mtime(@file),
129
+ :md5 => get_file_md5(@file)
130
+ }
131
+ checkpoint
132
+
133
+ logger.info("Done initiate transaction, id: #{id}")
134
+ end
135
+
136
+ # Upload a part
137
+ def upload_part(p)
138
+ logger.debug("Begin upload part: #{p}")
139
+
140
+ result = nil
141
+ File.open(@file) do |f|
142
+ range = p[:range]
143
+ pos = range.first
144
+ f.seek(pos)
145
+
146
+ result = @protocol.upload_part(bucket, object, id, p[:number]) do |sw|
147
+ while pos < range.at(1)
148
+ bytes = [READ_SIZE, range.at(1) - pos].min
149
+ sw << f.read(bytes)
150
+ pos += bytes
151
+ end
152
+ end
153
+ end
154
+ p[:done] = true
155
+ p[:etag] = result.etag
156
+
157
+ checkpoint
158
+
159
+ logger.debug("Done upload part: #{p}")
160
+ end
161
+
162
+ # Devide the file into parts to upload
163
+ def divide_parts
164
+ logger.info("Begin divide parts, file: #{@file}")
165
+
166
+ max_parts = 10000
167
+ file_size = File.size(@file)
168
+ part_size = [@options[:part_size] || PART_SIZE, file_size / max_parts].max
169
+ num_parts = (file_size - 1) / part_size + 1
170
+ @parts = (1..num_parts).map do |i|
171
+ {
172
+ :number => i,
173
+ :range => [(i-1) * part_size, [i * part_size, file_size].min],
174
+ :done => false
175
+ }
176
+ end
177
+
178
+ checkpoint
179
+
180
+ logger.info("Done divide parts, parts: #{@parts}")
181
+ end
182
+
183
+ # Ensure file not changed during uploading
184
+ def ensure_file_not_changed
185
+ return if File.mtime(@file) == @file_meta[:mtime]
186
+
187
+ if @file_meta[:md5] != get_file_md5(@file)
188
+ fail FileInconsistentError, "The file to upload is changed."
189
+ end
190
+ end
191
+ end # Upload
192
+
193
+ end # Multipart
194
+ end # OSS
195
+ end # Aliyun
@@ -0,0 +1,88 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'time'
4
+ require 'base64'
5
+ require 'openssl'
6
+
7
+ module Aliyun
8
+ module OSS
9
+ ##
10
+ # Util functions to help generate formatted Date, signatures,
11
+ # etc.
12
+ #
13
+ module Util
14
+
15
+ # Prefix for OSS specific HTTP headers
16
+ HEADER_PREFIX = "x-oss-"
17
+
18
+ class << self
19
+
20
+ include Logging
21
+
22
+ # Calculate request signatures
23
+ def get_signature(key, verb, headers, resources)
24
+ logger.debug("Sign, headers: #{headers}, resources: #{resources}")
25
+
26
+ content_md5 = headers['Content-MD5'] || ""
27
+ content_type = headers['Content-Type'] || ""
28
+ date = headers['Date']
29
+
30
+ cano_headers = headers.select { |k, v| k.start_with?(HEADER_PREFIX) }
31
+ .map { |k, v| [k.downcase.strip, v.strip] }
32
+ .sort.map { |k, v| [k, v].join(":") + "\n" }.join
33
+
34
+ cano_res = resources[:path] || "/"
35
+ sub_res = (resources[:sub_res] || {})
36
+ .sort.map { |k, v| v ? [k, v].join("=") : k }.join("&")
37
+ cano_res += "?#{sub_res}" unless sub_res.empty?
38
+
39
+ string_to_sign =
40
+ "#{verb}\n#{content_md5}\n#{content_type}\n#{date}\n" +
41
+ "#{cano_headers}#{cano_res}"
42
+
43
+ logger.debug("String to sign: #{string_to_sign}")
44
+
45
+ Util.sign(key, string_to_sign)
46
+ end
47
+
48
+ # Sign a string using HMAC and BASE64
49
+ # @param [String] key the secret key
50
+ # @param [String] string_to_sign the string to sign
51
+ # @return [String] the signature
52
+ def sign(key, string_to_sign)
53
+ Base64.strict_encode64(
54
+ OpenSSL::HMAC.digest('sha1', key, string_to_sign))
55
+ end
56
+
57
+ # Calculate content md5
58
+ def get_content_md5(content)
59
+ Base64.strict_encode64(OpenSSL::Digest::MD5.digest(content))
60
+ end
61
+
62
+ end # self
63
+ end # Util
64
+ end # OSS
65
+ end # Aliyun
66
+
67
+ # Monkey patch to support #to_bool
68
+ class String
69
+ def to_bool
70
+ return true if self =~ /^true$/i
71
+ false
72
+ end
73
+ end
74
+
75
+ # Monkey patch to support #symbolize_keys!
76
+ class Array
77
+ def symbolize_keys!
78
+ self.each { |v| v.symbolize_keys! if v.is_a?(Hash) or v.is_a?(Array) }
79
+ end
80
+ end
81
+
82
+ # Monkey patch to support #symbolize_keys!
83
+ class Hash
84
+ def symbolize_keys!
85
+ self.keys.each { |k| self[k.to_sym] = self.delete(k) }
86
+ self.values.each { |v| v.symbolize_keys! if v.is_a?(Hash) or v.is_a?(Array) }
87
+ end
88
+ end