001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.fileupload.disk;
018
019import static java.lang.String.format;
020
021import java.io.ByteArrayInputStream;
022import java.io.File;
023import java.io.FileInputStream;
024import java.io.FileOutputStream;
025import java.io.IOException;
026import java.io.InputStream;
027import java.io.OutputStream;
028import java.io.UnsupportedEncodingException;
029import java.util.Map;
030import java.util.UUID;
031import java.util.concurrent.atomic.AtomicInteger;
032
033import org.apache.commons.fileupload.FileItem;
034import org.apache.commons.fileupload.FileItemHeaders;
035import org.apache.commons.fileupload.FileUploadException;
036import org.apache.commons.fileupload.ParameterParser;
037import org.apache.commons.fileupload.util.Streams;
038import org.apache.commons.io.FileUtils;
039import org.apache.commons.io.IOUtils;
040import org.apache.commons.io.output.DeferredFileOutputStream;
041
042/**
043 * <p> The default implementation of the
044 * {@link org.apache.commons.fileupload.FileItem FileItem} interface.
045 *
046 * <p> After retrieving an instance of this class from a {@link
047 * DiskFileItemFactory} instance (see
048 * {@link org.apache.commons.fileupload.servlet.ServletFileUpload
049 * #parseRequest(javax.servlet.http.HttpServletRequest)}), you may
050 * either request all contents of file at once using {@link #get()} or
051 * request an {@link java.io.InputStream InputStream} with
052 * {@link #getInputStream()} and process the file without attempting to load
053 * it into memory, which may come handy with large files.
054 *
055 * <p>Temporary files, which are created for file items, should be
056 * deleted later on. The best way to do this is using a
057 * {@link org.apache.commons.io.FileCleaningTracker}, which you can set on the
058 * {@link DiskFileItemFactory}. However, if you do use such a tracker,
059 * then you must consider the following: Temporary files are automatically
060 * deleted as soon as they are no longer needed. (More precisely, when the
061 * corresponding instance of {@link java.io.File} is garbage collected.)
062 * This is done by the so-called reaper thread, which is started and stopped
063 * automatically by the {@link org.apache.commons.io.FileCleaningTracker} when
064 * there are files to be tracked.
065 * It might make sense to terminate that thread, for example, if
066 * your web application ends. See the section on "Resource cleanup"
067 * in the users guide of commons-fileupload.</p>
068 *
069 * @since FileUpload 1.1
070 *
071 * @version $Id$
072 */
073public class DiskFileItem
074    implements FileItem {
075
076    // ----------------------------------------------------- Manifest constants
077
078    /**
079     * The UID to use when serializing this instance.
080     */
081    private static final long serialVersionUID = 2237570099615271025L;
082
083    /**
084     * Default content charset to be used when no explicit charset
085     * parameter is provided by the sender. Media subtypes of the
086     * "text" type are defined to have a default charset value of
087     * "ISO-8859-1" when received via HTTP.
088     */
089    public static final String DEFAULT_CHARSET = "ISO-8859-1";
090
091    // ----------------------------------------------------------- Data members
092
093    /**
094     * UID used in unique file name generation.
095     */
096    private static final String UID =
097            UUID.randomUUID().toString().replace('-', '_');
098
099    /**
100     * Counter used in unique identifier generation.
101     */
102    private static final AtomicInteger COUNTER = new AtomicInteger(0);
103
104    /**
105     * The name of the form field as provided by the browser.
106     */
107    private String fieldName;
108
109    /**
110     * The content type passed by the browser, or <code>null</code> if
111     * not defined.
112     */
113    private final String contentType;
114
115    /**
116     * Whether or not this item is a simple form field.
117     */
118    private boolean isFormField;
119
120    /**
121     * The original filename in the user's filesystem.
122     */
123    private final String fileName;
124
125    /**
126     * The size of the item, in bytes. This is used to cache the size when a
127     * file item is moved from its original location.
128     */
129    private long size = -1;
130
131
132    /**
133     * The threshold above which uploads will be stored on disk.
134     */
135    private final int sizeThreshold;
136
137    /**
138     * The directory in which uploaded files will be stored, if stored on disk.
139     */
140    private final File repository;
141
142    /**
143     * Cached contents of the file.
144     */
145    private byte[] cachedContent;
146
147    /**
148     * Output stream for this item.
149     */
150    private transient DeferredFileOutputStream dfos;
151
152    /**
153     * The temporary file to use.
154     */
155    private transient File tempFile;
156
157    /**
158     * File to allow for serialization of the content of this item.
159     */
160    private File dfosFile;
161
162    /**
163     * The file items headers.
164     */
165    private FileItemHeaders headers;
166
167    // ----------------------------------------------------------- Constructors
168
169    /**
170     * Constructs a new <code>DiskFileItem</code> instance.
171     *
172     * @param fieldName     The name of the form field.
173     * @param contentType   The content type passed by the browser or
174     *                      <code>null</code> if not specified.
175     * @param isFormField   Whether or not this item is a plain form field, as
176     *                      opposed to a file upload.
177     * @param fileName      The original filename in the user's filesystem, or
178     *                      <code>null</code> if not specified.
179     * @param sizeThreshold The threshold, in bytes, below which items will be
180     *                      retained in memory and above which they will be
181     *                      stored as a file.
182     * @param repository    The data repository, which is the directory in
183     *                      which files will be created, should the item size
184     *                      exceed the threshold.
185     */
186    public DiskFileItem(String fieldName,
187            String contentType, boolean isFormField, String fileName,
188            int sizeThreshold, File repository) {
189        this.fieldName = fieldName;
190        this.contentType = contentType;
191        this.isFormField = isFormField;
192        this.fileName = fileName;
193        this.sizeThreshold = sizeThreshold;
194        this.repository = repository;
195    }
196
197    // ------------------------------- Methods from javax.activation.DataSource
198
199    /**
200     * Returns an {@link java.io.InputStream InputStream} that can be
201     * used to retrieve the contents of the file.
202     *
203     * @return An {@link java.io.InputStream InputStream} that can be
204     *         used to retrieve the contents of the file.
205     *
206     * @throws IOException if an error occurs.
207     */
208    public InputStream getInputStream()
209        throws IOException {
210        if (!isInMemory()) {
211            return new FileInputStream(dfos.getFile());
212        }
213
214        if (cachedContent == null) {
215            cachedContent = dfos.getData();
216        }
217        return new ByteArrayInputStream(cachedContent);
218    }
219
220    /**
221     * Returns the content type passed by the agent or <code>null</code> if
222     * not defined.
223     *
224     * @return The content type passed by the agent or <code>null</code> if
225     *         not defined.
226     */
227    public String getContentType() {
228        return contentType;
229    }
230
231    /**
232     * Returns the content charset passed by the agent or <code>null</code> if
233     * not defined.
234     *
235     * @return The content charset passed by the agent or <code>null</code> if
236     *         not defined.
237     */
238    public String getCharSet() {
239        ParameterParser parser = new ParameterParser();
240        parser.setLowerCaseNames(true);
241        // Parameter parser can handle null input
242        Map<String, String> params = parser.parse(getContentType(), ';');
243        return params.get("charset");
244    }
245
246    /**
247     * Returns the original filename in the client's filesystem.
248     *
249     * @return The original filename in the client's filesystem.
250     * @throws org.apache.commons.fileupload.InvalidFileNameException The file name contains a NUL character,
251     *   which might be an indicator of a security attack. If you intend to
252     *   use the file name anyways, catch the exception and use
253     *   {@link org.apache.commons.fileupload.InvalidFileNameException#getName()}.
254     */
255    public String getName() {
256        return Streams.checkFileName(fileName);
257    }
258
259    // ------------------------------------------------------- FileItem methods
260
261    /**
262     * Provides a hint as to whether or not the file contents will be read
263     * from memory.
264     *
265     * @return <code>true</code> if the file contents will be read
266     *         from memory; <code>false</code> otherwise.
267     */
268    public boolean isInMemory() {
269        if (cachedContent != null) {
270            return true;
271        }
272        return dfos.isInMemory();
273    }
274
275    /**
276     * Returns the size of the file.
277     *
278     * @return The size of the file, in bytes.
279     */
280    public long getSize() {
281        if (size >= 0) {
282            return size;
283        } else if (cachedContent != null) {
284            return cachedContent.length;
285        } else if (dfos.isInMemory()) {
286            return dfos.getData().length;
287        } else {
288            return dfos.getFile().length();
289        }
290    }
291
292    /**
293     * Returns the contents of the file as an array of bytes.  If the
294     * contents of the file were not yet cached in memory, they will be
295     * loaded from the disk storage and cached.
296     *
297     * @return The contents of the file as an array of bytes
298     * or {@code null} if the data cannot be read
299     */
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    public String getString(final String charset)
336        throws UnsupportedEncodingException {
337        return new String(get(), charset);
338    }
339
340    /**
341     * Returns the contents of the file as a String, using the default
342     * character encoding.  This method uses {@link #get()} to retrieve the
343     * contents of the file.
344     *
345     * <b>TODO</b> Consider making this method throw UnsupportedEncodingException.
346     *
347     * @return The contents of the file, as a string.
348     */
349    public String getString() {
350        byte[] rawdata = get();
351        String charset = getCharSet();
352        if (charset == null) {
353            charset = DEFAULT_CHARSET;
354        }
355        try {
356            return new String(rawdata, charset);
357        } catch (UnsupportedEncodingException e) {
358            return new String(rawdata);
359        }
360    }
361
362    /**
363     * A convenience method to write an uploaded item to disk. The client code
364     * is not concerned with whether or not the item is stored in memory, or on
365     * disk in a temporary location. They just want to write the uploaded item
366     * to a file.
367     * <p>
368     * This implementation first attempts to rename the uploaded item to the
369     * specified destination file, if the item was originally written to disk.
370     * Otherwise, the data will be copied to the specified file.
371     * <p>
372     * This method is only guaranteed to work <em>once</em>, the first time it
373     * is invoked for a particular item. This is because, in the event that the
374     * method renames a temporary file, that file will no longer be available
375     * to copy or rename again at a later time.
376     *
377     * @param file The <code>File</code> into which the uploaded item should
378     *             be stored.
379     *
380     * @throws Exception if an error occurs.
381     */
382    public void write(File file) throws Exception {
383        if (isInMemory()) {
384            FileOutputStream fout = null;
385            try {
386                fout = new FileOutputStream(file);
387                fout.write(get());
388                fout.close();
389            } finally {
390                IOUtils.closeQuietly(fout);
391            }
392        } else {
393            File outputFile = getStoreLocation();
394            if (outputFile != null) {
395                // Save the length of the file
396                size = outputFile.length();
397                /*
398                 * The uploaded file is being stored on disk
399                 * in a temporary location so move it to the
400                 * desired file.
401                 */
402                FileUtils.moveFile(outputFile, file);
403            } else {
404                /*
405                 * For whatever reason we cannot write the
406                 * file to disk.
407                 */
408                throw new FileUploadException(
409                    "Cannot write uploaded file to disk!");
410            }
411        }
412    }
413
414    /**
415     * Deletes the underlying storage for a file item, including deleting any
416     * associated temporary disk file. Although this storage will be deleted
417     * automatically when the <code>FileItem</code> instance is garbage
418     * collected, this method can be used to ensure that this is done at an
419     * earlier time, thus preserving system resources.
420     */
421    public void delete() {
422        cachedContent = null;
423        File outputFile = getStoreLocation();
424        if (outputFile != null && outputFile.exists()) {
425            outputFile.delete();
426        }
427    }
428
429    /**
430     * Returns the name of the field in the multipart form corresponding to
431     * this file item.
432     *
433     * @return The name of the form field.
434     *
435     * @see #setFieldName(java.lang.String)
436     *
437     */
438    public String getFieldName() {
439        return fieldName;
440    }
441
442    /**
443     * Sets the field name used to reference this file item.
444     *
445     * @param fieldName The name of the form field.
446     *
447     * @see #getFieldName()
448     *
449     */
450    public void setFieldName(String fieldName) {
451        this.fieldName = fieldName;
452    }
453
454    /**
455     * Determines whether or not a <code>FileItem</code> instance represents
456     * a simple form field.
457     *
458     * @return <code>true</code> if the instance represents a simple form
459     *         field; <code>false</code> if it represents an uploaded file.
460     *
461     * @see #setFormField(boolean)
462     *
463     */
464    public boolean isFormField() {
465        return isFormField;
466    }
467
468    /**
469     * Specifies whether or not a <code>FileItem</code> instance represents
470     * a simple form field.
471     *
472     * @param state <code>true</code> if the instance represents a simple form
473     *              field; <code>false</code> if it represents an uploaded file.
474     *
475     * @see #isFormField()
476     *
477     */
478    public void setFormField(boolean state) {
479        isFormField = state;
480    }
481
482    /**
483     * Returns an {@link java.io.OutputStream OutputStream} that can
484     * be used for storing the contents of the file.
485     *
486     * @return An {@link java.io.OutputStream OutputStream} that can be used
487     *         for storing the contents of the file.
488     *
489     * @throws IOException if an error occurs.
490     */
491    public OutputStream getOutputStream()
492        throws IOException {
493        if (dfos == null) {
494            File outputFile = getTempFile();
495            dfos = new DeferredFileOutputStream(sizeThreshold, outputFile);
496        }
497        return dfos;
498    }
499
500    // --------------------------------------------------------- Public methods
501
502    /**
503     * Returns the {@link java.io.File} object for the <code>FileItem</code>'s
504     * data's temporary location on the disk. Note that for
505     * <code>FileItem</code>s that have their data stored in memory,
506     * this method will return <code>null</code>. When handling large
507     * files, you can use {@link java.io.File#renameTo(java.io.File)} to
508     * move the file to new location without copying the data, if the
509     * source and destination locations reside within the same logical
510     * volume.
511     *
512     * @return The data file, or <code>null</code> if the data is stored in
513     *         memory.
514     */
515    public File getStoreLocation() {
516        if (dfos == null) {
517            return null;
518        }
519        if (isInMemory()) {
520                return null;
521        }
522        return dfos.getFile();
523    }
524
525    // ------------------------------------------------------ Protected methods
526
527    /**
528     * Removes the file contents from the temporary storage.
529     */
530    @Override
531    protected void finalize() {
532        if (dfos == null) {
533            return;
534        }
535        File outputFile = dfos.getFile();
536
537        if (outputFile != null && outputFile.exists()) {
538            outputFile.delete();
539        }
540    }
541
542    /**
543     * Creates and returns a {@link java.io.File File} representing a uniquely
544     * named temporary file in the configured repository path. The lifetime of
545     * the file is tied to the lifetime of the <code>FileItem</code> instance;
546     * the file will be deleted when the instance is garbage collected.
547     * <p>
548     * <b>Note: Subclasses that override this method must ensure that they return the
549     * same File each time.</b>
550     *
551     * @return The {@link java.io.File File} to be used for temporary storage.
552     */
553    protected File getTempFile() {
554        if (tempFile == null) {
555            File tempDir = repository;
556            if (tempDir == null) {
557                tempDir = new File(System.getProperty("java.io.tmpdir"));
558            }
559
560            String tempFileName = format("upload_%s_%s.tmp", UID, getUniqueId());
561
562            tempFile = new File(tempDir, tempFileName);
563        }
564        return tempFile;
565    }
566
567    // -------------------------------------------------------- Private methods
568
569    /**
570     * Returns an identifier that is unique within the class loader used to
571     * load this class, but does not have random-like appearance.
572     *
573     * @return A String with the non-random looking instance identifier.
574     */
575    private static String getUniqueId() {
576        final int limit = 100000000;
577        int current = COUNTER.getAndIncrement();
578        String id = Integer.toString(current);
579
580        // If you manage to get more than 100 million of ids, you'll
581        // start getting ids longer than 8 characters.
582        if (current < limit) {
583            id = ("00000000" + id).substring(id.length());
584        }
585        return id;
586    }
587
588    /**
589     * Returns a string representation of this object.
590     *
591     * @return a string representation of this object.
592     */
593    @Override
594    public String toString() {
595        return format("name=%s, StoreLocation=%s, size=%s bytes, isFormField=%s, FieldName=%s",
596                      getName(), getStoreLocation(), Long.valueOf(getSize()),
597                      Boolean.valueOf(isFormField()), getFieldName());
598    }
599
600    /**
601     * Returns the file item headers.
602     * @return The file items headers.
603     */
604    public FileItemHeaders getHeaders() {
605        return headers;
606    }
607
608    /**
609     * Sets the file item headers.
610     * @param pHeaders The file items headers.
611     */
612    public void setHeaders(FileItemHeaders pHeaders) {
613        headers = pHeaders;
614    }
615
616}