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 class Client include S3 attr_accessor :chunk_size, :default_headers def initialize(aws_access_key, aws_secret_access_key) @client = HTTP.new(HOST, PORT) # 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 = {} 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 # add the S3 headers which are always required headers = add_default_headers(headers) # add any client-specific default headers headers = add_client_headers(headers) headers['Authorization'] = generate_auth_header_value(method, path, headers, @aws_access_key, @aws_secret_access_key) 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) end 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) end # post def do_post(path='/', data=nil, headers={}) do_request('POST', path, data, headers) end # put def do_put(path='/', data=nil, headers={}) do_request('PUT', path, data, 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 # 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}") 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) 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={}) # default to the file path as the resource key if none explicitly set if resource_key.nil? resource_key = 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) mime_type = MIME::Types[headers[:content_type]][0] else mime_type = guess_mime_type(filename) end content_type = mime_type.simplified headers['Content-Type'] = content_type 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) 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 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) headers.merge!(@client_headers) { |key, arg, default| arg } end end end