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

StackOverflow https://stackoverflow.com/questions/184178

  •  06-07-2019
  •  | 
  •  

Вопрос

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

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

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

Это было полезно?

Решение

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

Другие советы

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

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

Вот небольшой пример того, как его использовать из ПРОЧТИ МЕНЯ:

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|

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

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

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

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

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

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

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]

Перенесемся в 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 для потоковой передачи данных формы.

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

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

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

Он создавал Content-Disposition: multipart / form-data ' в каждой части, где он должен быть & # 8216; Content-Disposition: form-data & # 8217; .

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

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

Хорошо, у решения с 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

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

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

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

Драгоценный камень, состоящий из нескольких частей, прекрасно работает с 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 / патч-1

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top