Ruby: Bagaimana cara memposting file melalui HTTP sebagai multipart / form-data?


113

Saya ingin melakukan HTTP POST yang terlihat seperti formulir HMTL yang diposting dari browser. Secara khusus, posting beberapa bidang teks dan bidang file.

Posting text field sangat mudah, ada contohnya di net / http rdocs, tapi saya tidak tahu bagaimana cara memposting file bersamanya.

Net :: HTTP sepertinya bukan ide terbaik. trotoar terlihat bagus.

Jawaban:


103

Saya suka RestClient . Ini merangkum net / http dengan fitur-fitur keren seperti data formulir multi bagian:

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

Ini juga mendukung streaming.

gem install rest-client akan membantu Anda memulai.


Saya ambil kembali, unggahan file sekarang berfungsi. Masalah yang saya hadapi sekarang adalah server memberikan 302 dan klien lainnya mengikuti RFC (yang tidak dilakukan browser) dan memberikan pengecualian (karena browser seharusnya memperingatkan tentang perilaku ini). Alternatif lain adalah trotoar tetapi saya tidak pernah beruntung menginstal trotoar di windows.
Matt Wolfe

7
API telah sedikit berubah sejak ini pertama kali diposting, multipart sekarang dipanggil seperti: RestClient.post ' localhost: 3000 / foo ',: upload => File.new ('/ path / tofile')) Lihat github.com/ archiloque / rest-client untuk lebih jelasnya.
Clinton

2
rest_client tidak mendukung penyediaan header permintaan. Banyak aplikasi REST memerlukan / mengharapkan jenis header tertentu sehingga klien lainnya tidak akan berfungsi dalam kasus itu. Misalnya JIRA membutuhkan token X-Atlassian-Token.
mengetahui

Apakah mungkin untuk mendapatkan kemajuan upload file? mis. 40% diunggah.
Ankush

1
1 untuk menambahkan gem install rest-clientdan require 'rest_client'bagian. Info itu ditinggalkan dari terlalu banyak contoh ruby.
dansalmo

36

Saya tidak bisa mengatakan cukup banyak hal baik tentang perpustakaan multi bagian Nick Sieger.

Ini menambahkan dukungan untuk posting multi bagian langsung ke Net :: HTTP, menghilangkan kebutuhan Anda untuk secara manual khawatir tentang batasan atau pustaka besar yang mungkin memiliki tujuan berbeda dari Anda sendiri.

Berikut ini sedikit contoh tentang cara menggunakannya dari 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

Anda dapat melihat perpustakaan di sini: http://github.com/nicksieger/multipart-post

atau instal dengan:

$ sudo gem install multipart-post

Jika Anda terhubung melalui SSL, Anda perlu memulai koneksi seperti ini:

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
Yang itu melakukannya untuk saya, persis apa yang saya cari dan persis apa yang harus dimasukkan tanpa perlu permata. Ruby sangat jauh di depan, namun sangat jauh di belakang.
Trey

mengagumkan, ini datang sebagai kiriman Tuhan! menggunakan ini untuk mencocokkan permata OAuth untuk mendukung unggahan file. hanya butuh 5 menit.
Matthias

@matthias Saya mencoba mengunggah foto dengan permata OAuth, tetapi gagal. Bisakah Anda memberi saya beberapa contoh monkeypatch Anda?
Hooopo

1
Tambalan itu cukup spesifik untuk skrip saya (cepat-dan-kotor), tetapi lihatlah dan mungkin Anda dapat membuat beberapa dengan pendekatan yang lebih umum ( gist.github.com/974084 )
Matthias

3
Multipart tidak mendukung header permintaan. Jadi jika Anda misalnya ingin menggunakan antarmuka JIRA REST, multipart hanya akan membuang-buang waktu yang berharga.
mengetahui

30

curbSepertinya solusi yang bagus, tetapi jika tidak sesuai dengan kebutuhan Anda, Anda dapat melakukannya dengan Net::HTTP. Postingan formulir multi bagian hanyalah string yang diformat dengan cermat dengan beberapa header tambahan. Sepertinya setiap programmer Ruby yang perlu melakukan posting multi bagian akhirnya menulis perpustakaan kecil mereka sendiri untuk itu, yang membuat saya bertanya-tanya mengapa fungsi ini tidak ada di dalamnya. Mungkin itu ... Pokoknya, untuk kesenangan membaca Anda, saya akan teruskan dan memberikan solusi saya di sini. Kode ini didasarkan pada contoh yang saya temukan di beberapa blog, tetapi saya menyesal tidak dapat menemukan tautannya lagi. Jadi saya rasa saya harus mengambil semua pujian untuk diri saya sendiri ...

Modul yang saya tulis untuk ini berisi satu kelas publik, untuk menghasilkan data formulir dan header dari hash Stringdan Fileobjek. Jadi misalnya, jika Anda ingin memposting formulir dengan parameter string bernama "title" dan parameter file bernama "document", Anda akan melakukan hal berikut:

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

Kemudian Anda hanya melakukan yang normal POSTdengan Net::HTTP:

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

