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 static java.lang.String.format;
20
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.UnsupportedEncodingException;
29 import java.util.Map;
30 import java.util.UUID;
31 import java.util.concurrent.atomic.AtomicInteger;
32
33 import org.apache.commons.fileupload.FileItem;
34 import org.apache.commons.fileupload.FileItemHeaders;
35 import org.apache.commons.fileupload.FileUploadException;
36 import org.apache.commons.fileupload.ParameterParser;
37 import org.apache.commons.fileupload.util.Streams;
38 import org.apache.commons.io.FileUtils;
39 import org.apache.commons.io.IOUtils;
40 import org.apache.commons.io.output.DeferredFileOutputStream;
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 * DiskFileItemFactory} instance (see
48 * {@link org.apache.commons.fileupload.servlet.ServletFileUpload
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>Temporary files, which are created for file items, should be
56 * deleted later on. The best way to do this is using a
57 * {@link org.apache.commons.io.FileCleaningTracker}, which you can set on the
58 * {@link DiskFileItemFactory}. However, if you do use such a tracker,
59 * then you must consider the following: Temporary files are automatically
60 * deleted as soon as they are no longer needed. (More precisely, when the
61 * corresponding instance of {@link java.io.File} is garbage collected.)
62 * This is done by the so-called reaper thread, which is started and stopped
63 * automatically by the {@link org.apache.commons.io.FileCleaningTracker} when
64 * there are files to be tracked.
65 * It might make sense to terminate that thread, for example, if
66 * your web application ends. See the section on "Resource cleanup"
67 * in the users guide of commons-fileupload.</p>
68 *
69 * @since FileUpload 1.1
70 */
71 public class DiskFileItem
72 implements FileItem {
73
74 // ----------------------------------------------------- Manifest constants
75
76 /**
77 * Default content charset to be used when no explicit charset
78 * parameter is provided by the sender. Media subtypes of the
79 * "text" type are defined to have a default charset value of
80 * "ISO-8859-1" when received via HTTP.
81 */
82 public static final String DEFAULT_CHARSET = "ISO-8859-1";
83
84 // ----------------------------------------------------------- Data members
85
86 /**
87 * UID used in unique file name generation.
88 */
89 private static final String UID =
90 UUID.randomUUID().toString().replace('-', '_');
91
92 /**
93 * Counter used in unique identifier generation.
94 */
95 private static final AtomicInteger COUNTER = new AtomicInteger(0);
96
97 /**
98 * The name of the form field as provided by the browser.
99 */
100 private String fieldName;
101
102 /**
103 * The content type passed by the browser, or <code>null</code> if
104 * not defined.
105 */
106 private final String contentType;
107
108 /**
109 * Whether or not this item is a simple form field.
110 */
111 private boolean isFormField;
112
113 /**
114 * The original filename in the user's filesystem.
115 */
116 private final String fileName;
117
118 /**
119 * The size of the item, in bytes. This is used to cache the size when a
120 * file item is moved from its original location.
121 */
122 private long size = -1;
123
124
125 /**
126 * The threshold above which uploads will be stored on disk.
127 */
128 private final int sizeThreshold;
129
130 /**
131 * The directory in which uploaded files will be stored, if stored on disk.
132 */
133 private final File repository;
134
135 /**
136 * Cached contents of the file.
137 */
138 private byte[] cachedContent;
139
140 /**
141 * Output stream for this item.
142 */
143 private transient DeferredFileOutputStream dfos;
144
145 /**
146 * The temporary file to use.
147 */
148 private transient File tempFile;
149
150 /**
151 * The file items headers.
152 */
153 private FileItemHeaders headers;
154
155 /**
156 * Default content charset to be used when no explicit charset
157 * parameter is provided by the sender.
158 */
159 private String defaultCharset = DEFAULT_CHARSET;
160
161 // ----------------------------------------------------------- Constructors
162
163 /**
164 * Constructs a new <code>DiskFileItem</code> instance.
165 *
166 * @param fieldName The name of the form field.
167 * @param contentType The content type passed by the browser or
168 * <code>null</code> if not specified.
169 * @param isFormField Whether or not this item is a plain form field, as
170 * opposed to a file upload.
171 * @param fileName The original filename in the user's filesystem, or
172 * <code>null</code> if not specified.
173 * @param sizeThreshold The threshold, in bytes, below which items will be
174 * retained in memory and above which they will be
175 * stored as a file.
176 * @param repository The data repository, which is the directory in
177 * which files will be created, should the item size
178 * exceed the threshold.
179 */
180 public DiskFileItem(String fieldName,
181 String contentType, boolean isFormField, String fileName,
182 int sizeThreshold, File repository) {
183 this.fieldName = fieldName;
184 this.contentType = contentType;
185 this.isFormField = isFormField;
186 this.fileName = fileName;
187 this.sizeThreshold = sizeThreshold;
188 this.repository = repository;
189 }
190
191 // ------------------------------- Methods from javax.activation.DataSource
192
193 /**
194 * Returns an {@link java.io.InputStream InputStream} that can be
195 * used to retrieve the contents of the file.
196 *
197 * @return An {@link java.io.InputStream InputStream} that can be
198 * used to retrieve the contents of the file.
199 *
200 * @throws IOException if an error occurs.
201 */
202 @Override
203 public InputStream getInputStream()
204 throws IOException {
205 if (!isInMemory()) {
206 return new FileInputStream(dfos.getFile());
207 }
208
209 if (cachedContent == null) {
210 cachedContent = dfos.getData();
211 }
212 return new ByteArrayInputStream(cachedContent);
213 }
214
215 /**
216 * Returns the content type passed by the agent or <code>null</code> if
217 * not defined.
218 *
219 * @return The content type passed by the agent or <code>null</code> if
220 * not defined.
221 */
222 @Override
223 public String getContentType() {
224 return contentType;
225 }
226
227 /**
228 * Returns the content charset passed by the agent or <code>null</code> if
229 * not defined.
230 *
231 * @return The content charset passed by the agent or <code>null</code> if
232 * not defined.
233 */
234 public String getCharSet() {
235 ParameterParser parser = new ParameterParser();
236 parser.setLowerCaseNames(true);
237 // Parameter parser can handle null input
238 Map<String, String> params = parser.parse(getContentType(), ';');
239 return params.get("charset");
240 }
241
242 /**
243 * Returns the original filename in the client's filesystem.
244 *
245 * @return The original filename in the client's filesystem.
246 * @throws org.apache.commons.fileupload.InvalidFileNameException The file name contains a NUL character,
247 * which might be an indicator of a security attack. If you intend to
248 * use the file name anyways, catch the exception and use
249 * {@link org.apache.commons.fileupload.InvalidFileNameException#getName()}.
250 */
251 @Override
252 public String getName() {
253 return Streams.checkFileName(fileName);
254 }
255
256 // ------------------------------------------------------- FileItem methods
257
258 /**
259 * Provides a hint as to whether or not the file contents will be read
260 * from memory.
261 *
262 * @return <code>true</code> if the file contents will be read
263 * from memory; <code>false</code> otherwise.
264 */
265 @Override
266 public boolean isInMemory() {
267 if (cachedContent != null) {
268 return true;
269 }
270 return dfos.isInMemory();
271 }
272
273 /**
274 * Returns the size of the file.
275 *
276 * @return The size of the file, in bytes.
277 */
278 @Override
279 public long getSize() {
280 if (size >= 0) {
281 return size;
282 } else if (cachedContent != null) {
283 return cachedContent.length;
284 } else if (dfos.isInMemory()) {
285 return dfos.getData().length;
286 } else {
287 return dfos.getFile().length();
288 }
289 }
290
291 /**
292 * Returns the contents of the file as an array of bytes. If the
293 * contents of the file were not yet cached in memory, they will be
294 * loaded from the disk storage and cached.
295 *
296 * @return The contents of the file as an array of bytes
297 * or {@code null} if the data cannot be read
298 */
299 @Override
300 public byte[] get() {
301 if (isInMemory()) {
302 if (cachedContent == null && dfos != null) {
303 cachedContent = dfos.getData();
304 }
305 return cachedContent;
306 }
307
308 byte[] fileData = new byte[(int) getSize()];
309 InputStream fis = null;
310
311 try {
312 fis = new FileInputStream(dfos.getFile());
313 IOUtils.readFully(fis, fileData);
314 } catch (IOException e) {
315 fileData = null;
316 } finally {
317 IOUtils.closeQuietly(fis);
318 }
319
320 return fileData;
321 }
322
323 /**
324 * Returns the contents of the file as a String, using the specified
325 * encoding. This method uses {@link #get()} to retrieve the
326 * contents of the file.
327 *
328 * @param charset The charset to use.
329 *
330 * @return The contents of the file, as a string.
331 *
332 * @throws UnsupportedEncodingException if the requested character
333 * encoding is not available.
334 */
335 @Override
336 public String getString(final String charset)
337 throws UnsupportedEncodingException {
338 return new String(get(), charset);
339 }
340
341 /**
342 * Returns the contents of the file as a String, using the default
343 * character encoding. This method uses {@link #get()} to retrieve the
344 * contents of the file.
345 *
346 * <b>TODO</b> Consider making this method throw UnsupportedEncodingException.
347 *
348 * @return The contents of the file, as a string.
349 */
350 @Override
351 public String getString() {
352 byte[] rawdata = get();
353 String charset = getCharSet();
354 if (charset == null) {
355 charset = defaultCharset;
356 }
357 try {
358 return new String(rawdata, charset);
359 } catch (UnsupportedEncodingException e) {
360 return new String(rawdata);
361 }
362 }
363
364 /**
365 * A convenience method to write an uploaded item to disk. The client code
366 * is not concerned with whether or not the item is stored in memory, or on
367 * disk in a temporary location. They just want to write the uploaded item
368 * to a file.
369 * <p>
370 * This implementation first attempts to rename the uploaded item to the
371 * specified destination file, if the item was originally written to disk.
372 * Otherwise, the data will be copied to the specified file.
373 * <p>
374 * This method is only guaranteed to work <em>once</em>, the first time it
375 * is invoked for a particular item. This is because, in the event that the
376 * method renames a temporary file, that file will no longer be available
377 * to copy or rename again at a later time.
378 *
379 * @param file The <code>File</code> into which the uploaded item should
380 * be stored.
381 *
382 * @throws Exception if an error occurs.
383 */
384 @Override
385 public void write(File file) throws Exception {
386 if (isInMemory()) {
387 FileOutputStream fout = null;
388 try {
389 fout = new FileOutputStream(file);
390 fout.write(get());
391 fout.close();
392 } finally {
393 IOUtils.closeQuietly(fout);
394 }
395 } else {
396 File outputFile = getStoreLocation();
397 if (outputFile != null) {
398 // Save the length of the file
399 size = outputFile.length();
400 /*
401 * The uploaded file is being stored on disk
402 * in a temporary location so move it to the
403 * desired file.
404 */
405 if (file.exists()) {
406 file.delete();
407 }
408 FileUtils.moveFile(outputFile, file);
409 } else {
410 /*
411 * For whatever reason we cannot write the
412 * file to disk.
413 */
414 throw new FileUploadException(
415 "Cannot write uploaded file to disk!");
416 }
417 }
418 }
419
420 /**
421 * Deletes the underlying storage for a file item, including deleting any
422 * associated temporary disk file. Although this storage will be deleted
423 * automatically when the <code>FileItem</code> instance is garbage
424 * collected, this method can be used to ensure that this is done at an
425 * earlier time, thus preserving system resources.
426 */
427 @Override
428 public void delete() {
429 cachedContent = null;
430 File outputFile = getStoreLocation();
431 if (outputFile != null && !isInMemory() && outputFile.exists()) {
432 outputFile.delete();
433 }
434 }
435
436 /**
437 * Returns the name of the field in the multipart form corresponding to
438 * this file item.
439 *
440 * @return The name of the form field.
441 *
442 * @see #setFieldName(java.lang.String)
443 *
444 */
445 @Override
446 public String getFieldName() {
447 return fieldName;
448 }
449
450 /**
451 * Sets the field name used to reference this file item.
452 *
453 * @param fieldName The name of the form field.
454 *
455 * @see #getFieldName()
456 *
457 */
458 @Override
459 public void setFieldName(String fieldName) {
460 this.fieldName = fieldName;
461 }
462
463 /**
464 * Determines whether or not a <code>FileItem</code> instance represents
465 * a simple form field.
466 *
467 * @return <code>true</code> if the instance represents a simple form
468 * field; <code>false</code> if it represents an uploaded file.
469 *
470 * @see #setFormField(boolean)
471 *
472 */
473 @Override
474 public boolean isFormField() {
475 return isFormField;
476 }
477
478 /**
479 * Specifies whether or not a <code>FileItem</code> instance represents
480 * a simple form field.
481 *
482 * @param state <code>true</code> if the instance represents a simple form
483 * field; <code>false</code> if it represents an uploaded file.
484 *
485 * @see #isFormField()
486 *
487 */
488 @Override
489 public void setFormField(boolean state) {
490 isFormField = state;
491 }
492
493 /**
494 * Returns an {@link java.io.OutputStream OutputStream} that can
495 * be used for storing the contents of the file.
496 *
497 * @return An {@link java.io.OutputStream OutputStream} that can be used
498 * for storing the contents of the file.
499 *
500 * @throws IOException if an error occurs.
501 */
502 @Override
503 public OutputStream getOutputStream()
504 throws IOException {
505 if (dfos == null) {
506 File outputFile = getTempFile();
507 dfos = new DeferredFileOutputStream(sizeThreshold, outputFile);
508 }
509 return dfos;
510 }
511
512 // --------------------------------------------------------- Public methods
513
514 /**
515 * Returns the {@link java.io.File} object for the <code>FileItem</code>'s
516 * data's temporary location on the disk. Note that for
517 * <code>FileItem</code>s that have their data stored in memory,
518 * this method will return <code>null</code>. When handling large
519 * files, you can use {@link java.io.File#renameTo(java.io.File)} to
520 * move the file to new location without copying the data, if the
521 * source and destination locations reside within the same logical
522 * volume.
523 *
524 * @return The data file, or <code>null</code> if the data is stored in
525 * memory.
526 */
527 public File getStoreLocation() {
528 if (dfos == null) {
529 return null;
530 }
531 if (isInMemory()) {
532 return null;
533 }
534 return dfos.getFile();
535 }
536
537 // ------------------------------------------------------ Protected methods
538
539 /**
540 * Removes the file contents from the temporary storage.
541 */
542 @Override
543 protected void finalize() {
544 if (dfos == null || dfos.isInMemory()) {
545 return;
546 }
547 File outputFile = dfos.getFile();
548
549 if (outputFile != null && outputFile.exists()) {
550 outputFile.delete();
551 }
552 }
553
554 /**
555 * Creates and returns a {@link java.io.File File} representing a uniquely
556 * named temporary file in the configured repository path. The lifetime of
557 * the file is tied to the lifetime of the <code>FileItem</code> instance;
558 * the file will be deleted when the instance is garbage collected.
559 * <p>
560 * <b>Note: Subclasses that override this method must ensure that they return the
561 * same File each time.</b>
562 *
563 * @return The {@link java.io.File File} to be used for temporary storage.
564 */
565 protected File getTempFile() {
566 if (tempFile == null) {
567 File tempDir = repository;
568 if (tempDir == null) {
569 tempDir = new File(System.getProperty("java.io.tmpdir"));
570 }
571
572 String tempFileName = format("upload_%s_%s.tmp", UID, getUniqueId());
573
574 tempFile = new File(tempDir, tempFileName);
575 }
576 return tempFile;
577 }
578
579 // -------------------------------------------------------- Private methods
580
581 /**
582 * Returns an identifier that is unique within the class loader used to
583 * load this class, but does not have random-like appearance.
584 *
585 * @return A String with the non-random looking instance identifier.
586 */
587 private static String getUniqueId() {
588 final int limit = 100000000;
589 int current = COUNTER.getAndIncrement();
590 String id = Integer.toString(current);
591
592 // If you manage to get more than 100 million of ids, you'll
593 // start getting ids longer than 8 characters.
594 if (current < limit) {
595 id = ("00000000" + id).substring(id.length());
596 }
597 return id;
598 }
599
600 /**
601 * Returns a string representation of this object.
602 *
603 * @return a string representation of this object.
604 */
605 @Override
606 public String toString() {
607 return format("name=%s, StoreLocation=%s, size=%s bytes, isFormField=%s, FieldName=%s",
608 getName(), getStoreLocation(), Long.valueOf(getSize()),
609 Boolean.valueOf(isFormField()), getFieldName());
610 }
611
612 /**
613 * Returns the file item headers.
614 * @return The file items headers.
615 */
616 @Override
617 public FileItemHeaders getHeaders() {
618 return headers;
619 }
620
621 /**
622 * Sets the file item headers.
623 * @param pHeaders The file items headers.
624 */
625 @Override
626 public void setHeaders(FileItemHeaders pHeaders) {
627 headers = pHeaders;
628 }
629
630 /**
631 * Returns the default charset for use when no explicit charset
632 * parameter is provided by the sender.
633 * @return the default charset
634 */
635 public String getDefaultCharset() {
636 return defaultCharset;
637 }
638
639 /**
640 * Sets the default charset for use when no explicit charset
641 * parameter is provided by the sender.
642 * @param charset the default charset
643 */
644 public void setDefaultCharset(String charset) {
645 defaultCharset = charset;
646 }
647 }