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

113

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

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

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

KCH
источник

Ответы:

103

Мне нравится 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 поможет вам начать.

Pedro
источник
Беру это обратно, загрузка файлов теперь работает. Проблема, с которой я сейчас сталкиваюсь, заключается в том, что сервер выдает 302, а остальной клиент следует RFC (чего не делает ни один браузер) и выдает исключение (поскольку браузеры должны предупреждать об этом поведении). Другая альтернатива - бордюр, но мне никогда не удавалось установить бордюр в окнах.
Мэтт Вулф,
7
API немного изменился с тех пор, как это было впервые опубликовано, теперь multipart вызывается как: RestClient.post ' localhost: 3000 / foo ',: upload => File.new ('/ path / tofile')) См. Github.com/ archiloque / rest-client для более подробной информации.
Клинтон
2
rest_client не поддерживает отправку заголовков запросов. Многие приложения REST требуют / ожидают определенного типа заголовков, поэтому клиент отдыха в этом случае не будет работать. Например, JIRA требует токен X-Atlassian-Token.
onknows
Можно ли узнать прогресс загрузки файла? например, загружено 40%.
Ankush
1
+1 для добавления gem install rest-clientи require 'rest_client'частей. Эта информация отсутствует в слишком большом количестве примеров рубинов.
dansalmo 01
36

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

Он добавляет поддержку многокомпонентной публикации непосредственно в 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
Он сделал это за меня, именно то, что я искал, и именно то, что должно быть включено без необходимости в драгоценном камне. Руби так далеко впереди, но так далеко позади.
Trey
круто, это послано Богом! использовал это, чтобы обезвредить гем OAuth для поддержки загрузки файлов. занял у меня всего 5 минут.
Маттиас,
@matthias Я пытаюсь загрузить фотографию с помощью OAuth gem, но не удалось. не могли бы вы привести мне пример вашей обезьяньей повязки?
Hooopo
1
Патч был довольно специфичным для моего сценария (быстрым и грязным), но взгляните на него, и, возможно, вы сможете применить более общий подход ( gist.github.com/974084 )
Маттиас
3
Multipart не поддерживает заголовки запросов. Так что, если вы, например, хотите использовать интерфейс JIRA REST, multipart будет пустой тратой драгоценного времени.
onknows
30

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:brimhall@somuchwit.com>
# 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
Коди Бримхолл
источник
Здравствуй! Какая лицензия у этого кода? Также: было бы неплохо добавить URL-адрес этого сообщения в комментариях вверху. Спасибо!
docwhat
5
Код в этом посте находится под лицензией WTFPL ( sam.zoy.org/wtfpl ). Наслаждайтесь!
Коди Бримхолл,
вы не должны передавать файловый поток в вызов инициализации FileParamкласса. Назначение в to_multipartметоде снова копирует содержимое файла, в чем нет необходимости! Вместо этого передайте только дескриптор файла и to_multipart
считайте
1
Этот код замечательный! Потому что это работает. Rest-client и Siegers Multipart-post НЕ поддерживают заголовки запросов. Если вам нужны заголовки запросов, вы потратите много драгоценного времени на rest-client и Siegers Multipart post.
onknows
Собственно, @Onno теперь поддерживает заголовки запросов. См. Мой комментарий к ответу
Эрика
24

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

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

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

Владимир Рожков
источник
3
Спасибо за это. Один второстепенный момент, строка 1 должна быть такой: таким uri = URI('https://some.end.point/some/path') образом вы можете позвонить uri.portи uri.hostбез ошибок позже.
давидковский
1
один изменение второстепенного, если не временный файл , и вы хотите , чтобы загрузить файл с диска, вы должны использовать File.openнеFile.read
Анил Yanduri
1
в большинстве случаев требуется имя файла, это форма, которую я добавил: form_data = [['file', File.read (file_name), {filename: file_name}]]
ZsJoska
4
Это правильный ответ. люди должны прекратить использовать драгоценные камни-обертки, когда это возможно, и вернуться к основам.
Карлос Роке
18

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

  def upload(photo)
    `curl -F media=@#{photo.path} -F username=#{@username} -F password=#{@password} -F message='#{photo.title}' http://twitpic.com/api/uploadAndPost`
  end
Alex
источник
1
Несмотря на то, что это кажется немного хакерским, это, вероятно, лучшее решение для меня, поэтому большое спасибо за это предложение!
Бо Джинс
Замечание для неосторожных: media = @ ... делает curl thing ... файлом, а не просто строкой. Немного сбивает с толку синтаксис ruby, но @ # {photo.path} не то же самое, что #{@photo.path}. Это решение одно из лучших, имхо.
Евгений
7
Выглядит неплохо, но если ваше @username содержит "foo && rm -rf /", это будет очень плохо :-P
gaspard
8

Перенесемся в 2017, ruby stdlib net/httpесть ли это встроенное, начиная с 1.9.3

Net :: HTTPRequest # set_form): добавлено для поддержки как application / 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потоковую передачу данных формы.

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

PS Я тестировал это только на Ruby 2.3.1

летчикx86
источник
7

Хорошо, вот простой пример использования бордюра.

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]
KCH
источник
3

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

Он создавал «Content-Disposition: multipart / form-data» в каждой части, где это должно быть «Content-Disposition: form-data» .

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

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


источник
Это исправлено в последней версии restclient.
1

Что ж, решение с 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

источник
Что такое класс StreamPart?
Марлин Пирс
1

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

Ян Беркель
источник
1
multipart-post не поддерживает заголовки запросов.
onknows
Собственно, @Onno теперь поддерживает заголовки запросов. См. Мой комментарий к ответу
Эрика
0

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

Я копался в документации rest-client, не нашел указания на поддержку multipart. Я пробовал приведенные выше примеры rest-client, но jboss сказал, что сообщение http не является составным.


источник
0

Гем multipart-post очень хорошо работает с 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

Feuda
источник