001/**
002 * Logback: the reliable, generic, fast and flexible logging framework.
003 * Copyright (C) 1999-2015, QOS.ch. All rights reserved.
004 *
005 * This program and the accompanying materials are dual-licensed under
006 * either the terms of the Eclipse Public License v1.0 as published by
007 * the Eclipse Foundation
008 *
009 *   or (per the licensee's choosing)
010 *
011 * under the terms of the GNU Lesser General Public License version 2.1
012 * as published by the Free Software Foundation.
013 */
014package ch.qos.logback.core.rolling.helper;
015
016import java.io.BufferedInputStream;
017import java.io.File;
018import java.io.FileInputStream;
019import java.io.FileOutputStream;
020import java.util.concurrent.ExecutorService;
021import java.util.concurrent.Future;
022import java.util.zip.GZIPOutputStream;
023import java.util.zip.ZipEntry;
024import java.util.zip.ZipOutputStream;
025
026import ch.qos.logback.core.rolling.RolloverFailure;
027import ch.qos.logback.core.spi.ContextAwareBase;
028import ch.qos.logback.core.status.ErrorStatus;
029import ch.qos.logback.core.status.WarnStatus;
030import ch.qos.logback.core.util.FileUtil;
031
032/**
033 * The <code>Compression</code> class implements ZIP and GZ file
034 * compression/decompression methods.
035 *
036 * @author Ceki G&uuml;lc&uuml;
037 */
038public class Compressor extends ContextAwareBase {
039
040    final CompressionMode compressionMode;
041
042    static final int BUFFER_SIZE = 8192;
043
044    public Compressor(CompressionMode compressionMode) {
045        this.compressionMode = compressionMode;
046    }
047
048    /**
049     * @param nameOfFile2Compress
050     * @param nameOfCompressedFile
051     * @param innerEntryName       The name of the file within the zip file. Use for
052     *                             ZIP compression.
053     */
054    public void compress(String nameOfFile2Compress, String nameOfCompressedFile, String innerEntryName) {
055        switch (compressionMode) {
056        case GZ:
057            gzCompress(nameOfFile2Compress, nameOfCompressedFile);
058            break;
059        case ZIP:
060            zipCompress(nameOfFile2Compress, nameOfCompressedFile, innerEntryName);
061            break;
062        case NONE:
063            throw new UnsupportedOperationException("compress method called in NONE compression mode");
064        }
065    }
066
067    private void zipCompress(String nameOfFile2zip, String nameOfZippedFile, String innerEntryName) {
068        File file2zip = new File(nameOfFile2zip);
069
070        if (!file2zip.exists()) {
071            addStatus(new WarnStatus("The file to compress named [" + nameOfFile2zip + "] does not exist.", this));
072
073            return;
074        }
075
076        if (innerEntryName == null) {
077            addStatus(new WarnStatus("The innerEntryName parameter cannot be null", this));
078            return;
079        }
080
081        if (!nameOfZippedFile.endsWith(".zip")) {
082            nameOfZippedFile = nameOfZippedFile + ".zip";
083        }
084
085        File zippedFile = new File(nameOfZippedFile);
086
087        if (zippedFile.exists()) {
088            addStatus(
089                    new WarnStatus("The target compressed file named [" + nameOfZippedFile + "] exist already.", this));
090
091            return;
092        }
093
094        addInfo("ZIP compressing [" + file2zip + "] as [" + zippedFile + "]");
095        createMissingTargetDirsIfNecessary(zippedFile);
096
097        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(nameOfFile2zip));
098                ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(nameOfZippedFile))) {
099
100            ZipEntry zipEntry = computeZipEntry(innerEntryName);
101            zos.putNextEntry(zipEntry);
102
103            byte[] inbuf = new byte[BUFFER_SIZE];
104            int n;
105
106            while ((n = bis.read(inbuf)) != -1) {
107                zos.write(inbuf, 0, n);
108            }
109
110            addInfo("Done ZIP compressing [" + file2zip + "] as [" + zippedFile + "]");
111        } catch (Exception e) {
112            addStatus(new ErrorStatus(
113                    "Error occurred while compressing [" + nameOfFile2zip + "] into [" + nameOfZippedFile + "].", this,
114                    e));
115        }
116        if (!file2zip.delete()) {
117            addStatus(new WarnStatus("Could not delete [" + nameOfFile2zip + "].", this));
118        }
119    }
120
121    // http://jira.qos.ch/browse/LBCORE-98
122    // The name of the compressed file as nested within the zip archive
123    //
124    // Case 1: RawFile = null, Pattern = foo-%d.zip
125    // nestedFilename = foo-${current-date}
126    //
127    // Case 2: RawFile = hello.txt, Pattern = = foo-%d.zip
128    // nestedFilename = foo-${current-date}
129    //
130    // in both cases, the strategy consisting of removing the compression
131    // suffix of zip file works reasonably well. The alternative strategy
132    // whereby the nested file name was based on the value of the raw file name
133    // (applicable to case 2 only) has the disadvantage of the nested files
134    // all having the same name, which could make it harder for the user
135    // to unzip the file without collisions
136    ZipEntry computeZipEntry(File zippedFile) {
137        return computeZipEntry(zippedFile.getName());
138    }
139
140    ZipEntry computeZipEntry(String filename) {
141        String nameOfFileNestedWithinArchive = computeFileNameStrWithoutCompSuffix(filename, compressionMode);
142        return new ZipEntry(nameOfFileNestedWithinArchive);
143    }
144
145    private void gzCompress(String nameOfFile2gz, String nameOfgzedFile) {
146        File file2gz = new File(nameOfFile2gz);
147
148        if (!file2gz.exists()) {
149            addStatus(new WarnStatus("The file to compress named [" + nameOfFile2gz + "] does not exist.", this));
150
151            return;
152        }
153
154        if (!nameOfgzedFile.endsWith(".gz")) {
155            nameOfgzedFile = nameOfgzedFile + ".gz";
156        }
157
158        File gzedFile = new File(nameOfgzedFile);
159
160        if (gzedFile.exists()) {
161            addWarn("The target compressed file named [" + nameOfgzedFile
162                    + "] exist already. Aborting file compression.");
163            return;
164        }
165
166        addInfo("GZ compressing [" + file2gz + "] as [" + gzedFile + "]");
167        createMissingTargetDirsIfNecessary(gzedFile);
168
169        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(nameOfFile2gz));
170                GZIPOutputStream gzos = new GZIPOutputStream(new FileOutputStream(nameOfgzedFile))) {
171
172            byte[] inbuf = new byte[BUFFER_SIZE];
173            int n;
174
175            while ((n = bis.read(inbuf)) != -1) {
176                gzos.write(inbuf, 0, n);
177            }
178
179            addInfo("Done GZ compressing [" + file2gz + "] as [" + gzedFile + "]");
180        } catch (Exception e) {
181            addStatus(new ErrorStatus(
182                    "Error occurred while compressing [" + nameOfFile2gz + "] into [" + nameOfgzedFile + "].", this,
183                    e));
184        }
185
186        if (!file2gz.delete()) {
187            addStatus(new WarnStatus("Could not delete [" + nameOfFile2gz + "].", this));
188        }
189
190    }
191
192    static public String computeFileNameStrWithoutCompSuffix(String fileNamePatternStr,
193            CompressionMode compressionMode) {
194        int len = fileNamePatternStr.length();
195        switch (compressionMode) {
196        case GZ:
197            if (fileNamePatternStr.endsWith(".gz"))
198                return fileNamePatternStr.substring(0, len - 3);
199            else
200                return fileNamePatternStr;
201        case ZIP:
202            if (fileNamePatternStr.endsWith(".zip"))
203                return fileNamePatternStr.substring(0, len - 4);
204            else
205                return fileNamePatternStr;
206        case NONE:
207            return fileNamePatternStr;
208        }
209        throw new IllegalStateException("Execution should not reach this point");
210    }
211
212    void createMissingTargetDirsIfNecessary(File file) {
213        boolean result = FileUtil.createMissingParentDirectories(file);
214        if (!result) {
215            addError("Failed to create parent directories for [" + file.getAbsolutePath() + "]");
216        }
217    }
218
219    @Override
220    public String toString() {
221        return this.getClass().getName();
222    }
223
224    public Future<?> asyncCompress(String nameOfFile2Compress, String nameOfCompressedFile, String innerEntryName)
225            throws RolloverFailure {
226        CompressionRunnable runnable = new CompressionRunnable(nameOfFile2Compress, nameOfCompressedFile,
227                innerEntryName);
228        ExecutorService executorService = context.getExecutorService();
229        Future<?> future = executorService.submit(runnable);
230        return future;
231    }
232
233    class CompressionRunnable implements Runnable {
234        final String nameOfFile2Compress;
235        final String nameOfCompressedFile;
236        final String innerEntryName;
237
238        public CompressionRunnable(String nameOfFile2Compress, String nameOfCompressedFile, String innerEntryName) {
239            this.nameOfFile2Compress = nameOfFile2Compress;
240            this.nameOfCompressedFile = nameOfCompressedFile;
241            this.innerEntryName = innerEntryName;
242        }
243
244        public void run() {
245
246            Compressor.this.compress(nameOfFile2Compress, nameOfCompressedFile, innerEntryName);
247        }
248    }
249
250}