File: reader.rb

package info (click to toggle)
ruby-multipart-parser 0.1.1-1~bpo70%2B1
  • links: PTS, VCS
  • area: main
  • in suites: wheezy-backports
  • size: 124 kB
  • sloc: ruby: 607; makefile: 2
file content (152 lines) | stat: -rw-r--r-- 3,924 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
require 'multipart_parser/parser'

module MultipartParser
  class NotMultipartError < StandardError; end;

  # A more high level interface to MultipartParser.
  class Reader

    # Initializes a MultipartReader, that will
    # read a request with the given boundary value.
    def initialize(boundary)
      @parser = Parser.new
      @parser.init_with_boundary(boundary)
      @header_field = ''
      @header_value = ''
      @part = nil
      @ended = false
      @on_error = nil
      @on_part = nil

      init_parser_callbacks
    end

    # Returns true if the parser has finished parsing
    def ended?
      @ended
    end

    # Sets to a code block to call
    # when part headers have been parsed.
    def on_part(&callback)
      @on_part = callback
    end

    # Sets a code block to call when
    # a parser error occurs.
    def on_error(&callback)
      @on_error = callback
    end

    # Write data from the given buffer (String)
    # into the reader.
    def write(buffer)
      bytes_parsed = @parser.write(buffer)
      if bytes_parsed != buffer.size
        msg = "Parser error, #{bytes_parsed} of #{buffer.length} bytes parsed"
        @on_error.call(msg) unless @on_error.nil?
      end
    end

    # Extracts a boundary value from a Content-Type header.
    # Note that it is the header value you provide here.
    # Raises NotMultipartError if content_type is invalid.
    def self.extract_boundary_value(content_type)
      if content_type =~ /multipart/i
        if match = (content_type =~ /boundary=(?:"([^"]+)"|([^;]+))/i)
          $1 || $2
        else
          raise NotMultipartError.new("No multipart boundary")
        end
      else
        raise NotMultipartError.new("Not a multipart content type!")
      end
    end

    class Part
      attr_accessor :filename, :headers, :name, :mime

      def initialize
        @headers = {}
        @data_callback = nil
        @end_callback = nil
      end

      # Calls the data callback with the given data
      def emit_data(data)
        @data_callback.call(data) unless @data_callback.nil?
      end

      # Calls the end callback
      def emit_end
        @end_callback.call unless @end_callback.nil?
      end

      # Sets a block to be called when part data
      # is read. The block should take one parameter,
      # namely the read data.
      def on_data(&callback)
        @data_callback = callback
      end

      # Sets a block to be called when all data
      # for the part has been read.
      def on_end(&callback)
        @end_callback = callback
      end
    end

    private

    def init_parser_callbacks
      @parser.on(:part_begin) do
        @part = Part.new
        @header_field = ''
        @header_value = ''
      end

      @parser.on(:header_field) do |b, start, the_end|
        @header_field << b[start...the_end]
      end

      @parser.on(:header_value) do |b, start, the_end|
        @header_value << b[start...the_end]
      end

      @parser.on(:header_end) do
        @header_field.downcase!
        @part.headers[@header_field] = @header_value
        if @header_field == 'content-disposition'
          if @header_value =~ /name="([^"]+)"/i
            @part.name = $1
          end
          if @header_value =~ /filename="([^;]+)"/i
            match = $1
            start = (match.rindex("\\") || -1)+1
            @part.filename = match[start...(match.length)]
          end
        elsif @header_field == 'content-type'
          @part.mime = @header_value
        end
        @header_field = ''
        @header_value = ''
      end

      @parser.on(:headers_end) do
        @on_part.call(@part) unless @on_part.nil?
      end

      @parser.on(:part_data) do |b, start, the_end|
        @part.emit_data b[start...the_end]
      end

      @parser.on(:part_end) do
        @part.emit_end
      end

      @parser.on(:end) do
        @ended = true
      end
    end
  end
end