Ruby: Come pubblicare un file via HTTP come multipart / form-data?
Domanda
Voglio fare un POST HTTP che assomigli a un modulo HMTL pubblicato da un browser. In particolare, pubblica alcuni campi di testo e un campo file.
La pubblicazione di campi di testo è semplice, c'è un esempio proprio in rete / http rdocs, ma non riesco a capire come pubblicare un file con esso.
Net :: HTTP non sembra la migliore idea. marciapiede sta bene.
Soluzione
Mi piace RestClient . Incapsula net / http con funzioni interessanti come i dati di 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.
Altri suggerimenti
Non posso dire abbastanza cose positive sulla libreria multipart post di Nick Sieger.
Aggiunge il supporto per la pubblicazione multipart direttamente su Net :: HTTP, eliminando la necessità di preoccuparsi manualmente dei confini 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 consultare la libreria qui: http://github.com/nicksieger/multipart-post
o 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|
frenare
sembra un'ottima soluzione, ma nel caso in cui non soddisfi le tue esigenze, puoi farlo con Net :: HTTP
. Un post di modulo multipart è solo una stringa formattata con cura con alcune intestazioni extra. Sembra che ogni programmatore di 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 sia integrata. Forse è ... Comunque, per il tuo piacere di leggere, andrò avanti e darò la mia soluzione qui. Questo codice si basa su esempi che ho trovato su un paio di blog, ma mi dispiace di non poter più trovare i collegamenti. Quindi immagino che devo solo prendermi tutto il merito per me ...
Il modulo che ho scritto per questo contiene una classe pubblica, per generare i dati del modulo e le intestazioni da un hash di oggetti String
e File
. Ad esempio, se desideri pubblicare un modulo con un parametro stringa denominato " title " e un parametro di file chiamato " document " ;, dovresti fare quanto segue:
#prepare the query
data, headers = Multipart::Post.prepare_query("title" => my_string, "document" => my_file)
Quindi fai semplicemente un POST
normale con 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 vuoi fare il POST
. Il punto è che Multipart
restituisce i dati e le intestazioni che è necessario inviare. E questo è tutto! Semplice vero? Ecco il codice per il modulo Multipart (è necessaria la gemma 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
Ecco la mia soluzione dopo averne provate 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
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.
Ok, ecco un semplice esempio usando il marciapiede.
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]
Avanti veloce al 2017, ruby ??
stdlib
net / http
ha questo built-in dall'1.9.3
Net :: HTTPRequest # set_form): aggiunto per supportare sia application / x-www-form-urlencoded che multipart / form-data.
https://ruby-doc.org/stdlib-2.3.1/libdoc/net/http/rdoc/Net/HTTPHeader.html#method-i-set_form
Possiamo persino usare IO
che non supporta : size
per lo streaming dei dati del modulo.
Sperando che questa risposta possa davvero aiutare qualcuno :)
P.S. L'ho provato solo in ruby ??2.3.1
restclient non ha funzionato per me fino a quando non ho scavalcato create_file_field in RestClient :: Payload :: Multipart.
Stava creando un "Disposizione dei contenuti: multipart / form-data" in ogni parte dove dovrebbe essere 'Disposizione dei contenuti: form-data' .
http://www.ietf.org/rfc/rfc2388.txt
Il mio fork è qui se ne hai bisogno: git@github.com: kcrawford / rest-client.git
Beh, 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
c'è anche il multipart-post di nick sieger da aggiungere alla lunga lista di possibili soluzioni.
Ho avuto lo stesso problema (è necessario pubblicare sul server Web jboss). Curb funziona bene per me, tranne per il fatto che ha causato l'arresto anomalo di ruby ??(ruby 1.8.7 su Ubuntu 8.10) quando uso variabili di sessione nel codice.
Scavo nei documenti del resto 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.
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