View Javadoc
1   /*
2    * Copyright (c) 2001-2017, Zoltan Farkas All Rights Reserved.
3    *
4    * This library is free software; you can redistribute it and/or
5    * modify it under the terms of the GNU Lesser General Public
6    * License as published by the Free Software Foundation; either
7    * version 2.1 of the License, or (at your option) any later version.
8    *
9    * This library is distributed in the hope that it will be useful,
10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12   * GNU General Public License for more details.
13   *
14   * You should have received a copy of the GNU Lesser General Public
15   * License along with this program; if not, write to the Free Software
16   * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
17   *
18   * Additionally licensed with:
19   *
20   * Licensed under the Apache License, Version 2.0 (the "License");
21   * you may not use this file except in compliance with the License.
22   * You may obtain a copy of the License at
23   *
24   *      http://www.apache.org/licenses/LICENSE-2.0
25   *
26   * Unless required by applicable law or agreed to in writing, software
27   * distributed under the License is distributed on an "AS IS" BASIS,
28   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
29   * See the License for the specific language governing permissions and
30   * limitations under the License.
31   */
32  package org.spf4j.io.compress;
33  
34  import com.google.common.annotations.Beta;
35  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
36  import java.io.BufferedOutputStream;
37  import java.io.IOException;
38  import java.io.InputStream;
39  import java.io.OutputStream;
40  import java.io.UncheckedIOException;
41  import java.net.URI;
42  import java.nio.charset.StandardCharsets;
43  import java.nio.file.FileSystem;
44  import java.nio.file.FileSystems;
45  import java.nio.file.FileVisitResult;
46  import java.nio.file.Files;
47  import java.nio.file.Path;
48  import java.nio.file.Paths;
49  import java.nio.file.SimpleFileVisitor;
50  import java.nio.file.StandardCopyOption;
51  import java.nio.file.attribute.BasicFileAttributes;
52  import java.util.ArrayList;
53  import java.util.Collections;
54  import java.util.HashSet;
55  import java.util.List;
56  import java.util.Set;
57  import java.util.function.Predicate;
58  import java.util.stream.Stream;
59  import java.util.zip.ZipEntry;
60  import java.util.zip.ZipInputStream;
61  import java.util.zip.ZipOutputStream;
62  import javax.annotation.Nonnull;
63  import javax.annotation.ParametersAreNonnullByDefault;
64  import org.spf4j.io.BufferedInputStream;
65  import org.spf4j.io.Streams;
66  import org.spf4j.recyclable.impl.ArraySuppliers;
67  
68  /**
69   * @author zoly
70   */
71  @Beta
72  @SuppressFBWarnings("AFBR_ABNORMAL_FINALLY_BLOCK_RETURN")
73  @ParametersAreNonnullByDefault
74  public final class Compress {
75  
76    private Compress() {
77    }
78  
79    /**
80     * Zip a file or folder.
81     * @param fileOrFolderToCompress file or folder to compress.
82     * @return the Path of the compressed file. It will created in the same folder as the input parent.
83     * @throws IOException
84     */
85    @Nonnull
86    public static Path zip(final Path fileOrFolderToCompress) throws IOException {
87      Path parent = fileOrFolderToCompress.getParent();
88      if (parent == null) {
89        throw new IllegalArgumentException("Not a file: " + fileOrFolderToCompress);
90      }
91      Path destFile = parent.resolve(fileOrFolderToCompress.getFileName() + ".zip");
92      zip(fileOrFolderToCompress, destFile);
93      return destFile;
94    }
95  
96    /**
97     * Zip a file or folder.
98     * @param fileOrFolderToCompress file or folder to compress.
99     * @param destFile the destination zip file.
100    * @throws IOException
101    */
102   @Nonnull
103   public static void zip(final Path fileOrFolderToCompress,
104           final Path destFile) throws IOException {
105     zip(fileOrFolderToCompress, destFile, (p) -> true);
106   }
107 
108   /**
109    * Zip a file or folder.
110    * @param fileOrFolderToCompress file or folder to compress.
111    * @param destFile the destination zip file.
112    * @throws IOException
113    */
114   @Nonnull
115   public static void zip(final Path fileOrFolderToCompress,
116           final Path destFile, final Predicate<Path> filter) throws IOException {
117     Path parent = destFile.getParent();
118     if (parent == null) {
119       throw new IllegalArgumentException("Parent is null for: " + fileOrFolderToCompress);
120     }
121     Path tmpFile = Files.createTempFile(parent, ".", "tmp");
122     try {
123       Path relativePath;
124       if (Files.isDirectory(fileOrFolderToCompress)) {
125         relativePath = fileOrFolderToCompress;
126       } else {
127         relativePath = fileOrFolderToCompress.getParent();
128       }
129       try (BufferedOutputStream fos = new BufferedOutputStream(Files.newOutputStream(tmpFile));
130               ZipOutputStream zos = new ZipOutputStream(fos, StandardCharsets.UTF_8)) {
131           try (Stream<Path> ws = Files.walk(fileOrFolderToCompress)) {
132             ws.forEach((path) -> {
133               if (Files.isDirectory(path)) {
134                 return;
135               }
136               if (!filter.test(path)) {
137                 return;
138               }
139               String fileName = relativePath.relativize(path).toString();
140               try (InputStream in = new BufferedInputStream(Files.newInputStream(path),
141                       8192, ArraySuppliers.Bytes.TL_SUPPLIER)) {
142                 ZipEntry ze = new ZipEntry(fileName);
143                 zos.putNextEntry(ze);
144                 Streams.copy(in, zos);
145               } catch (IOException ex) {
146                 throw new UncheckedIOException("Error compressing " + path, ex);
147               }
148             });
149           }
150       }
151       Files.move(tmpFile, destFile,
152               StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
153     } finally {
154       Files.deleteIfExists(tmpFile);
155     }
156   }
157 
158   /**
159    * Copy file atomic.
160    * file will be written to a tmp file in the destination folder, and atomically renamed (if file system supports)
161    * @param source
162    * @param destinationFile
163    * @throws IOException
164    */
165   public static void copyFileAtomic(final Path source, final Path destinationFile) throws IOException {
166     Path parent = destinationFile.getParent();
167     if (parent == null) {
168       throw new IllegalArgumentException("Destination " + destinationFile + " is not a file");
169     }
170     Path tmpFile = Files.createTempFile(parent, ".", null);
171     try {
172       try (InputStream in = new BufferedInputStream(Files.newInputStream(source),
173               8192, ArraySuppliers.Bytes.TL_SUPPLIER);
174               OutputStream os = new BufferedOutputStream(Files.newOutputStream(tmpFile))) {
175         Streams.copy(in, os);
176       }
177       Files.move(tmpFile, destinationFile,
178               StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
179     } finally {
180       Files.deleteIfExists(tmpFile);
181     }
182   }
183 
184 
185   /**
186    * Unzip a zip archive to same folder.
187    * @param zipFile
188    * @return list of unzipped files.
189    * @throws IOException
190    */
191   @Nonnull
192   public static List<Path> unzip(final Path zipFile) throws IOException {
193     Path parent = zipFile.getParent();
194     if (parent == null) {
195       throw new IllegalArgumentException("File " + zipFile + " cannot be unzipped to null parent folder");
196     }
197     return unzip(zipFile, parent);
198   }
199 
200   /**
201    * Unzip a zip file to a destination folder.
202    * @param zipFile
203    * @param destinationDirectory
204    * @return the list of files that were extracted.
205    * @throws IOException in case extraction fails for whatever reason.
206    */
207   @Nonnull
208   public static List<Path> unzip(final Path zipFile, final Path destinationDirectory) throws IOException {
209     return unzip(zipFile, destinationDirectory, (p) -> true);
210   }
211 
212   /**
213    * Unzip a zip file to a destination folder.
214    * @param zipFile
215    * @param destinationDirectory
216    * @return the list of files that were extracted.
217    * @throws IOException in case extraction fails for whatever reason.
218    */
219   @Nonnull
220   public static List<Path> unzip(final Path zipFile, final Path destinationDirectory,
221           final Predicate<Path> filter) throws IOException {
222     if (!Files.exists(destinationDirectory)) {
223       Files.createDirectories(destinationDirectory);
224     }
225     if (!Files.isDirectory(destinationDirectory)) {
226       throw new IllegalArgumentException("Destination " + destinationDirectory + " must be a directory");
227     }
228     final List<Path> response = new ArrayList<>();
229     URI zipUri = URI.create("jar:" + zipFile.toUri().toURL());
230     synchronized (zipUri.toString().intern()) { // newFileSystem fails if already one there...
231       try (FileSystem zipFs = FileSystems.newFileSystem(zipUri, Collections.emptyMap())) {
232         for (Path root : zipFs.getRootDirectories()) {
233           Path dest =  destinationDirectory.resolve(root.toString().substring(1));
234           Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
235 
236             private final Set<Path> created = new HashSet<>();
237 
238             @Override
239             public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs)
240                     throws IOException {
241               if (!filter.test(file)) {
242                 return FileVisitResult.CONTINUE;
243               }
244               Path destination = dest.resolve(root.relativize(file).toString());
245               Path parent = destination.getParent();
246               if (parent != null && created.add(parent)) {
247                 Files.createDirectories(parent);
248               }
249               copyFileAtomic(file, destination);
250               response.add(destination);
251               return FileVisitResult.CONTINUE;
252             }
253 
254             @Override
255             public FileVisitResult preVisitDirectory(final Path dir, final BasicFileAttributes attrs) {
256               if (!filter.test(dir)) {
257                 return FileVisitResult.CONTINUE;
258               }
259               return FileVisitResult.CONTINUE;
260             }
261           });
262         }
263       } catch (IOException | RuntimeException ex) {
264         for (Path path : response) {
265           try {
266             Files.delete(path);
267           } catch (IOException | RuntimeException ex2) {
268             ex.addSuppressed(ex2);
269           }
270         }
271         throw ex;
272       }
273     }
274     return response;
275   }
276 
277   @Nonnull
278   public static List<Path> unzip2(final Path zipFile, final Path destinationDirectory) throws IOException {
279     return unzip2(zipFile, destinationDirectory, (p) -> true);
280   }
281 
282   @Nonnull
283   @SuppressFBWarnings("PATH_TRAVERSAL_IN")
284   public static List<Path> unzip2(final Path zipFile, final Path destDir,
285           final Predicate<Path> filter) throws IOException {
286     final List<Path> response = new ArrayList<>();
287     final Set<Path> created = new HashSet<>();
288     try (ZipInputStream zis = new ZipInputStream(new BufferedInputStream(Files.newInputStream(zipFile)))) {
289       ZipEntry zipEntry = zis.getNextEntry();
290       while (zipEntry != null) {
291         String fName = zipEntry.getName();
292         if (fName.contains("..")) {
293           throw new IllegalArgumentException("Backreference " + fName + " not allowed in " + zipFile);
294         }
295         if (filter.test(Paths.get(fName))) {
296           Path newFile = destDir.resolve(fName);
297           Path parent = newFile.getParent();
298           if (parent != null && created.add(parent)) {
299             Files.createDirectories(parent);
300           }
301           try (OutputStream fos = new BufferedOutputStream(Files.newOutputStream(newFile))) {
302             Streams.copy(zis, fos);
303           }
304           response.add(newFile);
305         }
306         zipEntry = zis.getNextEntry();
307       }
308       zis.closeEntry();
309     }
310     return response;
311   }
312 
313 
314 
315 }