aboutsummaryrefslogtreecommitdiffstats
path: root/configserver/src/main/java/com/yahoo/vespa/config/server/application/CompressedApplicationInputStream.java
blob: dae5c6aecef82a27a1fc7c055f5a68ccb894eb56 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
// Copyright 2019 Oath Inc. 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.google.common.io.ByteStreams;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.logging.Level;
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 org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.ArchiveInputStream;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;

import java.util.logging.Logger;
import java.util.zip.GZIPInputStream;

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 ArchiveInputStream ais;

    /**
     * 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.
     * @return An instance of an unpacked application.
     */
    public static CompressedApplicationInputStream createFromCompressedStream(InputStream is, String contentType) {
        try {
            ArchiveInputStream ais = getArchiveInputStream(is, contentType);
            return createFromCompressedStream(ais);
        } catch (IOException e) {
            throw new InternalServerException("Unable to create compressed application stream", e);
        }
    }

    static CompressedApplicationInputStream createFromCompressedStream(ArchiveInputStream ais) {
        return new CompressedApplicationInputStream(ais);
    }

    private static ArchiveInputStream getArchiveInputStream(InputStream is, String contentTypeHeader) throws IOException {
        ArchiveInputStream ais;
        switch (contentTypeHeader) {
            case ApplicationApiHandler.APPLICATION_X_GZIP:
                ais = new TarArchiveInputStream(new GZIPInputStream(is));
                break;
            case ApplicationApiHandler.APPLICATION_ZIP:
                ais = new ZipArchiveInputStream(is);
                break;
            default:
                throw new BadRequestException("Unable to decompress");
        }
        return ais;
    }

    private CompressedApplicationInputStream(ArchiveInputStream ais) {
        this.ais = ais;
    }

    /**
     * Close this stream.
     * @throws IOException if the stream could not be closed
     */
    public void close() throws IOException {
        ais.close();
    }

    File decompress() throws IOException {
        return decompress(uncheck(() -> java.nio.file.Files.createTempDirectory("decompress")).toFile());
    }

    public File decompress(File dir) throws IOException {
        decompressInto(dir);
        dir = findActualApplicationDir(dir);
        return dir;
    }

    private void decompressInto(File application) throws IOException {
        log.log(Level.FINE, "Application is in " + application.getAbsolutePath());
        int entries = 0;
        ArchiveEntry entry;
        while ((entry = ais.getNextEntry()) != null) {
            log.log(Level.FINE, "Unpacking " + entry.getName());
            File outFile = new File(application, entry.getName());
            // FIXME/TODO: write more tests that break this logic. I have a feeling it is not very robust.
            if (entry.isDirectory()) {
                if (!(outFile.exists() && outFile.isDirectory())) {
                    log.log(Level.FINE, "Creating dir: " + outFile.getAbsolutePath());
                    boolean res = outFile.mkdirs();
                    if (!res) {
                        log.log(Level.WARNING, "Could not create dir " + entry.getName());
                    }
                }
            } else {
                log.log(Level.FINE, "Creating output file: " + outFile.getAbsolutePath());

                // Create parent dir if necessary
                String parent = outFile.getParent();
                new File(parent).mkdirs();

                FileOutputStream fos = new FileOutputStream(outFile);
                ByteStreams.copy(ais, fos);
                fos.close();
            }
            entries++;
        }
        if (entries == 0) {
            log.log(Level.WARNING, "Not able to read any entries from " + application.getName());
        }
    }

    private File findActualApplicationDir(File application) {
        // If application is in e.g. application/, use that as root for UnpackedApplication
        // TODO: Vespa 8: Remove application/ directory support
        File[] files = application.listFiles();
        if (files != null && files.length == 1 && files[0].isDirectory()) {
            application = files[0];
        }
        return application;
    }
}