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

ruby http post

110107 просмотра

12 ответа

48272 Репутация автора

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

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

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

Автор: kch Источник Размещён: 08.10.2008 06:31

Ответы (12)


7 плюса

48272 Репутация автора

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

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 Размещён: 08.10.2008 06:55

29 плюса

1000 Репутация автора

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
Автор: Cody Brimhall Размещён: 17.10.2008 06:24

96 плюса

2273 Репутация автора

Решение

Мне нравится 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 Размещён: 25.11.2008 04:03

1 плюс

0 Репутация автора

У решения с 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
Автор: Stanislav Vitvitskiy Размещён: 25.12.2008 03:46

17 плюса

12771 Репутация автора

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

0 плюса

0 Репутация автора

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

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

Автор: zd Размещён: 05.02.2009 07:01

1 плюс

3033 Репутация автора

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

Автор: Jan Berkel Размещён: 23.04.2009 12:26

3 плюса

0 Репутация автора

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

Автор: user243633 Размещён: 14.01.2010 11:05

36 плюса

1040 Репутация автора

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

Он добавляет поддержку составной публикации непосредственно в 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|
Автор: eric Размещён: 08.04.2010 10:12

0 плюса

1305 Репутация автора

Gem, состоящий из нескольких частей, довольно хорошо работает с 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 Размещён: 16.12.2016 03:41

8 плюса

939 Репутация автора

Перенесемся в 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

Автор: airmanx86 Размещён: 31.08.2017 08:00

18 плюса

870 Репутация автора

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

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

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

Автор: Vladimir Rozhkov Размещён: 10.10.2017 02:29
Вопросы из категории :
32x32