Ruby: Как опубликовать файл через HTTP как multipart / form-data? - PullRequest
101 голосов
/ 08 октября 2008

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

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

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

Ответы [ 12 ]

93 голосов
/ 25 ноября 2008

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

36 голосов
/ 09 апреля 2010

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

Добавлена ​​поддержка многокомпонентной публикации непосредственно в 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|
28 голосов
/ 17 октября 2008

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
17 голосов
/ 26 декабря 2008

Вот мое решение после того, как я попробовал другие, доступные в этом посте, я использую его для загрузки фото на 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
14 голосов
/ 10 октября 2017

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

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 голосов
/ 08 октября 2008

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

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]
5 голосов
/ 31 августа 2017

Быстрая перемотка вперед до 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 / сеть / HTTP / RDoc / Net / HTTPHeader.html # метод-я-set_form

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

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

P.S. Я проверял это только в ruby ​​2.3.1

3 голосов
/ 15 января 2010

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

1 голос
/ 23 апреля 2009

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

1 голос
/ 25 декабря 2008

Что ж, решение с 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
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...