package eu.fbk.knowledgestore.filestore; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.zip.Deflater; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; import javax.annotation.Nullable; import com.google.common.base.Function; import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import eu.fbk.knowledgestore.data.Data; import eu.fbk.knowledgestore.data.Stream; /** * A {@code FileStore} decorator that GZIPs all the compressible files written to it. * <p> * A {@code GzippedFileStore} intercepts reads and writes to an underlying {@code FileStore}, * respectively applying GZIP compression and decompression to stored files in case they are * compressible. Compression level and size of buffer used for compression / decompression can be * configured by the user. Whether a file can be compressed is detected starting from its * extension and the matching Internet MIME type using the facilities of {@link Data}. In case * compression is applied, the name of the stored file is changed adding a {@code .gz} suffix. * </p> */ public final class GzippedFileStore extends ForwardingFileStore { private static final Logger LOGGER = LoggerFactory.getLogger(GzippedFileStore.class); private static final int DEFAULT_COMPRESSION_LEVEL = Deflater.DEFAULT_COMPRESSION; private static final int DEFAULT_BUFFER_SIZE = 512; private static final boolean DEFAULT_FORCE_COMPRESSION = true; private final FileStore delegate; private final int compressionLevel; private final int bufferSize; private final boolean forceCompression; /** * Creates a new instance wrapping the {@code FileStore} supplied and using the default * compression level and buffer size. * * @param delegate * the wrapped {@code FileStore} * @see #GzippedFileStore(FileStore, Integer, Integer) */ public GzippedFileStore(final FileStore delegate) { this(delegate, null, null, null); } /** * Creates a new instance wrapping the {@code FileStore} supplied and using the specified * compression level. * * @param delegate * the wrapped {@code FileStore}, not null * @param compressionLevel * the desired compression level for compressible files, on a 0-9 scale (from no * compression to best compression) or -1 for default compression; if null defaults * to -1 * @param bufferSize * the size of the buffer used by the GZIP inflaters / deflaters; if null defaults * to 512 * @param forceCompression * if true, supplied files are always compressed independently of their (detected) * MIME type; if null defaults to false */ public GzippedFileStore(final FileStore delegate, @Nullable final Integer compressionLevel, @Nullable final Integer bufferSize, @Nullable final Boolean forceCompression) { Preconditions.checkNotNull(delegate); Preconditions.checkArgument(compressionLevel == null || compressionLevel == Deflater.DEFAULT_COMPRESSION // || compressionLevel >= Deflater.BEST_SPEED && compressionLevel <= Deflater.BEST_COMPRESSION); Preconditions.checkArgument(bufferSize > 0); this.delegate = Preconditions.checkNotNull(delegate); this.compressionLevel = MoreObjects.firstNonNull(compressionLevel, DEFAULT_COMPRESSION_LEVEL); this.bufferSize = MoreObjects.firstNonNull(bufferSize, DEFAULT_BUFFER_SIZE); this.forceCompression = MoreObjects.firstNonNull(forceCompression, DEFAULT_FORCE_COMPRESSION); LOGGER.info("GZippedFileStore configured, compression={}, buffer={}", compressionLevel, bufferSize); } @Override protected FileStore delegate() { return this.delegate; } @Override public InputStream read(final String filename) throws FileMissingException, IOException { final String internalFilename = toInternalFilename(filename); if (internalFilename.equals(filename)) { return super.read(filename); } else { return new GZIPInputStream(super.read(internalFilename), this.bufferSize); } } @Override public OutputStream write(final String filename) throws FileExistsException, IOException { final String internalFilename = toInternalFilename(filename); if (internalFilename.equals(filename)) { return super.write(filename); } else { return new GZIPOutputStream(super.write(internalFilename), this.bufferSize) { { this.def.setLevel(GzippedFileStore.this.compressionLevel); } }; } } @Override public void delete(final String filename) throws FileMissingException, IOException { super.delete(toInternalFilename(filename)); } @Override public Stream<String> list() throws IOException { return super.list().transform(new Function<String, String>() { @Override public String apply(final String filename) { return toExternalFilename(filename); } }, 0); } private String toInternalFilename(final String filename) { return this.forceCompression || Data.isMimeTypeCompressible( // Data.extensionToMimeType(filename)) ? filename + ".gz" : filename; } private String toExternalFilename(final String filename) { return filename.endsWith(".gz") ? filename.substring(0, filename.length() - 3) : filename; } }