1 module hunt.http.MultipartForm;
2 
3 import hunt.http.MultipartOptions;
4 
5 import hunt.collection;
6 import hunt.stream;
7 import hunt.Exceptions;
8 import hunt.logging;
9 import hunt.text.StringUtils;
10 
11 import std.array;
12 import std.conv;
13 import std.file;
14 import std.path;
15 import std.string;
16 import std.uni;
17 
18 deprecated("Using MultipartForm instead.")
19 alias MultiPart = MultipartForm;
20 
21 /**
22  * <p> This class represents a part or form item that was received within a
23  * <code>multipart/form-data</code> POST request.
24  */
25 interface Part {
26 
27     /**
28      * Gets the content of this part as an <tt>InputStream</tt>
29      * 
30      * @return The content of this part as an <tt>InputStream</tt>
31      * @throws IOException If an error occurs in retrieving the contet
32      * as an <tt>InputStream</tt>
33      */
34     InputStream getInputStream();
35 
36     /**
37      * Gets the content type of this part.
38      *
39      * @return The content type of this part.
40      */
41     string getContentType();
42 
43     /**
44      * Gets the name of this part
45      *
46      * @return The name of this part as a <tt>string</tt>
47      */
48     string getName();
49 
50     /**
51      * Gets the file name specified by the client
52      *
53      * @return the submitted file name
54      */
55     string getSubmittedFileName();
56 
57     /**
58      * Returns the size of this fille.
59      *
60      * @return a <code>long</code> specifying the size of this part, in bytes.
61      */
62     long getSize();
63 
64     string getFile();
65 
66     byte[] getBytes();
67 
68     /**
69      * A convenience method to write this uploaded item to disk.
70      * 
71      * <p>This method is not guaranteed to succeed if called more than once for
72      * the same part. This allows a particular implementation to use, for
73      * example, file renaming, where possible, rather than copying all of the
74      * underlying data, thus gaining a significant performance benefit.
75      *
76      * @param fileName the name of the file to which the stream will be
77      * written. The file is created relative to the location as
78      * specified in the MultipartOptions
79      *
80      * @throws IOException if an error occurs.
81      */
82     void writeTo(string fileName, bool canOverwrite=false);
83 
84     deprecated("Using writeTo instead.")
85     void write(string fileName);
86 
87 
88     /**
89      * Save the content to a temp file 
90      */
91     void flush();
92 
93     /**
94      * Deletes the underlying storage for a file item, including deleting any
95      * associated temporary disk file.
96      *
97      * @throws IOException if an error occurs.
98      */
99     void remove();
100 
101     /**
102      *
103      * Returns the value of the specified mime header
104      * as a <code>string</code>. If the Part did not include a header
105      * of the specified name, this method returns <code>null</code>.
106      * If there are multiple headers with the same name, this method
107      * returns the first header in the part.
108      * The header name is case insensitive. You can use
109      * this method with any request header.
110      *
111      * @param name		a <code>string</code> specifying the
112      *				header name
113      *
114      * @return			a <code>string</code> containing the
115      *				value of the requested
116      *				header, or <code>null</code>
117      *				if the part does not
118      *				have a header of that name
119      */
120     string getHeader(string name);
121 
122     /**
123      * Gets the values of the Part header with the given name.
124      *
125      * <p>Any changes to the returned <code>Collection</code> must not 
126      * affect this <code>Part</code>.
127      *
128      * <p>Part header names are case insensitive.
129      *
130      * @param name the header name whose values to return
131      *
132      * @return a (possibly empty) <code>Collection</code> of the values of
133      * the header with the given name
134      */
135     Collection!string getHeaders(string name);
136 
137     /**
138      * Gets the header names of this Part.
139      *
140      * <p>Some servlet containers do not allow
141      * servlets to access headers using this method, in
142      * which case this method returns <code>null</code>
143      *
144      * <p>Any changes to the returned <code>Collection</code> must not 
145      * affect this <code>Part</code>.
146      *
147      * @return a (possibly empty) <code>Collection</code> of the header
148      * names of this Part
149      */
150     string[] getHeaderNames();
151 
152     string toString();
153 
154 }
155 
156 /**
157  * 
158  */
159 class MultipartForm : Part {
160     private string _name;
161     private string _filename;
162     private string _file;
163     private OutputStream _out;
164     private ByteArrayOutputStream _bout;
165     private string _contentType;
166     private MultiMap!string _headers;
167     private long _size = 0;
168 
169     private MultipartOptions _config;
170     private bool _isWriteToFile = false;
171     private bool _temporary = true;
172     private bool _writeFilesWithFilenames;
173     private string _tmpDir;
174 
175     this(string name, string filename, MultipartOptions options) {
176         _name = name;
177         _filename = filename;
178         _config = options;
179         _tmpDir = tempDir();
180     }
181 
182     override
183     string toString() {
184         return format("Part{name=%s, fileName=%s, contentType=%s, size=%d, tmp=%b, file=%s}", 
185             _name, _filename, _contentType, _size, _temporary, _file);
186     }
187 
188     void setTmpDir(string dir) {
189         _tmpDir = dir;
190     }
191 
192     package void setContentType(string contentType) {
193         _contentType = contentType;
194     }
195 
196     package void open() {
197         // We will either be writing to a file, if it has a filename on the content-disposition
198         // and otherwise a byte-array-input-stream, OR if we exceed the getFileSizeThreshold, we
199         // will need to change to write to a file.
200         if (_config.isWriteFilesWithFilenames() && !_filename.empty) {
201             createFile();
202         } else {
203             // Write to a buffer in memory until we discover we've exceed the
204             // MultipartOptions fileSizeThreshold
205             _out = _bout = new ByteArrayOutputStream();
206         }
207     }
208 
209     package void close() {
210         _out.close();
211     }
212 
213     package void write(int b) {
214         if (_config.getMaxFileSize() > 0 && _size + 1 > _config.getMaxFileSize())
215             throw new IllegalStateException("Multipart Mime part " ~ _name ~ " exceeds max filesize");
216 
217         if (_config.getFileSizeThreshold() > 0 && 
218             _size + 1 > _config.getFileSizeThreshold() && 
219             !_filename.empty() && _file.empty()) {
220             createFile();
221         } 
222         _out.write(b);
223         _size++;
224     }
225 
226     package void write(byte[] bytes, int offset, int length) {
227         if (_config.getMaxFileSize() > 0 && _size + length > _config.getMaxFileSize())
228             throw new IllegalStateException("Multipart Mime part " ~ _name ~ " exceeds max filesize");
229 
230         if (_config.getFileSizeThreshold() > 0
231                 && _size + length > _config.getFileSizeThreshold() 
232                 && !_filename.empty() && _file.empty()) {
233             createFile();
234         } 
235         _out.write(bytes, offset, length);
236         _size += length;
237     }
238 
239     private void createFile() {
240         /*
241          * Some statics just to make the code below easier to understand This get optimized away during the compile anyway
242          */
243         // bool USER = true;
244         // bool WORLD = false;
245         _file= buildPath(_tmpDir, "Multipart-" ~ StringUtils.randomId());
246         version(HUNT_HTTP_DEBUG) infof("Creating temp file for multipart: %s", _file);
247 
248         // _file = File.createTempFile("Multipart", "", _tmpDir);
249         // _file.setReadable(false, WORLD); // (reset) disable it for everyone first
250         // _file.setReadable(true, USER); // enable for user only
251 
252         // if (_deleteOnExit)
253         //     _file.deleteOnExit();
254         FileOutputStream fos = new FileOutputStream(_file);
255         BufferedOutputStream bos = new BufferedOutputStream(fos);
256 
257         if (_size > 0 && _out !is null) {
258             // already written some bytes, so need to copy them into the file
259             _out.flush();
260             _bout.writeTo(bos);
261             _out.close();
262         }
263         _bout = null;
264         _out = bos;
265         _isWriteToFile = true;
266     }
267 
268     package void setHeaders(MultiMap!string headers) {
269         _headers = headers;
270     }
271 
272     /**
273      * @see Part#getContentType()
274      */
275     override
276     string getContentType() {
277         return _contentType;
278     }
279 
280     /**
281      * @see Part#getHeader(string)
282      */
283     override
284     string getHeader(string name) {
285         if (name is null)
286             return null;
287         return _headers.getValue(std.uni.toLower(name), 0);
288     }
289 
290     /**
291      * @see Part#getHeaderNames()
292      */
293     // override
294     string[] getHeaderNames() {
295         return _headers.keySet();
296     }
297 
298     /**
299      * @see Part#getHeaders(string)
300      */
301     override
302     Collection!string getHeaders(string name) {
303         return _headers.getValues(name);
304     }
305 
306     /**
307      * @see Part#getInputStream()
308      */
309     override
310     InputStream getInputStream() {
311         if (_file !is null) {
312             // written to a file, whether temporary or not
313             return new FileInputStream(_file);
314         } else {
315             // part content is in memory
316             return new ByteArrayInputStream(_bout.getBuffer(), 0, _bout.size());
317         }
318     }
319 
320     /**
321      * @see Part#getSubmittedFileName()
322      */
323     override
324     string getSubmittedFileName() {
325         return getContentDispositionFilename();
326     }
327 
328     byte[] getBytes() {
329         if (_bout !is null)
330             return _bout.toByteArray();
331         return null;
332     }
333 
334     /**
335      * @see Part#getName()
336      */
337     override
338     string getName() {
339         return _name;
340     }
341 
342     /**
343      * @see Part#getSize()
344      */
345     override
346     long getSize() {
347         return _size;
348     }
349     
350     deprecated("Using writeTo instead.")
351     void write(string fileName) {
352         writeTo(fileName);
353     }
354 
355     /**
356      * @see Part#write(string)
357      */
358     void writeTo(string fileName, bool canOverwrite = false) {
359         if(fileName.empty()) {
360             warning("The target file name can't be empty.");
361             return;
362         }
363 
364         _temporary = false;
365         if (_file.empty) {
366             // part data is only in the ByteArrayOutputStream and never been written to disk
367             _file = buildPath(_tmpDir, fileName);
368             version(HUNT_HTTP_DEBUG) infof("writing to file: _file=%s", _file);
369 
370             scope FileOutputStream bos = null;
371             try {
372                 bos = new FileOutputStream(_file);
373                 _bout.writeTo(bos);
374                 bos.flush();
375             } finally {
376                 if (bos !is null)
377                     bos.close();
378 
379                 version(HUNT_HTTP_DEBUG_MORE) infof("closing file: _file=%s", _file);
380                 _bout = null;
381             }
382         } else {
383             // the part data is already written to a temporary file, just rename it
384             string target = buildPath(dirName(_file), fileName);
385             // version(HUNT_HTTP_DEBUG) {
386             //     tracef("target: %s, source: %s", target, _file);
387             // }
388             
389             if(_file != target) {
390                 bool isExisted = target.exists;
391                 if(!isExisted || (isExisted && canOverwrite)) {
392                     version(HUNT_DEBUG) tracef("moving file, src: %s, target: %s", _file, target);
393                     rename(_file, target);
394                     _file = target;
395                 }
396 
397                 version(HUNT_DEBUG) {
398                     if(isExisted && !canOverwrite) {
399                         warningf("The file exists: %s, overwrited: %s", target, canOverwrite);
400                     }
401                 }
402             }
403         }
404     }
405 
406     void flush() {
407         if(_file.empty) {
408             writeTo("Multipart-" ~ StringUtils.randomId());
409         }
410     }
411 
412     /**
413      * Remove the file, whether or not Part.write() was called on it (ie no longer temporary)
414      *
415      * @see Part#delete()
416      */
417     override
418     void remove() {
419         if (!_file.empty && _file.exists())
420             _file.remove();
421     }
422 
423     /**
424      * Only remove tmp files.
425      *
426      * @throws IOException if unable to delete the file
427      */
428     void cleanUp() {
429         if (_temporary && _file !is null && _file.exists())
430             _file.remove();
431     }
432 
433     /**
434      * Get the file
435      *
436      * @return the file, if any, the data has been written to.
437      */
438     string getFile() {
439         return _file;
440     }
441 
442     /**
443      * Get the filename from the content-disposition.
444      *
445      * @return null or the filename
446      */
447     string getContentDispositionFilename() {
448         return _filename;
449     }
450 }
451