001 /* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 package org.apache.commons.fileupload.disk; 018 019 import java.io.BufferedInputStream; 020 import java.io.BufferedOutputStream; 021 import java.io.ByteArrayInputStream; 022 import java.io.File; 023 import java.io.FileInputStream; 024 import java.io.FileOutputStream; 025 import java.io.IOException; 026 import java.io.InputStream; 027 import java.io.ObjectInputStream; 028 import java.io.ObjectOutputStream; 029 import java.io.OutputStream; 030 import java.io.UnsupportedEncodingException; 031 import java.util.Map; 032 033 import org.apache.commons.fileupload.FileItem; 034 import org.apache.commons.fileupload.FileItemHeaders; 035 import org.apache.commons.fileupload.FileItemHeadersSupport; 036 import org.apache.commons.fileupload.FileUploadException; 037 import org.apache.commons.fileupload.ParameterParser; 038 import org.apache.commons.io.IOUtils; 039 import org.apache.commons.io.output.DeferredFileOutputStream; 040 041 042 /** 043 * <p> The default implementation of the 044 * {@link org.apache.commons.fileupload.FileItem FileItem} interface. 045 * 046 * <p> After retrieving an instance of this class from a {@link 047 * org.apache.commons.fileupload.DiskFileUpload DiskFileUpload} instance (see 048 * {@link org.apache.commons.fileupload.DiskFileUpload 049 * #parseRequest(javax.servlet.http.HttpServletRequest)}), you may 050 * either request all contents of file at once using {@link #get()} or 051 * request an {@link java.io.InputStream InputStream} with 052 * {@link #getInputStream()} and process the file without attempting to load 053 * it into memory, which may come handy with large files. 054 * 055 * <p>When using the <code>DiskFileItemFactory</code>, then you should 056 * consider the following: Temporary files are automatically deleted as 057 * soon as they are no longer needed. (More precisely, when the 058 * corresponding instance of {@link java.io.File} is garbage collected.) 059 * This is done by the so-called reaper thread, which is started 060 * automatically when the class {@link org.apache.commons.io.FileCleaner} 061 * is loaded. 062 * It might make sense to terminate that thread, for example, if 063 * your web application ends. See the section on "Resource cleanup" 064 * in the users guide of commons-fileupload.</p> 065 * 066 * @author <a href="mailto:Rafal.Krzewski@e-point.pl">Rafal Krzewski</a> 067 * @author <a href="mailto:sean@informage.net">Sean Legassick</a> 068 * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a> 069 * @author <a href="mailto:jmcnally@apache.org">John McNally</a> 070 * @author <a href="mailto:martinc@apache.org">Martin Cooper</a> 071 * @author Sean C. Sullivan 072 * 073 * @since FileUpload 1.1 074 * 075 * @version $Id: DiskFileItem.java 607869 2008-01-01 16:42:17Z jochen $ 076 */ 077 public class DiskFileItem 078 implements FileItem, FileItemHeadersSupport { 079 080 // ----------------------------------------------------- Manifest constants 081 082 /** 083 * The UID to use when serializing this instance. 084 */ 085 private static final long serialVersionUID = 2237570099615271025L; 086 087 088 /** 089 * Default content charset to be used when no explicit charset 090 * parameter is provided by the sender. Media subtypes of the 091 * "text" type are defined to have a default charset value of 092 * "ISO-8859-1" when received via HTTP. 093 */ 094 public static final String DEFAULT_CHARSET = "ISO-8859-1"; 095 096 097 // ----------------------------------------------------------- Data members 098 099 100 /** 101 * UID used in unique file name generation. 102 */ 103 private static final String UID = 104 new java.rmi.server.UID().toString() 105 .replace(':', '_').replace('-', '_'); 106 107 /** 108 * Counter used in unique identifier generation. 109 */ 110 private static int counter = 0; 111 112 113 /** 114 * The name of the form field as provided by the browser. 115 */ 116 private String fieldName; 117 118 119 /** 120 * The content type passed by the browser, or <code>null</code> if 121 * not defined. 122 */ 123 private String contentType; 124 125 126 /** 127 * Whether or not this item is a simple form field. 128 */ 129 private boolean isFormField; 130 131 132 /** 133 * The original filename in the user's filesystem. 134 */ 135 private String fileName; 136 137 138 /** 139 * The size of the item, in bytes. This is used to cache the size when a 140 * file item is moved from its original location. 141 */ 142 private long size = -1; 143 144 145 /** 146 * The threshold above which uploads will be stored on disk. 147 */ 148 private int sizeThreshold; 149 150 151 /** 152 * The directory in which uploaded files will be stored, if stored on disk. 153 */ 154 private File repository; 155 156 157 /** 158 * Cached contents of the file. 159 */ 160 private byte[] cachedContent; 161 162 163 /** 164 * Output stream for this item. 165 */ 166 private transient DeferredFileOutputStream dfos; 167 168 /** 169 * The temporary file to use. 170 */ 171 private transient File tempFile; 172 173 /** 174 * File to allow for serialization of the content of this item. 175 */ 176 private File dfosFile; 177 178 /** 179 * The file items headers. 180 */ 181 private FileItemHeaders headers; 182 183 // ----------------------------------------------------------- Constructors 184 185 186 /** 187 * Constructs a new <code>DiskFileItem</code> instance. 188 * 189 * @param fieldName The name of the form field. 190 * @param contentType The content type passed by the browser or 191 * <code>null</code> if not specified. 192 * @param isFormField Whether or not this item is a plain form field, as 193 * opposed to a file upload. 194 * @param fileName The original filename in the user's filesystem, or 195 * <code>null</code> if not specified. 196 * @param sizeThreshold The threshold, in bytes, below which items will be 197 * retained in memory and above which they will be 198 * stored as a file. 199 * @param repository The data repository, which is the directory in 200 * which files will be created, should the item size 201 * exceed the threshold. 202 */ 203 public DiskFileItem(String fieldName, 204 String contentType, boolean isFormField, String fileName, 205 int sizeThreshold, File repository) { 206 this.fieldName = fieldName; 207 this.contentType = contentType; 208 this.isFormField = isFormField; 209 this.fileName = fileName; 210 this.sizeThreshold = sizeThreshold; 211 this.repository = repository; 212 } 213 214 215 // ------------------------------- Methods from javax.activation.DataSource 216 217 218 /** 219 * Returns an {@link java.io.InputStream InputStream} that can be 220 * used to retrieve the contents of the file. 221 * 222 * @return An {@link java.io.InputStream InputStream} that can be 223 * used to retrieve the contents of the file. 224 * 225 * @throws IOException if an error occurs. 226 */ 227 public InputStream getInputStream() 228 throws IOException { 229 if (!isInMemory()) { 230 return new FileInputStream(dfos.getFile()); 231 } 232 233 if (cachedContent == null) { 234 cachedContent = dfos.getData(); 235 } 236 return new ByteArrayInputStream(cachedContent); 237 } 238 239 240 /** 241 * Returns the content type passed by the agent or <code>null</code> if 242 * not defined. 243 * 244 * @return The content type passed by the agent or <code>null</code> if 245 * not defined. 246 */ 247 public String getContentType() { 248 return contentType; 249 } 250 251 252 /** 253 * Returns the content charset passed by the agent or <code>null</code> if 254 * not defined. 255 * 256 * @return The content charset passed by the agent or <code>null</code> if 257 * not defined. 258 */ 259 public String getCharSet() { 260 ParameterParser parser = new ParameterParser(); 261 parser.setLowerCaseNames(true); 262 // Parameter parser can handle null input 263 Map params = parser.parse(getContentType(), ';'); 264 return (String) params.get("charset"); 265 } 266 267 268 /** 269 * Returns the original filename in the client's filesystem. 270 * 271 * @return The original filename in the client's filesystem. 272 */ 273 public String getName() { 274 return fileName; 275 } 276 277 278 // ------------------------------------------------------- FileItem methods 279 280 281 /** 282 * Provides a hint as to whether or not the file contents will be read 283 * from memory. 284 * 285 * @return <code>true</code> if the file contents will be read 286 * from memory; <code>false</code> otherwise. 287 */ 288 public boolean isInMemory() { 289 if (cachedContent != null) { 290 return true; 291 } 292 return dfos.isInMemory(); 293 } 294 295 296 /** 297 * Returns the size of the file. 298 * 299 * @return The size of the file, in bytes. 300 */ 301 public long getSize() { 302 if (size >= 0) { 303 return size; 304 } else if (cachedContent != null) { 305 return cachedContent.length; 306 } else if (dfos.isInMemory()) { 307 return dfos.getData().length; 308 } else { 309 return dfos.getFile().length(); 310 } 311 } 312 313 314 /** 315 * Returns the contents of the file as an array of bytes. If the 316 * contents of the file were not yet cached in memory, they will be 317 * loaded from the disk storage and cached. 318 * 319 * @return The contents of the file as an array of bytes. 320 */ 321 public byte[] get() { 322 if (isInMemory()) { 323 if (cachedContent == null) { 324 cachedContent = dfos.getData(); 325 } 326 return cachedContent; 327 } 328 329 byte[] fileData = new byte[(int) getSize()]; 330 FileInputStream fis = null; 331 332 try { 333 fis = new FileInputStream(dfos.getFile()); 334 fis.read(fileData); 335 } catch (IOException e) { 336 fileData = null; 337 } finally { 338 if (fis != null) { 339 try { 340 fis.close(); 341 } catch (IOException e) { 342 // ignore 343 } 344 } 345 } 346 347 return fileData; 348 } 349 350 351 /** 352 * Returns the contents of the file as a String, using the specified 353 * encoding. This method uses {@link #get()} to retrieve the 354 * contents of the file. 355 * 356 * @param charset The charset to use. 357 * 358 * @return The contents of the file, as a string. 359 * 360 * @throws UnsupportedEncodingException if the requested character 361 * encoding is not available. 362 */ 363 public String getString(final String charset) 364 throws UnsupportedEncodingException { 365 return new String(get(), charset); 366 } 367 368 369 /** 370 * Returns the contents of the file as a String, using the default 371 * character encoding. This method uses {@link #get()} to retrieve the 372 * contents of the file. 373 * 374 * @return The contents of the file, as a string. 375 * 376 * @todo Consider making this method throw UnsupportedEncodingException. 377 */ 378 public String getString() { 379 byte[] rawdata = get(); 380 String charset = getCharSet(); 381 if (charset == null) { 382 charset = DEFAULT_CHARSET; 383 } 384 try { 385 return new String(rawdata, charset); 386 } catch (UnsupportedEncodingException e) { 387 return new String(rawdata); 388 } 389 } 390 391 392 /** 393 * A convenience method to write an uploaded item to disk. The client code 394 * is not concerned with whether or not the item is stored in memory, or on 395 * disk in a temporary location. They just want to write the uploaded item 396 * to a file. 397 * <p> 398 * This implementation first attempts to rename the uploaded item to the 399 * specified destination file, if the item was originally written to disk. 400 * Otherwise, the data will be copied to the specified file. 401 * <p> 402 * This method is only guaranteed to work <em>once</em>, the first time it 403 * is invoked for a particular item. This is because, in the event that the 404 * method renames a temporary file, that file will no longer be available 405 * to copy or rename again at a later time. 406 * 407 * @param file The <code>File</code> into which the uploaded item should 408 * be stored. 409 * 410 * @throws Exception if an error occurs. 411 */ 412 public void write(File file) throws Exception { 413 if (isInMemory()) { 414 FileOutputStream fout = null; 415 try { 416 fout = new FileOutputStream(file); 417 fout.write(get()); 418 } finally { 419 if (fout != null) { 420 fout.close(); 421 } 422 } 423 } else { 424 File outputFile = getStoreLocation(); 425 if (outputFile != null) { 426 // Save the length of the file 427 size = outputFile.length(); 428 /* 429 * The uploaded file is being stored on disk 430 * in a temporary location so move it to the 431 * desired file. 432 */ 433 if (!outputFile.renameTo(file)) { 434 BufferedInputStream in = null; 435 BufferedOutputStream out = null; 436 try { 437 in = new BufferedInputStream( 438 new FileInputStream(outputFile)); 439 out = new BufferedOutputStream( 440 new FileOutputStream(file)); 441 IOUtils.copy(in, out); 442 } finally { 443 if (in != null) { 444 try { 445 in.close(); 446 } catch (IOException e) { 447 // ignore 448 } 449 } 450 if (out != null) { 451 try { 452 out.close(); 453 } catch (IOException e) { 454 // ignore 455 } 456 } 457 } 458 } 459 } else { 460 /* 461 * For whatever reason we cannot write the 462 * file to disk. 463 */ 464 throw new FileUploadException( 465 "Cannot write uploaded file to disk!"); 466 } 467 } 468 } 469 470 471 /** 472 * Deletes the underlying storage for a file item, including deleting any 473 * associated temporary disk file. Although this storage will be deleted 474 * automatically when the <code>FileItem</code> instance is garbage 475 * collected, this method can be used to ensure that this is done at an 476 * earlier time, thus preserving system resources. 477 */ 478 public void delete() { 479 cachedContent = null; 480 File outputFile = getStoreLocation(); 481 if (outputFile != null && outputFile.exists()) { 482 outputFile.delete(); 483 } 484 } 485 486 487 /** 488 * Returns the name of the field in the multipart form corresponding to 489 * this file item. 490 * 491 * @return The name of the form field. 492 * 493 * @see #setFieldName(java.lang.String) 494 * 495 */ 496 public String getFieldName() { 497 return fieldName; 498 } 499 500 501 /** 502 * Sets the field name used to reference this file item. 503 * 504 * @param fieldName The name of the form field. 505 * 506 * @see #getFieldName() 507 * 508 */ 509 public void setFieldName(String fieldName) { 510 this.fieldName = fieldName; 511 } 512 513 514 /** 515 * Determines whether or not a <code>FileItem</code> instance represents 516 * a simple form field. 517 * 518 * @return <code>true</code> if the instance represents a simple form 519 * field; <code>false</code> if it represents an uploaded file. 520 * 521 * @see #setFormField(boolean) 522 * 523 */ 524 public boolean isFormField() { 525 return isFormField; 526 } 527 528 529 /** 530 * Specifies whether or not a <code>FileItem</code> instance represents 531 * a simple form field. 532 * 533 * @param state <code>true</code> if the instance represents a simple form 534 * field; <code>false</code> if it represents an uploaded file. 535 * 536 * @see #isFormField() 537 * 538 */ 539 public void setFormField(boolean state) { 540 isFormField = state; 541 } 542 543 544 /** 545 * Returns an {@link java.io.OutputStream OutputStream} that can 546 * be used for storing the contents of the file. 547 * 548 * @return An {@link java.io.OutputStream OutputStream} that can be used 549 * for storing the contensts of the file. 550 * 551 * @throws IOException if an error occurs. 552 */ 553 public OutputStream getOutputStream() 554 throws IOException { 555 if (dfos == null) { 556 File outputFile = getTempFile(); 557 dfos = new DeferredFileOutputStream(sizeThreshold, outputFile); 558 } 559 return dfos; 560 } 561 562 563 // --------------------------------------------------------- Public methods 564 565 566 /** 567 * Returns the {@link java.io.File} object for the <code>FileItem</code>'s 568 * data's temporary location on the disk. Note that for 569 * <code>FileItem</code>s that have their data stored in memory, 570 * this method will return <code>null</code>. When handling large 571 * files, you can use {@link java.io.File#renameTo(java.io.File)} to 572 * move the file to new location without copying the data, if the 573 * source and destination locations reside within the same logical 574 * volume. 575 * 576 * @return The data file, or <code>null</code> if the data is stored in 577 * memory. 578 */ 579 public File getStoreLocation() { 580 return dfos == null ? null : dfos.getFile(); 581 } 582 583 584 // ------------------------------------------------------ Protected methods 585 586 587 /** 588 * Removes the file contents from the temporary storage. 589 */ 590 protected void finalize() { 591 File outputFile = dfos.getFile(); 592 593 if (outputFile != null && outputFile.exists()) { 594 outputFile.delete(); 595 } 596 } 597 598 599 /** 600 * Creates and returns a {@link java.io.File File} representing a uniquely 601 * named temporary file in the configured repository path. The lifetime of 602 * the file is tied to the lifetime of the <code>FileItem</code> instance; 603 * the file will be deleted when the instance is garbage collected. 604 * 605 * @return The {@link java.io.File File} to be used for temporary storage. 606 */ 607 protected File getTempFile() { 608 if (tempFile == null) { 609 File tempDir = repository; 610 if (tempDir == null) { 611 tempDir = new File(System.getProperty("java.io.tmpdir")); 612 } 613 614 String tempFileName = 615 "upload_" + UID + "_" + getUniqueId() + ".tmp"; 616 617 tempFile = new File(tempDir, tempFileName); 618 } 619 return tempFile; 620 } 621 622 623 // -------------------------------------------------------- Private methods 624 625 626 /** 627 * Returns an identifier that is unique within the class loader used to 628 * load this class, but does not have random-like apearance. 629 * 630 * @return A String with the non-random looking instance identifier. 631 */ 632 private static String getUniqueId() { 633 final int limit = 100000000; 634 int current; 635 synchronized (DiskFileItem.class) { 636 current = counter++; 637 } 638 String id = Integer.toString(current); 639 640 // If you manage to get more than 100 million of ids, you'll 641 // start getting ids longer than 8 characters. 642 if (current < limit) { 643 id = ("00000000" + id).substring(id.length()); 644 } 645 return id; 646 } 647 648 649 650 651 /** 652 * Returns a string representation of this object. 653 * 654 * @return a string representation of this object. 655 */ 656 public String toString() { 657 return "name=" + this.getName() 658 + ", StoreLocation=" 659 + String.valueOf(this.getStoreLocation()) 660 + ", size=" 661 + this.getSize() 662 + "bytes, " 663 + "isFormField=" + isFormField() 664 + ", FieldName=" 665 + this.getFieldName(); 666 } 667 668 669 // -------------------------------------------------- Serialization methods 670 671 672 /** 673 * Writes the state of this object during serialization. 674 * 675 * @param out The stream to which the state should be written. 676 * 677 * @throws IOException if an error occurs. 678 */ 679 private void writeObject(ObjectOutputStream out) throws IOException { 680 // Read the data 681 if (dfos.isInMemory()) { 682 cachedContent = get(); 683 } else { 684 cachedContent = null; 685 dfosFile = dfos.getFile(); 686 } 687 688 // write out values 689 out.defaultWriteObject(); 690 } 691 692 /** 693 * Reads the state of this object during deserialization. 694 * 695 * @param in The stream from which the state should be read. 696 * 697 * @throws IOException if an error occurs. 698 * @throws ClassNotFoundException if class cannot be found. 699 */ 700 private void readObject(ObjectInputStream in) 701 throws IOException, ClassNotFoundException { 702 // read values 703 in.defaultReadObject(); 704 705 OutputStream output = getOutputStream(); 706 if (cachedContent != null) { 707 output.write(cachedContent); 708 } else { 709 FileInputStream input = new FileInputStream(dfosFile); 710 IOUtils.copy(input, output); 711 dfosFile.delete(); 712 dfosFile = null; 713 } 714 output.close(); 715 716 cachedContent = null; 717 } 718 719 /** 720 * Returns the file item headers. 721 * @return The file items headers. 722 */ 723 public FileItemHeaders getHeaders() { 724 return headers; 725 } 726 727 /** 728 * Sets the file item headers. 729 * @param pHeaders The file items headers. 730 */ 731 public void setHeaders(FileItemHeaders pHeaders) { 732 headers = pHeaders; 733 } 734 }