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