Ruby: come inviare un file tramite HTTP come multipart / form-data?


113

Voglio fare un HTTP POST che assomigli a un modulo HMTL pubblicato da un browser. In particolare, inserisci alcuni campi di testo e un campo file.

La pubblicazione di campi di testo è semplice, c'è un esempio proprio lì in net / http rdocs, ma non riesco a capire come inserire un file insieme ad esso.

Net :: HTTP non sembra l'idea migliore. il marciapiede ha un bell'aspetto.

Risposte:


103

Mi piace RestClient . Incapsula net / http con funzioni interessanti come i dati dei moduli multipart:

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

Supporta anche lo streaming.

gem install rest-client ti farà iniziare.


Lo riprendo, i caricamenti di file ora funzionano. Il problema che sto riscontrando ora è che il server fornisce un 302 e il rest-client segue l'RFC (che nessun browser fa) e lancia un'eccezione (poiché i browser dovrebbero avvertire di questo comportamento). L'altra alternativa è il marciapiede, ma non ho mai avuto fortuna nell'installare il marciapiede in Windows.
Matt Wolfe

7
L'API è cambiata leggermente da quando è stata pubblicata per la prima volta, ora multipart viene richiamato come: RestClient.post ' localhost: 3000 / foo ',: upload => File.new ('/ path / tofile')) Vedi github.com/ archiloque / rest-client per maggiori dettagli.
Clinton

2
rest_client non supporta la fornitura di intestazioni di richiesta. Molte applicazioni REST richiedono / prevedono un tipo specifico di intestazioni, quindi il client rest non funzionerà in quel caso. Ad esempio JIRA richiede un token X-Atlassian-Token.
onknows

È possibile ottenere l'avanzamento del caricamento del file? ad es. viene caricato il 40%.
Ankush

1
+1 per aggiungere le parti gem install rest-cliente require 'rest_client'. Quelle informazioni sono tralasciate da troppi esempi di rubino.
dansalmo

36

Non posso dire abbastanza cose positive sulla libreria multipart-post di Nick Sieger.

Aggiunge il supporto per la pubblicazione in più parti direttamente su Net :: HTTP, eliminando la necessità di preoccuparsi manualmente dei limiti o delle grandi librerie che potrebbero avere obiettivi diversi dai propri.

Ecco un piccolo esempio su come usarlo dal 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

Puoi controllare la libreria qui: http://github.com/nicksieger/multipart-post

oppure installalo con:

$ sudo gem install multipart-post

Se ti connetti tramite SSL devi avviare la connessione in questo modo:

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
Quello lo ha fatto per me, esattamente quello che stavo cercando ed esattamente quello che dovrebbe essere incluso senza la necessità di una gemma. Ruby è così avanti, eppure così indietro.
Trey

fantastico, questo arriva come un messaggio di Dio! usato questo per monkeypatch la gemma OAuth per supportare i caricamenti di file. mi ci sono voluti solo 5 minuti.
Matthias

@ matthias Sto cercando di caricare una foto con la gemma OAuth, ma non è riuscito. potresti farmi qualche esempio del tuo monkeypatch?
Hooopo

1
La patch era abbastanza specifica per il mio script (veloce e sporca), ma dai un'occhiata e forse puoi crearne qualcuno con un approccio più generico ( gist.github.com/974084 )
Matthias

3
Multipart non supporta le intestazioni delle richieste. Quindi, se ad esempio vuoi usare l'interfaccia JIRA REST, il multipart sarà solo una perdita di tempo prezioso.
onknows

30

curbsembra un'ottima soluzione, ma nel caso in cui non soddisfi le tue esigenze, puoi farlo con Net::HTTP. Un post in un modulo multiparte è solo una stringa formattata con cura con alcune intestazioni extra. Sembra che ogni programmatore Ruby che ha bisogno di fare post in più parti finisca per scrivere la propria piccola libreria per questo, il che mi fa chiedere perché questa funzionalità non è integrata. Forse è ... Comunque, per il tuo piacere di lettura, vado avanti e fornisco la mia soluzione qui. Questo codice si basa su esempi che ho trovato su un paio di blog, ma mi dispiace di non riuscire più a trovare i collegamenti. Quindi immagino di dover prendere tutto il merito per me stesso ...

Il modulo che ho scritto per questo contiene una classe pubblica, per generare i dati del modulo e le intestazioni da un hash di Stringe Fileoggetti. Quindi, ad esempio, se volessi pubblicare un modulo con un parametro stringa denominato "titolo" e un parametro file denominato "documento", dovresti fare quanto segue:

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

