From de1672097a584566ea4278826039aac67b347057 Mon Sep 17 00:00:00 2001 From: Tianlong Wu Date: Wed, 23 Dec 2015 12:33:51 +0800 Subject: [PATCH 1/3] Allow setting custom headers in put/append/resumable_upload. fix #16 --- examples/aliyun/oss/resumable_download.rb | 4 +- examples/aliyun/oss/resumable_upload.rb | 5 +- lib/aliyun/oss/bucket.rb | 6 ++ lib/aliyun/oss/http.rb | 14 ++-- lib/aliyun/oss/object.rb | 2 +- lib/aliyun/oss/protocol.rb | 65 +++++++++++----- lib/aliyun/oss/util.rb | 6 +- spec/aliyun/oss/client/bucket_spec.rb | 31 ++++++++ .../oss/client/resumable_upload_spec.rb | 58 +++++++------- spec/aliyun/oss/util_spec.rb | 8 +- tests/test_content_type.rb | 20 ++--- tests/test_custom_headers.rb | 75 +++++++++++++++++++ 12 files changed, 216 insertions(+), 78 deletions(-) create mode 100644 tests/test_custom_headers.rb diff --git a/examples/aliyun/oss/resumable_download.rb b/examples/aliyun/oss/resumable_download.rb index 758a545..fac4b12 100644 --- a/examples/aliyun/oss/resumable_download.rb +++ b/examples/aliyun/oss/resumable_download.rb @@ -25,10 +25,12 @@ def demo(msg) demo "Resumable download" do # 下载一个100M的文件 + cpt_file = '/tmp/y.cpt' + File.delete(cpt_file) if File.exist?(cpt_file) start = Time.now puts "Start download: resumable => /tmp/y" bucket.resumable_download( - 'resumable', '/tmp/y', :cpt_file => '/tmp/y.cpt') do |progress| + 'resumable', '/tmp/y', :cpt_file => cpt_file) do |progress| puts "Progress: #{(progress * 100).round(2)} %" end puts "Download complete. Cost: #{Time.now - start} seconds." diff --git a/examples/aliyun/oss/resumable_upload.rb b/examples/aliyun/oss/resumable_upload.rb index 514ac3c..ae53a9e 100644 --- a/examples/aliyun/oss/resumable_upload.rb +++ b/examples/aliyun/oss/resumable_upload.rb @@ -30,11 +30,14 @@ def demo(msg) (1..1024*1024).each{ |i| f.puts i.to_s.rjust(99, '0') } end + cpt_file = '/tmp/x.cpt' + File.delete(cpt_file) if File.exist?(cpt_file) + # 上传一个100M的文件 start = Time.now puts "Start upload: /tmp/x => resumable" bucket.resumable_upload( - 'resumable', '/tmp/x', :cpt_file => '/tmp/x.cpt') do |progress| + 'resumable', '/tmp/x', :cpt_file => cpt_file) do |progress| puts "Progress: #{(progress * 100).round(2)} %" end puts "Upload complete. Cost: #{Time.now - start} seconds." diff --git a/lib/aliyun/oss/bucket.rb b/lib/aliyun/oss/bucket.rb index 2b48f94..a881950 100644 --- a/lib/aliyun/oss/bucket.rb +++ b/lib/aliyun/oss/bucket.rb @@ -181,6 +181,8 @@ def list_objects(opts = {}) # @option opts [Callback] :callback 指定操作成功后OSS的 # 上传回调,上传成功后OSS会向用户的应用服务器发一个HTTP POST请 # 求,`:callback`参数指定这个请求的相关参数 + # @option opts [Hash] :headers 指定请求的HTTP Header,不区分大小 + # 写。这里指定的值会覆盖通过`:content_type`和`:metas`设置的值。 # @yield [HTTP::StreamWriter] 如果调用的时候传递了block,则写入 # 到object的数据由block指定 # @example 流式上传数据 @@ -315,6 +317,8 @@ def object_exists?(key) # @option opts [Hash] :metas 设置object的meta,这是一些用户自定 # 义的属性,它们会和object一起存储,在{#get_object}的时候会 # 返回这些meta。属性的key不区分大小写。例如:{ 'year' => '2015' } + # @option opts [Hash] :headers 指定请求的HTTP Header,不区分大小 + # 写。这里指定的值会覆盖通过`:content_type`和`:metas`设置的值。 # @example 流式上传数据 # pos = append_object('x', 0){ |stream| 100.times { |i| stream << i.to_s } } # append_object('x', pos){ |stream| stream << get_data } @@ -439,6 +443,8 @@ def get_object_cors(key) # @option opts [Callback] :callback 指定文件上传成功后OSS的 # 上传回调,上传成功后OSS会向用户的应用服务器发一个HTTP POST请 # 求,`:callback`参数指定这个请求的相关参数 + # @option opts [Hash] :headers 指定请求的HTTP Header,不区分大小 + # 写。这里指定的值会覆盖通过`:content_type`和`:metas`设置的值。 # @yield [Float] 如果调用的时候传递了block,则会将上传进度交由 # block处理,进度值是一个0-1之间的小数 # @raise [CheckpointBrokenError] 如果cpt文件被损坏,则抛出此错误 diff --git a/lib/aliyun/oss/http.rb b/lib/aliyun/oss/http.rb index fd8d5a9..c4645c6 100644 --- a/lib/aliyun/oss/http.rb +++ b/lib/aliyun/oss/http.rb @@ -207,16 +207,16 @@ def do_request(verb, resources = {}, http_options = {}, &block) sub_res = resources[:sub_res] headers = http_options[:headers] || {} - headers['User-Agent'] = get_user_agent - headers['Date'] = Time.now.httpdate - headers['Content-Type'] ||= DEFAULT_CONTENT_TYPE + headers['user-agent'] = get_user_agent + headers['date'] = Time.now.httpdate + headers['content-type'] ||= DEFAULT_CONTENT_TYPE headers[STS_HEADER] = @config.sts_token if @config.sts_token if body = http_options[:body] if body.respond_to?(:read) - headers['Transfer-Encoding'] = 'chunked' + headers['transfer-encoding'] = 'chunked' else - headers['Content-MD5'] = Util.get_content_md5(body) + headers['content-md5'] = Util.get_content_md5(body) end end @@ -227,7 +227,7 @@ def do_request(verb, resources = {}, http_options = {}, &block) if @config.access_key_id and @config.access_key_secret sig = Util.get_signature(@config.access_key_secret, verb, headers, res) - headers['Authorization'] = "OSS #{@config.access_key_id}:#{sig}" + headers['authorization'] = "OSS #{@config.access_key_id}:#{sig}" end logger.debug("Send HTTP request, verb: #{verb}, resources: " \ @@ -295,7 +295,7 @@ module RestClient module Payload class Base def headers - ({'Content-Length' => size.to_s} if size) || {} + ({'content-length' => size.to_s} if size) || {} end end end diff --git a/lib/aliyun/oss/object.rb b/lib/aliyun/oss/object.rb index 0b6ec0f..fe52ba0 100644 --- a/lib/aliyun/oss/object.rb +++ b/lib/aliyun/oss/object.rb @@ -8,7 +8,7 @@ module OSS # class Object < Common::Struct::Base - attrs :key, :type, :size, :etag, :metas, :last_modified, :content_type + attrs :key, :type, :size, :etag, :metas, :last_modified, :headers end # Object end # OSS diff --git a/lib/aliyun/oss/protocol.rb b/lib/aliyun/oss/protocol.rb index c675860..7bd556c 100644 --- a/lib/aliyun/oss/protocol.rb +++ b/lib/aliyun/oss/protocol.rb @@ -508,6 +508,9 @@ def delete_bucket(name) # with the object # @option opts [Callback] :callback the HTTP callback performed # by OSS after `put_object` succeeds + # @option opts [Hash] :headers custom HTTP headers, case + # insensitive. Headers specified here will overwrite `:metas` + # and `:content_type` # @yield [HTTP::StreamWriter] a stream writer is # yielded to the caller to which it can write chunks of data # streamingly @@ -518,14 +521,16 @@ def put_object(bucket_name, object_name, opts = {}, &block) logger.debug("Begin put object, bucket: #{bucket_name}, object: "\ "#{object_name}, options: #{opts}") - headers = {'Content-Type' => opts[:content_type]} + headers = {'content-type' => opts[:content_type]} + to_lower_case(opts[:metas] || {}) + .each { |k, v| headers["x-oss-meta-#{k.to_s}"] = v.to_s } + + headers.merge!(to_lower_case(opts[:headers])) if opts.key?(:headers) + if opts.key?(:callback) headers[CALLBACK_HEADER] = opts[:callback].serialize end - (opts[:metas] || {}) - .each { |k, v| headers["x-oss-meta-#{k.to_s}"] = v.to_s } - r = @http.put( {:bucket => bucket_name, :object => object_name}, {:headers => headers, :body => HTTP::StreamPayload.new(&block)}) @@ -553,6 +558,9 @@ def put_object(bucket_name, object_name, opts = {}, &block) # @option opts [Hash] :metas key-value pairs # that serve as the object meta which will be stored together # with the object + # @option opts [Hash] :headers custom HTTP headers, case + # insensitive. Headers specified here will overwrite `:metas` + # and `:content_type` # @return [Integer] next position to append # @yield [HTTP::StreamWriter] a stream writer is # yielded to the caller to which it can write chunks of data @@ -566,10 +574,12 @@ def append_object(bucket_name, object_name, position, opts = {}, &block) "#{object_name}, position: #{position}, options: #{opts}") sub_res = {'append' => nil, 'position' => position} - headers = {'Content-Type' => opts[:content_type]} - (opts[:metas] || {}) + headers = {'content-type' => opts[:content_type]} + to_lower_case(opts[:metas] || {}) .each { |k, v| headers["x-oss-meta-#{k.to_s}"] = v.to_s } + headers.merge!(to_lower_case(opts[:headers])) if opts.key?(:headers) + r = @http.post( {:bucket => bucket_name, :object => object_name, :sub_res => sub_res}, {:headers => headers, :body => HTTP::StreamPayload.new(&block)}) @@ -721,7 +731,7 @@ def get_object(bucket_name, object_name, opts = {}, &block) rewrites = opts[:rewrite] headers = {} - headers['Range'] = get_bytes_range(range) if range + headers['range'] = get_bytes_range(range) if range headers.merge!(get_conditions(conditions)) if conditions sub_res = {} @@ -758,7 +768,7 @@ def get_object(bucket_name, object_name, opts = {}, &block) :etag => h[:etag], :metas => metas, :last_modified => wrap(h[:last_modified]) { |x| Time.parse(x) }, - :content_type => h[:content_type]) + :headers => h) logger.debug("Done get object") @@ -801,7 +811,7 @@ def get_object_meta(bucket_name, object_name, opts = {}) :etag => h[:etag], :metas => metas, :last_modified => wrap(h[:last_modified]) { |x| Time.parse(x) }, - :content_type => h[:content_type]) + :headers => h) logger.debug("Done get object meta") @@ -840,7 +850,7 @@ def copy_object(bucket_name, src_object_name, dst_object_name, opts = {}) headers = { 'x-oss-copy-source' => @http.get_resource_path(bucket_name, src_object_name), - 'Content-Type' => opts[:content_type] + 'content-type' => opts[:content_type] } (opts[:metas] || {}) .each { |k, v| headers["x-oss-meta-#{k.to_s}"] = v.to_s } @@ -982,9 +992,9 @@ def get_object_cors(bucket_name, object_name, origin, method, headers = []) "headers: #{headers.join(',')}") h = { - 'Origin' => origin, - 'Access-Control-Request-Method' => method, - 'Access-Control-Request-Headers' => headers.join(',') + 'origin' => origin, + 'access-control-request-method' => method, + 'access-control-request-headers' => headers.join(',') } r = @http.options( @@ -1017,16 +1027,21 @@ def get_object_cors(bucket_name, object_name, origin, method, headers = []) # @option opts [Hash] :metas key-value pairs # that serve as the object meta which will be stored together # with the object + # @option opts [Hash] :headers custom HTTP headers, case + # insensitive. Headers specified here will overwrite `:metas` + # and `:content_type` # @return [String] the upload id def initiate_multipart_upload(bucket_name, object_name, opts = {}) logger.info("Begin initiate multipart upload, bucket: "\ "#{bucket_name}, object: #{object_name}, options: #{opts}") sub_res = {'uploads' => nil} - headers = {'Content-Type' => opts[:content_type]} - (opts[:metas] || {}) + headers = {'content-type' => opts[:content_type]} + to_lower_case(opts[:metas] || {}) .each { |k, v| headers["x-oss-meta-#{k.to_s}"] = v.to_s } + headers.merge!(to_lower_case(opts[:headers])) if opts.key?(:headers) + r = @http.post( {:bucket => bucket_name, :object => object_name, :sub_res => sub_res}, @@ -1092,7 +1107,7 @@ def upload_part_by_copy( 'x-oss-copy-source' => @http.get_resource_path(bucket_name, source_object) } - headers['Range'] = get_bytes_range(range) if range + headers['range'] = get_bytes_range(range) if range headers.merge!(get_copy_conditions(conditions)) if conditions sub_res = {'partNumber' => part_no, 'uploadId' => txn_id} @@ -1392,14 +1407,14 @@ def wrap(x, &block) # @return [Hash] conditions for HTTP headers def get_conditions(conditions) { - :if_modified_since => 'If-Modified-Since', - :if_unmodified_since => 'If-Unmodified-Since', + :if_modified_since => 'if-modified-since', + :if_unmodified_since => 'if-unmodified-since', }.reduce({}) { |h, (k, v)| conditions.key?(k)? h.merge(v => conditions[k].httpdate) : h }.merge( { - :if_match_etag => 'If-Match', - :if_unmatch_etag => 'If-None-Match' + :if_match_etag => 'if-match', + :if_unmatch_etag => 'if-none-match' }.reduce({}) { |h, (k, v)| conditions.key?(k)? h.merge(v => conditions[k]) : h } @@ -1445,6 +1460,16 @@ def update_if_exists(hash, kv) kv.each { |k, v| hash[k] = v.call(hash[k]) if hash.key?(k) } end + # Convert hash keys to lower case Non-Recursively + # @param hash [Hash] the hash to be converted + # @return [Hash] hash with lower case keys + def to_lower_case(hash) + hash.reduce({}) do |result, (k, v)| + result[k.to_s.downcase] = v + result + end + end + end # Protocol end # OSS end # Aliyun diff --git a/lib/aliyun/oss/util.rb b/lib/aliyun/oss/util.rb index c42c57f..b00fb42 100644 --- a/lib/aliyun/oss/util.rb +++ b/lib/aliyun/oss/util.rb @@ -24,9 +24,9 @@ class << self def get_signature(key, verb, headers, resources) logger.debug("Sign, headers: #{headers}, resources: #{resources}") - content_md5 = headers['Content-MD5'] || "" - content_type = headers['Content-Type'] || "" - date = headers['Date'] + content_md5 = headers['content-md5'] || "" + content_type = headers['content-type'] || "" + date = headers['date'] cano_headers = headers.select { |k, v| k.start_with?(HEADER_PREFIX) } .map { |k, v| [k.downcase.strip, v.strip] } diff --git a/spec/aliyun/oss/client/bucket_spec.rb b/spec/aliyun/oss/client/bucket_spec.rb index 12710b9..95e3f16 100644 --- a/spec/aliyun/oss/client/bucket_spec.rb +++ b/spec/aliyun/oss/client/bucket_spec.rb @@ -276,6 +276,37 @@ def err(msg, reqid = '0000') .with { |req| req.headers.key?('X-Oss-Callback') } end + it "should set custom headers when put object" do + key = 'ruby' + stub_request(:put, object_url(key)) + + @bucket.put_object( + key, headers: {'cache-control' => 'xxx', 'expires' => 'yyy'}) + + headers = {} + expect(WebMock).to have_requested(:put, object_url(key)) + .with { |req| headers = req.headers } + expect(headers['Cache-Control']).to eq('xxx') + expect(headers['Expires']).to eq('yyy') + end + + it "should set custom headers when append object" do + key = 'ruby' + query = {'append' => '', 'position' => 11} + stub_request(:post, object_url(key)).with(:query => query) + + @bucket.append_object( + key, 11, + headers: {'CACHE-CONTROL' => 'nocache', 'EXPIRES' => 'seripxe'}) + + headers = {} + expect(WebMock).to have_requested(:post, object_url(key)) + .with(:query => query) + .with { |req| headers = req.headers } + expect(headers['Cache-Control']).to eq('nocache') + expect(headers['Expires']).to eq('seripxe') + end + it "should get object to file" do key = 'ruby' # 100 KB diff --git a/spec/aliyun/oss/client/resumable_upload_spec.rb b/spec/aliyun/oss/client/resumable_upload_spec.rb index 26f93a2..8216293 100644 --- a/spec/aliyun/oss/client/resumable_upload_spec.rb +++ b/spec/aliyun/oss/client/resumable_upload_spec.rb @@ -115,27 +115,15 @@ def err(msg, reqid = '0000') body: 'hello world', host: 'server.com' ) - prg = [] @bucket.resumable_upload( - @object_key, @file, - :part_size => 10, :callback => callback) { |p| prg << p } + @object_key, @file, part_size: 10, callback: callback) expect(WebMock).to have_requested( :post, /#{object_url}\?uploads.*/).times(1) - part_numbers = Set.new([]) - upload_ids = Set.new([]) - expect(WebMock).to have_requested( - :put, /#{object_url}\?partNumber.*/).with{ |req| - query = parse_query_from_uri(req.uri) - part_numbers << query['partNumber'] - upload_ids << query['uploadId'] - }.times(10) - - expect(part_numbers.to_a).to match_array((1..10).map{ |x| x.to_s }) - expect(upload_ids.to_a).to match_array(['upload_id']) - + :put, /#{object_url}\?partNumber.*/) + .times(10) expect(WebMock) .to have_requested( :post, /#{object_url}\?uploadId.*/) @@ -143,7 +131,6 @@ def err(msg, reqid = '0000') .times(1) expect(File.exist?("#{@file}.cpt")).to be false - expect(prg.size).to eq(10) end it "should raise CallbackError when callback failed" do @@ -162,28 +149,17 @@ def err(msg, reqid = '0000') body: 'hello world', host: 'server.com' ) - prg = [] expect { @bucket.resumable_upload( - @object_key, @file, - :part_size => 10, :callback => callback) { |p| prg << p } + @object_key, @file, part_size: 10, callback: callback) }.to raise_error(CallbackError, err(message)) expect(WebMock).to have_requested( :post, /#{object_url}\?uploads.*/).times(1) - part_numbers = Set.new([]) - upload_ids = Set.new([]) - expect(WebMock).to have_requested( - :put, /#{object_url}\?partNumber.*/).with{ |req| - query = parse_query_from_uri(req.uri) - part_numbers << query['partNumber'] - upload_ids << query['uploadId'] - }.times(10) - - expect(part_numbers.to_a).to match_array((1..10).map{ |x| x.to_s }) - expect(upload_ids.to_a).to match_array(['upload_id']) + :put, /#{object_url}\?partNumber.*/) + .times(10) expect(WebMock) .to have_requested( @@ -192,7 +168,27 @@ def err(msg, reqid = '0000') .times(1) expect(File.exist?("#{@file}.cpt")).to be true - expect(prg.size).to eq(10) + end + + it "should upload file with custom headers" do + stub_request(:post, /#{object_url}\?uploads.*/) + .to_return(:body => mock_txn_id('upload_id')) + stub_request(:put, /#{object_url}\?partNumber.*/) + stub_request(:post, /#{object_url}\?uploadId.*/) + + @bucket.resumable_upload( + @object_key, @file, + part_size: 10, + headers: {'cache-CONTROL' => 'cacheit', 'CONTENT-disposition' => 'oh;yeah'}) + + headers = {} + expect(WebMock).to have_requested( + :post, /#{object_url}\?uploads.*/) + .with { |req| headers = req.headers }.times(1) + + expect(headers['Cache-Control']).to eq('cacheit') + expect(headers['Content-Disposition']).to eq('oh;yeah') + expect(File.exist?("#{@file}.cpt")).to be false end it "should restart when begin txn fails" do diff --git a/spec/aliyun/oss/util_spec.rb b/spec/aliyun/oss/util_spec.rb index de47ec4..682e795 100644 --- a/spec/aliyun/oss/util_spec.rb +++ b/spec/aliyun/oss/util_spec.rb @@ -23,22 +23,22 @@ module OSS key = 'helloworld' date = 'Fri, 30 Oct 2015 07:21:00 GMT' - signature = Util.get_signature(key, 'GET', {'Date' => date}, {}) + signature = Util.get_signature(key, 'GET', {'date' => date}, {}) expect(signature).to eq("u8QKAAj/axKX4JhHXa5DYfYSPxE=") signature = Util.get_signature( - key, 'PUT', {'Date' => date}, {:path => '/bucket'}) + key, 'PUT', {'date' => date}, {:path => '/bucket'}) expect(signature).to eq("lMKrMCJIuGygd8UsdMA+S0QOAsQ=") signature = Util.get_signature( key, 'PUT', - {'Date' => date, 'x-oss-copy-source' => '/bucket/object-old'}, + {'date' => date, 'x-oss-copy-source' => '/bucket/object-old'}, {:path => '/bucket/object-new'}) expect(signature).to eq("McYUmBaErN//yvE9voWRhCgvsIc=") signature = Util.get_signature( key, 'PUT', - {'Date' => date}, + {'date' => date}, {:path => '/bucket/object-new', :sub_res => {'append' => nil, 'position' => 0}}) expect(signature).to eq("7Oh2wobzeg6dw/cWYbF/2m6s6qc=") diff --git a/tests/test_content_type.rb b/tests/test_content_type.rb index be2720b..63c95c5 100644 --- a/tests/test_content_type.rb +++ b/tests/test_content_type.rb @@ -43,15 +43,15 @@ def test_type_from_key @types.each do |k, v| key = get_key('from_key', k) @bucket.put_object(key) - assert_equal v, @bucket.get_object(key).content_type + assert_equal v, @bucket.get_object(key).headers[:content_type] copy_key = get_key('copy.from_key', k) @bucket.copy_object(key, copy_key) - assert_equal v, @bucket.get_object(copy_key).content_type + assert_equal v, @bucket.get_object(copy_key).headers[:content_type] append_key = get_key('append.from_key', k) @bucket.append_object(append_key, 0) - assert_equal v, @bucket.get_object(append_key).content_type + assert_equal v, @bucket.get_object(append_key).headers[:content_type] end end @@ -63,15 +63,15 @@ def test_type_from_file key = get_key('from_file', k) @bucket.put_object(key, :file => upload_file) - assert_equal v, @bucket.get_object(key).content_type + assert_equal v, @bucket.get_object(key).headers[:content_type] append_key = get_key('append.from_file', k) @bucket.append_object(append_key, 0, :file => upload_file) - assert_equal v, @bucket.get_object(append_key).content_type + assert_equal v, @bucket.get_object(append_key).headers[:content_type] multipart_key = get_key('multipart.from_file', k) @bucket.resumable_upload(multipart_key, upload_file) - assert_equal v, @bucket.get_object(multipart_key).content_type + assert_equal v, @bucket.get_object(multipart_key).headers[:content_type] end end @@ -82,19 +82,19 @@ def test_type_from_user key = get_key('from_user', k) @bucket.put_object(key, :file => upload_file, :content_type => v) - assert_equal v, @bucket.get_object(key).content_type + assert_equal v, @bucket.get_object(key).headers[:content_type] copy_key = get_key('copy.from_user', k) @bucket.copy_object(key, copy_key, :content_type => v) - assert_equal v, @bucket.get_object(copy_key).content_type + assert_equal v, @bucket.get_object(copy_key).headers[:content_type] append_key = get_key('append.from_user', k) @bucket.append_object(append_key, 0, :file => upload_file, :content_type => v) - assert_equal v, @bucket.get_object(append_key).content_type + assert_equal v, @bucket.get_object(append_key).headers[:content_type] multipart_key = get_key('multipart.from_file', k) @bucket.resumable_upload(multipart_key, upload_file, :content_type => v) - assert_equal v, @bucket.get_object(multipart_key).content_type + assert_equal v, @bucket.get_object(multipart_key).headers[:content_type] end end end diff --git a/tests/test_custom_headers.rb b/tests/test_custom_headers.rb new file mode 100644 index 0000000..4785330 --- /dev/null +++ b/tests/test_custom_headers.rb @@ -0,0 +1,75 @@ +require 'minitest/autorun' +require 'yaml' +$LOAD_PATH.unshift(File.expand_path("../../lib", __FILE__)) +require 'aliyun/oss' +require 'zlib' + +class TestCustomHeaders < Minitest::Test + def setup + Aliyun::Common::Logging.set_log_level(Logger::DEBUG) + conf_file = '~/.oss.yml' + conf = YAML.load(File.read(File.expand_path(conf_file))) + client = Aliyun::OSS::Client.new( + :endpoint => conf['endpoint'], + :cname => conf['cname'], + :access_key_id => conf['access_key_id'], + :access_key_secret => conf['access_key_secret']) + @bucket = client.get_bucket(conf['bucket']) + + @prefix = "tests/custom_headers/" + end + + def get_key(k) + "#{@prefix}#{k}" + end + + def test_custom_headers + key = get_key('ruby') + cache_control = 'max-age: 3600' + @bucket.put_object(key, headers: {'cache-control' => cache_control}) + obj = @bucket.get_object(key) + assert_equal cache_control, obj.headers[:cache_control] + + content_disposition = 'attachment; filename="fname.ext"' + @bucket.put_object( + key, + headers: {'cache-control' => cache_control, + 'CONTENT-DISPOSITION' => content_disposition}) + obj = @bucket.get_object(key) + assert_equal cache_control, obj.headers[:cache_control] + assert_equal content_disposition, obj.headers[:content_disposition] + + content_encoding = 'deflate' + expires = (Time.now + 3600).httpdate + @bucket.put_object( + key, + headers: {'cache-control' => cache_control, + 'CONTENT-DISPOSITION' => content_disposition, + 'content-ENCODING' => content_encoding, + 'EXPIRES' => expires }) do |s| + s << Zlib::Deflate.deflate('hello world') + end + + content = '' + obj = @bucket.get_object(key) { |c| content << c } + assert_equal 'hello world', content + assert_equal cache_control, obj.headers[:cache_control] + assert_equal content_disposition, obj.headers[:content_disposition] + assert_equal content_encoding, obj.headers[:content_encoding] + assert_equal expires, obj.headers[:expires] + end + + def test_headers_overwrite + key = get_key('rails') + @bucket.put_object( + key, + content_type: 'text/html', + metas: {'hello' => 'world'}, + headers: {'content-type' => 'application/json', + 'x-oss-meta-hello' => 'bar'}) { |s| s << 'hello world' } + obj = @bucket.get_object(key) + + assert_equal 'application/json', obj.headers[:content_type] + assert_equal ({'hello' => 'bar'}), obj.metas + end +end From 2282de232a8aa98259163a971ae55421ae2710b0 Mon Sep 17 00:00:00 2001 From: Tianlong Wu Date: Wed, 23 Dec 2015 13:02:14 +0800 Subject: [PATCH 2/3] add object acl in put/append --- lib/aliyun/oss/protocol.rb | 6 +++ spec/aliyun/oss/client/bucket_spec.rb | 24 ++++++++++++ tests/test_object_acl.rb | 54 +++++++++++++++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 tests/test_object_acl.rb diff --git a/lib/aliyun/oss/protocol.rb b/lib/aliyun/oss/protocol.rb index 7bd556c..af2f3e3 100644 --- a/lib/aliyun/oss/protocol.rb +++ b/lib/aliyun/oss/protocol.rb @@ -499,6 +499,8 @@ def delete_bucket(name) # @param bucket_name [String] the bucket name # @param object_name [String] the object name # @param opts [Hash] Options + # @option opts [String] :acl specify the object's ACL. See + # {OSS::ACL} # @option opts [String] :content_type the HTTP Content-Type # for the file, if not specified client will try to determine # the type itself and fall back to HTTP::DEFAULT_CONTENT_TYPE @@ -522,6 +524,7 @@ def put_object(bucket_name, object_name, opts = {}, &block) "#{object_name}, options: #{opts}") headers = {'content-type' => opts[:content_type]} + headers['x-oss-object-acl'] = opts[:acl] if opts.key?(:acl) to_lower_case(opts[:metas] || {}) .each { |k, v| headers["x-oss-meta-#{k.to_s}"] = v.to_s } @@ -551,6 +554,8 @@ def put_object(bucket_name, object_name, opts = {}, &block) # @param object_name [String] the object name # @param position [Integer] the position to append # @param opts [Hash] Options + # @option opts [String] :acl specify the object's ACL. See + # {OSS::ACL} # @option opts [String] :content_type the HTTP Content-Type # for the file, if not specified client will try to determine # the type itself and fall back to HTTP::DEFAULT_CONTENT_TYPE @@ -575,6 +580,7 @@ def append_object(bucket_name, object_name, position, opts = {}, &block) sub_res = {'append' => nil, 'position' => position} headers = {'content-type' => opts[:content_type]} + headers['x-oss-object-acl'] = opts[:acl] if opts.key?(:acl) to_lower_case(opts[:metas] || {}) .each { |k, v| headers["x-oss-meta-#{k.to_s}"] = v.to_s } diff --git a/spec/aliyun/oss/client/bucket_spec.rb b/spec/aliyun/oss/client/bucket_spec.rb index 95e3f16..b514e89 100644 --- a/spec/aliyun/oss/client/bucket_spec.rb +++ b/spec/aliyun/oss/client/bucket_spec.rb @@ -239,6 +239,17 @@ def err(msg, reqid = '0000') .with(:body => content, :query => {}) end + it "should put object with acl" do + key = 'ruby' + stub_request(:put, object_url(key)) + + @bucket.put_object(key, :acl => ACL::PUBLIC_READ) + + expect(WebMock) + .to have_requested(:put, object_url(key)) + .with(:headers => {'X-Oss-Object-Acl' => ACL::PUBLIC_READ}) + end + it "should put object with callback" do key = 'ruby' stub_request(:put, object_url(key)) @@ -365,6 +376,19 @@ def err(msg, reqid = '0000') :headers => {'Content-Type' => 'text/html'}) end + it "should append object with acl" do + key = 'ruby' + query = {'append' => '', 'position' => 11} + stub_request(:post, object_url(key)).with(:query => query) + + @bucket.append_object(key, 11, :acl => ACL::PUBLIC_READ_WRITE) + + expect(WebMock) + .to have_requested(:post, object_url(key)) + .with(:query => query, + :headers => {'X-Oss-Object-Acl' => ACL::PUBLIC_READ_WRITE}) + end + it "should answer object exists?" do key = 'ruby' diff --git a/tests/test_object_acl.rb b/tests/test_object_acl.rb new file mode 100644 index 0000000..2ba974f --- /dev/null +++ b/tests/test_object_acl.rb @@ -0,0 +1,54 @@ +require 'minitest/autorun' +require 'yaml' +$LOAD_PATH.unshift(File.expand_path("../../lib", __FILE__)) +require 'aliyun/oss' + +class TestObjectACL < Minitest::Test + def setup + Aliyun::Common::Logging.set_log_level(Logger::DEBUG) + conf_file = '~/.oss.yml' + conf = YAML.load(File.read(File.expand_path(conf_file))) + client = Aliyun::OSS::Client.new( + :endpoint => conf['endpoint'], + :cname => conf['cname'], + :access_key_id => conf['access_key_id'], + :access_key_secret => conf['access_key_secret']) + @bucket = client.get_bucket(conf['bucket']) + + @prefix = "tests/object_acl/" + end + + def get_key(k) + "#{@prefix}#{k}" + end + + def test_put_object + key = get_key('put') + + @bucket.put_object(key, acl: Aliyun::OSS::ACL::PRIVATE) + acl = @bucket.get_object_acl(key) + + assert_equal Aliyun::OSS::ACL::PRIVATE, acl + + @bucket.put_object(key, acl: Aliyun::OSS::ACL::PUBLIC_READ) + acl = @bucket.get_object_acl(key) + + assert_equal Aliyun::OSS::ACL::PUBLIC_READ, acl + end + + def test_append_object + key = get_key('append-1') + + @bucket.append_object(key, 0, acl: Aliyun::OSS::ACL::PRIVATE) + acl = @bucket.get_object_acl(key) + + assert_equal Aliyun::OSS::ACL::PRIVATE, acl + + key = get_key('append-2') + + @bucket.put_object(key, acl: Aliyun::OSS::ACL::PUBLIC_READ) + acl = @bucket.get_object_acl(key) + + assert_equal Aliyun::OSS::ACL::PUBLIC_READ, acl + end +end From 43760dce02c2230e9896e1d0d62f676ddf2d6c8e Mon Sep 17 00:00:00 2001 From: Tianlong Wu Date: Wed, 23 Dec 2015 13:04:09 +0800 Subject: [PATCH 3/3] Version => 0.3.2 --- CHANGELOG.md | 5 +++++ lib/aliyun/version.rb | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f064af..b938955 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ ## Change Log +### v0.3.2 + +- Allow setting custom HTTP headers in put/append/resumable_upload +- Allow setting object acl in put/append + ### v0.3.1 - Fix frozen string issue in OSSClient/STSClient config diff --git a/lib/aliyun/version.rb b/lib/aliyun/version.rb index 760fb16..a06f821 100644 --- a/lib/aliyun/version.rb +++ b/lib/aliyun/version.rb @@ -2,6 +2,6 @@ module Aliyun - VERSION = "0.3.1" + VERSION = "0.3.2" end # Aliyun