summaryrefslogtreecommitdiffstats
path: root/configserver/src/main/java/com/yahoo/vespa/config/server/application/CompressedApplicationInputStream.java
blob: 2cab0ea5e20f2f203f9f918c4399f23f3fb6070b (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
// 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 com.google.common.io.Files;
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.io.*;
import java.util.logging.Logger;
import java.util.zip.GZIPInputStream;

/**
 * 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(Files.createTempDir());
    }

    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(LogLevel.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(LogLevel.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;
    }
}