Atau bagaimanapun Anda ingin melakukan POST. Intinya adalah Multipartmengembalikan data dan header yang perlu Anda kirim. Dan itu dia! Sederhana bukan? Berikut kode untuk modul Multipart (Anda membutuhkan mime-typespermata):

# 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

Hai! Apa lisensi pada kode ini? Juga: Mungkin menyenangkan untuk menambahkan URL untuk posting ini di komentar di atas. Terima kasih!
docwhat

5
Kode dalam posting ini dilisensikan di bawah WTFPL ( sam.zoy.org/wtfpl ). Nikmati!
Cody Brimhall

Anda tidak boleh meneruskan filestream ke dalam panggilan inisialisasi FileParamkelas. Penetapan dalam to_multipartmetode menyalin konten file lagi, yang tidak diperlukan! Alih-alih hanya meneruskan deskriptor file dan membacanya dito_multipart
mober

1
Kode ini HEBAT! Karena itu berhasil. Rest-client and Siegers Multipart-post DON'T support request header. Jika Anda membutuhkan header permintaan, Anda akan membuang banyak waktu berharga dengan klien istirahat dan posting Multipart Siegers.
Mengetahui

Sebenarnya, @Onno, sekarang mendukung header permintaan. Lihat komentar saya tentang jawaban eric
alexanderbird

24

Yang lain hanya menggunakan pustaka standar:

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

Mencoba banyak pendekatan tetapi hanya ini yang berhasil untuk saya.


3
Terima kasih untuk ini. Satu hal kecil, baris 1 harus: uri = URI('https://some.end.point/some/path') Dengan cara itu Anda dapat menelepon uri.portdan uri.hosttanpa kesalahan di kemudian hari.
davidkovsky

1
satu perubahan kecil, jika bukan tempfile dan Anda ingin mengunggah file dari disk Anda, Anda harus menggunakan File.opentidakFile.read
Anil Yanduri

1
sebagian besar kasus nama file diperlukan, ini adalah bentuk yang saya tambahkan: form_data = [['file', File.read (nama_file), {nama_file: nama_file}]]
ZsJoska

4
inilah jawaban yang benar. orang harus berhenti menggunakan permata pembungkus bila memungkinkan dan kembali ke dasar.
Carlos Roque

18

Inilah solusi saya setelah mencoba yang lain yang tersedia di posting ini, saya menggunakannya untuk mengunggah foto di 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
Meskipun tampak agak hackish, ini mungkin solusi terbaik bagi saya, terima kasih banyak atas saran ini!
Bo Jeanes

Sekadar catatan untuk yang tidak waspada, media = @ ... inilah yang membuat curl itu ... adalah file dan bukan sekedar string. Agak membingungkan dengan sintaks ruby, tetapi @ # {photo.path} tidak sama dengan #{@photo.path}. Solusi ini adalah salah satu imho terbaik.
Evgeny

7
Ini terlihat bagus tetapi jika @namapengguna Anda mengandung "foo && rm -rf /", ini menjadi sangat buruk :-P
gaspard

8

Maju cepat ke 2017, ruby stdlib net/httpmemiliki ini bawaan sejak 1.9.3

Net :: HTTPRequest # set_form): Ditambahkan untuk mendukung aplikasi / x-www-form-urlencoded dan multipart / form-data.

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

Kami bahkan dapat menggunakan IOyang tidak mendukung :sizeuntuk mengalirkan data formulir.

Berharap jawaban ini benar-benar dapat membantu seseorang :)

PS Saya hanya menguji ini di ruby ​​2.3.1


7

Oke, berikut contoh sederhana menggunakan curb.

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 tidak bekerja untuk saya sampai saya mengganti create_file_field di RestClient :: Payload :: Multipart.

Itu menciptakan 'Content-Disposition: multipart / form-data' di setiap bagian yang seharusnya menjadi 'Content-Disposition: form-data' .

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

Garpu saya ada di sini jika Anda membutuhkannya: git@github.com: kcrawford / rest-client.git


Ini telah diperbaiki di restclient terbaru.

1

Solusi dengan NetHttp memiliki kelemahan yaitu ketika memposting file besar, ia memuat seluruh file ke dalam memori terlebih dahulu.

Setelah bermain sedikit dengannya, saya menemukan solusi berikut:

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

Apa itu StreamPart kelas?
Marlin Pierce

1

ada juga multipart-post nick sieger untuk ditambahkan ke daftar panjang solusi yang mungkin.


1
multipart-post tidak mendukung header permintaan.
mengetahui

Sebenarnya, @Onno, sekarang mendukung header permintaan. Lihat komentar saya tentang jawaban eric
alexanderbird

0

Saya memiliki masalah yang sama (perlu memposting ke server web jboss). Curb berfungsi dengan baik untuk saya, kecuali itu menyebabkan ruby ​​crash (ruby 1.8.7 di ubuntu 8.10) ketika saya menggunakan variabel sesi dalam kode.

Saya menggali ke dalam dokumen klien lainnya, tidak dapat menemukan indikasi dukungan multi bagian. Saya mencoba contoh klien lainnya di atas tetapi jboss mengatakan pos http bukan multipart.


0

Permata multi-bagian bekerja cukup baik dengan Rails 4 Net :: HTTP, tidak ada permata khusus lainnya

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

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.