• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 The Guava Authors
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5  * in compliance with the License. You may obtain a copy of the License at
6  *
7  * http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the License
10  * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11  * or implied. See the License for the specific language governing permissions and limitations under
12  * the License.
13  */
14 
15 package com.google.common.io;
16 
17 import static com.google.common.base.Preconditions.checkArgument;
18 import static java.util.Objects.requireNonNull;
19 
20 import com.google.common.annotations.Beta;
21 import com.google.common.annotations.GwtIncompatible;
22 import com.google.common.annotations.J2ktIncompatible;
23 import com.google.common.annotations.VisibleForTesting;
24 import com.google.errorprone.annotations.concurrent.GuardedBy;
25 import com.google.j2objc.annotations.J2ObjCIncompatible;
26 import java.io.ByteArrayInputStream;
27 import java.io.ByteArrayOutputStream;
28 import java.io.File;
29 import java.io.FileInputStream;
30 import java.io.FileOutputStream;
31 import java.io.IOException;
32 import java.io.InputStream;
33 import java.io.OutputStream;
34 import javax.annotation.CheckForNull;
35 
36 /**
37  * An {@link OutputStream} that starts buffering to a byte array, but switches to file buffering
38  * once the data reaches a configurable size.
39  *
40  * <p>When this stream creates a temporary file, it restricts the file's permissions to the current
41  * user or, in the case of Android, the current app. If that is not possible (as is the case under
42  * the very old Android Ice Cream Sandwich release), then this stream throws an exception instead of
43  * creating a file that would be more accessible. (This behavior is new in Guava 32.0.0. Previous
44  * versions would create a file that is more accessible, as discussed in <a
45  * href="https://github.com/google/guava/issues/2575">Guava issue 2575</a>. TODO: b/283778848 - Fill
46  * in CVE number once it's available.)
47  *
48  * <p>Temporary files created by this stream may live in the local filesystem until either:
49  *
50  * <ul>
51  *   <li>{@link #reset} is called (removing the data in this stream and deleting the file), or...
52  *   <li>this stream (or, more precisely, its {@link #asByteSource} view) is finalized during
53  *       garbage collection, <strong>AND</strong> this stream was not constructed with {@linkplain
54  *       #FileBackedOutputStream(int) the 1-arg constructor} or the {@linkplain
55  *       #FileBackedOutputStream(int, boolean) 2-arg constructor} passing {@code false} in the
56  *       second parameter.
57  * </ul>
58  *
59  * <p>This class is thread-safe.
60  *
61  * @author Chris Nokleberg
62  * @since 1.0
63  */
64 @Beta
65 @J2ktIncompatible
66 @GwtIncompatible
67 @J2ObjCIncompatible
68 @ElementTypesAreNonnullByDefault
69 public final class FileBackedOutputStream extends OutputStream {
70   private final int fileThreshold;
71   private final boolean resetOnFinalize;
72   private final ByteSource source;
73 
74   @GuardedBy("this")
75   private OutputStream out;
76 
77   @GuardedBy("this")
78   @CheckForNull
79   private MemoryOutput memory;
80 
81   @GuardedBy("this")
82   @CheckForNull
83   private File file;
84 
85   /** ByteArrayOutputStream that exposes its internals. */
86   private static class MemoryOutput extends ByteArrayOutputStream {
getBuffer()87     byte[] getBuffer() {
88       return buf;
89     }
90 
getCount()91     int getCount() {
92       return count;
93     }
94   }
95 
96   /** Returns the file holding the data (possibly null). */
97   @VisibleForTesting
98   @CheckForNull
getFile()99   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    */
FileBackedOutputStream(int fileThreshold)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    */
FileBackedOutputStream(int fileThreshold, boolean resetOnFinalize)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", "Finalize"}) // 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    */
asByteSource()165   public ByteSource asByteSource() {
166     return source;
167   }
168 
openInputStream()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    */
reset()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
write(int b)206   public synchronized void write(int b) throws IOException {
207     update(1);
208     out.write(b);
209   }
210 
211   @Override
write(byte[] b)212   public synchronized void write(byte[] b) throws IOException {
213     write(b, 0, b.length);
214   }
215 
216   @Override
write(byte[] b, int off, int len)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
close()223   public synchronized void close() throws IOException {
224     out.close();
225   }
226 
227   @Override
flush()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")
update(int len)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 }
261