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