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