    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 */
    071public class DiskFileItem
    072    implements FileItem {
    073
    074    // ----------------------------------------------------- Manifest constants
    075
    076    /**
    077     * Default content charset to be used when no explicit charset
    078     * parameter is provided by the sender. Media subtypes of the
    079     * "text" type are defined to have a default charset value of
    080     * "ISO-8859-1" when received via HTTP.
    081     */
    082    public static final String DEFAULT_CHARSET = "ISO-8859-1";
    083
    084    // ----------------------------------------------------------- Data members
    085
    086    /**
    087     * UID used in unique file name generation.
    088     */
    089    private static final String UID =
    090            UUID.randomUUID().toString().replace('-', '_');
    091
    092    /**
    093     * Counter used in unique identifier generation.
    094     */
    095    private static final AtomicInteger COUNTER = new AtomicInteger(0);
    096
    097    /**
    098     * The name of the form field as provided by the browser.
    099     */
    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}