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 &gt; 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 &gt;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}