Ruby: Как отправить файл через HTTP как multipart/form-data?

Я хочу сделать HTTP POST, который выглядит как форма HMTL, размещенная в браузере. В частности, разместите некоторые текстовые поля и поле файла.

Проводка текстовых полей проста, есть пример прямо в net/http rdocs, но я не могу понять, как отправить файл вместе с ним.

Net:: HTTP не похож на лучшую идею. curb выглядит хорошо.

Ответ 1

Мне нравится RestClient. Он инкапсулирует net/http с такими замечательными функциями, как данные с несколькими форматами:

require 'rest_client'
RestClient.post('http://localhost:3000/foo', 
  :name_of_file_param => File.new('/path/to/file'))

Он также поддерживает потоковое вещание.

gem install rest-client начнется.

Ответ 2

Я не могу сказать достаточно хороших вещей о многопартийной библиотеке Ника Сигера.

Он добавляет поддержку многостраничной проводки непосредственно в Net:: HTTP, устраняя необходимость вручную беспокоиться о границах или больших библиотеках, которые могут иметь разные цели, чем ваши собственные.

Вот небольшой пример того, как использовать его из README:

require 'net/http/post/multipart'

url = URI.parse('http://www.example.com/upload')
File.open("./image.jpg") do |jpg|
  req = Net::HTTP::Post::Multipart.new url.path,
    "file" => UploadIO.new(jpg, "image/jpeg", "image.jpg")
  res = Net::HTTP.start(url.host, url.port) do |http|
    http.request(req)
  end
end

Вы можете проверить библиотеку здесь: http://github.com/nicksieger/multipart-post

или установите его с помощью:

$ sudo gem install multipart-post

Если вы подключаетесь через SSL, вам необходимо запустить соединение следующим образом:

n = Net::HTTP.new(url.host, url.port) 
n.use_ssl = true
# for debugging dev server
#n.verify_mode = OpenSSL::SSL::VERIFY_NONE
res = n.start do |http|

Ответ 3

curb выглядит как отличное решение, но если оно не соответствует вашим потребностям, вы можете сделать это с помощью Net::HTTP. Сообщение о многостраничной форме - это только тщательно отформатированная строка с некоторыми дополнительными заголовками. Кажется, что каждый программист Ruby, которому нужно делать многостраничные сообщения, заканчивает писать свою небольшую библиотеку, что заставляет меня задаться вопросом, почему эта функциональность не встроена. Может быть, это... Во всяком случае, ради удовольствия от чтения, я пойду дальше и дам свое решение здесь. Этот код основан на примерах, которые я нашел в нескольких блогах, но я сожалею, что больше не могу найти ссылки. Поэтому, я думаю, мне просто нужно взять на себя все заслуги...

Модуль, который я написал для этого, содержит один открытый класс для генерации данных формы и заголовков из хеша объектов String и File. Например, если вы хотите отправить форму со строковым параметром с именем "title" и параметром файла с именем "document", вы должны сделать следующее:

#prepare the query
data, headers = Multipart::Post.prepare_query("title" => my_string, "document" => my_file)

Затем вы просто делаете обычный POST с Net::HTTP:

http = Net::HTTP.new(upload_uri.host, upload_uri.port)
res = http.start {|con| con.post(upload_uri.path, data, headers) }

Или иначе вы хотите сделать POST. Дело в том, что Multipart возвращает данные и заголовки, которые необходимо отправить. И это! Простой, не так ли? Вот код для модуля Multipart (вам нужен камень mime-types):

# Takes a hash of string and file parameters and returns a string of text
# formatted to be sent as a multipart form post.
#
# Author:: Cody Brimhall <mailto:[email protected]>
# Created:: 22 Feb 2008
# License:: Distributed under the terms of the WTFPL (http://www.wtfpl.net/txt/copying/)

require 'rubygems'
require 'mime/types'
require 'cgi'


