lib/condo/strata/amazon_s3.rb in condo-1.0.4 vs lib/condo/strata/amazon_s3.rb in condo-1.0.6

- old
+ new

@@ -1,301 +1,301 @@ -module Condo; end -module Condo::Strata; end - - -class Condo::Strata::AmazonS3 - - def initialize(options) - @options = { - :name => :AmazonS3, - :location => :'us-east-1', - :fog => { - :provider => :AWS, - :aws_access_key_id => options[:access_id], - :aws_secret_access_key => options[:secret_key], - :region => (options[:location] || 'us-east-1') - } - }.merge!(options) - - - raise ArgumentError, 'Amazon Access ID missing' if @options[:access_id].nil? - raise ArgumentError, 'Amazon Secret Key missing' if @options[:secret_key].nil? - - - @options[:location] = @options[:location].to_sym - @options[:region] = @options[:location] == :'us-east-1' ? 's3.amazonaws.com' : "s3-#{@options[:location]}.amazonaws.com" - end - - - def name - @options[:name] - end - - - def location - @options[:location] - end - - - - # - # Create a signed URL for accessing a private file - # - def get_object(options) - options = {}.merge!(options) # Need to deep copy here - options[:object_options] = { - :expires => 5.minutes.from_now, - :date => Time.now, - :verb => :get, # Post for multi-part uploads http://docs.amazonwebservices.com/AmazonS3/latest/API/mpUploadInitiate.html - :headers => {}, - :parameters => {}, - :protocol => :https - }.merge!(options[:object_options] || {}) - options.merge!(@options) - - # - # provide the signed request - # - sign_request(options)[:url] - end - - - # - # Creates a new upload request (either single shot or multi-part) - # => Passed: bucket_name, object_key, object_options, file_size - # - def new_upload(options) - options = {}.merge!(options) # Need to deep copy here - options[:object_options] = { - :permissions => :private, - :expires => 5.minutes.from_now, - :date => Time.now, - :verb => :post, # Post for multi-part uploads http://docs.amazonwebservices.com/AmazonS3/latest/API/mpUploadInitiate.html - :headers => {}, - :parameters => {}, - :protocol => :https - }.merge!(options[:object_options]) - options.merge!(@options) - - # - # Set the access control headers - # - if options[:object_options][:headers]['x-amz-acl'].nil? - options[:object_options][:headers]['x-amz-acl'] = case options[:object_options][:permissions] - when :public - :'public-read' - else - :private - end - end - - # - # Decide what type of request is being sent - # - request = {} - if options[:file_size] > 5.megabytes # 5 mb (minimum chunk size) - options[:object_options][:parameters][:uploads] = '' # Customise the request to be a chunked upload - options.delete(:file_id) # Does not apply to chunked uploads - - request[:type] = :chunked_upload - else - if options[:file_id].present? && options[:object_options][:headers]['Content-Md5'].nil? - # - # The client side is sending hex formatted ids that will match the amazon etag - # => We need this to be base64 for the md5 header (this is now done at the client side) - # - # options[:file_id] = [[options[:file_id]].pack("H*")].pack("m0") # (the 0 avoids the call to strip - now done client side) - # [ options[:file_id] ].pack('m').strip # This wasn't correct - # Base64.encode64(options[:file_id]).strip # This also wasn't correct - # - options[:object_options][:headers]['Content-Md5'] = options[:file_id] - end - options[:object_options][:headers]['Content-Type'] = 'binary/octet-stream' if options[:object_options][:headers]['Content-Type'].nil? - options[:object_options][:verb] = :put # Put for direct uploads http://docs.amazonwebservices.com/AmazonS3/latest/API/RESTObjectPUT.html - - request[:type] = :direct_upload - end - - - # - # provide the signed request - # - request[:signature] = sign_request(options) - request - end - - - # - # Returns the request to get the parts of a resumable upload - # - def get_parts(options) - options[:object_options] = { - :expires => 5.minutes.from_now, - :date => Time.now, - :verb => :get, - :headers => {}, - :parameters => {}, - :protocol => :https - }.merge!(options[:object_options]) - options.merge!(@options) - - # - # Set the upload - # - options[:object_options][:parameters]['uploadId'] = options[:resumable_id] - - # - # provide the signed request - # - { - :type => :parts, - :signature => sign_request(options) - } - end - - - # - # Returns the requests for uploading parts and completing a resumable upload - # - def set_part(options) - options[:object_options] = { - :expires => 5.minutes.from_now, - :date => Time.now, - :headers => {}, - :parameters => {}, - :protocol => :https - }.merge!(options[:object_options]) - options.merge!(@options) - - - request = {} - if options[:part] == 'finish' - # - # Send the commitment response - # - options[:object_options][:headers]['Content-Type'] = 'application/xml; charset=UTF-8' if options[:object_options][:headers]['Content-Type'].nil? - options[:object_options][:verb] = :post - request[:type] = :finish - else - # - # Send the part upload request - # - options[:object_options][:headers]['Content-Md5'] = options[:file_id] if options[:file_id].present? && options[:object_options][:headers]['Content-Md5'].nil? - options[:object_options][:headers]['Content-Type'] = 'binary/octet-stream' if options[:object_options][:headers]['Content-Type'].nil? - options[:object_options][:parameters]['partNumber'] = options[:part] - options[:object_options][:verb] = :put - request[:type] = :part_upload - end - - - # - # Set the upload - # - options[:object_options][:parameters]['uploadId'] = options[:resumable_id] - - - # - # provide the signed request - # - request[:signature] = sign_request(options) - request - end - - - def fog_connection - @fog = @fog || Fog::Storage.new(@options[:fog]) - return @fog - end - - - def destroy(upload) - connection = fog_connection - directory = connection.directories.get(upload.bucket_name) # it is assumed this exists - if not then the upload wouldn't have taken place - file = directory.files.get(upload.object_key) - - if upload.resumable - return file.destroy unless file.nil? - begin - if upload.resumable_id.present? - connection.abort_multipart_upload(upload.bucket_name, upload.object_key, upload.resumable_id) - return true - end - rescue - # In-case resumable_id was invalid or did not match the object key - end - - # - # The user may have provided an invalid upload key, we'll need to search for the upload and destroy it - # - begin - resp = connection.list_multipart_uploads(upload.bucket_name, {'prefix' => upload.object_key}) - resp.body['Upload'].each do |file| - # - # TODO:: BUGBUG:: there is an edge case where there may be more multi-part uploads with this this prefix then will be provided in a single request - # => We'll need to handle this edge case to avoid abuse and dangling objects - # - connection.abort_multipart_upload(upload.bucket_name, upload.object_key, file['UploadId']) if file['Key'] == upload.object_key # Ensure an exact match - end - return true # The upload was either never initialised or has been destroyed - rescue - return false - end - else - return true if file.nil? - return file.destroy - end - end - - - - protected - - - - def sign_request(options) - - # - # Build base URL - # - options[:object_options][:date] = options[:object_options][:date].utc.httpdate - options[:object_options][:expires] = options[:object_options][:expires].utc.to_i - url = "/#{options[:bucket_name]}/#{options[:object_key]}" - - # - # Add request params - # - url << '?' - options[:object_options][:parameters].each do |key, value| - url += value.empty? ? "#{key}&" : "#{key}=#{value}&" - end - url.chop! - - # - # Build a request signature - # - signature = "#{options[:object_options][:verb].to_s.upcase}\n#{options[:file_id]}\n#{options[:object_options][:headers]['Content-Type']}\n#{options[:object_options][:expires]}\n" - options[:object_options][:headers].each do |key, value| - signature << "#{key}:#{value}\n" if key =~ /x-amz-/ - end - signature << url - - - # - # Encode the request signature - # - signature = CGI::escape(Base64.encode64(OpenSSL::HMAC.digest(OpenSSL::Digest::Digest.new('sha1'), @options[:secret_key], signature)).gsub("\n","")) - - - # - # Finish building the request - # - url += options[:object_options][:parameters].present? ? '&' : '?' - return { - :verb => options[:object_options][:verb].to_s.upcase, - :url => "#{options[:object_options][:protocol]}://#{options[:region]}#{url}AWSAccessKeyId=#{@options[:access_id]}&Expires=#{options[:object_options][:expires]}&Signature=#{signature}", - :headers => options[:object_options][:headers] - } - end - - -end - +module Condo; end +module Condo::Strata; end + + +class Condo::Strata::AmazonS3 + + def initialize(options) + @options = { + :name => :AmazonS3, + :location => :'us-east-1', + :fog => { + :provider => :AWS, + :aws_access_key_id => options[:access_id], + :aws_secret_access_key => options[:secret_key], + :region => (options[:location] || 'us-east-1') + } + }.merge!(options) + + + raise ArgumentError, 'Amazon Access ID missing' if @options[:access_id].nil? + raise ArgumentError, 'Amazon Secret Key missing' if @options[:secret_key].nil? + + + @options[:location] = @options[:location].to_sym + @options[:region] = @options[:location] == :'us-east-1' ? 's3.amazonaws.com' : "s3-#{@options[:location]}.amazonaws.com" + end + + + def name + @options[:name] + end + + + def location + @options[:location] + end + + + + # + # Create a signed URL for accessing a private file + # + def get_object(options) + options = {}.merge!(options) # Need to deep copy here + options[:object_options] = { + :expires => 5.minutes.from_now, + :date => Time.now, + :verb => :get, # Post for multi-part uploads http://docs.amazonwebservices.com/AmazonS3/latest/API/mpUploadInitiate.html + :headers => {}, + :parameters => {}, + :protocol => :https + }.merge!(options[:object_options] || {}) + options.merge!(@options) + + # + # provide the signed request + # + sign_request(options)[:url] + end + + + # + # Creates a new upload request (either single shot or multi-part) + # => Passed: bucket_name, object_key, object_options, file_size + # + def new_upload(options) + options = {}.merge!(options) # Need to deep copy here + options[:object_options] = { + :permissions => :private, + :expires => 5.minutes.from_now, + :date => Time.now, + :verb => :post, # Post for multi-part uploads http://docs.amazonwebservices.com/AmazonS3/latest/API/mpUploadInitiate.html + :headers => {}, + :parameters => {}, + :protocol => :https + }.merge!(options[:object_options]) + options.merge!(@options) + + # + # Set the access control headers + # + if options[:object_options][:headers]['x-amz-acl'].nil? + options[:object_options][:headers]['x-amz-acl'] = case options[:object_options][:permissions] + when :public + :'public-read' + else + :private + end + end + + # + # Decide what type of request is being sent + # + request = {} + if options[:file_size] > 5.megabytes # 5 mb (minimum chunk size) + options[:object_options][:parameters][:uploads] = '' # Customise the request to be a chunked upload + options.delete(:file_id) # Does not apply to chunked uploads + + request[:type] = :chunked_upload + else + if options[:file_id].present? && options[:object_options][:headers]['Content-Md5'].nil? + # + # The client side is sending hex formatted ids that will match the amazon etag + # => We need this to be base64 for the md5 header (this is now done at the client side) + # + # options[:file_id] = [[options[:file_id]].pack("H*")].pack("m0") # (the 0 avoids the call to strip - now done client side) + # [ options[:file_id] ].pack('m').strip # This wasn't correct + # Base64.encode64(options[:file_id]).strip # This also wasn't correct + # + options[:object_options][:headers]['Content-Md5'] = options[:file_id] + end + options[:object_options][:headers]['Content-Type'] = 'binary/octet-stream' if options[:object_options][:headers]['Content-Type'].nil? + options[:object_options][:verb] = :put # Put for direct uploads http://docs.amazonwebservices.com/AmazonS3/latest/API/RESTObjectPUT.html + + request[:type] = :direct_upload + end + + + # + # provide the signed request + # + request[:signature] = sign_request(options) + request + end + + + # + # Returns the request to get the parts of a resumable upload + # + def get_parts(options) + options[:object_options] = { + :expires => 5.minutes.from_now, + :date => Time.now, + :verb => :get, + :headers => {}, + :parameters => {}, + :protocol => :https + }.merge!(options[:object_options]) + options.merge!(@options) + + # + # Set the upload + # + options[:object_options][:parameters]['uploadId'] = options[:resumable_id] + + # + # provide the signed request + # + { + :type => :parts, + :signature => sign_request(options) + } + end + + + # + # Returns the requests for uploading parts and completing a resumable upload + # + def set_part(options) + options[:object_options] = { + :expires => 5.minutes.from_now, + :date => Time.now, + :headers => {}, + :parameters => {}, + :protocol => :https + }.merge!(options[:object_options]) + options.merge!(@options) + + + request = {} + if options[:part] == 'finish' + # + # Send the commitment response + # + options[:object_options][:headers]['Content-Type'] = 'application/xml; charset=UTF-8' if options[:object_options][:headers]['Content-Type'].nil? + options[:object_options][:verb] = :post + request[:type] = :finish + else + # + # Send the part upload request + # + options[:object_options][:headers]['Content-Md5'] = options[:file_id] if options[:file_id].present? && options[:object_options][:headers]['Content-Md5'].nil? + options[:object_options][:headers]['Content-Type'] = 'binary/octet-stream' if options[:object_options][:headers]['Content-Type'].nil? + options[:object_options][:parameters]['partNumber'] = options[:part] + options[:object_options][:verb] = :put + request[:type] = :part_upload + end + + + # + # Set the upload + # + options[:object_options][:parameters]['uploadId'] = options[:resumable_id] + + + # + # provide the signed request + # + request[:signature] = sign_request(options) + request + end + + + def fog_connection + @fog = @fog || Fog::Storage.new(@options[:fog]) + return @fog + end + + + def destroy(upload) + connection = fog_connection + directory = connection.directories.get(upload.bucket_name) # it is assumed this exists - if not then the upload wouldn't have taken place + file = directory.files.get(upload.object_key) + + if upload.resumable + return file.destroy unless file.nil? + begin + if upload.resumable_id.present? + connection.abort_multipart_upload(upload.bucket_name, upload.object_key, upload.resumable_id) + return true + end + rescue + # In-case resumable_id was invalid or did not match the object key + end + + # + # The user may have provided an invalid upload key, we'll need to search for the upload and destroy it + # + begin + resp = connection.list_multipart_uploads(upload.bucket_name, {'prefix' => upload.object_key}) + resp.body['Upload'].each do |file| + # + # TODO:: BUGBUG:: there is an edge case where there may be more multi-part uploads with this this prefix then will be provided in a single request + # => We'll need to handle this edge case to avoid abuse and dangling objects + # + connection.abort_multipart_upload(upload.bucket_name, upload.object_key, file['UploadId']) if file['Key'] == upload.object_key # Ensure an exact match + end + return true # The upload was either never initialised or has been destroyed + rescue + return false + end + else + return true if file.nil? + return file.destroy + end + end + + + + protected + + + + def sign_request(options) + + # + # Build base URL + # + options[:object_options][:date] = options[:object_options][:date].utc.httpdate + options[:object_options][:expires] = options[:object_options][:expires].utc.to_i + url = "/#{options[:bucket_name]}/#{options[:object_key]}" + + # + # Add request params + # + url << '?' + options[:object_options][:parameters].each do |key, value| + url += value.empty? ? "#{key}&" : "#{key}=#{value}&" + end + url.chop! + + # + # Build a request signature + # + signature = "#{options[:object_options][:verb].to_s.upcase}\n#{options[:file_id]}\n#{options[:object_options][:headers]['Content-Type']}\n#{options[:object_options][:expires]}\n" + options[:object_options][:headers].each do |key, value| + signature << "#{key}:#{value}\n" if key =~ /x-amz-/ + end + signature << url + + + # + # Encode the request signature + # + signature = CGI::escape(Base64.encode64(OpenSSL::HMAC.digest(OpenSSL::Digest::Digest.new('sha1'), @options[:secret_key], signature)).gsub("\n","")) + + + # + # Finish building the request + # + url += options[:object_options][:parameters].present? ? '&' : '?' + return { + :verb => options[:object_options][:verb].to_s.upcase, + :url => "#{options[:object_options][:protocol]}://#{options[:region]}#{url}AWSAccessKeyId=#{@options[:access_id]}&Expires=#{options[:object_options][:expires]}&Signature=#{signature}", + :headers => options[:object_options][:headers] + } + end + + +end +