Ruby: Bir dosyayı HTTP yoluyla multipart / form-data olarak nasıl yayınlayabilirim?


113

Bir tarayıcıdan yayınlanan bir HMTL formuna benzeyen bir HTTP POST yapmak istiyorum. Özellikle, bazı metin alanları ve bir dosya alanı gönderin.

Metin alanlarını göndermek basittir, net / http rdocs'ta bir örnek var, ancak bununla birlikte bir dosyanın nasıl gönderileceğini bulamıyorum.

Net :: HTTP en iyi fikir gibi görünmüyor. bordür iyi görünüyor.

Yanıtlar:


103

Gibi RestClient . Çok parçalı form verileri gibi harika özelliklerle net / http'yi kapsüller:

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

Ayrıca akışı destekler.

gem install rest-client Başlayacaksın.


Bunu geri alıyorum, dosya yüklemeleri artık çalışıyor. Şu anda yaşadığım sorun, sunucunun bir 302 vermesi ve rest-istemcinin RFC'yi takip etmesi (hiçbir tarayıcının yapmadığı) ve bir istisna atması (çünkü tarayıcıların bu davranış hakkında uyarması gerekiyor). Diğer alternatif ise kaldırım taşı ama pencerelere kaldırım yerleştirme şansım olmadı.
Matt Wolfe

7
API, ilk gönderildikten sonra biraz değişti, multipart artık şu şekilde çağrılıyor: RestClient.post ' localhost: 3000 / foo ',: upload => File.new ('/ path / tofile')) Bkz. Github.com/ daha fazla ayrıntı için archiloque / rest-client .
Clinton

2
rest_client, istek başlıklarının sağlanmasını desteklemez. Birçok REST uygulaması, belirli türde başlıklar gerektirir / bekler, bu nedenle geri kalan istemci bu durumda çalışmaz. Örneğin JIRA, bir X-Atlassian-Token belirteci gerektirir.
2013

Dosya yükleme ilerlemesini almak mümkün mü? örneğin% 40 yüklendi.
Ankush

1
gem install rest-clientVe require 'rest_client'parçalarını eklemek için +1 . Bu bilgi çok fazla yakut örneğinden kalmıştır.
dansalmo

36

Nick Sieger'ın çok bölümlü kitaplığı hakkında yeterince iyi şeyler söyleyemem.

Doğrudan Net :: HTTP'ye çok parçalı gönderme desteği ekleyerek, sizinkinden farklı hedefleri olabilecek sınırlar veya büyük kitaplıklar hakkında manuel olarak endişelenme ihtiyacınızı ortadan kaldırır.

İşte README'den nasıl kullanılacağına dair küçük bir örnek :

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

Kütüphaneye buradan göz atabilirsiniz: http://github.com/nicksieger/multipart-post

veya şununla kurun:

$ sudo gem install multipart-post

SSL ile bağlanıyorsanız, bağlantıyı şu şekilde başlatmanız gerekir:

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
Bu benim için yaptı, tam olarak aradığım şey ve tam olarak neyin bir mücevhere ihtiyaç duymadan dahil edilmesi gerektiği. Ruby çok ileride, ancak çok geride.
Trey

harika, bu bir Tanrı gönderimi olarak geliyor! bunu, dosya yüklemelerini desteklemek için OAuth cevherini maymunlamak için kullandı. beni sadece 5 dakika sürdü.
Matthias

@matthias OAuth taş ile fotoğraf yüklemeye çalışıyorum, ancak başarısız oldu. bana maymun ekinden bir örnek verebilir misin?
Hooopo

1
Yama, senaryoma oldukça özeldi (hızlı ve kirli), ancak ona bir göz atın ve belki daha genel bir yaklaşımla bazılarını bulabilirsiniz ( gist.github.com/974084 )
Matthias

3
Multipart, istek başlıklarını desteklemez. Dolayısıyla, örneğin JIRA REST arayüzünü kullanmak istiyorsanız, çok bölümlü sadece değerli bir zaman kaybı olacaktır.
2013

30

curbgörünüyor büyük bir çözüm gibi ama o ihtiyaçlarınızı karşılamıyorsa durumda, sen yapabilirsiniz ile bunu Net::HTTP. Çok parçalı form gönderisi, bazı ekstra başlıklarla birlikte dikkatlice biçimlendirilmiş bir dizedir. Görünüşe göre çok parçalı gönderi yapması gereken her Ruby programcısı bunun için kendi küçük kitaplığını yazıyor, bu da bu işlevselliğin neden yerleşik olmadığını merak etmeme neden oluyor. Belki öyledir ... Neyse, okuma zevkiniz için, devam edip çözümümü burada vereceğim. Bu kod, birkaç blogda bulduğum örneklere dayanıyor, ancak artık bağlantıları bulamadığım için üzgünüm. Bu yüzden sanırım tüm övgüyü kendime almalıyım ...

Bunun için yazdığım modül, bir karma Stringve Filenesneden form verilerini ve başlıkları oluşturmak için bir genel sınıf içeriyor . Bu nedenle, örneğin, "başlık" adlı bir dize parametresi ve "belge" adlı bir dosya parametresi olan bir form göndermek istiyorsanız, aşağıdakileri yaparsınız:

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

Sonra sadece normal do POSTile Net::HTTP:

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

