class Chef::Knife::Core::CookbookSiteStreamingUploader

Chef::Knife::Core::CookbookSiteStreamingUploader

A streaming multipart HTTP upload implementation. Used to upload cookbooks (in tarball form) to supermarket.chef.io

inspired by stanislavvitvitskiy.blogspot.com/2008/12/multipart-post-in-ruby.html

Constants

DefaultHeaders

Public Class Methods

create_build_dir(cookbook) click to toggle source
# File lib/dpl/helper/cookbook_site_streaming_uploader.rb, line 47
def create_build_dir(cookbook)
  tmp_cookbook_path = Tempfile.new("#{ChefUtils::Dist::Infra::SHORT}-#{cookbook.name}-build")
  tmp_cookbook_path.close
  tmp_cookbook_dir = tmp_cookbook_path.path
  File.unlink(tmp_cookbook_dir)
  FileUtils.mkdir_p(tmp_cookbook_dir)
  Chef::Log.trace("Staging at #{tmp_cookbook_dir}")
  checksums_to_on_disk_paths = cookbook.checksums
  cookbook.each_file do |manifest_record|
    path_in_cookbook = manifest_record[:path]
    on_disk_path = checksums_to_on_disk_paths[manifest_record[:checksum]]
    dest = File.join(tmp_cookbook_dir, cookbook.name.to_s, path_in_cookbook)
    FileUtils.mkdir_p(File.dirname(dest))
    Chef::Log.trace("Staging #{on_disk_path} to #{dest}")
    FileUtils.cp(on_disk_path, dest)
  end

  # First, generate metadata
  Chef::Log.trace("Generating metadata")
  kcm = Chef::Knife::CookbookMetadata.new
  kcm.config[:cookbook_path] = [ tmp_cookbook_dir ]
  kcm.name_args = [ cookbook.name.to_s ]
  kcm.run

  tmp_cookbook_dir
end
make_request(http_verb, to_url, user_id, secret_key_filename, params = {}, headers = {}) click to toggle source
# File lib/dpl/helper/cookbook_site_streaming_uploader.rb, line 82
def make_request(http_verb, to_url, user_id, secret_key_filename, params = {}, headers = {})
  boundary = "----RubyMultipartClient" + rand(1000000).to_s + "ZZZZZ"
  parts = []
  content_file = nil

  secret_key = OpenSSL::PKey::RSA.new(File.read(secret_key_filename))

  unless params.nil? || params.empty?
    params.each do |key, value|
      if value.is_a?(File)
        content_file = value
        filepath = value.path
        filename = File.basename(filepath)
        parts << StringPart.new( "--" + boundary + "\r\n" +
                                "Content-Disposition: form-data; name=\"" + key.to_s + "\"; filename=\"" + filename + "\"\r\n" +
                                "Content-Type: application/octet-stream\r\n\r\n")
        parts << StreamPart.new(value, File.size(filepath))
        parts << StringPart.new("\r\n")
      else
        parts << StringPart.new( "--" + boundary + "\r\n" +
                                "Content-Disposition: form-data; name=\"" + key.to_s + "\"\r\n\r\n")
        parts << StringPart.new(value.to_s + "\r\n")
      end
    end
    parts << StringPart.new("--" + boundary + "--\r\n")
  end

  body_stream = MultipartStream.new(parts)

  timestamp = Time.now.utc.iso8601

  url = URI.parse(to_url)

  Chef::Log.logger.debug("Signing: method: #{http_verb}, url: #{url}, file: #{content_file}, User-id: #{user_id}, Timestamp: #{timestamp}")

  # We use the body for signing the request if the file parameter
  # wasn't a valid file or wasn't included. Extract the body (with
  # multi-part delimiters intact) to sign the request.
  # TODO: tim: 2009-12-28: It'd be nice to remove this special case, and
  # always hash the entire request body. In the file case it would just be
  # expanded multipart text - the entire body of the POST.
  content_body = parts.inject("") { |result, part| result + part.read(0, part.size) }
  content_file.rewind if content_file # we consumed the file for the above operation, so rewind it.

  signing_options = {
    http_method: http_verb,
    path: url.path,
    user_id: user_id,
    timestamp: timestamp }
  (content_file && signing_options[:file] = content_file) || (signing_options[:body] = (content_body || ""))

  headers.merge!(Mixlib::Authentication::SignedHeaderAuth.signing_object(signing_options).sign(secret_key))

  content_file.rewind if content_file

  # net/http doesn't like symbols for header keys, so we'll to_s each one just in case
  headers = DefaultHeaders.merge(Hash[*headers.map { |k, v| [k.to_s, v] }.flatten])

  req = case http_verb
        when :put
          Net::HTTP::Put.new(url.path, headers)
        when :post
          Net::HTTP::Post.new(url.path, headers)
        end
  req.content_length = body_stream.size
  req.content_type = "multipart/form-data; boundary=" + boundary unless parts.empty?
  req.body_stream = body_stream

  http = Chef::HTTP::BasicClient.new(url).http_client
  res = http.request(req)

  # alias status to code and to_s to body for test purposes
  # TODO: stop the following madness!
  class << res
    alias :to_s :body

    # BUG this makes the response compatible with what response_steps expects to test headers (response.headers[] -> response[])
    def headers # rubocop:disable Lint/NestedMethodDefinition
      self
    end

    def status  # rubocop:disable Lint/NestedMethodDefinition
      code.to_i
    end
  end
  res
end
post(to_url, user_id, secret_key_filename, params = {}, headers = {}) click to toggle source
# File lib/dpl/helper/cookbook_site_streaming_uploader.rb, line 74
def post(to_url, user_id, secret_key_filename, params = {}, headers = {})
  make_request(:post, to_url, user_id, secret_key_filename, params, headers)
end
put(to_url, user_id, secret_key_filename, params = {}, headers = {}) click to toggle source
# File lib/dpl/helper/cookbook_site_streaming_uploader.rb, line 78
def put(to_url, user_id, secret_key_filename, params = {}, headers = {})
  make_request(:put, to_url, user_id, secret_key_filename, params, headers)
end