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