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 org.jspecify.annotations.Nullable;
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
068public final class FileBackedOutputStream extends OutputStream {
069  private final int fileThreshold;
070  private final boolean resetOnFinalize;
071  private final ByteSource source;
072
073  @GuardedBy("this")
074  private OutputStream out;
075
076  @GuardedBy("this")
077  private @Nullable MemoryOutput memory;
078
079  @GuardedBy("this")
080  private @Nullable File file;
081
082  /** ByteArrayOutputStream that exposes its internals. */
083  private static class MemoryOutput extends ByteArrayOutputStream {
084    byte[] getBuffer() {
085      return buf;
086    }
087
088    int getCount() {
089      return count;
090    }
091  }
092
093  /** Returns the file holding the data (possibly null). */
094  @VisibleForTesting
095  synchronized @Nullable File getFile() {
096    return file;
097  }
098
099  /**
100   * Creates a new instance that uses the given file threshold, and does not reset the data when the
101   * {@link ByteSource} returned by {@link #asByteSource} is finalized.
102   *
103   * @param fileThreshold the number of bytes before the stream should switch to buffering to a file
104   * @throws IllegalArgumentException if {@code fileThreshold} is negative
105   */
106  public FileBackedOutputStream(int fileThreshold) {
107    this(fileThreshold, false);
108  }
109
110  /**
111   * Creates a new instance that uses the given file threshold, and optionally resets the data when
112   * the {@link ByteSource} returned by {@link #asByteSource} is finalized.
113   *
114   * @param fileThreshold the number of bytes before the stream should switch to buffering to a file
115   * @param resetOnFinalize if true, the {@link #reset} method will be called when the {@link
116   *     ByteSource} returned by {@link #asByteSource} is finalized.
117   * @throws IllegalArgumentException if {@code fileThreshold} is negative
118   */
119  public FileBackedOutputStream(int fileThreshold, boolean resetOnFinalize) {
120    checkArgument(
121        fileThreshold >= 0, "fileThreshold must be non-negative, but was %s", fileThreshold);
122    this.fileThreshold = fileThreshold;
123    this.resetOnFinalize = resetOnFinalize;
124    memory = new MemoryOutput();
125    out = memory;
126
127    if (resetOnFinalize) {
128      source =
129          new ByteSource() {
130            @Override
131            public InputStream openStream() throws IOException {
132              return openInputStream();
133            }
134
135            @SuppressWarnings({"removal", "Finalize"}) // b/260137033
136            @Override
137            protected void finalize() {
138              try {
139                reset();
140              } catch (Throwable t) {
141                t.printStackTrace(System.err);
142              }
143            }
144          };
145    } else {
146      source =
147          new ByteSource() {
148            @Override
149            public InputStream openStream() throws IOException {
150              return openInputStream();
151            }
152          };
153    }
154  }
155
156  /**
157   * Returns a readable {@link ByteSource} view of the data that has been written to this stream.
158   *
159   * @since 15.0
160   */
161  public ByteSource asByteSource() {
162    return source;
163  }
164
165  private synchronized InputStream openInputStream() throws IOException {
166    if (file != null) {
167      return new FileInputStream(file);
168    } else {
169      // requireNonNull is safe because we always have either `file` or `memory`.
170      requireNonNull(memory);
171      return new ByteArrayInputStream(memory.getBuffer(), 0, memory.getCount());
172    }
173  }
174
175  /**
176   * Calls {@link #close} if not already closed, and then resets this object back to its initial
177   * state, for reuse. If data was buffered to a file, it will be deleted.
178   *
179   * @throws IOException if an I/O error occurred while deleting the file buffer
180   */
181  public synchronized void reset() throws IOException {
182    try {
183      close();
184    } finally {
185      if (memory == null) {
186        memory = new MemoryOutput();
187      } else {
188        memory.reset();
189      }
190      out = memory;
191      if (file != null) {
192        File deleteMe = file;
193        file = null;
194        if (!deleteMe.delete()) {
195          throw new IOException("Could not delete: " + deleteMe);
196        }
197      }
198    }
199  }
200
201  @Override
202  public synchronized void write(int b) throws IOException {
203    update(1);
204    out.write(b);
205  }
206
207  @Override
208  public synchronized void write(byte[] b) throws IOException {
209    write(b, 0, b.length);
210  }
211
212  @Override
213  public synchronized void write(byte[] b, int off, int len) throws IOException {
214    update(len);
215    out.write(b, off, len);
216  }
217
218  @Override
219  public synchronized void close() throws IOException {
220    out.close();
221  }
222
223  @Override
224  public synchronized void flush() throws IOException {
225    out.flush();
226  }
227
228  /**
229   * Checks if writing {@code len} bytes would go over threshold, and switches to file buffering if
230   * so.
231   */
232  @GuardedBy("this")
233  private void update(int len) throws IOException {
234    if (memory != null && (memory.getCount() + len > fileThreshold)) {
235      File temp = TempFileCreator.INSTANCE.createTempFile("FileBackedOutputStream");
236      if (resetOnFinalize) {
237        // Finalizers are not guaranteed to be called on system shutdown;
238        // this is insurance.
239        temp.deleteOnExit();
240      }
241      try {
242        FileOutputStream transfer = new FileOutputStream(temp);
243        transfer.write(memory.getBuffer(), 0, memory.getCount());
244        transfer.flush();
245        // We've successfully transferred the data; switch to writing to file
246        out = transfer;
247      } catch (IOException e) {
248        temp.delete();
249        throw e;
250      }
251
252      file = temp;
253      memory = null;
254    }
255  }
256}