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 
052     *            The name of the file within the zip file. Use for 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(new WarnStatus("The target compressed file named [" + nameOfZippedFile + "] exist already.", this));
089
090            return;
091        }
092
093        addInfo("ZIP compressing [" + file2zip + "] as [" + zippedFile + "]");
094        createMissingTargetDirsIfNecessary(zippedFile);
095
096        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(nameOfFile2zip));
097                        ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(nameOfZippedFile))) {
098
099            ZipEntry zipEntry = computeZipEntry(innerEntryName);
100            zos.putNextEntry(zipEntry);
101
102            byte[] inbuf = new byte[BUFFER_SIZE];
103            int n;
104
105            while ((n = bis.read(inbuf)) != -1) {
106                zos.write(inbuf, 0, n);
107            }
108
109            addInfo("Done ZIP compressing [" + file2zip + "] as [" + zippedFile + "]");
110        } catch (Exception e) {
111            addStatus(new ErrorStatus("Error occurred while compressing [" + nameOfFile2zip + "] into [" + nameOfZippedFile + "].", this, e));
112        }
113        if (!file2zip.delete()) {
114            addStatus(new WarnStatus("Could not delete [" + nameOfFile2zip + "].", this));
115        }
116    }
117
118    // http://jira.qos.ch/browse/LBCORE-98
119    // The name of the compressed file as nested within the zip archive
120    //
121    // Case 1: RawFile = null, Patern = foo-%d.zip
122    // nestedFilename = foo-${current-date}
123    //
124    // Case 2: RawFile = hello.txt, Pattern = = foo-%d.zip
125    // nestedFilename = foo-${current-date}
126    //
127    // in both cases, the strategy consisting of removing the compression
128    // suffix of zip file works reasonably well. The alternative strategy
129    // whereby the nested file name was based on the value of the raw file name
130    // (applicable to case 2 only) has the disadvantage of the nested files
131    // all having the same name, which could make it harder for the user
132    // to unzip the file without collisions
133    ZipEntry computeZipEntry(File zippedFile) {
134        return computeZipEntry(zippedFile.getName());
135    }
136
137    ZipEntry computeZipEntry(String filename) {
138        String nameOfFileNestedWithinArchive = computeFileNameStrWithoutCompSuffix(filename, compressionMode);
139        return new ZipEntry(nameOfFileNestedWithinArchive);
140    }
141
142    private void gzCompress(String nameOfFile2gz, String nameOfgzedFile) {
143        File file2gz = new File(nameOfFile2gz);
144
145        if (!file2gz.exists()) {
146            addStatus(new WarnStatus("The file to compress named [" + nameOfFile2gz + "] does not exist.", this));
147
148            return;
149        }
150
151        if (!nameOfgzedFile.endsWith(".gz")) {
152            nameOfgzedFile = nameOfgzedFile + ".gz";
153        }
154
155        File gzedFile = new File(nameOfgzedFile);
156
157        if (gzedFile.exists()) {
158            addWarn("The target compressed file named [" + nameOfgzedFile + "] exist already. Aborting file compression.");
159            return;
160        }
161
162        addInfo("GZ compressing [" + file2gz + "] as [" + gzedFile + "]");
163        createMissingTargetDirsIfNecessary(gzedFile);
164
165        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(nameOfFile2gz));
166                        GZIPOutputStream gzos = new GZIPOutputStream(new FileOutputStream(nameOfgzedFile))) {
167
168            byte[] inbuf = new byte[BUFFER_SIZE];
169            int n;
170
171            while ((n = bis.read(inbuf)) != -1) {
172                gzos.write(inbuf, 0, n);
173            }
174
175            addInfo("Done ZIP compressing [" + file2gz + "] as [" + gzedFile + "]");
176        } catch (Exception e) {
177            addStatus(new ErrorStatus("Error occurred while compressing [" + nameOfFile2gz + "] into [" + nameOfgzedFile + "].", this, e));
178        }
179
180        if (!file2gz.delete()) {
181            addStatus(new WarnStatus("Could not delete [" + nameOfFile2gz + "].", this));
182        }
183
184    }
185
186    static public String computeFileNameStrWithoutCompSuffix(String fileNamePatternStr, CompressionMode compressionMode) {
187        int len = fileNamePatternStr.length();
188        switch (compressionMode) {
189        case GZ:
190            if (fileNamePatternStr.endsWith(".gz"))
191                return fileNamePatternStr.substring(0, len - 3);
192            else
193                return fileNamePatternStr;
194        case ZIP:
195            if (fileNamePatternStr.endsWith(".zip"))
196                return fileNamePatternStr.substring(0, len - 4);
197            else
198                return fileNamePatternStr;
199        case NONE:
200            return fileNamePatternStr;
201        }
202        throw new IllegalStateException("Execution should not reach this point");
203    }
204
205    void createMissingTargetDirsIfNecessary(File file) {
206        boolean result = FileUtil.createMissingParentDirectories(file);
207        if (!result) {
208            addError("Failed to create parent directories for [" + file.getAbsolutePath() + "]");
209        }
210    }
211
212    @Override
213    public String toString() {
214        return this.getClass().getName();
215    }
216
217    public Future<?> asyncCompress(String nameOfFile2Compress, String nameOfCompressedFile, String innerEntryName) throws RolloverFailure {
218        CompressionRunnable runnable = new CompressionRunnable(nameOfFile2Compress, nameOfCompressedFile, innerEntryName);
219        ExecutorService executorService = context.getScheduledExecutorService();
220        Future<?> future = executorService.submit(runnable);
221        return future;
222    }
223
224    class CompressionRunnable implements Runnable {
225        final String nameOfFile2Compress;
226        final String nameOfCompressedFile;
227        final String innerEntryName;
228
229        public CompressionRunnable(String nameOfFile2Compress, String nameOfCompressedFile, String innerEntryName) {
230            this.nameOfFile2Compress = nameOfFile2Compress;
231            this.nameOfCompressedFile = nameOfCompressedFile;
232            this.innerEntryName = innerEntryName;
233        }
234
235        public void run() {
236
237            Compressor.this.compress(nameOfFile2Compress, nameOfCompressedFile, innerEntryName);
238        }
239    }
240
241}