Ruby : HTTP를 통해 multipart / form-data로 파일을 게시하는 방법은 무엇입니까?


112

브라우저에서 게시 된 HMTL 양식처럼 보이는 HTTP POST를 수행하고 싶습니다. 특히 일부 텍스트 필드와 파일 필드를 게시하십시오.

텍스트 필드를 게시하는 것은 간단합니다. net / http rdocs에 예제가 있지만 함께 파일을 게시하는 방법을 알 수 없습니다.

Net :: HTTP는 최선의 생각이 아닙니다. 연석 이 좋아 보인다.

답변:


102

나는 RestClient를 좋아 한다 . 멀티 파트 양식 데이터와 같은 멋진 기능으로 net / http를 캡슐화합니다.

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

스트리밍도 지원합니다.

gem install rest-client 시작합니다.


다시 가져 가면 파일 업로드가 작동합니다. 내가 지금 가지고있는 문제는 서버가 302를 제공하고 나머지 클라이언트는 RFC를 따르고 (브라우저가하지 않음) 예외를 던집니다 (브라우저 가이 동작에 대해 경고해야하기 때문에). 다른 대안은 연석이지만 창문에 연석을 설치하는 데 행운이 없었습니다.
Matt Wolfe

7
API가 처음 게시 된 이후로 약간 변경되었습니다. 이제 multipart가 다음과 같이 호출됩니다. RestClient.post ' localhost : 3000 / foo ', : upload => File.new ( '/ path / tofile')) github.com/ 참조 자세한 내용은 archiloque / rest-client 를 참조하세요.
Clinton

2
rest_client는 요청 헤더 제공을 지원하지 않습니다. 많은 REST 애플리케이션은 특정 유형의 헤더를 요구 / 예상하므로 나머지 클라이언트는이 경우 작동하지 않습니다. 예를 들어 JIRA에는 X-Atlassian-Token 토큰이 필요합니다.
onknows

파일 업로드 진행률을 얻을 수 있습니까? 예 : 40 %가 업로드됩니다.
Ankush 2014 년

1
gem install rest-clientrequire 'rest_client'부품 을 추가하려면 +1 . 그 정보는 너무 많은 루비 예제에서 제외되었습니다.
dansalmo

36

Nick Sieger의 multipart-post 라이브러리에 대해 충분히 좋은 말을 할 수 없습니다.

Net :: HTTP에 직접 멀티 파트 게시에 대한 지원을 추가하여 자신과 다른 목표를 가질 수있는 경계 또는 큰 라이브러리에 대해 수동으로 걱정할 필요가 없습니다.

다음은 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

여기에서 라이브러리를 확인할 수 있습니다 : http://github.com/nicksieger/multipart-post

또는 다음과 함께 설치하십시오.

$ sudo gem install multipart-post

SSL을 통해 연결하는 경우 다음과 같이 연결을 시작해야합니다.

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
그것은 나를 위해 해냈고, 정확히 내가 찾던 것과 보석이 필요없이 포함되어야 할 것입니다. 루비는 훨씬 앞서 있지만 훨씬 뒤처져 있습니다.
Trey

굉장합니다, 이것은 하나님이 보내실 때 온 것입니다! 파일 업로드를 지원하기 위해 OAuth gem을 monkeypatch하는 데 사용했습니다. 5 분 밖에 걸리지 않았습니다.
Matthias 2011 년

@matthias OAuth gem으로 사진을 업로드하려고하는데 실패했습니다. 멍키 패치의 예를 좀 들어 주 시겠어요?
Hooopo

1
패치는 내 스크립트 (빠르고 더러움)에 매우 구체적 이었지만 좀 더 일반적인 접근 방식을 사용할 수도 있습니다 ( gist.github.com/974084 )
Matthias

3
Multipart는 요청 헤더를 지원하지 않습니다. 예를 들어 JIRA REST 인터페이스를 사용하려는 경우 멀티 파트는 귀중한 시간 낭비입니다.
onknows

30

curb외모는 훌륭한 솔루션을 좋아하지만, 그것은 당신의 요구 사항을 충족하지 않는 경우에, 당신은 할 수 와 함께 할 Net::HTTP. 다중 부분 양식 게시물은 몇 가지 추가 헤더가있는 신중하게 형식화 된 문자열입니다. 여러 부분으로 된 게시물을 작성해야하는 모든 Ruby 프로그래머는 결국 자신의 작은 라이브러리를 작성하는 것 같습니다.이 기능이 왜 내장되어 있지 않은지 궁금합니다. 아마도 ... 어쨌든, 당신의 독서의 즐거움을 위해, 여기에 내 해결책을 제시 할 것입니다. 이 코드는 몇 개의 블로그에서 찾은 예제를 기반으로하지만 더 이상 링크를 찾을 수 없습니다. 그래서 저는 제 자신에 대한 모든 공로를 인정해야합니다 ...

내가 작성한 모듈에는 해시 StringFile개체 에서 양식 데이터와 헤더를 생성하기위한 하나의 공용 클래스가 포함되어 있습니다. 예를 들어 "title"이라는 문자열 매개 변수와 "document"라는 파일 매개 변수가있는 양식을 게시하려면 다음을 수행합니다.

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

그런 다음 다음으로 정상 POST을 수행하십시오 Net::HTTP.

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

