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            @SuppressWarnings("removal") // b/260137033
140            @Override
141            protected void finalize() {
142              try {
143                reset();
144              } catch (Throwable t) {
145                t.printStackTrace(System.err);
146              }
147            }
148          };
149    } else {
150      source =
151          new ByteSource() {
152            @Override
153            public InputStream openStream() throws IOException {
154              return openInputStream();
155            }
156          };
157    }
158  }
159
160  /**
161   * Returns a readable {@link ByteSource} view of the data that has been written to this stream.
162   *
163   * @since 15.0
164   */
165  public ByteSource asByteSource() {
166    return source;
167  }
168
169  private synchronized InputStream openInputStream() throws IOException {
170    if (file != null) {
171      return new FileInputStream(file);
172    } else {
173      // requireNonNull is safe because we always have either `file` or `memory`.
174      requireNonNull(memory);
175      return new ByteArrayInputStream(memory.getBuffer(), 0, memory.getCount());
176    }
177  }
178
179  /**
180   * Calls {@link #close} if not already closed, and then resets this object back to its initial
181   * state, for reuse. If data was buffered to a file, it will be deleted.
182   *
183   * @throws IOException if an I/O error occurred while deleting the file buffer
184   */
185  public synchronized void reset() throws IOException {
186    try {
187      close();
188    } finally {
189      if (memory == null) {
190        memory = new MemoryOutput();
191      } else {
192        memory.reset();
193      }
194      out = memory;
195      if (file != null) {
196        File deleteMe = file;
197        file = null;
198        if (!deleteMe.delete()) {
199          throw new IOException("Could not delete: " + deleteMe);
200        }
201      }
202    }
203  }
204
205  @Override
206  public synchronized void write(int b) throws IOException {
207    update(1);
208    out.write(b);
209  }
210
211  @Override
212  public synchronized void write(byte[] b) throws IOException {
213    write(b, 0, b.length);
214  }
215
216  @Override
217  public synchronized void write(byte[] b, int off, int len) throws IOException {
218    update(len);
219    out.write(b, off, len);
220  }
221
222  @Override
223  public synchronized void close() throws IOException {
224    out.close();
225  }
226
227  @Override
228  public synchronized void flush() throws IOException {
229    out.flush();
230  }
231
232  /**
233   * Checks if writing {@code len} bytes would go over threshold, and switches to file buffering if
234   * so.
235   */
236  @GuardedBy("this")
237  private void update(int len) throws IOException {
238    if (memory != null && (memory.getCount() + len > fileThreshold)) {
239      File temp = TempFileCreator.INSTANCE.createTempFile("FileBackedOutputStream");
240      if (resetOnFinalize) {
241        // Finalizers are not guaranteed to be called on system shutdown;
242        // this is insurance.
243        temp.deleteOnExit();
244      }
245      try {
246        FileOutputStream transfer = new FileOutputStream(temp);
247        transfer.write(memory.getBuffer(), 0, memory.getCount());
248        transfer.flush();
249        // We've successfully transferred the data; switch to writing to file
250        out = transfer;
251      } catch (IOException e) {
252        temp.delete();
253        throw e;
254      }
255
256      file = temp;
257      memory = null;
258    }
259  }
260}