001/* 002 * Copyright 2002-2019 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.web.multipart.commons; 018 019import java.io.File; 020import java.io.IOException; 021import java.io.InputStream; 022import java.io.Serializable; 023import java.nio.file.Files; 024import java.nio.file.Path; 025 026import org.apache.commons.fileupload.FileItem; 027import org.apache.commons.fileupload.FileUploadException; 028import org.apache.commons.fileupload.disk.DiskFileItem; 029import org.apache.commons.logging.Log; 030import org.apache.commons.logging.LogFactory; 031 032import org.springframework.core.log.LogFormatUtils; 033import org.springframework.util.FileCopyUtils; 034import org.springframework.util.StreamUtils; 035import org.springframework.web.multipart.MultipartFile; 036 037/** 038 * {@link MultipartFile} implementation for Apache Commons FileUpload. 039 * 040 * @author Trevor D. Cook 041 * @author Juergen Hoeller 042 * @since 29.09.2003 043 * @see CommonsMultipartResolver 044 */ 045@SuppressWarnings("serial") 046public class CommonsMultipartFile implements MultipartFile, Serializable { 047 048 protected static final Log logger = LogFactory.getLog(CommonsMultipartFile.class); 049 050 private final FileItem fileItem; 051 052 private final long size; 053 054 private boolean preserveFilename = false; 055 056 057 /** 058 * Create an instance wrapping the given FileItem. 059 * @param fileItem the FileItem to wrap 060 */ 061 public CommonsMultipartFile(FileItem fileItem) { 062 this.fileItem = fileItem; 063 this.size = this.fileItem.getSize(); 064 } 065 066 067 /** 068 * Return the underlying {@code org.apache.commons.fileupload.FileItem} 069 * instance. There is hardly any need to access this. 070 */ 071 public final FileItem getFileItem() { 072 return this.fileItem; 073 } 074 075 /** 076 * Set whether to preserve the filename as sent by the client, not stripping off 077 * path information in {@link CommonsMultipartFile#getOriginalFilename()}. 078 * <p>Default is "false", stripping off path information that may prefix the 079 * actual filename e.g. from Opera. Switch this to "true" for preserving the 080 * client-specified filename as-is, including potential path separators. 081 * @since 4.3.5 082 * @see #getOriginalFilename() 083 * @see CommonsMultipartResolver#setPreserveFilename(boolean) 084 */ 085 public void setPreserveFilename(boolean preserveFilename) { 086 this.preserveFilename = preserveFilename; 087 } 088 089 090 @Override 091 public String getName() { 092 return this.fileItem.getFieldName(); 093 } 094 095 @Override 096 public String getOriginalFilename() { 097 String filename = this.fileItem.getName(); 098 if (filename == null) { 099 // Should never happen. 100 return ""; 101 } 102 if (this.preserveFilename) { 103 // Do not try to strip off a path... 104 return filename; 105 } 106 107 // Check for Unix-style path 108 int unixSep = filename.lastIndexOf('/'); 109 // Check for Windows-style path 110 int winSep = filename.lastIndexOf('\\'); 111 // Cut off at latest possible point 112 int pos = Math.max(winSep, unixSep); 113 if (pos != -1) { 114 // Any sort of path separator found... 115 return filename.substring(pos + 1); 116 } 117 else { 118 // A plain name 119 return filename; 120 } 121 } 122 123 @Override 124 public String getContentType() { 125 return this.fileItem.getContentType(); 126 } 127 128 @Override 129 public boolean isEmpty() { 130 return (this.size == 0); 131 } 132 133 @Override 134 public long getSize() { 135 return this.size; 136 } 137 138 @Override 139 public byte[] getBytes() { 140 if (!isAvailable()) { 141 throw new IllegalStateException("File has been moved - cannot be read again"); 142 } 143 byte[] bytes = this.fileItem.get(); 144 return (bytes != null ? bytes : new byte[0]); 145 } 146 147 @Override 148 public InputStream getInputStream() throws IOException { 149 if (!isAvailable()) { 150 throw new IllegalStateException("File has been moved - cannot be read again"); 151 } 152 InputStream inputStream = this.fileItem.getInputStream(); 153 return (inputStream != null ? inputStream : StreamUtils.emptyInput()); 154 } 155 156 @Override 157 public void transferTo(File dest) throws IOException, IllegalStateException { 158 if (!isAvailable()) { 159 throw new IllegalStateException("File has already been moved - cannot be transferred again"); 160 } 161 162 if (dest.exists() && !dest.delete()) { 163 throw new IOException( 164 "Destination file [" + dest.getAbsolutePath() + "] already exists and could not be deleted"); 165 } 166 167 try { 168 this.fileItem.write(dest); 169 LogFormatUtils.traceDebug(logger, traceOn -> { 170 String action = "transferred"; 171 if (!this.fileItem.isInMemory()) { 172 action = (isAvailable() ? "copied" : "moved"); 173 } 174 return "Part '" + getName() + "', filename '" + getOriginalFilename() + "'" + 175 (traceOn ? ", stored " + getStorageDescription() : "") + 176 ": " + action + " to [" + dest.getAbsolutePath() + "]"; 177 }); 178 } 179 catch (FileUploadException ex) { 180 throw new IllegalStateException(ex.getMessage(), ex); 181 } 182 catch (IllegalStateException | IOException ex) { 183 // Pass through IllegalStateException when coming from FileItem directly, 184 // or propagate an exception from I/O operations within FileItem.write 185 throw ex; 186 } 187 catch (Exception ex) { 188 throw new IOException("File transfer failed", ex); 189 } 190 } 191 192 @Override 193 public void transferTo(Path dest) throws IOException, IllegalStateException { 194 if (!isAvailable()) { 195 throw new IllegalStateException("File has already been moved - cannot be transferred again"); 196 } 197 198 FileCopyUtils.copy(this.fileItem.getInputStream(), Files.newOutputStream(dest)); 199 } 200 201 /** 202 * Determine whether the multipart content is still available. 203 * If a temporary file has been moved, the content is no longer available. 204 */ 205 protected boolean isAvailable() { 206 // If in memory, it's available. 207 if (this.fileItem.isInMemory()) { 208 return true; 209 } 210 // Check actual existence of temporary file. 211 if (this.fileItem instanceof DiskFileItem) { 212 return ((DiskFileItem) this.fileItem).getStoreLocation().exists(); 213 } 214 // Check whether current file size is different than original one. 215 return (this.fileItem.getSize() == this.size); 216 } 217 218 /** 219 * Return a description for the storage location of the multipart content. 220 * Tries to be as specific as possible: mentions the file location in case 221 * of a temporary file. 222 */ 223 public String getStorageDescription() { 224 if (this.fileItem.isInMemory()) { 225 return "in memory"; 226 } 227 else if (this.fileItem instanceof DiskFileItem) { 228 return "at [" + ((DiskFileItem) this.fileItem).getStoreLocation().getAbsolutePath() + "]"; 229 } 230 else { 231 return "on disk"; 232 } 233 } 234 235}