Upload a file to a key into a bucket on S3.

        :type fp: file
        :param fp: The file pointer to upload

        :type headers: dict
        :param headers: The headers to pass along with the PUT request

        :type cb: function
        :param cb: a callback function that will be called to report
                   progress on the upload.  The callback should accept
                   two integer parameters, the first representing the
                   number of bytes that have been successfully
                   transmitted to S3 and the second representing the
                   size of the to be transmitted object.

        :type num_cb: int
        :param num_cb: (optional) If a callback is specified with the cb
                       parameter this parameter determines the granularity
                       of the callback by defining the maximum number of
                       times the callback will be called during the file
                       transfer. Providing a negative integer will cause
                       your callback to be called with each buffer read.

def send_file(self, fp, headers=None, cb=None, num_cb=10,
                  query_args=None, chunked_transfer=False, callback=None):
        """
        Upload a file to a key into a bucket on S3.

        :type fp: file
        :param fp: The file pointer to upload

        :type headers: dict
        :param headers: The headers to pass along with the PUT request

        :type cb: function
        :param cb: a callback function that will be called to report
                   progress on the upload.  The callback should accept
                   two integer parameters, the first representing the
                   number of bytes that have been successfully
                   transmitted to S3 and the second representing the
                   size of the to be transmitted object.

        :type num_cb: int
        :param num_cb: (optional) If a callback is specified with the cb
                       parameter this parameter determines the granularity
                       of the callback by defining the maximum number of
                       times the callback will be called during the file
                       transfer. Providing a negative integer will cause
                       your callback to be called with each buffer read.

        """
        provider = self.bucket.connection.provider

        def sender(http_conn, method, path, data, headers, sendback=None):
            http_conn.putrequest(method, path)
            for key in headers:
                http_conn.putheader(key, headers[key])
            http_conn.endheaders()
            if chunked_transfer:
                # MD5 for the stream has to be calculated on the fly, as
                # we don't know the size of the stream before hand.
                m = md5()
            else:
                fp.seek(0)

            save_debug = self.bucket.connection.debug
            self.bucket.connection.debug = 0
            # If the debuglevel < 3 we don't want to show connection
            # payload, so turn off HTTP connection-level debug output (to
            # be restored below).
            # Use the getattr approach to allow this to work in AppEngine.
            if getattr(http_conn, 'debuglevel', 0) < 3:
                http_conn.set_debuglevel(0)
            if cb:
                if chunked_transfer:
                    # For chunked Transfer, we call the cb for every 1MB
                    # of data transferred.
                    cb_count = (1024 * 1024)/self.BufferSize
                    self.size = 0
                elif num_cb > 2:
                    cb_count = self.size / self.BufferSize / (num_cb-2)
                elif num_cb < 0:
                    cb_count = -1
                else:
                    cb_count = 0
                i = total_bytes = 0
                cb(total_bytes, self.size)
            l = fp.read(self.BufferSize)
            while len(l) > 0:
                if chunked_transfer:
                    http_conn.send('%x;\r\n' % len(l))
                    http_conn.send(l)
                    http_conn.send('\r\n')
                else:
                    http_conn.send(l)
                if cb:
                    total_bytes += len(l)
                    i += 1
                    if i == cb_count or cb_count == -1:
                        cb(total_bytes, self.size)
                        i = 0
                if chunked_transfer:
                    m.update(l)
                l = fp.read(self.BufferSize)
            if chunked_transfer:
                http_conn.send('0\r\n')
                http_conn.send('\r\n')
                if cb:
                    self.size = total_bytes
                # Get the md5 which is calculated on the fly.
                self.md5 = m.hexdigest()
            else:
                fp.seek(0)
            if cb:
                cb(total_bytes, self.size)
            def sender_sent(response):
                body = response.read()
                http_conn.set_debuglevel(save_debug)
                self.bucket.connection.debug = save_debug
                if ((response.status == 500 or response.status == 503 or
                        response.getheader('location')) and not chunked_transfer):
                    # we'll try again.
                    if callable(sendback):
                        sendback(response)
                elif response.status >= 200 and response.status <= 299:
                    self.etag = response.getheader('etag')
                    if self.etag != '"%s"'  % self.md5:
                        raise provider.storage_data_error(
                            'ETag from S3 did not match computed MD5')
                    if callable(sendback):
                        sendback(response)
                else:
                    raise provider.storage_response_error(
                        response.status, response.reason, body)
            http_conn.getresponse(callback=sender_sent)

        if not headers:
            headers = {}
        else:
            headers = headers.copy()
        headers['User-Agent'] = UserAgent
        if self.base64md5:
            headers['Content-MD5'] = self.base64md5
        if self.storage_class != 'STANDARD':
            headers[provider.storage_class_header] = self.storage_class
        if headers.has_key('Content-Encoding'):
            self.content_encoding = headers['Content-Encoding']
        if headers.has_key('Content-Type'):
            self.content_type = headers['Content-Type']
        elif self.path:
            self.content_type = mimetypes.guess_type(self.path)[0]
            if self.content_type == None:
                self.content_type = self.DefaultContentType
            headers['Content-Type'] = self.content_type
        else:
            headers['Content-Type'] = self.content_type
        if not chunked_transfer:
            headers['Content-Length'] = str(self.size)
#       headers['Expect'] = '100-Continue'
        headers = boto.utils.merge_meta(headers, self.metadata, provider)
        def file_sent(resp):
            self.handle_version_headers(resp, force=True)
            if callable(callback):
                callback(resp)
        self.bucket.connection.make_request('PUT', self.bucket.name,
                                            self.name, headers,
                                            sender=sender,
                                            query_args=query_args, callback=file_sent)