Ruby: Jak wysłać plik przez HTTP jako multipart / form-data?

113

Chcę wykonać HTTP POST, który wygląda jak formularz HMTL wysłany z przeglądarki. W szczególności opublikuj niektóre pola tekstowe i pole pliku.

Wysyłanie pól tekstowych jest proste, przykład znajduje się w net / http rdocs, ale nie mogę dowiedzieć się, jak opublikować plik wraz z nim.

Net :: HTTP nie wygląda na najlepszy pomysł. krawężnik wygląda dobrze.

kch
źródło

Odpowiedzi:

103

Lubię RestClient . Hermetyzuje net / http z fajnymi funkcjami, takimi jak dane formularza wieloczęściowego:

require 'rest_client'
RestClient.post('http://localhost:3000/foo', 
  :name_of_file_param => File.new('/path/to/file'))

Obsługuje również przesyłanie strumieniowe.

gem install rest-client pomoże ci zacząć.

Pedro
źródło
Cofam to, przesyłanie plików teraz działa. Problem, który mam teraz, polega na tym, że serwer podaje 302, a reszta klienta postępuje zgodnie z RFC (czego nie robi żadna przeglądarka) i zgłasza wyjątek (ponieważ przeglądarki mają ostrzegać o tym zachowaniu). Inną alternatywą jest krawężnik, ale nigdy nie miałem szczęścia instalując krawężnik w oknach.
Matt Wolfe,
7
Interfejs API zmienił się trochę od czasu opublikowania tego po raz pierwszy, teraz wywoływany jest multipart: RestClient.post ' localhost: 3000 / foo ',: upload => File.new ('/ path / tofile')) Zobacz github.com/ archiloque / rest-client, aby uzyskać więcej informacji.
Clinton
2
rest_client nie obsługuje dostarczania nagłówków żądań. Wiele aplikacji REST wymaga / oczekuje określonego typu nagłówków, więc pozostały klient nie będzie działał w takim przypadku. Na przykład JIRA wymaga tokena X-Atlassian-Token.
onznany
Czy można sprawdzić postęp przesyłania plików? np. 40% jest przesyłane.
Ankush
1
+1 za dodanie części gem install rest-clienti require 'rest_client'. Ta informacja została pominięta w zbyt wielu przykładach ruby.
dansalmo
36

Nie mogę powiedzieć wystarczająco dobrych rzeczy o wieloczęściowej bibliotece Nicka Siegera.

Dodaje obsługę wysyłania wieloczęściowego bezpośrednio do Net :: HTTP, eliminując potrzebę ręcznego martwienia się o granice lub duże biblioteki, które mogą mieć inne cele niż twoje własne.

Oto mały przykład, jak go używać z 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

Możesz sprawdzić bibliotekę tutaj: http://github.com/nicksieger/multipart-post

lub zainstaluj za pomocą:

$ sudo gem install multipart-post

Jeśli łączysz się przez SSL, musisz rozpocząć połączenie w następujący sposób:

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
źródło
3
Ten zrobił to dla mnie, dokładnie to, czego szukałem i dokładnie to, co powinno się znaleźć bez potrzeby posiadania klejnotu. Ruby jest tak daleko do przodu, ale tak daleko w tyle.
Trey
niesamowite, to przychodzi jako posłanie Boga! użył tego do monkeypatch klejnotu OAuth do obsługi przesyłania plików. zajęło mi tylko 5 minut.
Matthias
@matthias Próbuję przesłać zdjęcie z gemem OAuth, ale nie udało się. czy mógłbyś podać mi przykład swojej małpy?
Hooopo
1
Łatka była dość specyficzna dla mojego skryptu (szybka i brudna), ale spójrz na nią, a może uda ci się trochę bardziej ogólnego podejścia ( gist.github.com/974084 )
Matthias
3
Multipart nie obsługuje nagłówków żądań. Jeśli więc chcesz na przykład skorzystać z interfejsu JIRA REST, multipart będzie po prostu stratą cennego czasu.
onznany
30

curbwygląda na świetne rozwiązanie, ale jeśli nie odpowiada Twoim potrzebom, możesz to zrobić Net::HTTP. Post w postaci wieloczęściowej to po prostu starannie sformatowany ciąg znaków z kilkoma dodatkowymi nagłówkami. Wygląda na to, że każdy programista Ruby, który musi pisać wieloczęściowe posty, kończy pisanie własnej małej biblioteki, co sprawia, że ​​zastanawiam się, dlaczego ta funkcja nie jest wbudowana. Może tak jest ... W każdym razie, dla przyjemności czytania, podam tutaj moje rozwiązanie. Ten kod jest oparty na przykładach, które znalazłem na kilku blogach, ale żałuję, że nie mogę już znaleźć linków. Więc wydaje mi się, że muszę przypisać sobie całą zasługę ...

Moduł, który napisałem w tym celu, zawiera jedną publiczną klasę do generowania danych formularza i nagłówków z haszu obiektów Stringi File. Na przykład, jeśli chcesz opublikować formularz z parametrem ciągu o nazwie „tytuł” ​​i parametrem pliku o nazwie „dokument”, wykonaj następujące czynności:

#prepare the query
data, headers = Multipart::Post.prepare_query("title" => my_string, "document" => my_file)

Następnie po prostu robisz normalne POSTz Net::HTTP:

http = Net::HTTP.new(upload_uri.host, upload_uri.port)
res = http.start {|con| con.post(upload_uri.path, data, headers) }