또는 그러나 POST. 요점은 Multipart전송해야하는 데이터와 헤더 를 반환한다는 것입니다. 그리고 그게 다야! 간단 하지요? 다음은 Multipart 모듈의 코드입니다 ( 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

안녕하세요! 이 코드의 라이선스는 무엇입니까? 또한 : 상단의 댓글에이 게시물의 URL을 추가하는 것이 좋습니다. 감사!
docwhat

5
이 게시물의 코드는 WTFPL ( sam.zoy.org/wtfpl )에 따라 사용이 허가되었습니다 . 즐겨!
Cody Brimhall

FileParam클래스 의 초기화 호출에 파일 스트림을 전달해서는 안됩니다 . to_multipart메서드 의 할당 은 파일 내용을 다시 복사하므로 불필요합니다! 대신 단지 파일 기술자를 전달하고 그것을 읽기to_multipart
mober

1
이 코드는 훌륭합니다! 작동하기 때문입니다. Rest-client 및 Siegers Multipart-post는 요청 헤더를 지원하지 않습니다. 요청 헤더가 필요한 경우 나머지 클라이언트 및 Siegers Multipart 게시물에 귀중한 시간을 많이 낭비하게됩니다.
onknows 2013-09-11

실제로 @Onno, 이제 요청 헤더를 지원합니다. 에릭의 대답에 내 의견을 참조하십시오
alexanderbird

24

표준 라이브러리 만 사용하는 또 다른 하나 :

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

많은 접근 방식을 시도했지만 이것 만이 저에게 효과적이었습니다.


3
감사합니다. 하나의 작은 점, 라인 1은되어야합니다 : uri = URI('https://some.end.point/some/path') 당신이 호출 할 수있는 방법 uri.porturi.host나중에 오류없이.
davidkovsky

1
당신이 당신의 디스크에서 파일을 업로드하고 싶지 임시 파일 경우 하나의 작은 변화, 당신은 사용해야 File.open하지File.read
아닐 Yanduri

1
대부분의 경우 파일 이름이 필요합니다. 이것은 제가 추가 한 양식입니다. form_data = [[ 'file', File.read (file_name), {filename : file_name}]]
ZsJoska

4
이것은 정답입니다. 사람들은 가능하면 래퍼 젬 사용을 중단하고 기본으로 돌아 가야합니다.
Carlos Roque

18

이 게시물에서 사용할 수있는 다른 솔루션을 시도한 후 내 솔루션은 다음과 같습니다. 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
약간 엉망인 것처럼 보임에도 불구하고 이것은 아마도 저에게 가장 좋은 해결책 일 것입니다.이 제안에 대해 대단히 감사합니다!
Bo Jeanes

주의하지 않는 것에 대한 메모 일뿐입니다. media = @ ...는 컬이 ... 문자열이 아니라 파일이라는 것을 만드는 것입니다. 루비 구문과 약간 혼동되지만 @ # {photo.path}는 #{@photo.path}와 다릅니다. 이 솔루션은 최고의 imho 중 하나입니다.
Evgeny

7
이 외모에 멋진하지만 @username가 포함 된 경우 "foo는 && RF RM은 /", 이것은 아주 나쁜 - P 얻는다
가스파르

8

2017 년으로 빨리 감기, ruby stdlib net/http1.9.3부터이 기능이 내장되어 있습니다.

Net :: HTTPRequest # set_form) : application / x-www-form-urlencoded 및 multipart / form-data를 모두 지원하기 위해 추가되었습니다.

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

양식 데이터 스트리밍을 IO지원하지 않는 것을 사용할 수도 있습니다 :size.

이 답변이 누군가를 정말로 도울 수 있기를 바랍니다. :)

추신 나는 이것을 루비 2.3.1에서만 테스트했습니다.


7

자, 연석을 사용한 간단한 예가 있습니다.

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 :: Payload :: Multipart에서 create_file_field를 덮어 쓸 때까지 restclient가 작동하지 않았습니다.

이는 생성 된 '다중 / 폼 데이터 내용 - 처리 " 가되어야 각 부분에서 '형태 데이터 콘텐츠 처리를 ' .

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

필요한 경우 내 포크가 있습니다. git@github.com : kcrawford / rest-client.git


이것은 최신 restclient에서 수정되었습니다.

1

NetHttp의 솔루션은 큰 파일을 게시 할 때 전체 파일을 먼저 메모리에로드한다는 단점이 있습니다.

약간 놀아 본 후 다음과 같은 해결책을 찾았습니다.

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

클래스 StreamPart는 무엇입니까?
Marlin Pierce

1

가능한 솔루션의 긴 목록에 추가 할 nick sieger의 multipart-post 도 있습니다.


1
multipart-post는 요청 헤더를 지원하지 않습니다.
onknows

실제로 @Onno, 이제 요청 헤더를 지원합니다. 에릭의 대답에 내 의견을 참조하십시오
alexanderbird

0

나는 같은 문제가 있었다 (jboss 웹 서버에 게시해야 함). Curb는 코드에서 세션 변수를 사용할 때 Ruby가 충돌 (우분투 8.10의 경우 Ruby 1.8.7)하는 것을 제외하고는 잘 작동합니다.

나머지 클라이언트 문서를 파헤 치고 멀티 파트 지원 표시를 찾을 수 없습니다. 위의 나머지 클라이언트 예제를 시도했지만 jboss는 http 게시물이 다중 부분이 아니라고 말했습니다.


0

multipart-post gem은 Rails 4 Net :: HTTP와 잘 작동하며 다른 특별한 gem은 없습니다.

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

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.