// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.config.server.application; import com.yahoo.vespa.archive.ArchiveStreamReader; import com.yahoo.vespa.archive.ArchiveStreamReader.Options; import com.yahoo.vespa.config.server.http.BadRequestException; import com.yahoo.vespa.config.server.http.InternalServerException; import com.yahoo.vespa.config.server.http.v2.ApplicationApiHandler; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.logging.Level; import java.util.logging.Logger; import static com.yahoo.yolean.Exceptions.uncheck; /** * A compressed application points to an application package that can be decompressed. * * @author Ulf Lilleengen */ public class CompressedApplicationInputStream implements AutoCloseable { private static final Logger log = Logger.getLogger(CompressedApplicationInputStream.class.getPackage().getName()); private final ArchiveStreamReader reader; private CompressedApplicationInputStream(ArchiveStreamReader reader) { this.reader = reader; } /** * Create an instance of a compressed application from an input stream. * * @param is the input stream containing the compressed files. * @param contentType the content type for determining what kind of compressed stream should be used. * @param maxSizeInBytes the maximum allowed size of the decompressed content * @return An instance of an unpacked application. */ public static CompressedApplicationInputStream createFromCompressedStream(InputStream is, String contentType, long maxSizeInBytes) { try { Options options = Options.standard().maxSize(maxSizeInBytes).allowDotSegment(true); return switch (contentType) { case ApplicationApiHandler.APPLICATION_X_GZIP -> new CompressedApplicationInputStream(ArchiveStreamReader.ofTarGzip(is, options)); case ApplicationApiHandler.APPLICATION_ZIP -> new CompressedApplicationInputStream(ArchiveStreamReader.ofZip(is, options)); default -> throw new BadRequestException("Unable to decompress"); }; } catch (UncheckedIOException e) { throw new InternalServerException("Unable to create compressed application stream", e); } } /** * Close this stream. * @throws IOException if the stream could not be closed */ public void close() throws IOException { reader.close(); } File decompress() throws IOException { return decompress(uncheck(() -> java.nio.file.Files.createTempDirectory("decompress")).toFile()); } public File decompress(File dir) throws IOException { decompressInto(dir.toPath()); return dir; } private void decompressInto(Path dir) throws IOException { if (!Files.isDirectory(dir)) throw new IllegalArgumentException("Not a directory: " + dir.toAbsolutePath()); log.log(Level.FINE, () -> "Application is in " + dir.toAbsolutePath()); int entries = 0; Path tmpFile = null; OutputStream tmpStream = null; try { tmpFile = createTempFile(dir); tmpStream = Files.newOutputStream(tmpFile); ArchiveStreamReader.ArchiveFile file; while ((file = reader.readNextTo(tmpStream)) != null) { tmpStream.close(); log.log(Level.FINE, "Creating output file: " + file.path()); Path dstFile = dir.resolve(file.path().toString()).normalize(); Files.createDirectories(dstFile.getParent()); Files.move(tmpFile, dstFile); tmpFile = createTempFile(dir); tmpStream = Files.newOutputStream(tmpFile); entries++; } } finally { if (tmpStream != null) tmpStream.close(); if (tmpFile != null) Files.deleteIfExists(tmpFile); } if (entries == 0) { log.log(Level.WARNING, "Not able to decompress any entries to " + dir); } } private static Path createTempFile(Path applicationDir) throws IOException { return Files.createTempFile(applicationDir, "application", null); } }