001/*
002 * Copyright (C) 2008 The Guava Authors
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
005 * in compliance with the License. You may obtain a copy of the License at
006 *
007 * http://www.apache.org/licenses/LICENSE-2.0
008 *
009 * Unless required by applicable law or agreed to in writing, software distributed under the License
010 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
011 * or implied. See the License for the specific language governing permissions and limitations under
012 * the License.
013 */
014
015package com.google.common.io;
016
017import static com.google.common.base.Preconditions.checkArgument;
018import static java.util.Objects.requireNonNull;
019
020import com.google.common.annotations.Beta;
021import com.google.common.annotations.GwtIncompatible;
022import com.google.common.annotations.J2ktIncompatible;
023import com.google.common.annotations.VisibleForTesting;
024import com.google.errorprone.annotations.concurrent.GuardedBy;
025import com.google.j2objc.annotations.J2ObjCIncompatible;
026import java.io.ByteArrayInputStream;
027import java.io.ByteArrayOutputStream;
028import java.io.File;
029import java.io.FileInputStream;
030import java.io.FileOutputStream;
031import java.io.IOException;
032import java.io.InputStream;
033import java.io.OutputStream;
034import javax.annotation.CheckForNull;
035
036/**
037 * An {@link OutputStream} that starts buffering to a byte array, but switches to file buffering
038 * once the data reaches a configurable size.
039 *
040 * <p>When this stream creates a temporary file, it restricts the file's permissions to the current
041 * user or, in the case of Android, the current app. If that is not possible (as is the case under
042 * the very old Android Ice Cream Sandwich release), then this stream throws an exception instead of
043 * creating a file that would be more accessible. (This behavior is new in Guava 32.0.0. Previous
044 * versions would create a file that is more accessible, as discussed in <a
045 * href="https://github.com/google/guava/issues/2575">Guava issue 2575</a>. TODO: b/283778848 - Fill
046 * in CVE number once it's available.)
047 *
048 * <p>Temporary files created by this stream may live in the local filesystem until either:
049 *
050 * <ul>
051 *   <li>{@link #reset} is called (removing the data in this stream and deleting the file), or...
052 *   <li>this stream (or, more precisely, its {@link #asByteSource} view) is finalized during
053 *       garbage collection, <strong>AND</strong> this stream was not constructed with {@linkplain
054 *       #FileBackedOutputStream(int) the 1-arg constructor} or the {@linkplain
055 *       #FileBackedOutputStream(int, boolean) 2-arg constructor} passing {@code false} in the
056 *       second parameter.
057 * </ul>
058 *
059 * <p>This class is thread-safe.
060 *
061 * @author Chris Nokleberg
062 * @since 1.0
063 */
064@Beta
065@J2ktIncompatible
066@GwtIncompatible
067@J2ObjCIncompatible
068@ElementTypesAreNonnullByDefault
069public final class FileBackedOutputStream extends OutputStream {
070  private final int fileThreshold;
071  private final boolean resetOnFinalize;
072  private final ByteSource source;
073
074  @GuardedBy("this")
075  private OutputStream out;
076
077  @GuardedBy("this")
078  @CheckForNull
079  private MemoryOutput memory;
080
081  @GuardedBy("this")
082  @CheckForNull
083  private File file;
084
085  /** ByteArrayOutputStream that exposes its internals. */
086  private static class MemoryOutput extends ByteArrayOutputStream {
087    byte[] getBuffer() {
088      return buf;
089    }
090
091    int getCount() {
092      return count;
093    }
094  }
095
096  /** Returns the file holding the data (possibly null). */
097  @VisibleForTesting
098  @CheckForNull
099  synchronized File getFile() {
100    return file;
101  }
102
103  /**
104   * Creates a new instance that uses the given file threshold, and does not reset the data when the
105   * {@link ByteSource} returned by {@link #asByteSource} is finalized.
106   *
107   * @param fileThreshold the number of bytes before the stream should switch to buffering to a file
108   * @throws IllegalArgumentException if {@code fileThreshold} is negative
109   */
110  public FileBackedOutputStream(int fileThreshold) {
111    this(fileThreshold, false);
112  }
113
114  /**
115   * Creates a new instance that uses the given file threshold, and optionally resets the data when
116   * the {@link ByteSource} returned by {@link #asByteSource} is finalized.
117   *
118   * @param fileThreshold the number of bytes before the stream should switch to buffering to a file
119   * @param resetOnFinalize if true, the {@link #reset} method will be called when the {@link
120   *     ByteSource} returned by {@link #asByteSource} is finalized.
121   * @throws IllegalArgumentException if {@code fileThreshold} is negative
122   */
123  public FileBackedOutputStream(int fileThreshold, boolean resetOnFinalize) {
124    checkArgument(
125        fileThreshold >= 0, "fileThreshold must be non-negative, but was %s", fileThreshold);
126    this.fileThreshold = fileThreshold;
127    this.resetOnFinalize = resetOnFinalize;
128    memory = new MemoryOutput();
129    out = memory;
130
131    if (resetOnFinalize) {
132      source =
133          new ByteSource() {
134            @Override
135            public InputStream openStream() throws IOException {
136              return openInputStream();
137            }
138
139            @Override
140            protected void finalize() {
141              try {
142                reset();
143              } catch (Throwable t) {
144                t.printStackTrace(System.err);
145              }
146            }
147          };
148    } else {
149      source =
150          new ByteSource() {
151            @Override
152            public InputStream openStream() throws IOException {
153              return openInputStream();
154            }
155          };
156    }
157  }
158
159  /**
160   * Returns a readable {@link ByteSource} view of the data that has been written to this stream.
161   *
162   * @since 15.0
163   */
164  public ByteSource asByteSource() {
165    return source;
166  }
167
168  private synchronized InputStream openInputStream() throws IOException {
169    if (file != null) {
170      return new FileInputStream(file);
171    } else {
172      // requireNonNull is safe because we always have either `file` or `memory`.
173      requireNonNull(memory);
174      return new ByteArrayInputStream(memory.getBuffer(), 0, memory.getCount());
175    }
176  }
177
178  /**
179   * Calls {@link #close} if not already closed, and then resets this object back to its initial
180   * state, for reuse. If data was buffered to a file, it will be deleted.
181   *
182   * @throws IOException if an I/O error occurred while deleting the file buffer
183   */
184  public synchronized void reset() throws IOException {
185    try {
186      close();
187    } finally {
188      if (memory == null) {
189        memory = new MemoryOutput();
190      } else {
191        memory.reset();
192      }
193      out = memory;
194      if (file != null) {
195        File deleteMe = file;
196        file = null;
197        if (!deleteMe.delete()) {
198          throw new IOException("Could not delete: " + deleteMe);
199        }
200      }
201    }
202  }
203
204  @Override
205  public synchronized void write(int b) throws IOException {
206    update(1);
207    out.write(b);
208  }
209
210  @Override
211  public synchronized void write(byte[] b) throws IOException {
212    write(b, 0, b.length);
213  }
214
215  @Override
216  public synchronized void write(byte[] b, int off, int len) throws IOException {
217    update(len);
218    out.write(b, off, len);
219  }
220
221  @Override
222  public synchronized void close() throws IOException {
223    out.close();
224  }
225
226  @Override
227  public synchronized void flush() throws IOException {
228    out.flush();
229  }
230
231  /**
232   * Checks if writing {@code len} bytes would go over threshold, and switches to file buffering if
233   * so.
234   */
235  @GuardedBy("this")
236  private void update(int len) throws IOException {
237    if (memory != null && (memory.getCount() + len > fileThreshold)) {
238      File temp = TempFileCreator.INSTANCE.createTempFile("FileBackedOutputStream");
239      if (resetOnFinalize) {
240        // Finalizers are not guaranteed to be called on system shutdown;
241        // this is insurance.
242        temp.deleteOnExit();
243      }
244      try {
245        FileOutputStream transfer = new FileOutputStream(temp);
246        transfer.write(memory.getBuffer(), 0, memory.getCount());
247        transfer.flush();
248        // We've successfully transferred the data; switch to writing to file
249        out = transfer;
250      } catch (IOException e) {
251        temp.delete();
252        throw e;
253      }
254
255      file = temp;
256      memory = null;
257    }
258  }
259}