com.helger.web.fileupload.parse.DiskFileItem Maven / Gradle / Ivy
Show all versions of ph-web Show documentation
/*
* Copyright (C) 2014-2024 Philip Helger (www.helger.com)
* philip[at]helger[dot]com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.helger.web.fileupload.parse;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.ReturnsMutableCopy;
import com.helger.commons.annotation.ReturnsMutableObject;
import com.helger.commons.charset.CharsetHelper;
import com.helger.commons.collection.ArrayHelper;
import com.helger.commons.collection.impl.ICommonsMap;
import com.helger.commons.equals.EqualsHelper;
import com.helger.commons.io.file.FileHelper;
import com.helger.commons.io.file.FileIOError;
import com.helger.commons.io.file.FileOperations;
import com.helger.commons.io.file.FilenameHelper;
import com.helger.commons.io.file.SimpleFileIO;
import com.helger.commons.io.stream.NonBlockingByteArrayInputStream;
import com.helger.commons.io.stream.StreamHelper;
import com.helger.commons.state.ESuccess;
import com.helger.commons.state.ISuccessIndicator;
import com.helger.commons.string.StringHelper;
import com.helger.commons.string.ToStringGenerator;
import com.helger.commons.system.SystemProperties;
import com.helger.web.fileupload.IFileItem;
import com.helger.web.fileupload.IFileItemHeaders;
import com.helger.web.fileupload.IFileItemHeadersSupport;
import com.helger.web.fileupload.exception.FileUploadException;
import com.helger.web.fileupload.io.DeferredFileOutputStream;
import com.helger.web.fileupload.io.FileUploadHelper;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
/**
*
* The default implementation of the {@link IFileItem} interface.
*
* After retrieving an instance you may either request all contents of file at
* once using {@link #directGet()} or request an {@link java.io.InputStream
* InputStream} with {@link #getInputStream()} and process the file without
* attempting to load it into memory, which may come handy with large files.
*
* @author Rafal Krzewski
* @author Sean Legassick
* @author Jason van Zyl
* @author John McNally
* @author Martin Cooper
* @author Sean C. Sullivan
* @since FileUpload 1.1
* @version $Id: DiskFileItem.java 963609 2010-07-13 06:56:47Z jochen $
*/
@NotThreadSafe
public class DiskFileItem implements IFileItem, IFileItemHeadersSupport
{
private static final Logger LOGGER = LoggerFactory.getLogger (DiskFileItem.class);
/**
* Default content charset to be used when no explicit charset parameter is
* provided by the sender. Media subtypes of the "text" type are defined to
* have a default charset value of "ISO-8859-1" when received via HTTP.
*/
public static final Charset DEFAULT_CHARSET_OBJ = StandardCharsets.ISO_8859_1;
/**
* Default content charset to be used when no explicit charset parameter is
* provided by the sender. Media subtypes of the "text" type are defined to
* have a default charset value of "ISO-8859-1" when received via HTTP.
*/
public static final String DEFAULT_CHARSET = DEFAULT_CHARSET_OBJ.name ();
/**
* UID used in unique file name generation.
*/
private static final String UID = StringHelper.replaceAll (StringHelper.replaceAll (UUID.randomUUID ().toString (),
':',
'_'), '-', '_');
/**
* Counter used in unique identifier generation.
*/
private static final AtomicInteger TEMP_FILE_COUNTER = new AtomicInteger (0);
/**
* The name of the form field as provided by the browser.
*/
private String m_sFieldName;
/**
* The content type passed by the browser, or null
if not
* defined.
*/
private final String m_sContentType;
/**
* Whether or not this item is a simple form field.
*/
private boolean m_bIsFormField;
/**
* The original filename in the user's filesystem.
*/
private final String m_sFilename;
/**
* The size of the item, in bytes. This is used to cache the size when a file
* item is moved from its original location.
*/
private long m_nSize = -1;
/**
* The threshold above which uploads will be stored on disk.
*/
private final int m_nSizeThreshold;
/**
* The directory in which uploaded files will be stored, if stored on disk.
*/
private final File m_aTempDir;
/**
* Cached contents of the file.
*/
private byte [] m_aCachedContent;
/**
* Output stream for this item.
*/
private DeferredFileOutputStream m_aDFOS;
/**
* The temporary file to use.
*/
private File m_aTempFile;
/**
* File to allow for serialization of the content of this item.
*/
private File m_aDFOSFile;
/**
* The file items headers.
*/
private IFileItemHeaders m_aHeaders;
/**
* Constructs a new DiskFileItem
instance.
*
* @param sFieldName
* The name of the form field.
* @param sContentType
* The content type passed by the browser or null
if not
* specified.
* @param bIsFormField
* Whether or not this item is a plain form field, as opposed to a file
* upload.
* @param sFilename
* The original filename in the user's file system, or
* null
if not specified.
* @param nSizeThreshold
* The threshold, in bytes, below which items will be retained in
* memory and above which they will be stored as a file.
* @param aRepository
* The data repository, which is the directory in which files will be
* created, should the item size exceed the threshold.
* null
means default temp directory.
*/
public DiskFileItem (@Nullable final String sFieldName,
@Nullable final String sContentType,
final boolean bIsFormField,
@Nullable final String sFilename,
@Nonnegative final int nSizeThreshold,
@Nullable final File aRepository)
{
m_sFieldName = sFieldName;
m_sContentType = sContentType;
m_bIsFormField = bIsFormField;
m_sFilename = sFilename;
m_nSizeThreshold = ValueEnforcer.isGT0 (nSizeThreshold, "SizeThreshold");
m_aTempDir = aRepository != null ? aRepository : new File (SystemProperties.getTmpDir ());
if (!FileHelper.existsDir (m_aTempDir))
throw new IllegalArgumentException ("The tempory directory for file uploads is not existing: " +
m_aTempDir.getAbsolutePath ());
if (!m_aTempDir.canRead ())
throw new IllegalArgumentException ("The tempory directory for file uploads cannot be read: " +
m_aTempDir.getAbsolutePath ());
if (!m_aTempDir.canWrite ())
throw new IllegalArgumentException ("The tempory directory for file uploads cannot be written: " +
m_aTempDir.getAbsolutePath ());
}
/**
* Writes the state of this object during serialization.
*
* @param aOS
* The stream to which the state should be written.
* @throws IOException
* if an error occurs.
*/
private void writeObject (final ObjectOutputStream aOS) throws IOException
{
// Read the data
if (m_aDFOS.isInMemory ())
{
_ensureCachedContentIsPresent ();
}
else
{
m_aCachedContent = null;
m_aDFOSFile = m_aDFOS.getFile ();
}
// write out values
aOS.defaultWriteObject ();
}
/**
* Reads the state of this object during deserialization.
*
* @param aOIS
* The stream from which the state should be read.
* @throws IOException
* if an error occurs.
* @throws ClassNotFoundException
* if class cannot be found.
*/
private void readObject (@Nonnull final ObjectInputStream aOIS) throws IOException, ClassNotFoundException
{
// read values
aOIS.defaultReadObject ();
try (final OutputStream aOS = getOutputStream ())
{
if (m_aCachedContent != null)
{
aOS.write (m_aCachedContent);
}
else
{
final InputStream aIS = FileHelper.getInputStream (m_aDFOSFile);
StreamHelper.copyInputStreamToOutputStream (aIS, aOS);
FileOperations.deleteFile (m_aDFOSFile);
m_aDFOSFile = null;
}
}
m_aCachedContent = null;
}
/**
* @return The base directory for all temporary files.
*/
@Nonnull
public final File getTempDirectory ()
{
return m_aTempDir;
}
/**
* Creates and returns a {@link File} representing a uniquely named temporary
* file in the configured repository path. The lifetime of the file is tied to
* the lifetime of the FileItem
instance; the file will be
* deleted when the instance is garbage collected.
*
* @return The {@link File} to be used for temporary storage.
*/
@Nonnull
protected File getTempFile ()
{
if (m_aTempFile == null)
{
// If you manage to get more than 100 million of ids, you'll
// start getting ids longer than 8 characters.
final String sUniqueID = StringHelper.getLeadingZero (TEMP_FILE_COUNTER.getAndIncrement (), 8);
final String sTempFileName = "upload_" + UID + "_" + sUniqueID + ".tmp";
m_aTempFile = new File (m_aTempDir, sTempFileName);
}
return m_aTempFile;
}
private void _ensureCachedContentIsPresent ()
{
if (m_aCachedContent == null)
m_aCachedContent = m_aDFOS.getData ();
}
/**
* @return An {@link InputStream} that can be used to retrieve the contents of
* the file.
*/
@Nonnull
public InputStream getInputStream ()
{
if (isInMemory ())
{
_ensureCachedContentIsPresent ();
return new NonBlockingByteArrayInputStream (m_aCachedContent);
}
return FileHelper.getInputStream (m_aDFOS.getFile ());
}
public boolean isReadMultiple ()
{
return true;
}
@Nullable
public String getContentType ()
{
return m_sContentType;
}
/**
* Returns the content charset passed by the agent or null
if not
* defined.
*
* @return The content charset passed by the agent or null
if not
* defined.
*/
@Nullable
public String getCharSet ()
{
// Parameter parser can handle null input
final ICommonsMap aParams = new ParameterParser ().setLowerCaseNames (true)
.parse (getContentType (), ';');
return aParams.get ("charset");
}
@Nullable
public String getNameUnchecked ()
{
return m_sFilename;
}
@Nullable
public String getName ()
{
return FileUploadHelper.checkFileName (m_sFilename);
}
@Nullable
public String getNameSecure ()
{
final String sSecureName = FilenameHelper.getAsSecureValidFilename (m_sFilename);
if (!EqualsHelper.equals (sSecureName, m_sFilename))
LOGGER.info ("FileItem filename was changed from '" + m_sFilename + "' to '" + sSecureName + "'");
return sSecureName;
}
/**
* Provides a hint as to whether or not the file contents will be read from
* memory.
*
* @return true
if the file contents will be read from memory;
* false
otherwise.
*/
public boolean isInMemory ()
{
return m_aCachedContent != null || m_aDFOS.isInMemory ();
}
/**
* Returns the size of the file.
*
* @return The size of the file, in bytes.
*/
@Nonnegative
public long getSize ()
{
if (m_nSize >= 0)
return m_nSize;
if (m_aCachedContent != null)
return m_aCachedContent.length;
if (m_aDFOS.isInMemory ())
return m_aDFOS.getDataLength ();
return m_aDFOS.getFile ().length ();
}
/**
* Returns the contents of the file as an array of bytes. If the contents of
* the file were not yet cached in memory, they will be loaded from the disk
* storage and cached.
*
* @return The contents of the file as an array of bytes.
*/
@ReturnsMutableObject ("Speed")
@SuppressFBWarnings ("EI_EXPOSE_REP")
@Nullable
public byte [] directGet ()
{
if (isInMemory ())
{
_ensureCachedContentIsPresent ();
return m_aCachedContent;
}
return SimpleFileIO.getAllFileBytes (m_aDFOS.getFile ());
}
@Nullable
@ReturnsMutableCopy
public byte [] getCopy ()
{
if (isInMemory ())
{
_ensureCachedContentIsPresent ();
return ArrayHelper.getCopy (m_aCachedContent);
}
return SimpleFileIO.getAllFileBytes (m_aDFOS.getFile ());
}
@Nonnull
public String getString ()
{
return getStringWithFallback (DEFAULT_CHARSET_OBJ);
}
/**
* Get the string with the charset defined in the content type.
*
* @param aFallbackCharset
* The fallback charset to be used if the content type does not include
* a charset. May not be null
.
* @return The string representation of the item.
*/
@Nonnull
public String getStringWithFallback (@Nonnull final Charset aFallbackCharset)
{
final String sCharset = getCharSet ();
final Charset aCharset = CharsetHelper.getCharsetFromNameOrDefault (sCharset, aFallbackCharset);
return getString (aCharset);
}
/**
* A convenience method to write an uploaded item to disk. The client code is
* not concerned with whether or not the item is stored in memory, or on disk
* in a temporary location. They just want to write the uploaded item to a
* file.
*
* This implementation first attempts to rename the uploaded item to the
* specified destination file, if the item was originally written to disk.
* Otherwise, the data will be copied to the specified file.
*
* This method is only guaranteed to work once, the first time it is
* invoked for a particular item. This is because, in the event that the
* method renames a temporary file, that file will no longer be available to
* copy or rename again at a later time.
*
* @param aDstFile
* The File
into which the uploaded item should be stored.
* @throws FileUploadException
* if an error occurs.
*/
@Nonnull
public ISuccessIndicator write (@Nonnull final File aDstFile) throws FileUploadException
{
ValueEnforcer.notNull (aDstFile, "DstFile");
if (isInMemory ())
return SimpleFileIO.writeFile (aDstFile, directGet ());
final File aOutputFile = getStoreLocation ();
if (aOutputFile != null)
{
// Save the length of the file
m_nSize = aOutputFile.length ();
/*
* The uploaded file is being stored on disk in a temporary location so
* move it to the desired file.
*/
if (FileOperations.renameFile (aOutputFile, aDstFile).isSuccess ())
return ESuccess.SUCCESS;
// Copying needed
return FileOperations.copyFile (aOutputFile, aDstFile);
}
// For whatever reason we cannot write the file to disk.
throw new FileUploadException ("Cannot write uploaded file to: " + aDstFile.getAbsolutePath ());
}
/**
* Deletes the underlying storage for a file item, including deleting any
* associated temporary disk file. Although this storage will be deleted
* automatically when the FileItem
instance is garbage collected,
* this method can be used to ensure that this is done at an earlier time,
* thus preserving system resources.
*/
public void delete ()
{
m_aCachedContent = null;
final File aTempFile = getStoreLocation ();
if (aTempFile != null)
{
final FileIOError aIOError = FileOperations.deleteFileIfExisting (aTempFile);
if (aIOError.isFailure ())
LOGGER.error ("Failed to delete temporary file " + aTempFile + " with error " + aIOError.toString ());
}
}
/**
* Returns the name of the field in the multipart form corresponding to this
* file item.
*
* @return The name of the form field.
* @see #setFieldName(java.lang.String)
*/
@Nullable
public String getFieldName ()
{
return m_sFieldName;
}
/**
* Sets the field name used to reference this file item.
*
* @param sFieldName
* The name of the form field.
* @see #getFieldName()
*/
public void setFieldName (@Nullable final String sFieldName)
{
m_sFieldName = sFieldName;
}
/**
* Determines whether or not a FileItem
instance represents a
* simple form field.
*
* @return true
if the instance represents a simple form field;
* false
if it represents an uploaded file.
* @see #setFormField(boolean)
*/
public boolean isFormField ()
{
return m_bIsFormField;
}
/**
* Specifies whether or not a FileItem
instance represents a
* simple form field.
*
* @param bIsFormField
* true
if the instance represents a simple form field;
* false
if it represents an uploaded file.
* @see #isFormField()
*/
public void setFormField (final boolean bIsFormField)
{
m_bIsFormField = bIsFormField;
}
@Nullable
public IFileItemHeaders getHeaders ()
{
return m_aHeaders;
}
public void setHeaders (@Nullable final IFileItemHeaders aHeaders)
{
m_aHeaders = aHeaders;
}
@Nonnull
public DeferredFileOutputStream getOutputStream ()
{
if (m_aDFOS == null)
{
final File aTempFile = getTempFile ();
m_aDFOS = new DeferredFileOutputStream (m_nSizeThreshold, aTempFile);
}
return m_aDFOS;
}
/**
* Returns the {@link java.io.File} object for the FileItem
's
* data's temporary location on the disk. Note that for FileItem
s
* that have their data stored in memory, this method will return
* null
. When handling large files, you can use
* {@link java.io.File#renameTo(java.io.File)} to move the file to new
* location without copying the data, if the source and destination locations
* reside within the same logical volume.
*
* @return The data file, or null
if the data is stored in
* memory.
*/
@Nullable
public File getStoreLocation ()
{
return m_aDFOS == null ? null : m_aDFOS.getFile ();
}
/**
* Removes the file contents from the temporary storage. This was previously
* handled in a finalize
method.
*
* @since v10.0.0
*/
public void onEndOfRequest ()
{
if (m_aDFOS != null)
{
LOGGER.info ("Deleting temporary DiskFileItem " + m_aDFOS.getFile ());
FileOperations.deleteFileIfExisting (m_aDFOS.getFile ());
}
}
@Override
public String toString ()
{
return new ToStringGenerator (this).append ("nameSecure", getNameSecure ())
.appendIfNotNull ("storeLocation", getStoreLocation ())
.append ("size", getSize ())
.append ("isFormField", m_bIsFormField)
.append ("fieldName", m_sFieldName)
.append ("headers", m_aHeaders)
.getToString ();
}
}