form-data2.coffee
4.71 KB
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
# Standard library
path = require "path"
stream = require "stream"
# Third-party dependencies
uuid = require "uuid"
mime = require "mime"
combinedStream2 = require "combined-stream2"
Promise = require "bluebird"
_ = require "lodash"
debug = require("debug")("form-data2")
CRLF = "\r\n"
# Utility functions
ofTypes = (obj, types) ->
match = false
for type in types
match = match or obj instanceof type
return match
module.exports = class FormData
constructor: ->
@_firstHeader = false
@_closingHeaderAppended = false
@_boundary = "----" + uuid.v4()
@_headers = { "content-type": "multipart/form-data; boundary=#{@_boundary}" }
@_stream = combinedStream2.create()
_getStreamMetadata: (source, options) -> # FIXME: Make work with deferred sources (ie. callback-provided)
debug "obtaining metadata for source: %s", source.toString().replace(/\n/g, "\\n").replace(/\r/g, "\\r")
fullPath = options.filename ? source.client?._httpMessage?.path ? source.path
if fullPath? # This is a file...
filename = path.basename(fullPath)
contentType = options.contentType ? source.headers?["content-type"] ? mime.lookup(filename)
contentLength = options.knownLength ? options.contentLength ? source.headers?["content-length"] # FIXME: Is this even used anywhere?
else # Probably just a plaintext form value, or an unidentified stream
contentType = options.contentType ? source.headers?["content-type"]
contentLength = options.knownLength ? options.contentLength ? source.headers?["content-length"]
return {filename: filename, contentType: contentType, contentLength: contentLength}
_generateHeaderFields: (name, metadata) ->
debug "generating headers for: %s", metadata
headerFields = []
if metadata.filename?
escapedFilename = metadata.filename.replace '"', '\\"'
headerFields.push "Content-Disposition: form-data; name=\"#{name}\"; filename=\"#{escapedFilename}\""
else
headerFields.push "Content-Disposition: form-data; name=\"#{name}\""
if metadata.contentType?
headerFields.push "Content-Type: #{metadata.contentType}"
debug "generated headers: %s", headerFields
return headerFields.join CRLF
_appendHeader: (name, metadata) ->
if @_firstHeader == false
debug "appending header"
leadingCRLF = ""
@_firstHeader = true
else
debug "appending first header"
leadingCRLF = CRLF
headerFields = @_generateHeaderFields name, metadata
@_stream.append new Buffer(leadingCRLF + "--#{@_boundary}" + CRLF + headerFields + CRLF + CRLF)
_appendClosingHeader: ->
debug "appending closing header"
@_stream.append new Buffer(CRLF + "--#{@_boundary}--")
append: (name, source, options = {}) ->
debug "appending source"
if @_closingHeaderAppended
throw new Error "The stream has already been prepared for usage; you either piped it or generated the HTTP headers. No new sources can be appended anymore."
if not ofTypes(source, [stream.Readable, stream.Duplex, stream.Transform, Buffer, Function]) and typeof source != "string"
throw new Error "The provided value must be either a readable stream, a Buffer, a callback providing either of those, or a string."
if typeof source == "string"
source = new Buffer(source) # If the string isn't UTF-8, this won't end well!
options.contentType ?= "text/plain"
metadata = @_getStreamMetadata source, options
@_appendHeader name, metadata
@_stream.append source, options
done: ->
# This method should be called when the user is finished adding streams. It adds the termination header at the end of the combined stream. When piping, this method is automatically called!
debug "called 'done'"
if not @_closingHeaderAppended
@_closingHeaderAppended = true
@_appendClosingHeader()
getBoundary: ->
return @_boundary
getHeaders: (callback) ->
# Returns the headers needed to correctly transmit the generated multipart/form-data blob. We will first need to call @done() to make sure that the multipart footer is there - from this point on, no new sources can be appended anymore.
@done()
Promise.try =>
@_stream.getCombinedStreamLength()
.then (length) ->
debug "total combined stream length: %s", length
Promise.resolve { "content-length": length }
.catch (err) ->
# We couldn't get the stream length, most likely there was a stream involved that `stream-length` does not support.
debug "WARN: could not get total combined stream length"
Promise.resolve { "transfer-encoding": "chunked" }
.then (sizeHeaders) =>
Promise.resolve _.extend(sizeHeaders, @_headers)
.nodeify(callback)
getLength: (callback) ->
@_stream.getCombinedStreamLength(callback)
pipe: (target) ->
@done()
# Pass through to the underlying `combined-stream`.
debug "piping underlying combined-stream2 to target writable"
@_stream.pipe target