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