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