All Downloads are FREE. Search and download functionalities are using the official Maven repository.

dev.mccue.guava.io.FileBackedOutputStream Maven / Gradle / Ivy

/*
 * Copyright (C) 2008 The Guava Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */

package dev.mccue.guava.io;

import static dev.mccue.guava.base.Preconditions.checkArgument;
import static java.util.Objects.requireNonNull;

import com.google.errorprone.annotations.concurrent.GuardedBy;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import dev.mccue.jsr305.CheckForNull;

/**
 * An {@code OutputStream} that starts buffering to a byte array, but switches to file buffering
 * once the data reaches a configurable size.
 *
 * 

When this stream creates a temporary file, it restricts the file's permissions to the current * user or, in the case of Android, the current app. If that is not possible (as is the case under * the very old Android Ice Cream Sandwich release), then this stream throws an exception instead of * creating a file that would be more accessible. (This behavior is new in Guava 32.0.0. Previous * versions would create a file that is more accessible, as discussed in Guava issue 2575. TODO: b/283778848 - Fill * in CVE number once it's available.) * *

Temporary files created by this stream may live in the local filesystem until either: * *

    *
  • {@code #reset} is called (removing the data in this stream and deleting the file), or... *
  • this stream (or, more precisely, its {@code #asByteSource} view) is finalized during * garbage collection, AND this stream was not constructed with {@code * #FileBackedOutputStream(int) the 1-arg constructor} or the {@code * #FileBackedOutputStream(int, boolean) 2-arg constructor} passing {@code false} in the * second parameter. *
* *

This class is thread-safe. * * @author Chris Nokleberg * @since 1.0 */ @ElementTypesAreNonnullByDefault public final class FileBackedOutputStream extends OutputStream { private final int fileThreshold; private final boolean resetOnFinalize; private final ByteSource source; @GuardedBy("this") private OutputStream out; @GuardedBy("this") @CheckForNull private MemoryOutput memory; @GuardedBy("this") @CheckForNull private File file; /** ByteArrayOutputStream that exposes its internals. */ private static class MemoryOutput extends ByteArrayOutputStream { byte[] getBuffer() { return buf; } int getCount() { return count; } } /** Returns the file holding the data (possibly null). */ @CheckForNull synchronized File getFile() { return file; } /** * Creates a new instance that uses the given file threshold, and does not reset the data when the * {@code ByteSource} returned by {@code #asByteSource} is finalized. * * @param fileThreshold the number of bytes before the stream should switch to buffering to a file * @throws IllegalArgumentException if {@code fileThreshold} is negative */ public FileBackedOutputStream(int fileThreshold) { this(fileThreshold, false); } /** * Creates a new instance that uses the given file threshold, and optionally resets the data when * the {@code ByteSource} returned by {@code #asByteSource} is finalized. * * @param fileThreshold the number of bytes before the stream should switch to buffering to a file * @param resetOnFinalize if true, the {@code #reset} method will be called when the {@code * ByteSource} returned by {@code #asByteSource} is finalized. * @throws IllegalArgumentException if {@code fileThreshold} is negative */ private FileBackedOutputStream(int fileThreshold, boolean resetOnFinalize) { checkArgument( fileThreshold >= 0, "fileThreshold must be non-negative, but was %s", fileThreshold); this.fileThreshold = fileThreshold; this.resetOnFinalize = resetOnFinalize; memory = new MemoryOutput(); out = memory; if (resetOnFinalize) { source = new ByteSource() { @Override public InputStream openStream() throws IOException { return openInputStream(); } }; } else { source = new ByteSource() { @Override public InputStream openStream() throws IOException { return openInputStream(); } }; } } /** * Returns a readable {@code ByteSource} view of the data that has been written to this stream. * * @since 15.0 */ public ByteSource asByteSource() { return source; } private synchronized InputStream openInputStream() throws IOException { if (file != null) { return new FileInputStream(file); } else { // requireNonNull is safe because we always have either `file` or `memory`. requireNonNull(memory); return new ByteArrayInputStream(memory.getBuffer(), 0, memory.getCount()); } } /** * Calls {@code #close} if not already closed, and then resets this object back to its initial * state, for reuse. If data was buffered to a file, it will be deleted. * * @throws IOException if an I/O error occurred while deleting the file buffer */ public synchronized void reset() throws IOException { try { close(); } finally { if (memory == null) { memory = new MemoryOutput(); } else { memory.reset(); } out = memory; if (file != null) { File deleteMe = file; file = null; if (!deleteMe.delete()) { throw new IOException("Could not delete: " + deleteMe); } } } } @Override public synchronized void write(int b) throws IOException { update(1); out.write(b); } @Override public synchronized void write(byte[] b) throws IOException { write(b, 0, b.length); } @Override public synchronized void write(byte[] b, int off, int len) throws IOException { update(len); out.write(b, off, len); } @Override public synchronized void close() throws IOException { out.close(); } @Override public synchronized void flush() throws IOException { out.flush(); } /** * Checks if writing {@code len} bytes would go over threshold, and switches to file buffering if * so. */ @GuardedBy("this") private void update(int len) throws IOException { if (memory != null && (memory.getCount() + len > fileThreshold)) { File temp = TempFileCreator.INSTANCE.createTempFile("FileBackedOutputStream"); if (resetOnFinalize) { // Finalizers are not guaranteed to be called on system shutdown; // this is insurance. temp.deleteOnExit(); } try { FileOutputStream transfer = new FileOutputStream(temp); transfer.write(memory.getBuffer(), 0, memory.getCount()); transfer.flush(); // We've successfully transferred the data; switch to writing to file out = transfer; } catch (IOException e) { temp.delete(); throw e; } file = temp; memory = null; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy