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.ParameterParser;
38 import org.apache.commons.io.IOUtils;
39 import org.apache.commons.io.output.DeferredFileOutputStream;
40
41
42 /**
43 * <p> The default implementation of the
44 * {@link org.apache.commons.fileupload.FileItem FileItem} interface.
45 *
46 * <p> After retrieving an instance of this class from a {@link
47 * org.apache.commons.fileupload.DiskFileUpload DiskFileUpload} instance (see
48 * {@link org.apache.commons.fileupload.DiskFileUpload
49 * #parseRequest(javax.servlet.http.HttpServletRequest)}), you may
50 * either request all contents of file at once using {@link #get()} or
51 * request an {@link java.io.InputStream InputStream} with
52 * {@link #getInputStream()} and process the file without attempting to load
53 * it into memory, which may come handy with large files.
54 *
55 * <p>When using the <code>DiskFileItemFactory</code>, then you should
56 * consider the following: Temporary files are automatically deleted as
57 * soon as they are no longer needed. (More precisely, when the
58 * corresponding instance of {@link java.io.File} is garbage collected.)
59 * This is done by the so-called reaper thread, which is started
60 * automatically when the class {@link org.apache.commons.io.FileCleaner}
61 * is loaded.
62 * It might make sense to terminate that thread, for example, if
63 * your web application ends. See the section on "Resource cleanup"
64 * in the users guide of commons-fileupload.</p>
65 *
66 * @author <a href="mailto:Rafal.Krzewski@e-point.pl">Rafal Krzewski</a>
67 * @author <a href="mailto:sean@informage.net">Sean Legassick</a>
68 * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
69 * @author <a href="mailto:jmcnally@apache.org">John McNally</a>
70 * @author <a href="mailto:martinc@apache.org">Martin Cooper</a>
71 * @author Sean C. Sullivan
72 *
73 * @since FileUpload 1.1
74 *
75 * @version $Id: DiskFileItem.java 607869 2008-01-01 16:42:17Z jochen $
76 */
77 public class DiskFileItem
78 implements FileItem, FileItemHeadersSupport {
79
80 // ----------------------------------------------------- Manifest constants
81
82 /**
83 * The UID to use when serializing this instance.
84 */
85 private static final long serialVersionUID = 2237570099615271025L;
86
87
88 /**
89 * Default content charset to be used when no explicit charset
90 * parameter is provided by the sender. Media subtypes of the
91 * "text" type are defined to have a default charset value of
92 * "ISO-8859-1" when received via HTTP.
93 */
94 public static final String DEFAULT_CHARSET = "ISO-8859-1";
95
96
97 // ----------------------------------------------------------- Data members
98
99
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 }