Ruby: Làm thế nào để đăng một tệp qua HTTP dưới dạng dữ liệu đa phần / biểu mẫu?


113

Tôi muốn thực hiện một HTTP POST trông giống như một biểu mẫu HMTL được đăng từ trình duyệt. Cụ thể, đăng một số trường văn bản và trường tệp.

Đăng các trường văn bản rất đơn giản, có một ví dụ ngay trong net / http rdocs, nhưng tôi không thể tìm ra cách đăng một tệp cùng với nó.

Net :: HTTP không giống như ý tưởng tốt nhất. lề đường có vẻ tốt.

Câu trả lời:


103

Tôi thích RestClient . Nó đóng gói net / http với các tính năng thú vị như dữ liệu biểu mẫu nhiều phần:

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

Nó cũng hỗ trợ phát trực tuyến.

gem install rest-client sẽ giúp bạn bắt đầu.


Tôi rút lại điều đó, tải lên tệp hiện đã hoạt động. Vấn đề tôi gặp phải bây giờ là máy chủ đưa ra 302 và máy khách nghỉ theo RFC (không có trình duyệt nào làm được) và ném ra một ngoại lệ (vì các trình duyệt phải cảnh báo về hành vi này). Có một giải pháp thay thế khác là lề đường nhưng tôi chưa bao giờ gặp may mắn khi cài đặt lề đường trong cửa sổ.
Matt Wolfe

7
API đã thay đổi một chút kể từ lần đầu tiên được đăng, nhiều phần bây giờ được gọi như: RestClient.post ' localhost: 3000 / foo ',: upload => File.new ('/ path / tofile')) Xem github.com/ archiloque / rest-client để biết thêm chi tiết.
Clinton

2
rest_client không hỗ trợ cung cấp tiêu đề yêu cầu. Nhiều ứng dụng REST yêu cầu / mong đợi loại tiêu đề cụ thể nên ứng dụng khách nghỉ sẽ không hoạt động trong trường hợp đó. Ví dụ: JIRA yêu cầu mã thông báo X-Atlassian-Token.
onknows

Có thể nhận được tiến trình tải lên tệp không? ví dụ: 40% được tải lên.
Ankush ngày

1
+1 để thêm gem install rest-clientrequire 'rest_client'các bộ phận. Thông tin đó bị bỏ sót trong quá nhiều ví dụ về ruby.
dansalmo

36

Tôi không thể nói đủ những điều tốt đẹp về thư viện nhiều bài viết của Nick Sieger.

Nó bổ sung hỗ trợ đăng nhiều phần trực tiếp lên Net :: HTTP, giúp bạn không cần phải lo lắng về các ranh giới hoặc thư viện lớn có thể có các mục tiêu khác với mục tiêu của riêng bạn.

Dưới đây là một ví dụ nhỏ về cách sử dụng nó từ 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

Bạn có thể xem thư viện tại đây: http://github.com/nicksieger/multipart-post

hoặc cài đặt nó với:

$ sudo gem install multipart-post

Nếu bạn đang kết nối qua SSL, bạn cần bắt đầu kết nối như sau:

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
Điều đó đã làm điều đó cho tôi, chính xác những gì tôi đang tìm kiếm và chính xác những gì nên được bao gồm mà không cần đá quý. Ruby đang đi trước rất xa nhưng lại bị tụt lại rất xa.
Trey

tuyệt vời, điều này đến như một sự gửi gắm của Chúa! đã sử dụng điều này để bắt cặp đá quý OAuth nhằm hỗ trợ tải lên tệp. chỉ mất 5 phút cho tôi.
Matthias

@matthias Tôi đang cố tải lên ảnh bằng OAuth gem nhưng không thành công. bạn có thể cho tôi một số ví dụ về con khỉ của bạn?
Hooopo

1
Bản vá khá cụ thể đối với tập lệnh của tôi (nhanh chóng và bẩn thỉu), nhưng hãy xem nó và có thể bạn có thể thực hiện một số phương pháp tiếp cận chung chung hơn ( gist.github.com/974084 )
Matthias