Ya da başka bir şekilde POST. Önemli olan Multipart, göndermeniz gereken verileri ve başlıkları döndürmesidir. Ve bu kadar! Basit, değil mi? İşte Multipart modülünün kodu (cevhere ihtiyacınız var 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

Selam! Bu koddaki lisans nedir? Ayrıca: Bu gönderinin URL'sini en üstteki yorumlara eklemek güzel olabilir. Teşekkürler!
docwhat

5
Bu gönderideki kod WTFPL ( sam.zoy.org/wtfpl ) altında lisanslanmıştır . Zevk almak!
Cody Brimhall

filestream'i FileParamsınıfın ilklendirme çağrısına geçirmemelisiniz . to_multipartYöntemdeki atama , dosya içeriğini tekrar kopyalar, bu da gereksizdir! Bunun yerine sadece dosya tanımlayıcısını to_multipart
iletin

1
Bu kod BÜYÜK! Çünkü işe yarıyor. Rest-client ve Siegers Multipart-post DON'T, istek başlıklarını desteklemez. Talep başlıklarına ihtiyacınız varsa, rest-client ve Siegers Multipart gönderisiyle çok değerli zamanınızı boşa harcarsınız.
2013'ü

Aslında, @Onno, artık istek başlıklarını destekliyor. Eric'in cevabı hakkındaki yorumumu gör
alexanderbird

24

Yalnızca standart kitaplıkları kullanan bir başkası:

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

Pek çok yaklaşım denedim ama sadece bu benim için çalıştı.


3
Bunun için teşekkürler. Bir küçük nokta, satır 1 şöyle olmalıdır: uri = URI('https://some.end.point/some/path') Bu şekilde daha sonra hatasız uri.portve uri.hosthatasız arama yapabilirsiniz .
davidkovsky

1
Eğer diskten bir dosya yüklemek istemiyor tempfile eğer ve bir minör değişim, kullanmak gerekir File.opendeğilFile.read
Anıl Yanduri

1
çoğu durumda bir dosya adı gereklidir, bu nasıl eklediğim biçimdir: form_data = [['dosya', Dosya.read (dosya_adı), {dosyaadı: dosya_adı}]]
ZsJoska

4
bu doğru cevap. insanlar mümkün olduğunda ambalaj mücevherlerini kullanmayı bırakmalı ve temel bilgilere geri dönmelidir.
Carlos Roque

18

İşte bu yayında bulunan diğerlerini denedikten sonra çözümüm, TwitPic'e fotoğraf yüklemek için kullanıyorum:

  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
Biraz hile gibi görünse de, bu muhtemelen benim için en güzel çözüm, bu öneri için çok teşekkürler!
Bo Jeanes

Dikkatsiz olanlar için sadece bir not, media = @ ... curl olayını yapan şeydir ki ... sadece bir dize değil bir dosya. Ruby sözdizimiyle biraz kafa karıştırıcı, ancak @ # {photo.path}, #{@photo.path} ile aynı değil. Bu çözüm en iyi imho'lardan biridir.
Evgeny

7
Bu güzel görünüyor, ancak @ kullanıcı adınız "foo && rm -rf /" içeriyorsa, bu oldukça kötüleşir :-P
gaspard

8

2017'ye hızlı bir şekilde ilerleyin ruby stdlib net/http, bu 1.9.3'ten beri yerleşiktir

Net :: HTTPRequest # set_form): Hem application / x-www-form-urlencoded hem de multipart / form-data'yı desteklemek için eklendi.

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

Form verilerinin akışını IOdesteklemeyenleri bile kullanabiliriz :size.

Bu cevabın birine gerçekten yardımcı olabileceğini umuyoruz :)

Not: Bunu yalnızca Ruby 2.3.1'de test ettim


7

Tamam, işte bordür kullanan basit bir örnek.

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, RestClient :: Payload :: Multipart içinde create_file_field üzerine yazılıncaya kadar benim için çalışmadı.

Her bölümde, 'İçerik-Eğilim: form-veri' olması gereken bir 'İçerik-Eğilimli: çok bölümlü / form-veri' yaratıyordu .

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

İhtiyacınız olursa çatalım burada: git@github.com: kcrawford / rest-client.git


Bu, en son restclient'ta düzeltildi.

1

NetHttp ile çözümün bir dezavantajı vardır, çünkü büyük dosyaları gönderirken önce tüm dosyayı belleğe yükler.

Onunla biraz oynadıktan sonra aşağıdaki çözümü buldum:

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

Sınıf StreamPart nedir?
Marlin Pierce

1

ayrıca nick Sieger'ın çok parçalı gönderisi de uzun olası çözümler listesine eklenecek.


1
multipart-post, istek başlıklarını desteklemez.
2013

Aslında, @Onno, artık istek başlıklarını destekliyor. Eric'in cevabı hakkındaki yorumumu gör
alexanderbird

0

Aynı sorunu yaşadım (jboss web sunucusuna göndermem gerekiyor). Curb, kodda oturum değişkenlerini kullandığımda Ruby'nin çökmesine (ubuntu 8.10'da Ruby 1.8.7) neden olması dışında benim için iyi çalışıyor.

Geri kalan müşteri belgelerini araştırdım, çok parçalı desteğin göstergesini bulamadım. Yukarıdaki rest-client örneklerini denedim ama jboss http gönderisinin çok parçalı olmadığını söyledi.


0

Çok parçalı post mücevher, Rails 4 Net :: HTTP ile oldukça iyi çalışıyor, başka özel bir mücevher yok

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

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.