View Javadoc
1   package fr.ifremer.quadrige3.core.dao.technical;
2   
3   /*-
4    * #%L
5    * Quadrige3 Core :: Quadrige3 Core Shared
6    * $Id:$
7    * $HeadURL:$
8    * %%
9    * Copyright (C) 2017 Ifremer
10   * %%
11   * This program is free software: you can redistribute it and/or modify
12   * it under the terms of the GNU Affero General Public License as published by
13   * the Free Software Foundation, either version 3 of the License, or
14   * (at your option) any later version.
15   *
16   * This program is distributed in the hope that it will be useful,
17   * but WITHOUT ANY WARRANTY; without even the implied warranty of
18   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19   * GNU General Public License for more details.
20   *
21   * You should have received a copy of the GNU Affero General Public License
22   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
23   * #L%
24   */
25  
26  import com.google.common.collect.ImmutableList;
27  import fr.ifremer.quadrige3.core.ProgressionCoreModel;
28  import fr.ifremer.quadrige3.core.exception.QuadrigeTechnicalException;
29  import org.apache.commons.io.FileUtils;
30  import org.apache.commons.lang3.mutable.MutableLong;
31  import org.apache.commons.logging.Log;
32  import org.apache.commons.logging.LogFactory;
33  import org.apache.commons.net.io.CopyStreamListener;
34  import org.apache.commons.net.io.Util;
35  import org.apache.commons.vfs2.FileObject;
36  import org.nuiton.jaxx.application.ApplicationIOUtil;
37  
38  import java.io.File;
39  import java.io.IOException;
40  import java.io.InputStream;
41  import java.io.OutputStream;
42  import java.math.BigDecimal;
43  import java.math.BigInteger;
44  import java.math.RoundingMode;
45  import java.nio.ByteBuffer;
46  import java.nio.channels.FileChannel;
47  import java.nio.channels.SeekableByteChannel;
48  import java.nio.file.*;
49  import java.nio.file.attribute.BasicFileAttributes;
50  import java.util.ArrayList;
51  import java.util.List;
52  import java.util.Objects;
53  
54  import static java.nio.file.FileVisitResult.CONTINUE;
55  import static java.nio.file.FileVisitResult.SKIP_SUBTREE;
56  import static java.nio.file.StandardOpenOption.*;
57  
58  /**
59   * @author peck7 on 30/06/2017.
60   */
61  public class Files {
62  
63      private static final Log LOG = LogFactory.getLog(Files.class);
64      private static final int DEFAULT_IO_BUFFER_SIZE = 131072; // 128kb
65      private static final RoundingMode ROUNDING_MODE = RoundingMode.HALF_UP;
66  
67      private Files() {
68          // no instance
69      }
70  
71      enum FileSize {
72          EXABYTE("EB", FileUtils.ONE_EB_BI),
73          PETABYTE("PB", FileUtils.ONE_PB_BI),
74          TERABYTE("TB", FileUtils.ONE_TB_BI),
75          GIGABYTE("GB", FileUtils.ONE_GB_BI),
76          MEGABYTE("MB", FileUtils.ONE_MB_BI),
77          KILOBYTE("KB", FileUtils.ONE_KB_BI),
78          BYTE("bytes", BigInteger.ONE);
79  
80          private final String unit;
81          private final BigInteger byteCount;
82  
83          FileSize(String unit, BigInteger byteCount) {
84              this.unit = unit;
85              this.byteCount = byteCount;
86          }
87  
88          private String unit() {
89              return unit;
90          }
91  
92          private BigInteger byteCount() {
93              return byteCount;
94          }
95  
96      }
97  
98      /**
99       * Formats a file's size into a human readable format
100      *
101      * @param fileSize the file's size as BigInteger
102      * @return the size as human readable string
103      */
104     public static String byteCountToDisplaySize(final BigInteger fileSize) {
105 
106         String unit = FileSize.BYTE.unit;
107         BigDecimal fileSizeInUnit = BigDecimal.ZERO;
108         String val;
109 
110         for (FileSize fs : FileSize.values()) {
111             BigDecimal size_bd = new BigDecimal(fileSize);
112             fileSizeInUnit = size_bd.divide(new BigDecimal(fs.byteCount), 5, ROUNDING_MODE);
113             if (fileSizeInUnit.compareTo(BigDecimal.ONE) >= 0) {
114                 unit = fs.unit;
115                 break;
116             }
117         }
118 
119         // always round so that at least 3 numerics are displayed (###, ##.#, #.##)
120         if (fileSizeInUnit.divide(BigDecimal.valueOf(100.0), BigDecimal.ROUND_DOWN).compareTo(BigDecimal.ONE) >= 0) {
121             val = fileSizeInUnit.setScale(0, ROUNDING_MODE).toString();
122         } else if (fileSizeInUnit.divide(BigDecimal.valueOf(10.0), BigDecimal.ROUND_DOWN).compareTo(BigDecimal.ONE) >= 0) {
123             val = fileSizeInUnit.setScale(1, ROUNDING_MODE).toString();
124         } else {
125             val = fileSizeInUnit.setScale(2, ROUNDING_MODE).toString();
126         }
127 
128         // trim zeros at the end
129         if (val.endsWith(".00")) {
130             val = val.substring(0, val.length() - 3);
131         } else if (val.endsWith(".0")) {
132             val = val.substring(0, val.length() - 2);
133         }
134 
135         return String.format("%s %s", val, unit);
136     }
137 
138     /**
139      * Formats a file's size into a human readable format
140      *
141      * @param fileSize the file's size as long
142      * @return the size as human readable string
143      */
144     public static String byteCountToDisplaySize(final long fileSize) {
145         return byteCountToDisplaySize(BigInteger.valueOf(fileSize));
146     }
147 
148     /**
149      * Copy a file with progression monitor. Use VFS2 file resolver and streams
150      *
151      * @param sourceFileName      a {@link java.lang.String} object.
152      * @param destinationFileName a {@link java.lang.String} object.
153      * @param progressMonitor     a {@link org.apache.commons.net.io.CopyStreamListener} object.
154      * @throws java.io.IOException if any.
155      */
156     @Deprecated
157     public static void copy(String sourceFileName, String destinationFileName, CopyStreamListener progressMonitor) throws IOException {
158 
159         FileObject sourceFile = ApplicationIOUtil.resolveFile(sourceFileName, "Can't resolve " + sourceFileName);
160         FileObject destinationFile = ApplicationIOUtil.resolveFile(destinationFileName, "Can't resolve " + destinationFileName);
161 
162         try (InputStream sourceFileIn = sourceFile.getContent().getInputStream();
163              OutputStream destinationFileOut = destinationFile.getContent().getOutputStream()) {
164             Util.copyStream(sourceFileIn, destinationFileOut, DEFAULT_IO_BUFFER_SIZE, sourceFile.getContent().getSize(), progressMonitor);
165         }
166     }
167 
168     public static void copyFile(final Path source, final Path target) throws IOException {
169         copyFile(source, target, null);
170     }
171 
172     public static void copyFile(final Path source, final Path target, final ProgressionCoreModel progressionModel) throws IOException {
173         Assert.notNull(source);
174         Assert.notNull(target);
175         Assert.isTrue(java.nio.file.Files.exists(source), "the source to copy must exists");
176         Assert.isTrue(java.nio.file.Files.isRegularFile(source), "the source to copy must be a regular file");
177         if (java.nio.file.Files.exists(target))
178             Assert.isTrue(!java.nio.file.Files.isSameFile(source, target), "the source and target can't be the same");
179         java.nio.file.Files.createDirectories(target.getParent());
180 
181         if (progressionModel != null) {
182             progressionModel.adaptTotal(getSize(source));
183         }
184 
185         try (SeekableByteChannel inputChannel = java.nio.file.Files.newByteChannel(source, READ);
186              SeekableByteChannel outputChannel = java.nio.file.Files.newByteChannel(target, CREATE, WRITE, TRUNCATE_EXISTING)) {
187             ByteBuffer buffer = ByteBuffer.allocateDirect(DEFAULT_IO_BUFFER_SIZE);
188             while (inputChannel.read(buffer) != -1) {
189                 buffer.flip();
190                 while (buffer.hasRemaining()) {
191                     if (progressionModel != null) {
192                         progressionModel.increments(buffer.remaining());
193                     }
194                     outputChannel.write(buffer);
195                 }
196                 buffer.clear();
197             }
198         }
199     }
200 
201     public static void copyDirectory(final Path source, final Path target) throws IOException {
202         copyDirectory(source, target, null);
203     }
204 
205     public static void copyDirectory(final Path source, final Path target, ProgressionCoreModel progressionModel) throws IOException {
206         Assert.notNull(source, "source is null");
207         Assert.notNull(target, "target is null");
208         Assert.isTrue(java.nio.file.Files.exists(source), "source must exists");
209         Assert.isTrue(java.nio.file.Files.isDirectory(source), "source must be a directory");
210         if (progressionModel != null) {
211             progressionModel.adaptTotal(getSize(source));
212         }
213         java.nio.file.Files.walkFileTree(source, new TreeCopier(source, target, progressionModel));
214     }
215 
216     public static void moveDirectory(final Path source, final Path target, final ProgressionCoreModel progressionModel) throws IOException {
217         copyDirectory(source, target, progressionModel);
218         cleanDirectory(source, "Fail to clean directory " + source);
219     }
220 
221     static class TreeCopier implements FileVisitor<Path> {
222         private final Path source;
223         private final Path target;
224         private final ProgressionCoreModel progressionModel;
225 
226         TreeCopier(Path source, Path target, ProgressionCoreModel progressionModel) {
227             this.source = source;
228             this.target = target;
229             this.progressionModel = progressionModel;
230         }
231 
232         @Override
233         public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
234             // before visiting entries in a directory we create the target directory
235             Path newDir = target.resolve(source.relativize(dir));
236             try {
237                 java.nio.file.Files.createDirectories(newDir);
238             } catch (IOException x) {
239                 System.err.format("Unable to create: %s: %s%n", newDir, x);
240                 return SKIP_SUBTREE;
241             }
242             return CONTINUE;
243         }
244 
245         @Override
246         public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
247 
248             try {
249                 copyFile(file, target.resolve(source.relativize(file)), progressionModel);
250             } catch (IOException e) {
251                 System.err.format("Unable to copy: %s: %s%n", file, e);
252             }
253             return CONTINUE;
254         }
255 
256         @Override
257         public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
258             return CONTINUE;
259         }
260 
261         @Override
262         public FileVisitResult visitFileFailed(Path file, IOException exc) {
263             if (exc instanceof FileSystemLoopException) {
264                 System.err.println("cycle detected: " + file);
265             } else {
266                 System.err.format("Unable to copy: %s: %s%n", file, exc);
267             }
268             return CONTINUE;
269         }
270     }
271 
272     public static void copyStream(final InputStream inputStream, final OutputStream outputStream) throws IOException {
273         copyStream(inputStream, outputStream, null);
274     }
275 
276     public static void copyStream(final InputStream inputStream, final OutputStream outputStream, ProgressionCoreModel progressionModel) throws IOException {
277         byte[] buffer = new byte[DEFAULT_IO_BUFFER_SIZE];
278         int n;
279         while (-1 != (n = inputStream.read(buffer))) {
280             outputStream.write(buffer, 0, n);
281             if (progressionModel != null) {
282                 progressionModel.increments(n);
283             }
284         }
285     }
286 
287     public static void deleteQuietly(final List<Path> paths) {
288         if (paths == null) return;
289         paths.forEach(Files::deleteQuietly);
290     }
291 
292     public static void deleteQuietly(final Path path) {
293         if (path == null) return;
294         try {
295             if (java.nio.file.Files.isDirectory(path)) {
296                 cleanDirectory(path, "unable to clean " + path);
297             }
298             java.nio.file.Files.deleteIfExists(path);
299         } catch (IOException ignored) {
300         }
301     }
302 
303     /**
304      * <p>cleanDirectory.</p>
305      *
306      * @param directory   a {@link java.io.File} object.
307      * @param failMessage a {@link java.lang.String} object.
308      */
309     public static void cleanDirectory(final File directory, final String failMessage) {
310         cleanDirectory(directory.toPath(), failMessage);
311     }
312 
313     public static void cleanDirectory(final Path directory, final String failMessage) {
314         cleanDirectory(directory, failMessage, null);
315     }
316 
317     public static void cleanDirectory(final Path directory, final String failMessage, ProgressionCoreModel progressionModel) {
318 
319         if (progressionModel != null) {
320             progressionModel.setTotal(getDirectoryFileCount(directory));
321         }
322 
323         int nbAttempt = 0;
324         IOException lastException = null;
325         while (isDirectoryNotEmpty(directory) && nbAttempt < 10) {
326             nbAttempt++;
327             try {
328                 java.nio.file.Files.walkFileTree(directory, new RecursiveDeleteFileVisitor(directory, progressionModel));
329                 lastException = null;
330             } catch (NoSuchFileException ignored) {
331             } catch (AccessDeniedException ade) {
332                 if (java.nio.file.Files.exists(Paths.get(ade.getFile()))) {
333                     lastException = ade;
334                     break;
335                 }
336             } catch (IOException e) {
337                 lastException = e;
338                 // wait a while
339                 try {
340                     Thread.sleep(500);
341                 } catch (InterruptedException ignored) {
342                 }
343             }
344         }
345         if (lastException != null) {
346             throw new QuadrigeTechnicalException(failMessage, lastException);
347         }
348         if (LOG.isWarnEnabled() && nbAttempt > 1) {
349             LOG.warn(String.format("cleaning the directory '%s' successful after %d attempts", directory, nbAttempt));
350         }
351     }
352 
353     private static boolean isDirectoryNotEmpty(final Path path) {
354         if (!java.nio.file.Files.isDirectory(path)) {
355             return false;
356         }
357         try {
358             try (DirectoryStream<Path> dirStream = java.nio.file.Files.newDirectoryStream(path)) {
359                 return dirStream.iterator().hasNext();
360             }
361         } catch (IOException e) {
362             return false;
363         }
364     }
365 
366     private static class RecursiveDeleteFileVisitor extends SimpleFileVisitor<Path> {
367 
368         private final Path root;
369         private final ProgressionCoreModel progressionModel;
370 
371         private RecursiveDeleteFileVisitor(Path root, ProgressionCoreModel progressionModel) {
372             this.root = root;
373             this.progressionModel = progressionModel;
374         }
375 
376         @Override
377         public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
378 
379             // delete the file
380             java.nio.file.Files.deleteIfExists(file);
381             if (progressionModel != null)
382                 progressionModel.increments(1);
383             return FileVisitResult.CONTINUE;
384         }
385 
386         @Override
387         public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
388 
389             // don't delete the root
390             if (dir == root) {
391                 return FileVisitResult.TERMINATE;
392             }
393 
394             // delete the directory
395             java.nio.file.Files.deleteIfExists(dir);
396             return FileVisitResult.CONTINUE;
397         }
398 
399     }
400 
401     /**
402      * Split a file into pieces of chunkSize bytes
403      *
404      * @param file      the file to split
405      * @param chunkSize the chunk size in bytes
406      * @return the list of created splitted up files
407      * @throws IOException if any
408      */
409     public static List<Path> splitFile(final Path file, final long chunkSize) throws IOException {
410         long fileSize = java.nio.file.Files.size(file);
411         if (fileSize <= chunkSize) return ImmutableList.of(file);
412 
413         int partCounter = 0;
414         long readPosition = 0;
415         long remainingSize = fileSize;
416         List<Path> files = new ArrayList<>();
417 
418         try (FileChannel in = FileChannel.open(file, READ)) {
419             while (remainingSize > 0) {
420                 // write each chunk of data into separate file with different number in name
421                 String filePartName = String.format("%s.%03d", file.getFileName(), ++partCounter);
422                 Path newFile = file.resolveSibling(filePartName);
423                 try (FileChannel out = FileChannel.open(newFile, CREATE_NEW, WRITE)) {
424                     for (long position = 0; position < (remainingSize < chunkSize ? remainingSize : chunkSize); ) {
425                         position += in.transferTo(readPosition + position, chunkSize - position, out);
426                         readPosition += position;
427                         remainingSize -= position;
428                     }
429                 }
430                 files.add(newFile);
431             }
432         }
433         return files;
434     }
435 
436     /**
437      * Merge a list of files into 1 file
438      * the target file must not exists
439      *
440      * @param files the list of files to merge
441      * @param into  the target file
442      * @throws IOException if any
443      */
444     public static void mergeFiles(final List<Path> files, final Path into) throws IOException {
445         try (FileChannel out = FileChannel.open(into, CREATE_NEW, WRITE)) {
446             for (Path file : files) {
447                 try (FileChannel in = FileChannel.open(file, READ)) {
448                     for (long position = 0, length = in.size(); position < length; ) {
449                         position += in.transferTo(position, length - position, out);
450                     }
451                 }
452             }
453         }
454     }
455 
456     /**
457      * Get existing splitted up files names of current file
458      * The input file can not exists, but it scans the file parent directory for files with similar name ending with a numeric
459      *
460      * @param file the base file
461      * @return the list of splitted up files
462      */
463     public static List<Path> listOfFilesToMerge(final Path file) throws IOException {
464 
465         List<Path> files = getDirectoryContent(file.getParent(), entry -> entry.getFileName().toString().matches(file.getFileName().toString() + "[.]\\d+"));
466         if (files == null) return null;
467         files.sort(Path::compareTo); // ensuring order 001, 002, ..., 010, ...
468         return files;
469     }
470 
471     public static long getSize(final Path source) throws IOException {
472         if (source == null || !java.nio.file.Files.exists(source)) return 0;
473         if (java.nio.file.Files.isRegularFile(source)) return java.nio.file.Files.size(source);
474         MutableLong size = new MutableLong(0);
475         java.nio.file.Files.walkFileTree(source, new SimpleFileVisitor<Path>() {
476             @Override
477             public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
478                 Objects.requireNonNull(file);
479                 Objects.requireNonNull(attrs);
480                 size.add(attrs.size());
481                 return FileVisitResult.CONTINUE;
482             }
483         });
484         return size.getValue();
485     }
486 
487     public static List<Path> getDirectoryContent(final Path directory) throws IOException {
488         if (directory == null || !java.nio.file.Files.isDirectory(directory)) return null;
489         List<Path> content = new ArrayList<>();
490         try (DirectoryStream<Path> directoryStream = java.nio.file.Files.newDirectoryStream(directory)) {
491             directoryStream.forEach(content::add);
492         }
493         return content;
494     }
495 
496     public static List<Path> getDirectoryContent(final Path directory, final DirectoryStream.Filter<Path> filter) throws IOException {
497         if (directory == null || !java.nio.file.Files.isDirectory(directory)) return null;
498         List<Path> content = new ArrayList<>();
499         try (DirectoryStream<Path> directoryStream = java.nio.file.Files.newDirectoryStream(directory, filter)) {
500             directoryStream.forEach(content::add);
501         }
502         return content;
503     }
504 
505     public static long getDirectoryFileCount(Path directory) {
506         if (directory == null || !java.nio.file.Files.exists(directory) || !java.nio.file.Files.isDirectory(directory))
507             return 0;
508         try {
509             return java.nio.file.Files.walk(directory).parallel().filter(path -> !java.nio.file.Files.isDirectory(path)).count();
510         } catch (IOException e) {
511             // simply ignore it
512             return 0;
513         }
514     }
515 
516 }