3
Multipart không hỗ trợ tiêu đề yêu cầu. Vì vậy, nếu bạn muốn sử dụng giao diện JIRA REST chẳng hạn, nhiều phần sẽ chỉ là một sự lãng phí thời gian quý báu.
onknows

30

curbcó vẻ như là một giải pháp tuyệt vời, nhưng trong trường hợp nó không đáp ứng được nhu cầu của bạn, bạn có thể làm điều đó vớiNet::HTTP . Bài đăng dạng nhiều phần chỉ là một chuỗi được định dạng cẩn thận với một số tiêu đề bổ sung. Có vẻ như mọi lập trình viên Ruby cần thực hiện các bài viết nhiều phần đều phải viết thư viện nhỏ của riêng họ cho nó, điều này khiến tôi tự hỏi tại sao chức năng này không được tích hợp sẵn. Có lẽ nó là ... Dù sao, vì niềm vui đọc của bạn, tôi sẽ tiếp tục và đưa ra giải pháp của tôi ở đây. Mã này dựa trên các ví dụ tôi tìm thấy trên một vài blog, nhưng tôi rất tiếc vì tôi không thể tìm thấy các liên kết nữa. Vì vậy, tôi đoán tôi chỉ phải nhận tất cả công lao cho bản thân ...

Mô-đun tôi đã viết cho điều này chứa một lớp công khai, để tạo dữ liệu biểu mẫu và tiêu đề từ một băm của StringFilecác đối tượng. Vì vậy, ví dụ: nếu bạn muốn đăng một biểu mẫu có tham số chuỗi có tên "title" và tham số tệp có tên "document", bạn sẽ làm như sau:

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

Sau đó, bạn chỉ cần làm một điều bình thường POSTvới Net::HTTP:

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

Hoặc tuy nhiên bạn muốn làm điều đó POST. Vấn đề là Multiparttrả về dữ liệu và tiêu đề mà bạn cần gửi. Và đó là nó! Đơn giản, phải không? Đây là mã cho mô-đun Multipart (bạn cần mime-typesđá quý):

# 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

Chào! Giấy phép trên mã này là gì? Ngoài ra: Có thể rất hay khi thêm URL cho bài đăng này trong phần bình luận ở trên cùng. Cảm ơn!
docwhat

5
Mã trong bài đăng này được cấp phép theo WTFPL ( sam.zoy.org/wtfpl ). Thưởng thức!
Cody Brimhall, 14/10/10

bạn không nên chuyển luồng lọc vào lệnh gọi khởi tạo của FileParamlớp. Phép gán trong to_multipartphương thức sao chép lại nội dung tệp, điều này là không cần thiết! Thay vào đó, chỉ chuyển trình mô tả tệp và đọc từ nó trongto_multipart
mober

1
Mã này là TUYỆT VỜI! Bởi vì nó hoạt động. Rest-client và Siegers Multipart-post KHÔNG hỗ trợ tiêu đề yêu cầu. Nếu bạn cần tiêu đề yêu cầu, bạn sẽ lãng phí rất nhiều thời gian quý báu với phần còn lại của client và bài đăng của Siegers Multipart.
onknows

Trên thực tế, @Onno, nó hiện hỗ trợ tiêu đề yêu cầu. Xem nhận xét của tôi về câu trả lời của eric
alexanderbird

24

Một cái khác chỉ sử dụng các thư viện tiêu chuẩn:

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

Đã thử rất nhiều cách tiếp cận nhưng chỉ có cách này phù hợp với tôi.


3
Cảm ơn vì điều đó. Một điểm nhỏ, dòng 1 nên là: uri = URI('https://some.end.point/some/path') Bằng cách đó bạn có thể gọi uri.porturi.hostkhông bị lỗi sau này.
davidkovsky

1
một sự thay đổi nhỏ, nếu không muốn nói tempfile và bạn muốn tải lên một tập tin từ đĩa của bạn, bạn nên sử dụng File.openkhôngFile.read
Anil Yanduri

