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