Lub jakkolwiek chcesz to zrobić POST. Chodzi o to, że Multipartzwraca dane i nagłówki, które musisz wysłać. I to wszystko! Proste, prawda? Oto kod modułu Multipart (potrzebujesz mime-typesklejnotu):

# 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:[email protected]>
# 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
źródło
Cześć! Jaka jest licencja na ten kod? Ponadto: Fajnie byłoby dodać adres URL tego posta w komentarzach u góry. Dzięki!
docwhat
5
Kod w tym poście jest na licencji WTFPL ( sam.zoy.org/wtfpl ). Cieszyć się!
Cody Brimhall
nie powinieneś przekazywać strumienia pliku do wywołania inicjalizacji FileParamklasy. Przypisanie w to_multipartmetodzie ponownie kopiuje zawartość pliku, co jest niepotrzebne! Zamiast tego przekaż tylko deskryptor pliku i przeczytaj z niegoto_multipart
październiku
1
Ten kod jest WSPANIAŁY! Ponieważ to działa. Rest-client i Siegers Multipart-post NIE obsługują nagłówków żądań. Jeśli potrzebujesz nagłówków żądań, zmarnujesz dużo cennego czasu na posty Rest-Client i Siegers Multipart.
onknows
Właściwie @Onno obsługuje teraz nagłówki żądań. Zobacz mój komentarz do odpowiedzi
Erica
24

Kolejny korzystający tylko ze standardowych bibliotek:

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

Próbowałem wielu podejść, ale tylko to działało dla mnie.

Vladimir Rozhkov
źródło
3
Dzięki za to. Jedna drobna uwaga, linia 1 powinna brzmieć: uri = URI('https://some.end.point/some/path') W ten sposób możesz zadzwonić uri.porti uri.hostbezbłędnie później.
davidkovsky
1
jedna zmiana moll, jeśli nie tempfile i chcesz przesłać plik z dysku, należy użyć File.opennieFile.read
Anil Yanduri
1
w większości przypadków nazwa pliku jest wymagana, jest to forma, w której dodałem: form_data = [['file', File.read (file_name), {filename: file_name}]]
ZsJoska
4
to jest poprawna odpowiedź. ludzie powinni przestać używać klejnotów opakowujących, jeśli to możliwe, i wrócić do podstaw.
Carlos Roque
18

Oto moje rozwiązanie po wypróbowaniu innych dostępnych w tym poście, używam go do przesyłania zdjęcia na 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
źródło
1
Pomimo tego, że wydaje mi się trochę hackerskie, jest to chyba najmilsze rozwiązanie dla mnie, więc wielkie dzięki za tę sugestię!
Bo Jeanes
Tylko uwaga dla nieostrożnych, media = @ ... jest tym, co sprawia, że ​​curl jest ... plikiem, a nie tylko ciągiem. Trochę mylące ze składnią ruby, ale @ # {photo.path} to nie to samo co #{@photo.path}. To rozwiązanie jest jednym z najlepszych imho.
Evgeny
7
Wygląda to ładnie, ale jeśli twoja @ nazwa użytkownika zawiera „foo && rm -rf /”, robi się to bardzo źle :-P
gaspard
8

Przewiń do 2017 r., ruby stdlib net/httpMa to wbudowane od 1.9.3

Net :: HTTPRequest # set_form): Dodano, aby obsługiwać zarówno application / x-www-form-urlencoded, jak i multipart / form-data.

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

Możemy nawet użyć, IOktóry nie obsługuje :sizeprzesyłania strumieniowego danych formularza.

Mam nadzieję, że ta odpowiedź naprawdę komuś pomoże :)

PS Testowałem to tylko w Rubim 2.3.1

airmanx86
źródło
7

OK, oto prosty przykład użycia krawężnika.

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
źródło
3

restclient nie działał dla mnie, dopóki nie przesłoniłem create_file_field w RestClient :: Payload :: Multipart.

Tworzyło „Dyspozycję treści: multipart / dane formularza” w każdej części, gdzie powinno być „Dyspozycja treści: dane formularza” .

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

Mój fork jest tutaj, jeśli go potrzebujesz: [email protected]: kcrawford / rest-client.git


źródło
Zostało to naprawione w najnowszym restclient.
1

Cóż, rozwiązanie z NetHttp ma tę wadę, że podczas wysyłania dużych plików najpierw ładuje cały plik do pamięci.

Po odrobinie zabawy wpadłem na następujące rozwiązanie:

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

źródło
Co to jest klasa StreamPart?
Marlin Pierce,
1

jest też wieloczęściowy post Nicka Siegera, który można dodać do długiej listy możliwych rozwiązań.

Jan Berkel
źródło
1
multipart-post nie obsługuje nagłówków żądań.
onznany
Właściwie @Onno obsługuje teraz nagłówki żądań. Zobacz mój komentarz do odpowiedzi
Erica
0

Miałem ten sam problem (trzeba wysłać na serwer WWW jboss). Curb działa dla mnie dobrze, z wyjątkiem tego, że spowodował awarię Rubiego (Ruby 1.8.7 na Ubuntu 8.10), gdy używam zmiennych sesji w kodzie.

Zagłębiłem się w dokumentację dotyczącą pozostałych klientów, nie mogłem znaleźć informacji o obsłudze wielu części. Wypróbowałem powyższe przykłady klientów, ale jboss powiedział, że post http nie jest wieloczęściowy.


źródło
0

Klejnot wieloczęściowych postów działa całkiem dobrze z Rails 4 Net :: HTTP, żaden inny specjalny klejnot

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
źródło