Ruby: Як опублікувати файл через HTTP у вигляді багаточастинних / форм-даних?


113

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

Опублікування текстових полів є простим, тут є приклад прямо в net / http rdocs, але я не можу зрозуміти, як розмістити файл разом з ним.

Net :: HTTP не виглядає як найкраща ідея. бордюр добре виглядає.

Відповіді:


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 почнете вас розпочати.


Я повертаю це назад, завантаження файлів зараз працює. Проблема, яку я маю зараз, полягає в тому, що сервер дає 302, а решта-клієнт слідкує за RFC (чого не робить жоден браузер) і викидає виняток (оскільки браузери повинні попереджати про цю поведінку). Інша альтернатива - бордюр, але мені ніколи не пощастило встановити бордюр у windows.
Метт Вулф

7
API дещо змінився з моменту його публікації вперше, тепер викликається багатоповерховий на зразок: 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%.
Анкуш

1
+1 для додавання gem install rest-clientта require 'rest_client'частин. Ця інформація залишилася занадто великою кількістю рубінових прикладів.
дансальмо

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
Той, хто зробив це для мене, саме те, що я шукав, і саме те, що слід включати, не потребуючи дорогоцінного каміння. Рубі так далеко попереду, але поки що позаду.
Трей

дивовижно, це відбувається як Бог посилає! використовував це для маніпуляції коштовним каменем OAuth для підтримки завантаження файлів. зайняв у мене лише 5 хвилин.
Маттіас

@matthias Я намагаюся завантажити фото з дорогоцінним каменем OAuth, але не вдалося. ви могли б надати мені якийсь приклад вашої мавпочки?
Hooopo

1
Патч був досить специфічним для мого сценарію (швидкий і брудний), але подивіться на це, і, можливо, ви зможете розібратися з більш загальним підходом ( gist.github.com/974084 )
Маттіас

3
Multipart не підтримує заголовки запитів. Тож якщо ви, наприклад, хочете використовувати інтерфейс JIRA REST, багатопотужність буде просто марною витратою часу.
onknows

30

curbвиглядає як чудове рішення, але якщо воно не відповідає вашим потребам, ви можете це зробити Net::HTTP. Повідомлення з багаточастинкової форми - це лише ретельно відформатований рядок із додатковими заголовками. Здається, що кожен програміст Ruby, якому потрібно робити багатоповерхові пости, закінчує писати для цього свою маленьку бібліотеку, що змушує мене замислитися, чому ця функціональність не вбудована. Можливо, так і є ... У всякому разі, для вашого задоволення від читання я піду далі і дам тут своє рішення. Цей код заснований на прикладах, які я знайшов у кількох блогах, але шкодую, що більше не можу знайти посилання. Тож я гадаю, що я просто повинен взяти на себе всю заслугу ...

Я написав для цього модуль містить один загальнодоступний клас для генерування даних форми та заголовків із хешу Stringта Fileоб’єктів. Так, наприклад, якщо ви хочете опублікувати форму з параметром рядка під назвою "title" та параметром файлу з назвою "документ", ви зробите наступне:

#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-адресу для цієї публікації в коментарі вгорі. Дякую!
док

5
Код у цій публікації ліцензований відповідно до WTFPL ( sam.zoy.org/wtfpl ). Насолоджуйтесь!
Коді Брімхолл

не слід передавати потоки файлів у ініціалізаційний виклик FileParamкласу. Призначення в to_multipartметоді копіює вміст файлу знову, що зайве! Замість цього передайте лише дескриптор файлу та прочитайте з нього вto_multipart
mober

1
Цей код ВЕЛИКИЙ! Тому що це працює. Заголовок запиту на підтримку відпочинкового клієнта та Siegers Multipart-post DON'T. Якщо вам потрібні заголовки запитів, ви витратите багато цінного часу на відпочинок-клієнт та Siegers Multipart.
onknows

Насправді, @Onno, він тепер підтримує заголовки запитів. Дивіться мій коментар до відповіді eric
alexanderbird

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.read (ім'я файлу), {ім'я файлу: ім'я_файла}]]
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

1
Незважаючи на те, що здається трохи хакітським, це, мабуть, найкраще рішення для мене, настільки велике спасибі за цю пропозицію!
Bo Jeanes

Просто примітка для необережного, медіа = @ ... - це те, що робить curl річ, що ... - це файл, а не просто рядок. Трохи заплутаний у синтаксисі рубіну, але @ # {photo.path} - це не те саме, що #{@photo.path}. Це рішення є одним з найкращих імхо.
Євген

7
Це виглядає добре, але якщо ваше ім'я @username містить "foo && rm -rf /", це стає дуже погано :-P
gaspard

8

Швидкий перехід до 2017 року, ruby stdlib net/httpцей вбудований з 1.9.3

Net :: HTTPRequest # set_form): Додано для підтримки як застосунку / x-www-form-urlencoded, так і даних із багаточастинними / формами.

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

Ми навіть можемо використовувати те, IOщо не підтримує, :sizeщоб передавати дані форми.

Сподіваючись, що ця відповідь може справді комусь допомогти :)

PS Я це протестував лише в рубіні 2.3.1


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]

3

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

Він створював "Зміст-диспозиція: багаточастинні / форми-дані" у кожній частині, де він повинен бути "Зміст-диспозиція: форма-дані" .

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

Моя вилка тут, якщо вам вона потрібна: git@github.com: kcrawford / rest-client.git


Це фіксується в останньому реквізиті.

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?
Марлін Пірс


0

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

Я заглибився в документи інших клієнтів, не міг знайти вказівки на підтримку декількох частин. Я спробував приклади решти клієнтів вище, але jboss сказав, що http-повідомлення не є багаточастинним.


0

Самоцвіт з декількома публікаціями працює добре з 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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.