summaryrefslogtreecommitdiff
path: root/src/main/java/com/keuin/kbackupfabric/util/ZipUtil.java
blob: ce14c8a9315de5e1facdebbb2d5fffe1a7e8e15d (plain)
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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
package com.keuin.kbackupfabric.util;

import com.keuin.kbackupfabric.exception.ZipUtilException;
import com.keuin.kbackupfabric.metadata.BackupMetadata;

import java.io.*;
import java.nio.file.Files;
import java.util.*;
import java.util.zip.*;

public final class ZipUtil {

    private static final int unzipBufferSize = 1024 * 1024 * 8; // 8MB
    private static final int zipBufferSize = 1024 * 1024 * 8; // 8MB

    /**
     * 递归压缩文件夹
     *
     * @param srcRootDir      压缩文件夹根目录的子路径
     * @param file            当前递归压缩的文件或目录对象
     * @param zipOutputStream 压缩文件存储对象
     * @param filesSkipping   被忽略的文件
     * @throws IOException IO Error
     */
    private static void zip(String srcRootDir, File file, ZipOutputStream zipOutputStream, Set<String> filesSkipping, final byte[] buffer) throws IOException {
        if (file == null) {
            return;
        }

        boolean skipping = Optional.ofNullable(filesSkipping).orElse(Collections.emptySet()).contains(file.getName())
                || file.getName().equals(BackupMetadata.metadataFileName);
        if (skipping)
            return; // Reject

        // 如果是文件,则直接压缩该文件
        if (file.isFile()) {
            int count;

            // 获取文件相对于压缩文件夹根目录的子路径
            String subPath = file.getAbsolutePath();
            int index = subPath.indexOf(srcRootDir);
            if (index != -1) {
                subPath = subPath.substring(srcRootDir.length() + File.separator.length());
            }

            // 写入压缩包
            ZipEntry entry = new ZipEntry(subPath);
            zipOutputStream.putNextEntry(entry);
//            BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(file));
            try (InputStream inputStream = Files.newInputStream(file.toPath())) {
                while ((count = inputStream.read(buffer, 0, zipBufferSize)) != -1) {
                    zipOutputStream.write(buffer, 0, count);
                }
            } finally {
                zipOutputStream.closeEntry();
            }
        } else {
            // 如果是目录,则压缩整个目录
            // 压缩目录中的文件或子目录
            File[] childFileList = file.listFiles();
            if (childFileList != null) {
                for (File value : childFileList)
                    zip(srcRootDir, value, zipOutputStream, filesSkipping, buffer);
            }
        }
    }

