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