Search code examples
ruby-on-railsjrubypaperclipmultipart

can HTTP multipart/mixed xml part be converted to a Hash when mixed with other parts


The question is just above the last code snippet. Thank you. (environment details are the end )

posts_controller.rb

class PostsController < ApplicationController  
def create
    @post = Post.new(params[:post])
    respond_to do |format|
      format.xml { render :xml => @post.to_xml(:include => [ :assets])}
end
end

posts.rb

class Post < ActiveRecord::Base
  has_many    :assets, :as => :attachable, :dependent => :destroy
end

asset.rb

class Asset < ActiveRecord::Base
  belongs_to :attachable, :polymorphic => true
  has_attached_file :data,
                    :url  => "/assets/:id",
                    :path =>":rails_root/assets/:id_partition/:style/:basename.:extension"
  def name
    data_file_name
  end

  def content_type
    data_content_type
  end

  def file_size
    data_file_size
  end
  end

now when we post this information

POST /posts.xml HTTP/1.1
Accept-Encoding: gzip,deflate
Accept: application/xml
Content-Type: application/xml
User-Agent: Jakarta Commons-HttpClient/3.1
Host: localhost:8080
Content-Length: 60

<post><body>postbody</body><title>post_title</title></post>

a post entry gets created and when I post this

POST /posts.xml HTTP/1.1
Content-type: multipart/mixed; boundary=---------------------------7d226f700d0
Accept: application/xml,text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Cache-Control: no-cache
Pragma: no-cache
User-Agent: Java/1.6.0_21
Host: 192.168.0.105:8080
Connection: keep-alive
Content-Length: 1710

-----------------------------7d226f700d0
content-disposition: form-data; name="post[title]"
Content-Length: 10
post_title
-----------------------------7d226f700d0
content-disposition: form-data; name="post[body]"
Content-Length: 8
postbody
-----------------------------7d226f700d0
content-disposition: form-data; name="post[assets_attributes][0][data]"; filename="C:/Users/mv288/files/1.txt"
content-type: application/octet-stream
ÿþ
sample file content
-----------------------------7d226f700d0
content-disposition: form-data; name="post[assets_attributes][0][data]"; filename="C:/Users/mv288/Pictures/1.txt"
content-type: application/octet-stream
ÿþ
sample file content
-----------------------------7d226f700d0

a new post gets created with 2 file attachments.

now the question is, I want to get the following HTTP post ( please notice the xml part before the file attachments) to also create a post with 2 attachments, with no additional changes ( to posts_controller or routes.rb). is that possible?

POST /posts.xml HTTP/1.1
Content-type: multipart/mixed; boundary=---------------------------7d226f700d0
Accept: application/xml,text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Cache-Control: no-cache
Pragma: no-cache
User-Agent: Java/1.6.0_21
Host: 192.168.0.105:8080
Connection: keep-alive
Content-Length: 1710

-----------------------------7d226f700d0
Content-type: application/xml; charset=UTF-8
Content-Length: 59
<post><body>postbody</body><title>post_title</title></post>
-----------------------------7d226f700d0
content-disposition: form-data; name="post[assets_attributes][0][data]"; filename="C:/Users/mv288/files/1.txt"
content-type: application/octet-stream
ÿþ
sample file content
-----------------------------7d226f700d0
content-disposition: form-data; name="post[assets_attributes][0][data]"; filename="C:/Users/mv288/Pictures/1.txt"
content-type: application/octet-stream
ÿþ
sample file content
-----------------------------7d226f700d0Blockquotetest

using jruby 1.5.2/jdk1.6, rails 2.3.4, paperclip-2.3.3 on windows 2007 - 64 bit


Solution

  • At least rails 2.3.4 does not do this automatically. One needs to write a multipart/related parser and register it, inside initializers/mime_types.rb

    NOTE : Feel free to update the hard coded values( like main and attachment part prefix etc.,) in your copy.

    This same strategy can be used for multipart/related content as well. We are still debating whether to use multipart/related or multipart/mixed in this example below.

    Microsoft's notes on this subject. http://msdn.microsoft.com/en-us/library/ms527355(EXCHG.10).aspx

    Mime::Type.register "multipart/mixed", :mixed
    
    class ActionController::Request
      def initialize(env)
        Thread.current[:request]=self
        super
      end
    
    end
    
    class MultiPartParamsParser
    
      def main_part_name
       "main"
      end
    
      def attachment_part_prefix
        "my_company_attachment"
      end
    
      def content_type(main_part)
        # TODO ----
        :xml_simple
      end
    
      def content(main_part)
        # TODO implement this
         if main_part.is_a?(String) 
           main_part.gsub!("Content-Type: application/xml",'') # remove content type if it exists
           main_part.strip! # to remove any trailing or leading whitespaces
         else
          main_part[:tempfile].read
         end
      end
    
      def request
        Thread.current[:request]
      end
    
      def parse_formatted_parameters(data)
        multi_parts = Rack::Utils::Multipart.parse_multipart(request.try(:env))
        main_part   = multi_parts[main_part_name]
        data        = content(main_part)
        # TODO return an error if data is not found
        params = case content_type(main_part)
          when :xml_simple, :xml_node
            data.blank? ? {} : Hash.from_xml(data).with_indifferent_access
          when :yaml
            YAML.load(data)
          when :json
            if data.blank?
              {}
            else
              ret = ActiveSupport::JSON.decode(data)
              ret = {:_json => data} unless data.is_a?(Hash)
              ret.with_indifferent_access
            end
          else
            {}
        end
        process_attachments(params, multi_parts)
        params                 
      end
    
      def process_attachments(data, multi_parts)
        data.each do |key, value|
          value ||= key # when array value is nil
          if value.is_a?(Hash) or value.is_a?(Array)
            process_attachments(value, multi_parts)
          elsif value.respond_to?(:match) and value.match("^#{attachment_part_prefix}") and (attachment=multi_parts[value]) # there could Time,Numbers etc.., but we match only string.
            data[key] = create_uploaded_file(attachment) # TODO handle the scenarios for short strings
          end 
        end         
      end
    
      def create_uploaded_file (attachment)
        upload = attachment[:tempfile]
        upload.extend(ActionController::UploadedFile)
        upload.original_path = attachment[:filename]
        upload.content_type = attachment[:type]
        upload
      end
    end
    
    proc = Proc.new do |data|
      MultiPartParamsParser.new.parse_formatted_parameters(data)
    end
    
    ActionController::Base.param_parsers[Mime::Type.lookup('multipart/mixed')] = proc
    

    then, you can post your message like this. Nesting gets automatically taken with no further changes to the models or controllers.

    POST /posts.xml HTTP/1.1
    Content-type: multipart/mixed; boundary=---------------------------###987612345###
    Accept: application/xml,text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
    Cache-Control: no-cache
    Pragma: no-cache
    Connection: keep-alive
    Content-Length: ##
    
    -----------------------------###987612345###
    content-disposition: name="main"
    Content-Length: ##
    <post><title>post_title</title><body>post_body</body>
        <assets_attributes type="array">
                <asset><data>my_company_attachment_0</data> </asset>
                <asset><data>my_company_attachment_1</data> </asset>
        </assets_attributes>
    </post>
    -----------------------------###987612345###
    content-disposition: name="my_company_attachment_0"; filename="C:/Users/mv288/files/1.txt"
    content-type: application/octet-stream
    ÿþ
    sample file content
    -----------------------------###987612345###
    content-disposition: name="my_company_attachment_1"; filename="C:/Users/mv288/Pictures/1.png"
    content-type: image/png
    ÿþ
    sample file content
    -----------------------------###987612345###