Quindi fai solo una normale POSTcon Net::HTTP:

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

O comunque tu voglia fare il file POST. Il punto è che Multipartrestituisce i dati e le intestazioni che devi inviare. E questo è tutto! Semplice, vero? Ecco il codice per il modulo Multipart (serve la mime-typesgemma):

# 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

Ciao! Qual è la licenza su questo codice? Inoltre: potrebbe essere carino aggiungere l'URL di questo post nei commenti in alto. Grazie!
docwhat

5
Il codice in questo post è concesso in licenza con WTFPL ( sam.zoy.org/wtfpl ). Godere!
Cody Brimhall

non si dovrebbe passare il filestream nella chiamata di inizializzazione della FileParamclasse. L'assegnazione nel to_multipartmetodo copia nuovamente il contenuto del file, il che non è necessario! Invece passa solo il descrittore di file e leggi da esso into_multipart
mober

1
Questo codice è FANTASTICO! Perché funziona. Rest-client e Siegers Multipart-post NON supportano le intestazioni delle richieste. Se hai bisogno di intestazioni di richiesta, sprecherai molto tempo prezioso con rest-client e Siegers Multipart post.
onknows

In realtà, @Onno, ora supporta le intestazioni delle richieste. Vedi il mio commento sulla risposta di
Eric

24

Un altro che utilizza solo librerie standard:

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

Ho provato molti approcci ma solo questo ha funzionato per me.


3
Grazie per questo. Un punto minore, la riga 1 dovrebbe essere: in uri = URI('https://some.end.point/some/path') questo modo puoi chiamare uri.porte uri.hostsenza errori in seguito.
davidkovsky,

1
una piccola modifica, se non tempfile e vuoi caricare un file dal tuo disco, dovresti usare File.opennoFile.read
Anil Yanduri

1
nella maggior parte dei casi è richiesto un nome file, questo è il modulo in cui ho aggiunto: form_data = [['file', File.read (file_name), {filename: file_name}]]
ZsJoska

4
questa è la risposta corretta. le persone dovrebbero smettere di usare le gemme wrapper quando possibile e tornare alle basi.
Carlos Roque

18

Ecco la mia soluzione dopo aver provato altre disponibili in questo post, la sto usando per caricare foto su 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
Nonostante sembri un po 'hacker, questa è probabilmente la soluzione più carina per me, quindi grazie mille per questo suggerimento!
Bo Jeanes,

Solo una nota per gli incauti, il media = @ ... è ciò che rende curl cosa che ... è un file e non solo una stringa. Un po 'confuso con la sintassi di ruby, ma @ # {photo.path} non è la stessa cosa di #{@photo.path}. Questa soluzione è una delle migliori imho.
Evgeny

7
Sembra carino ma se il tuo @username contiene "foo && rm -rf /", diventa piuttosto brutto :-P
gaspard


7

Ok, ecco un semplice esempio di utilizzo del cordolo.

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 non ha funzionato per me fino a quando non ho sovrascritto create_file_field in RestClient :: Payload :: Multipart.

Stava creando un "Content-Disposition: multipart / form-data" in ogni parte dove dovrebbe essere "Content-Disposition: form-data" .

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

Il mio fork è qui se ne hai bisogno: git@github.com: kcrawford / rest-client.git


Questo problema è stato risolto nell'ultimo client rest.

1

Bene, la soluzione con NetHttp ha uno svantaggio che quando si pubblicano file di grandi dimensioni carica prima l'intero file in memoria.

Dopo averci giocato un po 'ho trovato la seguente soluzione:

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

Cos'è Class StreamPart?
Marlin Pierce

1

c'è anche il multipart-post di nick sieger da aggiungere alla lunga lista di possibili soluzioni.


1
multipart-post non supporta le intestazioni delle richieste.
onknows

In realtà, @Onno, ora supporta le intestazioni delle richieste. Vedi il mio commento sulla risposta di
Eric

0

Ho avuto lo stesso problema (è necessario inviare messaggi al server web jboss). Curb funziona bene per me, tranne per il fatto che ha causato il crash di ruby ​​(ruby 1.8.7 su ubuntu 8.10) quando utilizzo le variabili di sessione nel codice.

Ho scavato nella documentazione di rest-client, non sono riuscito a trovare l'indicazione del supporto multipart. Ho provato gli esempi di rest-client sopra ma jboss ha detto che il post http non è multipart.


0

La gemma multipart-post funziona abbastanza bene con Rails 4 Net :: HTTP, nessun'altra gemma speciale

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.