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 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 @CheckForNull 078 private MemoryOutput memory; 079 080 @GuardedBy("this") 081 @CheckForNull 082 private File file; 083 084 /** ByteArrayOutputStream that exposes its internals. */ 085 private static class MemoryOutput extends ByteArrayOutputStream { 086 byte[] getBuffer() { 087 return buf; 088 } 089 090 int getCount() { 091 return count; 092 } 093 } 094 095 /** Returns the file holding the data (possibly null). */ 096 @VisibleForTesting 097 @CheckForNull 098 synchronized File getFile() { 099 return file; 100 } 101 102 /** 103 * Creates a new instance that uses the given file threshold, and does not reset the data when the 104 * {@link ByteSource} returned by {@link #asByteSource} is finalized. 105 * 106 * @param fileThreshold the number of bytes before the stream should switch to buffering to a file 107 * @throws IllegalArgumentException if {@code fileThreshold} is negative 108 */ 109 public FileBackedOutputStream(int fileThreshold) { 110 this(fileThreshold, false); 111 } 112 113 /** 114 * Creates a new instance that uses the given file threshold, and optionally resets the data when 115 * the {@link ByteSource} returned by {@link #asByteSource} is finalized. 116 * 117 * @param fileThreshold the number of bytes before the stream should switch to buffering to a file 118 * @param resetOnFinalize if true, the {@link #reset} method will be called when the {@link 119 * ByteSource} returned by {@link #asByteSource} is finalized. 120 * @throws IllegalArgumentException if {@code fileThreshold} is negative 121 */ 122 public FileBackedOutputStream(int fileThreshold, boolean resetOnFinalize) { 123 checkArgument( 124 fileThreshold >= 0, "fileThreshold must be non-negative, but was %s", fileThreshold); 125 this.fileThreshold = fileThreshold; 126 this.resetOnFinalize = resetOnFinalize; 127 memory = new MemoryOutput(); 128 out = memory; 129 130 if (resetOnFinalize) { 131 source = 132 new ByteSource() { 133 @Override 134 public InputStream openStream() throws IOException { 135 return openInputStream(); 136 } 137 138 @SuppressWarnings({"removal", "Finalize"}) // b/260137033 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}