    /**
     * 对文件或文件目录进行压缩
     *
     * @param srcPath     要压缩的源文件路径。如果是目录,则将递归压缩这个目录及其所有子文件、子目录树。
     * @param zipPath     压缩文件保存的路径。注意:zipPath不能是srcPath路径下的子文件夹
     * @param zipFileName 压缩文件名
     * @throws IOException      IO Error
     * @throws ZipUtilException General exception, such as loop recursion.
     */
    public static void makeBackupZip(String srcPath, String zipPath, String zipFileName, BackupMetadata backupMetadata, int zipLevel) throws IOException, ZipUtilException {
        Objects.requireNonNull(srcPath);
        Objects.requireNonNull(zipPath);
        Objects.requireNonNull(zipFileName);
        Objects.requireNonNull(backupMetadata);
        Objects.requireNonNull(srcPath);
        if (srcPath.isEmpty()) {
            throw new IllegalArgumentException("srcPath cannot be empty");
        }
        if (zipPath.isEmpty()) {
            throw new IllegalArgumentException("zipPath cannot be empty");
        }
        if (zipFileName.isEmpty()) {
            throw new IllegalArgumentException("zipFileName cannot be empty");
        }

        File srcFile = new File(srcPath);

        //判断压缩文件保存的路径是否为源文件路径的子文件夹,如果是,则抛出异常(防止无限递归压缩的发生)
        if (srcFile.isDirectory() && zipPath.contains(srcPath)) {
            throw new ZipUtilException("Detected loop recursion in directory structure, please check symlink linking to parent directories.");
        }

        //判断压缩文件保存的路径是否存在,如果不存在,则创建目录
        File zipDir = new File(zipPath);
        if (!zipDir.exists() || !zipDir.isDirectory()) {
            if (!zipDir.mkdirs()) {
                throw new IOException(String.format("Failed to make directory tree %s", zipDir));
            }
        }

        //创建压缩文件保存的文件对象
        String zipFilePath = zipPath + File.separator + zipFileName;
        File zipFile = new File(zipFilePath);
        if (zipFile.exists()) {
            //删除已存在的目标文件
            if (!zipFile.delete()) {
                throw new IOException(String.format("Failed to delete existing file %s", zipFile));
            }
        }

        try (FileOutputStream fileOutputStream = new FileOutputStream(zipFile);
             CheckedOutputStream checkedOutputStream = new CheckedOutputStream(fileOutputStream, new CRC32());
             ZipOutputStream zipOutputStream = new ZipOutputStream(checkedOutputStream)) {
            zipOutputStream.setLevel(zipLevel);

            // If with backup metadata, we serialize it and write it into file "kbackup_metadata"
            ZipEntry metadataEntry = new ZipEntry(BackupMetadata.metadataFileName);

            zipOutputStream.putNextEntry(metadataEntry);
            try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
                 ObjectOutputStream oos = new ObjectOutputStream(baos)) {
                oos.writeObject(backupMetadata);
                zipOutputStream.write(baos.toByteArray());
            } finally {
                zipOutputStream.closeEntry();
            }

            //如果只是压缩一个文件,则需要截取该文件的父目录
            String srcRootDir = srcPath;
            if (srcFile.isFile()) { // (Disabled) Hack this stupid setting. We want to keep our least parent folder!
                int index = srcPath.lastIndexOf(File.separator);
                if (index != -1) {
                    srcRootDir = srcPath.substring(0, index);
                }
            }

            //调用递归压缩方法进行目录或文件压缩
            zip(srcRootDir, srcFile, zipOutputStream, Collections.singleton("session.lock"), new byte[zipBufferSize]);
            zipOutputStream.flush();
        }
    }

    public static void makeBackupZip(String srcPath, String zipPath, String zipFileName, BackupMetadata backupMetadata) throws IOException, ZipUtilException {
        makeBackupZip(srcPath, zipPath, zipFileName, backupMetadata, Deflater.BEST_SPEED);
    }

    /**
     * 解压缩zip包
     *
     * @param zipFilePath        zip文件的全路径
     * @param unzipFilePath      解压后的文件保存的路径
     * @param includeZipFileName 解压后的文件保存的路径是否包含压缩文件的文件名。true-包含;false-不包含
     */
    public static void unzip(String zipFilePath, String unzipFilePath, boolean includeZipFileName) throws IOException {
        final byte[] buffer = new byte[unzipBufferSize];
        if (zipFilePath.isEmpty() || unzipFilePath.isEmpty()) {
            throw new IllegalArgumentException("Parameter for unzip() contains null.");
        }
        File zipFile = new File(zipFilePath);
        // 如果解压后的文件保存路径包含压缩文件的文件名,则追加该文件名到解压路径
        if (includeZipFileName) {
            String fileName = zipFile.getName();
            if (!fileName.isEmpty()) {
                fileName = fileName.substring(0, fileName.lastIndexOf("."));
            }
            unzipFilePath = unzipFilePath + File.separator + fileName;
        }
        // 创建解压缩文件保存的路径
        File unzipFileDir = new File(unzipFilePath);
        if (!unzipFileDir.exists() || !unzipFileDir.isDirectory()) {
            if (!unzipFileDir.mkdirs())
                throw new IOException(String.format("Failed to make directory tree %s", unzipFileDir));
        }

        // 开始解压
        ZipEntry entry;
        String entryFilePath, entryDirPath;
        File entryFile, entryDir;
        int index, count;
        try (ZipFile zip = new ZipFile(zipFile)) {
            Enumeration<? extends ZipEntry> entries = zip.entries();
            // 循环对压缩包里的每一个文件进行解压
            while (entries.hasMoreElements()) {
                entry = entries.nextElement();
                // 构建压缩包中一个文件解压后保存的文件全路径
                entryFilePath = unzipFilePath + File.separator + entry.getName();

                // 构建解压后保存的文件夹路径
                index = entryFilePath.lastIndexOf(File.separator);
                if (index != -1) {
                    entryDirPath = entryFilePath.substring(0, index);
                } else {
                    entryDirPath = "";
                }
                entryDir = new File(entryDirPath);
                // 如果文件夹路径不存在,则创建文件夹
                if (!entryDir.exists() || !entryDir.isDirectory()) {
                    if (!entryDir.mkdirs())
                        throw new IOException(String.format("Failed to make directory tree %s", entryDir));
                }

                // 创建解压文件
                entryFile = new File(entryFilePath);
                if (entryFile.exists()) {
                    // 检测文件是否允许删除,如果不允许删除,将会抛出SecurityException
                    SecurityManager securityManager = new SecurityManager();
                    securityManager.checkDelete(entryFilePath);
                    // 删除已存在的目标文件
                    if (!entryFile.delete())
                        throw new IOException(String.format("Failed to delete existing file %s", entryFile));
                }
                if (entry.isDirectory()) {
                    // If the entry is a directory, we make its corresponding directory.
                    if (!entryFile.mkdir())
                        throw new IOException(String.format("Failed to create directory %s", entryFile));
                } else {
                    // Is a file, we write the data
                    // 写入文件
                    try (OutputStream outputStream = Files.newOutputStream(entryFile.toPath());
                         InputStream inputStream = zip.getInputStream(entry)) {
                        while ((count = inputStream.read(buffer, 0, unzipBufferSize)) != -1) {
                            outputStream.write(buffer, 0, count);
                        }
                        outputStream.flush();
                    }
                }
            }
        }
    }
}