module Multipart
  VERSION = "1.0.0"

  # Formats a given hash as a multipart form post
  # If a hash value responds to :string or :read messages, then it is
  # interpreted as a file and processed accordingly; otherwise, it is assumed
  # to be a string
  class Post
    # We have to pretend we're a web browser...
    USERAGENT = "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/523.10.6 (KHTML, like Gecko) Version/3.0.4 Safari/523.10.6"
    BOUNDARY = "0123456789ABLEWASIEREISAWELBA9876543210"
    CONTENT_TYPE = "multipart/form-data; boundary=#{ BOUNDARY }"
    HEADER = { "Content-Type" => CONTENT_TYPE, "User-Agent" => USERAGENT }

    def self.prepare_query(params)
      fp = []

      params.each do |k, v|
        # Are we trying to make a file parameter?
        if v.respond_to?(:path) and v.respond_to?(:read) then
          fp.push(FileParam.new(k, v.path, v.read))
        # We must be trying to make a regular parameter
        else
          fp.push(StringParam.new(k, v))
        end
      end

      # Assemble the request body using the special multipart format
      query = fp.collect {|p| "--" + BOUNDARY + "\r\n" + p.to_multipart }.join("") + "--" + BOUNDARY + "--"
      return query, HEADER
    end
  end

  private

  # Formats a basic string key/value pair for inclusion with a multipart post
  class StringParam
    attr_accessor :k, :v

    def initialize(k, v)
      @k = k
      @v = v
    end

    def to_multipart
      return "Content-Disposition: form-data; name=\"#{CGI::escape(k)}\"\r\n\r\n#{v}\r\n"
    end
  end

  # Formats the contents of a file or string for inclusion with a multipart
  # form post
  class FileParam
    attr_accessor :k, :filename, :content

    def initialize(k, filename, content)
      @k = k
      @filename = filename
      @content = content
    end

    def to_multipart
      # If we can tell the possible mime-type from the filename, use the
      # first in the list; otherwise, use "application/octet-stream"
      mime_type = MIME::Types.type_for(filename)[0] || MIME::Types["application/octet-stream"][0]
      return "Content-Disposition: form-data; name=\"#{CGI::escape(k)}\"; filename=\"#{ filename }\"\r\n" +
             "Content-Type: #{ mime_type.simplified }\r\n\r\n#{ content }\r\n"
    end
  end
end

Ответ 4

Вот мое решение, попробовав другие, доступные на этом посту, я использую его для загрузки фотографии на TwitPic:

  def upload(photo)
    `curl -F [email protected]#{photo.path} -F username=#{@username} -F password=#{@password} -F message='#{photo.title}' http://twitpic.com/api/uploadAndPost`
  end

Ответ 5

Хорошо, вот простой пример с использованием curb.

require 'yaml'
require 'curb'

# prepare post data
post_data = fields_hash.map { |k, v| Curl::PostField.content(k, v.to_s) }
post_data << Curl::PostField.file('file', '/path/to/file'), 

# post
c = Curl::Easy.new('http://localhost:3000/foo')
c.multipart_form_post = true
c.http_post(post_data)

# print response
y [c.response_code, c.body_str]

Ответ 6

Еще один, использующий только стандартные библиотеки:

uri = URI('https://some.end.point/some/path')
request = Net::HTTP::Post.new(uri)
request['Authorization'] = 'If you need some headers'
form_data = [['photos', photo.tempfile]] # or File.open() in case of local file

request.set_form form_data, 'multipart/form-data'
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| # pay attention to use_ssl if you need it
  http.request(request)
end

Перепробовал много подходов, но только это сработало для меня.

Ответ 7

Ускоренная перемотка вперед до 2017, ruby stdlib net/http имеет этот встроенный с 1.9.3

Net:: HTTPRequest # set_form): добавлено для поддержки как приложений /x -www-form-urlencoded, так и multipart/form-data.

https://ruby-doc.org/stdlib-2.3.1/libdoc/net/http/rdoc/Net/HTTPHeader.html#method-i-set_form

Мы даже можем использовать IO, который не поддерживает :size для потока данных формы.

Надеюсь, что этот ответ действительно может помочь кому-то:)

P.S. Я тестировал это только в рубине 2.3.1

Ответ 8

restclient не работал у меня, пока я не переопределял create_file_field в RestClient:: Payload:: Multipart.

Это создавало "Content-Disposition: multipart/form-data" в каждой части, где должно быть "Content-Disposition: form-data​​strong > .. p >

http://www.ietf.org/rfc/rfc2388.txt

