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 * https://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.fileupload2.core; 018 019import java.io.ByteArrayInputStream; 020import java.io.ByteArrayOutputStream; 021import java.io.IOException; 022import java.io.InputStream; 023import java.io.OutputStream; 024import java.nio.file.Files; 025import java.nio.file.Path; 026import java.util.function.Supplier; 027 028 029/** 030 * An {@link OutputStream}, which keeps its data in memory, until a configured 031 * threshold is reached. If that is the case, a temporary file is being created, 032 * and the in-memory data is transferred to that file. All following data will 033 * be written to that file, too. 034 * 035 * In other words: If an uploaded file is small, then it will be kept completely 036 * in memory. On the other hand, if the uploaded file's size exceeds the 037 * configured threshold, it it considered a large file, and the data is kept 038 * in a temporary file. 039 * 040 * More precisely, this output stream supports three modes of operation: 041 * <ol> 042 * <li>{@code threshold=-1}: <em>Always</em> create a temporary file, even if 043 * the uploaded file is empty.</li> 044 * <li>{@code threshold=0}: Don't create empty, temporary files. (Create a 045 * temporary file, as soon as the first byte is written.)</li> 046 * <li>{@code threshold>0}: Create a temporary file, if the size exceeds the 047 * threshold, otherwise keep the file in memory.</li> 048 * </ol> 049 * 050 * Technically, this is similar to 051 * {@link org.apache.commons.io.output.DeferredFileOutputStream}, which has 052 * been used in the past, except that this implementation observes 053 * a precisely specified behavior, and semantics, that match the needs of the 054 * {@link DiskFileItem}. 055 * 056 * Background: Over the various versions of commons-io, the 057 * {@link org.apache.commons.io.output.DeferredFileOutputStream} has changed 058 * semantics, and behavior more than once. 059 * (For details, see 060 * <a href="https://issues.apache.org/jira/browse/FILEUPLOAD-295">FILEUPLOAD-295</a>) 061 */ 062public class DeferrableOutputStream extends OutputStream { 063 064 /** 065 * Interface of a listener object, that wishes to be notified about 066 * state changes. 067 */ 068 public interface Listener { 069 070 /** 071 * Called, after {@link #persist()} has been invoked, 072 * and the temporary file has been created. 073 * @param path Path of the temporary file, that has been 074 * created. All in-memory data has been transferred to 075 * that file, but it is still opened. 076 */ 077 default void persisted(final Path path) { } 078 } 079 080 /** 081 * This enumeration represents the possible states of the {@link DeferrableOutputStream}. 082 */ 083 public enum State { 084 085 /** 086 * The stream object has been created with a non-negative threshold, 087 * but so far no data has been written. 088 */ 089 initialized, 090 091 /** 092 * The stream object has been created with a non-negative threshold, 093 * and some data has been written, but the threshold is not yet exceeded, 094 * and the data is still kept in memory. 095 */ 096 opened, 097 098 /** 099 * Either of the following conditions is given: 100 * <ol> 101 * <li>The stream object has been created with a threshold of -1, or</li> 102 * <li>the stream object has been created with a non-negative threshold, 103 * and some data has been written. The number of bytes, that have 104 * been written, exceeds the configured threshold.</li> 105 * </ol> 106 * In either case, a temporary file has been created, and all data has been 107 * written to the temporary file, erasing all existing data from memory. 108 */ 109 persisted, 110 111 /** 112 * The stream has been closed, and data can no longer be written. It is 113 * now valid to invoke {@link DeferrableOutputStream#getInputStream()}. 114 */ 115 closed 116 } 117 118 /** 119 * The configured threshold, as an integer. This variable isn't actually 120 * used. Instead {@link #longThreshold} is used. 121 * @see #longThreshold 122 */ 123 private final int threshold; 124 125 /** 126 * The configured threshold, as a long integer. (Using a long integer 127 * enables proper handling of the threshold, when the file size is 128 * approaching {@link Integer#MAX_VALUE}. 129 * @see #threshold 130 */ 131 private final long longThreshold; 132 133 /** 134 * This supplier will be invoked, if the temporary file is created, 135 * t 136 * determine the temporary file's location. 137 * @see #path 138 */ 139 private final Supplier<Path> pathSupplier; 140 141 /** 142 * If a temporary file has been created: Path of the temporary 143 * file. Otherwise null. 144 * @see #pathSupplier 145 */ 146 private Path path; 147 148 /** 149 * If no temporary file was created: A stream, to which the 150 * incoming data is being written, until the threshold is reached. 151 * Otherwise null. 152 */ 153 private ByteArrayOutputStream baos; 154 155 /** 156 * If no temporary file was created, and the stream is closed: 157 * The in-memory data, that was written to the stream. Otherwise null. 158 */ 159 private byte[] bytes; 160 161 /** 162 * If a temporary file has been created: An open stream 163 * for writing to that file. Otherwise null. 164 */ 165 private OutputStream out; 166 167 /** 168 * The streams current state. 169 */ 170 private State state; 171 172 /** 173 * True, if the stream has ever been in state {@link State#persisted}. 174 * Or, in other words: True, if a temporary file has been created. 175 */ 176 private boolean wasPersisted; 177 178 /** 179 * Number of bytes, that have been written to this stream so far. 180 */ 181 private long size; 182 183 /** 184 * The configured {@link Listener}, if any, or null. 185 */ 186 private final Listener listener; 187 188 /** 189 * Creates a new instance with the given threshold, and the given supplier for a 190 * temporary files path. 191 * If the threshold is -1, then the temporary file will be created immediately, and 192 * no in-memory data will be kept, at all. 193 * If the threshold is 0, then the temporary file will be created, as soon as the 194 * first byte will be written, but no in-memory data will be kept. 195 * If the threshold is > 0, then the temporary file will be created, as soon as that 196 * number of bytes have been written. Up to that point, data will be kept in an 197 * in-memory buffer. 198 * 199 * @param threshold Either of -1 (Create the temporary file immediately), 0 (Create 200 * the temporary file, as soon as data is being written for the first time), or >0 201 * (Keep data in memory, as long as the given number of bytes is reached, then 202 * create a temporary file, and continue using that). 203 * @param pathSupplier A supplier for the temporary files path. This supplier must 204 * not return null. The file's directory will be created, if necessary, by 205 * invoking {@link Files#createDirectories(Path, java.nio.file.attribute.FileAttribute...)}. 206 * @param listener An optional listener, which is being notified about important state 207 * changes. 208 * @throws IOException Creating the temporary file (in the case of threshold -1) 209 * has failed. 210 */ 211 public DeferrableOutputStream(final int threshold, final Supplier<Path> pathSupplier, final Listener listener) throws IOException { 212 if (threshold < 0) { 213 this.threshold = -1; 214 } else { 215 this.threshold = threshold; 216 } 217 longThreshold = (long) threshold; 218 this.pathSupplier = pathSupplier; 219 this.listener = listener; 220 checkThreshold(0); 221 } 222 223 /** 224 * Called to check, whether the threshold will be exceeded, if the given number 225 * of bytes are written to the stream. If so, persists the in-memory data by 226 * creating a new, temporary file, and writing the in-memory data to the file. 227 * @param numberOfIncomingBytes The number of bytes, which are about to be written. 228 * @return The actual output stream, to which the incoming data may be written. 229 * If the threshold is not yet exceeded, then this will be an internal 230 * {@link ByteArrayOutputStream}, otherwise a stream, which is writing to the 231 * temporary output file. 232 * @throws IOException Persisting the in-memory data to a temporary file 233 * has failed. 234 */ 235 protected OutputStream checkThreshold(final int numberOfIncomingBytes) throws IOException { 236 if (state == null) { 237 // Called from the constructor, state is unspecified. 238 if (threshold == -1) { 239 return persist(); 240 } else { 241 baos = new ByteArrayOutputStream(); 242 bytes = null; 243 state = State.initialized; 244 return baos; 245 } 246 } else { 247 switch (state) { 248 case initialized: 249 case opened: 250 final int bytesWritten = baos.size(); 251 if ((long) bytesWritten + (long) numberOfIncomingBytes >= longThreshold) { 252 return persist(); 253 } 254 if (numberOfIncomingBytes > 0) { 255 state = State.opened; 256 } 257 return baos; 258 case persisted: 259 // Do nothing, we're staying in the current state. 260 return out; 261 case closed: 262 // Do nothing, we're staying in the current state. 263 return null; 264 default: 265 throw illegalStateError(); 266 } 267 } 268 } 269 270 @Override 271 public void close() throws IOException { 272 switch (state) { 273 case initialized: 274 case opened: 275 bytes = baos.toByteArray(); 276 baos = null; 277 state = State.closed; 278 break; 279 case persisted: 280 bytes = null; 281 out.close(); 282 state = State.closed; 283 break; 284 case closed: 285 // Already closed, do nothing. 286 break; 287 default: 288 throw illegalStateError(); 289 } 290 } 291 292 /** 293 * Returns the data, that has been written, if the stream has 294 * been closed, and the stream is still in memory 295 * ({@link #isInMemory()} returns true). Otherwise, returns null. 296 * @return If the stream is closed (no more data can be written), 297 * and the data is still in memory (no temporary file has been 298 * created), returns the data, that has been written. Otherwise, 299 * returns null. 300 */ 301 public byte[] getBytes() { 302 return bytes; 303 } 304 305 /** 306 * If the stream is closed: Returns an {@link InputStream} on the 307 * data, that has been written to this stream. Otherwise, throws 308 * an {@link IllegalStateException}. 309 * @return An {@link InputStream} on the data, that has been 310 * written. Never null. 311 * @throws IllegalStateException The stream has not yet been 312 * closed. 313 * @throws IOException Creating the {@link InputStream} has 314 * failed. 315 */ 316 public InputStream getInputStream() throws IOException { 317 if (state == State.closed) { 318 if (bytes != null) { 319 return new ByteArrayInputStream(bytes); 320 } else { 321 return Files.newInputStream(path); 322 } 323 } else { 324 throw new IllegalStateException("This stream isn't yet closed."); 325 } 326 } 327 328 /** 329 * Returns the output file, that has been created, if any, or null. 330 * The latter is the case, if {@link #isInMemory()} returns true. 331 * @return The output file, that has been created, if any, or null. 332 */ 333 public Path getPath() { 334 return path; 335 } 336 337 /** 338 * Returns the number of bytes, that have been written to this stream. 339 * @return The number of bytes, that have been written to this stream. 340 */ 341 public long getSize() { 342 return size; 343 } 344 345 /** 346 * Returns the streams current state. 347 * @return The streams current state. 348 */ 349 public State getState() { 350 return state; 351 } 352 353 /** 354 * Returns the streams configured threshold. 355 * @return The streams configured threshold. 356 */ 357 public int getThreshold() { 358 return threshold; 359 } 360 361 /** 362 * Returns the path of the output file, if such a file has 363 * been created. That is the case, if {@link #isInMemory()} 364 * returns false. Otherwise, returns null. 365 * @return Path of the created output file, if any, or null. 366 */ 367 private IllegalStateException illegalStateError() { 368 throw new IllegalStateException("Expected state initialized|opened|persisted|closed, got " + state.name()); 369 } 370 371 /** 372 * Returns true, if this stream was never persisted, 373 * and no output file has been created. 374 * @return True, if the stream was never in state 375 * {@link State#persisted}, otherwise false. 376 */ 377 public boolean isInMemory() { 378 switch (state) { 379 case initialized: 380 case opened: 381 return true; 382 case persisted: 383 return false; 384 case closed: 385 return !wasPersisted; 386 default: 387 throw illegalStateError(); 388 } 389 } 390 391 /** 392 * Create the output file, change the state to {@code persisted}, and 393 * return an {@link OutputStream}, which is writing to that file. 394 * @return The {@link OutputStream}, which is writing to the created, 395 * temporary file. 396 * @throws IOException Creating the temporary file has failed. 397 */ 398 protected OutputStream persist() throws IOException { 399 final Path p = pathSupplier.get(); 400 final Path dir = p.getParent(); 401 if (dir != null) { 402 Files.createDirectories(dir); 403 } 404 final OutputStream os = Files.newOutputStream(p); 405 if (baos != null) { 406 baos.writeTo(os); 407 } 408 409 /** 410 * At this point, the output file has been successfully created, 411 * and we can safely switch state. 412 */ 413 state = State.persisted; 414 wasPersisted = true; 415 path = p; 416 out = os; 417 baos = null; 418 bytes = null; 419 if (listener != null) { 420 listener.persisted(p); 421 } 422 return os; 423 } 424 425 @Override 426 public void write(final byte[] buffer) throws IOException { 427 write(buffer, 0, buffer.length); 428 } 429 430 @Override 431 public void write(final byte[] buffer, final int offset, final int len) throws IOException { 432 if (len > 0) { 433 final OutputStream os = checkThreshold(len); 434 if (os == null) { 435 throw new IOException("This stream has already been closed."); 436 } 437 bytes = null; 438 os.write(buffer, offset, len); 439 size += len; 440 } 441 } 442 443 @Override 444 public void write(final int b) throws IOException { 445 final OutputStream os = checkThreshold(1); 446 if (os == null) { 447 throw new IOException("This stream has already been closed."); 448 } 449 bytes = null; 450 os.write(b); 451 size++; 452 } 453}