1
hầu hết các trường hợp, một tên tập tin là cần thiết, đây là hình thức làm thế nào tôi nói thêm: form_data = [[ 'file', File.read (file_name), {filename: file_name}]]
ZsJoska

4
Đây là câu trả lời chính xác. mọi người nên ngừng sử dụng đá quý wrapper khi có thể và quay lại những điều cơ bản.
Carlos Roque

18

Đây là giải pháp của tôi sau khi thử những giải pháp khác có sẵn trên bài đăng này, tôi đang sử dụng nó để tải ảnh lên 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
Mặc dù có vẻ hơi hackish, nhưng đây có lẽ là giải pháp tốt nhất đối với tôi, rất cảm ơn vì gợi ý này!
Bo Jeanes

Chỉ cần lưu ý cho những người không cẩn thận, media = @ ... là thứ khiến curl ... là một tệp chứ không chỉ là một chuỗi. Hơi khó hiểu với cú pháp ruby, nhưng @ # {photo.path} không giống với #{@photo.path}. Giải pháp này là một trong những imho tốt nhất.
Evgeny

7
Vẻ đẹp này nhưng nếu @username của bạn có chứa "foo && rm -rf /", điều này trở nên khá xấu :-P
Gaspard

8

Tua nhanh đến năm 2017, tính năng ruby stdlib net/httpnày đã được tích hợp từ 1.9.3

Net :: HTTPRequest # set_form): Được thêm vào để hỗ trợ cả ứng dụng / x-www-form-urlencoded và multiart / form-data.

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

Chúng tôi thậm chí có thể sử dụng IOmà không hỗ trợ :sizeđể truyền dữ liệu biểu mẫu.

Hy vọng rằng câu trả lời này thực sự có thể giúp một ai đó :)

PS Tôi chỉ thử nghiệm điều này trong ruby ​​2.3.1


7

Ok, đây là một ví dụ đơn giản sử dụng lề đường.

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 không hoạt động đối với tôi cho đến khi tôi ghi đè create_file_field trong RestClient :: Payload :: Multipart.

Nó đang tạo một 'Nội dung-Bố trí: nhiều phần / biểu mẫu-dữ liệu' trong mỗi phần mà nó phải là "Bố trí Nội dung: biểu mẫu-dữ liệu" .

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

Nĩa của tôi ở đây nếu bạn cần: git@github.com: kcrawford / rest-client.git


Điều này được khắc phục trong restclient mới nhất.

1

Giải pháp với NetHttp có một nhược điểm là khi đăng các tệp lớn, nó sẽ tải toàn bộ tệp vào bộ nhớ trước.

Sau khi chơi một chút với nó, tôi đã đưa ra giải pháp sau:

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

Class StreamPart là gì?
Marlin Pierce

1

cũng có nhiều phần của nick sieger để thêm vào danh sách dài các giải pháp khả thi.


1
multiart-post không hỗ trợ tiêu đề yêu cầu.
onknows

Trên thực tế, @Onno, nó hiện hỗ trợ tiêu đề yêu cầu. Xem nhận xét của tôi về câu trả lời của eric
alexanderbird

0

Tôi đã gặp vấn đề tương tự (cần đăng lên máy chủ web jboss). Kiềm chế hoạt động tốt đối với tôi, ngoại trừ việc nó khiến ruby ​​bị lỗi (ruby 1.8.7 trên ubuntu 8.10) khi tôi sử dụng các biến phiên trong mã.

Tôi tìm hiểu các tài liệu dành cho khách hàng còn lại, không thể tìm thấy dấu hiệu hỗ trợ nhiều phần. Tôi đã thử các ví dụ khách hàng còn lại ở trên nhưng jboss cho biết bài đăng http không phải là nhiều phần.


0

Đá quý nhiều phần sau hoạt động khá tốt với Rails 4 Net :: HTTP, không có đá quý đặc biệt nào khác

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

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.