Мой форк здесь, если вам это нужно: git @github.com: kcrawford/rest-client.git

Ответ 9

Ну, решение с NetHttp имеет недостаток, который при публикации больших файлов сначала загружает весь файл в память.

Поиграв немного с этим, я придумал следующее решение:

class Multipart

  def initialize( file_names )
    @file_names = file_names
  end

  def post( to_url )
    boundary = '----RubyMultipartClient' + rand(1000000).to_s + 'ZZZZZ'

    parts = []
    streams = []
    @file_names.each do |param_name, filepath|
      pos = filepath.rindex('/')
      filename = filepath[pos + 1, filepath.length - pos]
      parts << StringPart.new ( "--" + boundary + "\r\n" +
      "Content-Disposition: form-data; name=\"" + param_name.to_s + "\"; filename=\"" + filename + "\"\r\n" +
      "Content-Type: video/x-msvideo\r\n\r\n")
      stream = File.open(filepath, "rb")
      streams << stream
      parts << StreamPart.new (stream, File.size(filepath))
    end
    parts << StringPart.new ( "\r\n--" + boundary + "--\r\n" )

    post_stream = MultipartStream.new( parts )

    url = URI.parse( to_url )
    req = Net::HTTP::Post.new(url.path)
    req.content_length = post_stream.size
    req.content_type = 'multipart/form-data; boundary=' + boundary
    req.body_stream = post_stream
    res = Net::HTTP.new(url.host, url.port).start {|http| http.request(req) }

    streams.each do |stream|
      stream.close();
    end

    res
  end

end

class StreamPart
  def initialize( stream, size )
    @stream, @size = stream, size
  end

  def size
    @size
  end

  def read ( offset, how_much )
    @stream.read ( how_much )
  end
end

class StringPart
  def initialize ( str )
    @str = str
  end

  def size
    @str.length
  end

  def read ( offset, how_much )
    @str[offset, how_much]
  end
end

class MultipartStream
  def initialize( parts )
    @parts = parts
    @part_no = 0;
    @part_offset = 0;
  end

  def size
    total = 0
    @parts.each do |part|
      total += part.size
    end
    total
  end

  def read ( how_much )

    if @part_no >= @parts.size
      return nil;
    end

    how_much_current_part = @parts[@part_no].size - @part_offset

    how_much_current_part = if how_much_current_part > how_much
      how_much
    else
      how_much_current_part
    end

    how_much_next_part = how_much - how_much_current_part

    current_part = @parts[@part_no].read(@part_offset, how_much_current_part )

    if how_much_next_part > 0
      @part_no += 1
      @part_offset = 0
      next_part = read ( how_much_next_part  )
      current_part + if next_part
        next_part
      else
        ''
      end
    else
      @part_offset += how_much_current_part
      current_part
    end
  end
end

Ответ 10

там также nick sieger multipart-post, чтобы добавить к длинному списку возможных решений.

Ответ 11

У меня была такая же проблема (нужно отправить на веб-сервер jboss). Curb отлично работает для меня, за исключением того, что он вызвал рубин, который рухнул (ruby 1.8.7 на ubuntu 8.10), когда я использую переменные сеанса в коде.

Я копаюсь в документах rest-client, не могу найти указание на поддержку multipart. Я попробовал примеры для остальных клиентов выше, но jboss сказал, что HTTP-сообщение не является многочастным.

Ответ 12

Многопользовательский драгоценный камень работает очень хорошо с Rails 4 Net:: HTTP, никакой другой особый камень

def model_params
  require_params = params.require(:model).permit(:param_one, :param_two, :param_three, :avatar)
  require_params[:avatar] = model_params[:avatar].present? ? UploadIO.new(model_params[:avatar].tempfile, model_params[:avatar].content_type, model_params[:avatar].original_filename) : nil
  require_params
end

require 'net/http/post/multipart'

url = URI.parse('http://www.example.com/upload')
Net::HTTP.start(url.host, url.port) do |http|
  req = Net::HTTP::Post::Multipart.new(url, model_params)
  key = "authorization_key"
  req.add_field("Authorization", key) #add to Headers
  http.use_ssl = (url.scheme == "https")
  http.request(req)
end

https://github.com/Feuda/multipart-post/tree/patch-1