lib/s33r/client.rb in s33r-0.1 vs lib/s33r/client.rb in s33r-0.2

- old
+ new

@@ -1,37 +1,56 @@ require 'net/https' require 'cgi' -# this is a very thin layer over the S3 API -# TODO: need to wrap XML returned into object representation module S3 include Net + # the client actually performs operations over the network, + # using the core to build request headers and content; + # only client-specific headers are managed here: other headers + # can be handled by the core + # TODO: need to wrap XML returned into object representation + # TODO: use customisable thread pool for requests class Client include S3 + attr_accessor :chunk_size, :default_headers - def initialize(aws_access_key, aws_secret_access_key) - @client = HTTP.new(HOST, PORT) + # options: hash of optional client config. + # :ssl => false : only use plain HTTP + # :dump_requests => true: dump each request's initial line and headers + def initialize(aws_access_key, aws_secret_access_key, options={}) + if false == options[:ssl] + @client = HTTP.new(HOST, NON_SSL_PORT) + @client.use_ssl = false + else + @client = HTTP.new(HOST, PORT) + # turn off SSL certificate verification + @client.verify_mode = OpenSSL::SSL::VERIFY_NONE + @client.use_ssl = true + end + + @dump_requests = (true == options[:dump_requests]) - # turn off SSL certificate verification - @client.verify_mode = OpenSSL::SSL::VERIFY_NONE - - # always use SSL - @client.use_ssl = true - # set default chunk size for streaming request body (1 Mb) @chunk_size = 1048576 # Amazon S3 developer keys @aws_access_key = aws_access_key @aws_secret_access_key = aws_secret_access_key # headers sent with every request made by this client @client_headers = {} + + yield self if block_given? end + # wrapper round embedded client use_ssl accessor + def use_ssl? + @client.use_ssl + end + # send a request over the wire def do_request(method, path, data=nil, headers={}) req = get_requester(method, path) req.chunk_size = @chunk_size @@ -46,36 +65,40 @@ headers.each do |key, value| req[key] = value end - @client.start do - if req.request_body_permitted? - # for streaming large files - if data.respond_to?(:read) - req.body_stream = data - req['Content-Length'] = data.stat.size.to_s - return @client.request(req, nil) - # simple text strings etc. - else - return @client.request(req, data) - end - else - return @client.request(req) + if req.request_body_permitted? + # for streaming files; NB Content-Length will be set by Net::HTTP + # for character-based body content + if data.respond_to?(:stat) + req.body_stream = data + req['Content-Length'] = data.stat.size.to_s + data = nil end + else + data = nil end + + if @dump_requests + puts req.to_s + end + @client.start do + return @client.request(req, data) + end + end # get def do_get(path='/', headers={}) do_request('GET', path, headers) end # head def do_head(path='/', headers={}) - do_request('HEAD', path, headers) + do_request('HEAD', path, nil, headers) end # post def do_post(path='/', data=nil, headers={}) do_request('POST', path, data, headers) @@ -84,61 +107,86 @@ # put def do_put(path='/', data=nil, headers={}) do_request('PUT', path, data, headers) end + # delete + def do_delete(path, headers={}) + do_request('DELETE', path, nil, headers) + end + # return an instance of an appropriate request class def get_requester(method, path) raise S3Exception::UnsupportedHTTPMethod, "The #{method} HTTP method is not supported" if !(METHOD_VERBS.include?(method)) eval("HTTP::" + method[0,1].upcase + method[1..-1].downcase + ".new('#{path}')") end - # convert a hash of name/value pairs to querystring variables - def get_querystring(pairs={}) - str = '' - if pairs.size > 0 - str += "?" + pairs.map { |key, value| "#{key}=#{CGI::escape(value.to_s)}" }.join('&') - end - str - end - # list all buckets def list_all_buckets do_get('/') end + # list entries in a bucket + # query_params: hash of options on the bucket listing, passed as querystring parameters to S3 + # :prefix => 'some_string' : restrict results to keys beginning with 'some_string' + # :marker => 'some_string' : restict results to keys occurring lexicographically after 'some_string' + # :max_keys => 1000 : return at most this number of keys (maximum possible value is 1000) + # :delimiter => 'some_string' : + def list_bucket(bucket_name, query_params={}) + if query_params[:max_keys] + max_keys = query_params[:max_keys].to_i + raise S3Exception::BucketListingMaxKeysError, "max_keys option to list bucket cannot be > #{BUCKET_LIST_MAX_MAX_KEYS}" \ + if max_keys > BUCKET_LIST_MAX_MAX_KEYS + + # take out the max_keys parameter and move it to max-keys + query_params['max-keys'] = query_params.delete(:max_keys) + end + do_get("/#{bucket_name}" + generate_querystring(query_params)) + end + # create a bucket def create_bucket(bucket_name, headers={}) - bucket_name_valid?(bucket_name) - bucket_exists?(bucket_name) do_put("/#{bucket_name}", nil, headers) end - # list entries in a bucket - def list_bucket(bucket_name) - bucket_name_valid?(bucket_name) - bucket_exists?(bucket_name) - do_get("/#{bucket_name}") + # delete a bucket + # TODO: enable deletion of keys inside the bucket + # TODO: maybe delete keys matching a partial path + def delete_bucket(bucket_name, headers={}) + do_delete("/#{bucket_name}", headers) end - # put some resource onto S3 - def put_resource(data, bucket_name, resource_key, headers={}) - do_put(File.join("/#{bucket_name}", "#{CGI::escape(resource_key)}"), data, headers) + # return true if bucket exists + def bucket_exists?(bucket_name) + list_bucket(bucket_name).ok? end + # put some generic resource onto S3 + def put_resource(bucket_name, resource_key, data, headers={}) + do_put("/#{bucket_name}/" + "#{CGI::escape(resource_key)}", data, headers) + end + # put a string onto S3 def put_text(string, bucket_name, resource_key, headers={}) headers["Content-Type"] = "text/plain" put_resource(bucket_name, resource_key, string, headers) end # put a file onto S3 - def put_file(filename, bucket_name, resource_key=nil, headers={}) + # options: to simplify setting of some headers with specific meaning to S3 + # :render_as_attachment => true: set the Content-Disposition for this file to "attachment" and set + # the default filename for saving the file (when accessed by a web browser) to _filename_ + def put_file(filename, bucket_name, resource_key=nil, headers={}, options={}) # default to the file path as the resource key if none explicitly set if resource_key.nil? resource_key = filename end + + # set Content-Disposition header + if options[:render_as_attachment] + headers['Content-Disposition'] = "attachment; filename=#{File.basename(filename)}" + end # content type is explicitly set in the headers if headers[:content_type] # use the first MIME type corresponding to this content type string # (MIME::Types returns an array of possible MIME types) @@ -151,32 +199,15 @@ headers['Content-Transfer-Encoding'] = 'binary' if mime_type.binary? # the data we want to put (handle to file, so we can stream from it) File.open(filename) do |data| # send the put request - put_resource(data, bucket_name, resource_key, headers) + put_resource(bucket_name, resource_key, data, headers) end end - - # guess a file's mime type - # NB if the mime_type for a file cannot be guessed, "text/plain" is used - def guess_mime_type(filename) - mime_type = MIME::Types.type_for(filename)[0] - mime_type ||= MIME::Types['text/plain'][0] - mime_type - end - - # ensure that a bucket_name is well-formed - def bucket_name_valid?(bucket_name) - if '/' == bucket_name[0,1] - raise S3Exception::MalformedBucketName, "Bucket name cannot have a leading slash" - end - end - - # TODO: proper check for existence of bucket; - # throw error if bucket does not exist (see bucket_name_valid? for example) - def bucket_exists?(bucket_name) - false + + # TODO: delete resource by bucket and key + def delete_resource(bucket_name, resource_key) end # add any default headers which should be sent with every request from the client; # any headers passed into this method override the defaults in @client_headers def add_client_headers(headers) \ No newline at end of file