Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
DiskFileItem |
|
| 2.5;2,5 |
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 | 1 | UUID.randomUUID().toString().replace('-', '_'); |
91 | ||
92 | /** | |
93 | * Counter used in unique identifier generation. | |
94 | */ | |
95 | 1 | 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 | 2173 | 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 | // ----------------------------------------------------------- Constructors | |
156 | ||
157 | /** | |
158 | * Constructs a new <code>DiskFileItem</code> instance. | |
159 | * | |
160 | * @param fieldName The name of the form field. | |
161 | * @param contentType The content type passed by the browser or | |
162 | * <code>null</code> if not specified. | |
163 | * @param isFormField Whether or not this item is a plain form field, as | |
164 | * opposed to a file upload. | |
165 | * @param fileName The original filename in the user's filesystem, or | |
166 | * <code>null</code> if not specified. | |
167 | * @param sizeThreshold The threshold, in bytes, below which items will be | |
168 | * retained in memory and above which they will be | |
169 | * stored as a file. | |
170 | * @param repository The data repository, which is the directory in | |
171 | * which files will be created, should the item size | |
172 | * exceed the threshold. | |
173 | */ | |
174 | public DiskFileItem(String fieldName, | |
175 | String contentType, boolean isFormField, String fileName, | |
176 | 2173 | int sizeThreshold, File repository) { |
177 | 2173 | this.fieldName = fieldName; |
178 | 2173 | this.contentType = contentType; |
179 | 2173 | this.isFormField = isFormField; |
180 | 2173 | this.fileName = fileName; |
181 | 2173 | this.sizeThreshold = sizeThreshold; |
182 | 2173 | this.repository = repository; |
183 | 2173 | } |
184 | ||
185 | // ------------------------------- Methods from javax.activation.DataSource | |
186 | ||
187 | /** | |
188 | * Returns an {@link java.io.InputStream InputStream} that can be | |
189 | * used to retrieve the contents of the file. | |
190 | * | |
191 | * @return An {@link java.io.InputStream InputStream} that can be | |
192 | * used to retrieve the contents of the file. | |
193 | * | |
194 | * @throws IOException if an error occurs. | |
195 | */ | |
196 | public InputStream getInputStream() | |
197 | throws IOException { | |
198 | 0 | if (!isInMemory()) { |
199 | 0 | return new FileInputStream(dfos.getFile()); |
200 | } | |
201 | ||
202 | 0 | if (cachedContent == null) { |
203 | 0 | cachedContent = dfos.getData(); |
204 | } | |
205 | 0 | return new ByteArrayInputStream(cachedContent); |
206 | } | |
207 | ||
208 | /** | |
209 | * Returns the content type passed by the agent or <code>null</code> if | |
210 | * not defined. | |
211 | * | |
212 | * @return The content type passed by the agent or <code>null</code> if | |
213 | * not defined. | |
214 | */ | |
215 | public String getContentType() { | |
216 | 41 | return contentType; |
217 | } | |
218 | ||
219 | /** | |
220 | * Returns the content charset passed by the agent or <code>null</code> if | |
221 | * not defined. | |
222 | * | |
223 | * @return The content charset passed by the agent or <code>null</code> if | |
224 | * not defined. | |
225 | */ | |
226 | public String getCharSet() { | |
227 | 33 | ParameterParser parser = new ParameterParser(); |
228 | 33 | parser.setLowerCaseNames(true); |
229 | // Parameter parser can handle null input | |
230 | 33 | Map<String, String> params = parser.parse(getContentType(), ';'); |
231 | 33 | return params.get("charset"); |
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 | * @throws org.apache.commons.fileupload.InvalidFileNameException The file name contains a NUL character, | |
239 | * which might be an indicator of a security attack. If you intend to | |
240 | * use the file name anyways, catch the exception and use | |
241 | * {@link org.apache.commons.fileupload.InvalidFileNameException#getName()}. | |
242 | */ | |
243 | public String getName() { | |
244 | 19 | return Streams.checkFileName(fileName); |
245 | } | |
246 | ||
247 | // ------------------------------------------------------- FileItem methods | |
248 | ||
249 | /** | |
250 | * Provides a hint as to whether or not the file contents will be read | |
251 | * from memory. | |
252 | * | |
253 | * @return <code>true</code> if the file contents will be read | |
254 | * from memory; <code>false</code> otherwise. | |
255 | */ | |
256 | public boolean isInMemory() { | |
257 | 2165 | if (cachedContent != null) { |
258 | 1 | return true; |
259 | } | |
260 | 2164 | return dfos.isInMemory(); |
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 | 536 | if (size >= 0) { |
270 | 0 | return size; |
271 | 536 | } else if (cachedContent != null) { |
272 | 0 | return cachedContent.length; |
273 | 536 | } else if (dfos.isInMemory()) { |
274 | 4 | return dfos.getData().length; |
275 | } else { | |
276 | 532 | return dfos.getFile().length(); |
277 | } | |
278 | } | |
279 | ||
280 | /** | |
281 | * Returns the contents of the file as an array of bytes. If the | |
282 | * contents of the file were not yet cached in memory, they will be | |
283 | * loaded from the disk storage and cached. | |
284 | * | |
285 | * @return The contents of the file as an array of bytes | |
286 | * or {@code null} if the data cannot be read | |
287 | */ | |
288 | public byte[] get() { | |
289 | 1449 | if (isInMemory()) { |
290 | 920 | if (cachedContent == null && dfos != null) { |
291 | 919 | cachedContent = dfos.getData(); |
292 | } | |
293 | 920 | return cachedContent; |
294 | } | |
295 | ||
296 | 529 | byte[] fileData = new byte[(int) getSize()]; |
297 | 529 | InputStream fis = null; |
298 | ||
299 | try { | |
300 | 529 | fis = new FileInputStream(dfos.getFile()); |
301 | 529 | IOUtils.readFully(fis, fileData); |
302 | 0 | } catch (IOException e) { |
303 | 0 | fileData = null; |
304 | } finally { | |
305 | 529 | IOUtils.closeQuietly(fis); |
306 | 529 | } |
307 | ||
308 | 529 | return fileData; |
309 | } | |
310 | ||
311 | /** | |
312 | * Returns the contents of the file as a String, using the specified | |
313 | * encoding. This method uses {@link #get()} to retrieve the | |
314 | * contents of the file. | |
315 | * | |
316 | * @param charset The charset to use. | |
317 | * | |
318 | * @return The contents of the file, as a string. | |
319 | * | |
320 | * @throws UnsupportedEncodingException if the requested character | |
321 | * encoding is not available. | |
322 | */ | |
323 | public String getString(final String charset) | |
324 | throws UnsupportedEncodingException { | |
325 | 0 | return new String(get(), charset); |
326 | } | |
327 | ||
328 | /** | |
329 | * Returns the contents of the file as a String, using the default | |
330 | * character encoding. This method uses {@link #get()} to retrieve the | |
331 | * contents of the file. | |
332 | * | |
333 | * <b>TODO</b> Consider making this method throw UnsupportedEncodingException. | |
334 | * | |
335 | * @return The contents of the file, as a string. | |
336 | */ | |
337 | public String getString() { | |
338 | 33 | byte[] rawdata = get(); |
339 | 33 | String charset = getCharSet(); |
340 | 33 | if (charset == null) { |
341 | 33 | charset = DEFAULT_CHARSET; |
342 | } | |
343 | try { | |
344 | 33 | return new String(rawdata, charset); |
345 | 0 | } catch (UnsupportedEncodingException e) { |
346 | 0 | return new String(rawdata); |
347 | } | |
348 | } | |
349 | ||
350 | /** | |
351 | * A convenience method to write an uploaded item to disk. The client code | |
352 | * is not concerned with whether or not the item is stored in memory, or on | |
353 | * disk in a temporary location. They just want to write the uploaded item | |
354 | * to a file. | |
355 | * <p> | |
356 | * This implementation first attempts to rename the uploaded item to the | |
357 | * specified destination file, if the item was originally written to disk. | |
358 | * Otherwise, the data will be copied to the specified file. | |
359 | * <p> | |
360 | * This method is only guaranteed to work <em>once</em>, the first time it | |
361 | * is invoked for a particular item. This is because, in the event that the | |
362 | * method renames a temporary file, that file will no longer be available | |
363 | * to copy or rename again at a later time. | |
364 | * | |
365 | * @param file The <code>File</code> into which the uploaded item should | |
366 | * be stored. | |
367 | * | |
368 | * @throws Exception if an error occurs. | |
369 | */ | |
370 | public void write(File file) throws Exception { | |
371 | 0 | if (isInMemory()) { |
372 | 0 | FileOutputStream fout = null; |
373 | try { | |
374 | 0 | fout = new FileOutputStream(file); |
375 | 0 | fout.write(get()); |
376 | 0 | fout.close(); |
377 | } finally { | |
378 | 0 | IOUtils.closeQuietly(fout); |
379 | 0 | } |
380 | 0 | } else { |
381 | 0 | File outputFile = getStoreLocation(); |
382 | 0 | if (outputFile != null) { |
383 | // Save the length of the file | |
384 | 0 | size = outputFile.length(); |
385 | /* | |
386 | * The uploaded file is being stored on disk | |
387 | * in a temporary location so move it to the | |
388 | * desired file. | |
389 | */ | |
390 | 0 | FileUtils.moveFile(outputFile, file); |
391 | } else { | |
392 | /* | |
393 | * For whatever reason we cannot write the | |
394 | * file to disk. | |
395 | */ | |
396 | 0 | throw new FileUploadException( |
397 | "Cannot write uploaded file to disk!"); | |
398 | } | |
399 | } | |
400 | 0 | } |
401 | ||
402 | /** | |
403 | * Deletes the underlying storage for a file item, including deleting any | |
404 | * associated temporary disk file. Although this storage will be deleted | |
405 | * automatically when the <code>FileItem</code> instance is garbage | |
406 | * collected, this method can be used to ensure that this is done at an | |
407 | * earlier time, thus preserving system resources. | |
408 | */ | |
409 | public void delete() { | |
410 | 707 | cachedContent = null; |
411 | 707 | File outputFile = getStoreLocation(); |
412 | 707 | if (outputFile != null && outputFile.exists()) { |
413 | 265 | outputFile.delete(); |
414 | } | |
415 | 707 | } |
416 | ||
417 | /** | |
418 | * Returns the name of the field in the multipart form corresponding to | |
419 | * this file item. | |
420 | * | |
421 | * @return The name of the form field. | |
422 | * | |
423 | * @see #setFieldName(java.lang.String) | |
424 | * | |
425 | */ | |
426 | public String getFieldName() { | |
427 | 1445 | return fieldName; |
428 | } | |
429 | ||
430 | /** | |
431 | * Sets the field name used to reference this file item. | |
432 | * | |
433 | * @param fieldName The name of the form field. | |
434 | * | |
435 | * @see #getFieldName() | |
436 | * | |
437 | */ | |
438 | public void setFieldName(String fieldName) { | |
439 | 0 | this.fieldName = fieldName; |
440 | 0 | } |
441 | ||
442 | /** | |
443 | * Determines whether or not a <code>FileItem</code> instance represents | |
444 | * a simple form field. | |
445 | * | |
446 | * @return <code>true</code> if the instance represents a simple form | |
447 | * field; <code>false</code> if it represents an uploaded file. | |
448 | * | |
449 | * @see #setFormField(boolean) | |
450 | * | |
451 | */ | |
452 | public boolean isFormField() { | |
453 | 32 | return isFormField; |
454 | } | |
455 | ||
456 | /** | |
457 | * Specifies whether or not a <code>FileItem</code> instance represents | |
458 | * a simple form field. | |
459 | * | |
460 | * @param state <code>true</code> if the instance represents a simple form | |
461 | * field; <code>false</code> if it represents an uploaded file. | |
462 | * | |
463 | * @see #isFormField() | |
464 | * | |
465 | */ | |
466 | public void setFormField(boolean state) { | |
467 | 0 | isFormField = state; |
468 | 0 | } |
469 | ||
470 | /** | |
471 | * Returns an {@link java.io.OutputStream OutputStream} that can | |
472 | * be used for storing the contents of the file. | |
473 | * | |
474 | * @return An {@link java.io.OutputStream OutputStream} that can be used | |
475 | * for storing the contents of the file. | |
476 | * | |
477 | * @throws IOException if an error occurs. | |
478 | */ | |
479 | public OutputStream getOutputStream() | |
480 | throws IOException { | |
481 | 2171 | if (dfos == null) { |
482 | 2171 | File outputFile = getTempFile(); |
483 | 2171 | dfos = new DeferredFileOutputStream(sizeThreshold, outputFile); |
484 | } | |
485 | 2171 | return dfos; |
486 | } | |
487 | ||
488 | // --------------------------------------------------------- Public methods | |
489 | ||
490 | /** | |
491 | * Returns the {@link java.io.File} object for the <code>FileItem</code>'s | |
492 | * data's temporary location on the disk. Note that for | |
493 | * <code>FileItem</code>s that have their data stored in memory, | |
494 | * this method will return <code>null</code>. When handling large | |
495 | * files, you can use {@link java.io.File#renameTo(java.io.File)} to | |
496 | * move the file to new location without copying the data, if the | |
497 | * source and destination locations reside within the same logical | |
498 | * volume. | |
499 | * | |
500 | * @return The data file, or <code>null</code> if the data is stored in | |
501 | * memory. | |
502 | */ | |
503 | public File getStoreLocation() { | |
504 | 709 | if (dfos == null) { |
505 | 0 | return null; |
506 | } | |
507 | 709 | if (isInMemory()) { |
508 | 442 | return null; |
509 | } | |
510 | 267 | return dfos.getFile(); |
511 | } | |
512 | ||
513 | // ------------------------------------------------------ Protected methods | |
514 | ||
515 | /** | |
516 | * Removes the file contents from the temporary storage. | |
517 | */ | |
518 | @Override | |
519 | protected void finalize() { | |
520 | 1245 | if (dfos == null) { |
521 | 2 | return; |
522 | } | |
523 | 1243 | File outputFile = dfos.getFile(); |
524 | ||
525 | 1243 | if (outputFile != null && outputFile.exists()) { |
526 | 524 | outputFile.delete(); |
527 | } | |
528 | 1243 | } |
529 | ||
530 | /** | |
531 | * Creates and returns a {@link java.io.File File} representing a uniquely | |
532 | * named temporary file in the configured repository path. The lifetime of | |
533 | * the file is tied to the lifetime of the <code>FileItem</code> instance; | |
534 | * the file will be deleted when the instance is garbage collected. | |
535 | * <p> | |
536 | * <b>Note: Subclasses that override this method must ensure that they return the | |
537 | * same File each time.</b> | |
538 | * | |
539 | * @return The {@link java.io.File File} to be used for temporary storage. | |
540 | */ | |
541 | protected File getTempFile() { | |
542 | 2171 | if (tempFile == null) { |
543 | 2171 | File tempDir = repository; |
544 | 2171 | if (tempDir == null) { |
545 | 2164 | tempDir = new File(System.getProperty("java.io.tmpdir")); |
546 | } | |
547 | ||
548 | 2171 | String tempFileName = format("upload_%s_%s.tmp", UID, getUniqueId()); |
549 | ||
550 | 2171 | tempFile = new File(tempDir, tempFileName); |
551 | } | |
552 | 2171 | return tempFile; |
553 | } | |
554 | ||
555 | // -------------------------------------------------------- Private methods | |
556 | ||
557 | /** | |
558 | * Returns an identifier that is unique within the class loader used to | |
559 | * load this class, but does not have random-like appearance. | |
560 | * | |
561 | * @return A String with the non-random looking instance identifier. | |
562 | */ | |
563 | private static String getUniqueId() { | |
564 | 2171 | final int limit = 100000000; |
565 | 2171 | int current = COUNTER.getAndIncrement(); |
566 | 2171 | String id = Integer.toString(current); |
567 | ||
568 | // If you manage to get more than 100 million of ids, you'll | |
569 | // start getting ids longer than 8 characters. | |
570 | 2171 | if (current < limit) { |
571 | 2171 | id = ("00000000" + id).substring(id.length()); |
572 | } | |
573 | 2171 | return id; |
574 | } | |
575 | ||
576 | /** | |
577 | * Returns a string representation of this object. | |
578 | * | |
579 | * @return a string representation of this object. | |
580 | */ | |
581 | @Override | |
582 | public String toString() { | |
583 | 0 | return format("name=%s, StoreLocation=%s, size=%s bytes, isFormField=%s, FieldName=%s", |
584 | 0 | getName(), getStoreLocation(), Long.valueOf(getSize()), |
585 | 0 | Boolean.valueOf(isFormField()), getFieldName()); |
586 | } | |
587 | ||
588 | /** | |
589 | * Returns the file item headers. | |
590 | * @return The file items headers. | |
591 | */ | |
592 | public FileItemHeaders getHeaders() { | |
593 | 32 | return headers; |
594 | } | |
595 | ||
596 | /** | |
597 | * Sets the file item headers. | |
598 | * @param pHeaders The file items headers. | |
599 | */ | |
600 | public void setHeaders(FileItemHeaders pHeaders) { | |
601 | 2159 | headers = pHeaders; |
602 | 2159 | } |
603 | ||
604 | } |