View Javadoc
1   /*
2    * Licensed to The Apereo Foundation under one or more contributor license
3    * agreements. See the NOTICE file distributed with this work for additional
4    * information regarding copyright ownership.
5    *
6    *
7    * The Apereo Foundation licenses this file to you under the Educational
8    * Community License, Version 2.0 (the "License"); you may not use this file
9    * except in compliance with the License. You may obtain a copy of the License
10   * at:
11   *
12   *   http://opensource.org/licenses/ecl2.txt
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
17   * License for the specific language governing permissions and limitations under
18   * the License.
19   *
20   */
21  
22  package org.opencastproject.util;
23  
24  import static java.lang.String.format;
25  import static java.nio.file.Files.createLink;
26  import static java.nio.file.Files.deleteIfExists;
27  import static java.nio.file.Files.exists;
28  import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
29  import static java.util.Objects.requireNonNull;
30  
31  import org.apache.commons.lang3.exception.ExceptionUtils;
32  import org.slf4j.Logger;
33  import org.slf4j.LoggerFactory;
34  
35  import java.io.File;
36  import java.io.FileInputStream;
37  import java.io.FileOutputStream;
38  import java.io.IOException;
39  import java.nio.channels.FileChannel;
40  import java.nio.file.Files;
41  import java.nio.file.Path;
42  
43  /** Utility class, dealing with files. */
44  public final class FileSupport {
45  
46    /** Only files will be deleted, the directory structure remains untouched. */
47    public static final int DELETE_FILES = 0;
48  
49    /** Delete everything including the root directory. */
50    public static final int DELETE_ROOT = 1;
51  
52    /** Name of the java environment variable for the temp directory */
53    private static final String IO_TMPDIR = "java.io.tmpdir";
54  
55    /** Work directory */
56    private static File tmpDir = null;
57  
58    /** Logging facility provided by log4j */
59    private static final Logger logger = LoggerFactory.getLogger(FileSupport.class);
60  
61    /** Disable construction of this utility class */
62    private FileSupport() {
63    }
64  
65    /**
66     * Copies the specified file from <code>sourceLocation</code> to <code>targetLocation</code> and returns a reference
67     * to the newly created file or directory.
68     * <p>
69     * If <code>targetLocation</code> is an existing directory, then the source file or directory will be copied into this
70     * directory, otherwise the source file will be copied to the file identified by <code>targetLocation</code>.
71     * <p>
72     * Note that existing files and directories will be overwritten.
73     * <p>
74     * Also note that if <code>targetLocation</code> is a directory than the directory itself, not only its content is
75     * copied.
76     *
77     * @param sourceLocation
78     *          the source file or directory
79     * @param targetLocation
80     *          the directory to copy the source file or directory to
81     * @return the created copy
82     * @throws IOException
83     *           if copying of the file or directory failed
84     */
85    public static File copy(File sourceLocation, File targetLocation) throws IOException {
86      return copy(sourceLocation, targetLocation, true);
87    }
88  
89    /**
90     * Copies the specified <code>sourceLocation</code> to <code>targetLocation</code> and returns a reference to the
91     * newly created file or directory.
92     * <p>
93     * If <code>targetLocation</code> is an existing directory, then the source file or directory will be copied into this
94     * directory, otherwise the source file will be copied to the file identified by <code>targetLocation</code>.
95     * <p>
96     * If <code>overwrite</code> is set to <code>false</code>, this method throws an {@link IOException} if the target
97     * file already exists.
98     * <p>
99     * Note that if <code>targetLocation</code> is a directory than the directory itself, not only its content is copied.
100    *
101    * @param sourceFile
102    *          the source file or directory
103    * @param targetFile
104    *          the directory to copy the source file or directory to
105    * @param overwrite
106    *          <code>true</code> to overwrite existing files
107    * @return the created copy
108    * @throws IOException
109    *           if copying of the file or directory failed
110    */
111   public static File copy(File sourceFile, File targetFile, boolean overwrite) throws IOException {
112 
113     // This variable is used when the channel copy files, and stores the maximum size of the file parts copied from
114     // source to target
115     final int chunk = 1024 * 1024 * 512; // 512 MB
116 
117     // This variable is used when the cannel copy fails completely, as the size of the memory buffer used to copy the
118     // data from one stream to the other.
119     final int bufferSize = 1024 * 1024; // 1 MB
120 
121     File dest = determineDestination(targetFile, sourceFile, overwrite);
122 
123     // We are copying a directory
124     if (sourceFile.isDirectory()) {
125       if (!dest.exists()) {
126         dest.mkdirs();
127       }
128       File[] children = sourceFile.listFiles();
129       for (File child : children) {
130         copy(child, dest, overwrite);
131       }
132     }
133     // We are copying a file
134     else {
135       // If dest is not an "absolute file", getParentFile may return null, even if there *is* a parent file.
136       // That's why "getAbsoluteFile" is used here
137       dest.getAbsoluteFile().getParentFile().mkdirs();
138       if (dest.exists()) {
139         delete(dest);
140       }
141 
142       FileChannel sourceChannel = null;
143       FileChannel targetChannel = null;
144       FileInputStream sourceStream = null;
145       FileOutputStream targetStream = null;
146       long size = 0;
147 
148       try {
149         sourceStream = new FileInputStream(sourceFile);
150         targetStream = new FileOutputStream(dest);
151         try {
152           sourceChannel = sourceStream.getChannel();
153           targetChannel = targetStream.getChannel();
154           size = targetChannel.transferFrom(sourceChannel, 0, sourceChannel.size());
155         } catch (IOException ioe) {
156           logger.warn("Got IOException using Channels for copying.");
157         } finally {
158           // This has to be in "finally", because in 64-bit machines the channel copy may fail to copy the whole file
159           // without causing a exception
160           if ((sourceChannel != null) && (targetChannel != null) && (size < sourceFile.length())) {
161             // Failing back to using FileChannels *but* with chunks and not altogether
162             logger.info("Trying to copy the file in chunks using Channels");
163             while (size < sourceFile.length()) {
164               size += targetChannel.transferFrom(sourceChannel, size, chunk);
165             }
166           }
167         }
168       } catch (IOException ioe) {
169         if ((sourceStream != null) && (targetStream != null) && (size < sourceFile.length())) {
170           logger.warn("Got IOException using Channels for copying in chunks. Trying to use stream copy instead...");
171           int copied = 0;
172           byte[] buffer = new byte[bufferSize];
173           while ((copied = sourceStream.read(buffer, 0, buffer.length)) != -1) {
174             targetStream.write(buffer, 0, copied);
175           }
176         } else {
177           throw ioe;
178         }
179       } finally {
180         if (sourceChannel != null) {
181           sourceChannel.close();
182         }
183         if (sourceStream != null) {
184           sourceStream.close();
185         }
186         if (targetChannel != null) {
187           targetChannel.close();
188         }
189         if (targetStream != null) {
190           targetStream.close();
191         }
192       }
193 
194       if (sourceFile.length() != dest.length()) {
195         logger.warn("Source " + sourceFile + " and target " + dest + " do not have the same length");
196         // TOOD: Why would this happen?
197         // throw new IOException("Source " + sourceLocation + " and target " +
198         // dest + " do not have the same length");
199       }
200     }
201     return dest;
202   }
203 
204   /**
205    * Links the specified file or directory from <code>sourceLocation</code> to <code>targetLocation</code>. If
206    * <code>targetLocation</code> does not exist, it will be created, if the target file already exists, an
207    * {@link IOException} will be thrown.
208    * <p>
209    * If this fails (because linking is not supported on the current filesystem, then a copy is made.
210    * </p>
211    *
212    * @param sourceLocation
213    *          the source file or directory
214    * @param targetLocation
215    *          the targetLocation
216    * @return the created link
217    * @throws IOException
218    *           if linking of the file or directory failed
219    */
220   public static File link(File sourceLocation, File targetLocation) throws IOException {
221     return link(sourceLocation, targetLocation, false);
222   }
223 
224   /**
225    * Links the specified file or directory from <code>sourceLocation</code> to <code>targetLocation</code>. If
226    * <code>targetLocation</code> does not exist, it will be created.
227    * <p>
228    * If this fails (because linking is not supported on the current filesystem, then a copy is made.
229    * </p>
230    * If <code>overwrite</code> is set to <code>false</code>, this method throws an {@link IOException} if the target
231    * file already exists.
232    *
233    * @param sourceLocation
234    *          the source file or directory
235    * @param targetLocation
236    *          the targetLocation
237    * @param overwrite
238    *          <code>true</code> to overwrite existing files
239    * @return the created link
240    * @throws IOException
241    *           if linking of the file or directory failed
242    */
243   public static File link(final File sourceLocation, final File targetLocation, final boolean overwrite)
244           throws IOException {
245     final Path sourcePath = requireNonNull(sourceLocation).toPath();
246     final Path targetPath = requireNonNull(targetLocation).toPath();
247 
248     if (exists(sourcePath)) {
249       if (overwrite) {
250         deleteIfExists(targetPath);
251       } else {
252         if (exists(targetPath)) {
253           throw new IOException(format("There is already a file/directory at %s", targetPath));
254         }
255       }
256 
257       try {
258         createLink(targetPath, sourcePath);
259         targetPath.toFile().length(); // this forces a stat call which is a quickfix for a bug in ceph
260                                       // (https://www.mail-archive.com/ceph-users@lists.ceph.com/msg53368.html)
261       } catch (UnsupportedOperationException e) {
262         logger.debug("Copy file because creating hard-links is not supported by the current file system: {}",
263                 ExceptionUtils.getMessage(e));
264         Files.copy(sourcePath, targetPath);
265       } catch (IOException e) {
266         logger.debug("Copy file because creating a hard-link at '{}' for existing file '{}' did not work:",
267                 targetPath, sourcePath, e);
268         if (overwrite) {
269           Files.copy(sourcePath, targetPath, REPLACE_EXISTING);
270         } else {
271           Files.copy(sourcePath, targetPath);
272         }
273       }
274     } else {
275       throw new IOException(format("No file/directory found at %s", sourcePath));
276     }
277 
278     return targetPath.toFile();
279   }
280 
281   /**
282    * Returns <code>true</code> if the operating system as well as the disk layout support creating a hard link from
283    * <code>src</code> to <code>dest</code>. Note that this implementation requires two files rather than directories and
284    * will overwrite any existing file that might already be present at the destination.
285    *
286    * @param sourceLocation
287    *          the source file
288    * @param targetLocation
289    *          the target file
290    * @return <code>true</code> if the link was created, <code>false</code> otherwhise
291    */
292   public static boolean supportsLinking(File sourceLocation, File targetLocation) {
293     final Path sourcePath = requireNonNull(sourceLocation).toPath();
294     final Path targetPath = requireNonNull(targetLocation).toPath();
295 
296     if (!exists(sourcePath)) {
297       throw new IllegalArgumentException(format("Source %s does not exist", sourcePath));
298     }
299 
300     logger.debug("Creating hard link from {} to {}", sourcePath, targetPath);
301     try {
302       deleteIfExists(targetPath);
303       createLink(targetPath, sourcePath);
304       targetPath.toFile().length(); // this forces a stat call which is a quickfix for a bug in ceph
305                                     // (https://www.mail-archive.com/ceph-users@lists.ceph.com/msg53368.html)
306     } catch (Exception e) {
307       logger.debug("Unable to create a link from {} to {}", sourcePath, targetPath, e);
308       return false;
309     }
310 
311     return true;
312   }
313 
314   private static File determineDestination(File targetLocation, File sourceLocation, boolean overwrite)
315           throws IOException {
316     File dest = null;
317 
318     // Source location exists
319     if (sourceLocation.exists()) {
320       // Is the source file/directory readable
321       if (sourceLocation.canRead()) {
322         // If a directory...
323         if (targetLocation.isDirectory()) {
324           // Create a destination file within it, with the same name of the source target
325           dest = new File(targetLocation, sourceLocation.getName());
326         } else {
327           // targetLocation is either a normal file or doesn't exist
328           dest = targetLocation;
329         }
330 
331         // Source and target locations can not be the same
332         if (sourceLocation.equals(dest)) {
333           throw new IOException("Source and target locations must be different");
334         }
335 
336         // Search the first existing parent of the target file, to check if it can be written
337         // getParentFile can return null even though there *is* a parent file, if the file is not absolute
338         // That's the reason why getAbsoluteFile is used here
339         for (File iter = dest.getAbsoluteFile(); iter != null; iter = iter.getParentFile()) {
340           if (iter.exists()) {
341             if (iter.canWrite()) {
342               break;
343             } else {
344               throw new IOException("Destination " + dest + "cannot be written/modified");
345             }
346           }
347         }
348 
349         // Check the target file can be overwritten
350         if (dest.exists() && !dest.isDirectory() && !overwrite) {
351           throw new IOException("Destination " + dest + " already exists");
352         }
353 
354       } else {
355         throw new IOException(sourceLocation + " cannot be read");
356       }
357     } else {
358       throw new IOException("Source " + sourceLocation + " does not exist");
359     }
360 
361     return dest;
362   }
363 
364   /**
365    * Delete all directories from <code>start</code> up to directory <code>limit</code> if they are empty. Directory
366    * <code>limit</code> is <em>exclusive</em> and will not be deleted.
367    *
368    * @return true if the <em>complete</em> hierarchy has been deleted. false in any other case.
369    */
370   public static boolean deleteHierarchyIfEmpty(File limit, File start) {
371     return limit.isDirectory()
372         && start.isDirectory()
373         && (isEqual(limit, start)
374             || (isParent(limit, start) && start.list().length == 0 && start.delete() && deleteHierarchyIfEmpty(
375                     limit, start.getParentFile())));
376   }
377 
378   /** Compare two files by their canonical paths. */
379   public static boolean isEqual(File a, File b) {
380     try {
381       return a.getCanonicalPath().equals(b.getCanonicalPath());
382     } catch (IOException e) {
383       return false;
384     }
385   }
386 
387   /**
388    * Check if <code>a</code> is a parent of <code>b</code>. This can only be the case if <code>a</code> is a directory
389    * and a sub path of <code>b</code>. <code>isParent(a, a) == true</code>.
390    */
391   public static boolean isParent(File a, File b) {
392     try {
393       final String aCanonical = a.getCanonicalPath();
394       final String bCanonical = b.getCanonicalPath();
395       return (!aCanonical.equals(bCanonical) && bCanonical.startsWith(aCanonical));
396     } catch (IOException e) {
397       return false;
398     }
399   }
400 
401   /**
402    * Deletes the specified file and returns <code>true</code> if the file was deleted.
403    * <p>
404    * If <code>f</code> is a directory, it will only be deleted if it doesn't contain any other files or directories. To
405    * do a recursive delete, you may use {@link #delete(File, boolean)}.
406    *
407    * @param f
408    *          the file or directory
409    * @see #delete(File, boolean)
410    */
411   public static boolean delete(File f) throws IOException {
412     return delete(f, false);
413   }
414 
415   /**
416    * Like {@link #delete(File)} but does not throw any IO exceptions.
417    * In case of an IOException it will only be logged at warning level and the method returns false.
418    */
419   public static boolean deleteQuietly(File f) {
420     return deleteQuietly(f, false);
421   }
422 
423   /**
424    * Like {@link #delete(File, boolean)} but does not throw any IO exceptions.
425    * In case of an IOException it will only be logged at warning level and the method returns false.
426    */
427   public static boolean deleteQuietly(File f, boolean recurse) {
428     try {
429       return delete(f, recurse);
430     } catch (IOException e) {
431       logger.warn("Cannot delete " + f.getAbsolutePath() + " because of IOException"
432                        + (e.getMessage() != null ? " " + e.getMessage() : ""));
433       return false;
434     }
435   }
436 
437   /**
438    * Deletes the specified file and returns <code>true</code> if the file was deleted.
439    * <p>
440    * In the case that <code>f</code> references a directory, it will only be deleted if it doesn't contain other files
441    * or directories, unless <code>recurse</code> is set to <code>true</code>.
442    * </p>
443    *
444    * @param f
445    *          the file or directory
446    * @param recurse
447    *          <code>true</code> to do a recursive deletes for directories
448    */
449   public static boolean delete(File f, boolean recurse) throws IOException {
450     if (f == null) {
451       return false;
452     }
453     if (!f.exists()) {
454       return false;
455     }
456     if (f.isDirectory()) {
457       String[] children = f.list();
458       if (children == null) {
459         throw new IOException("Cannot list content of directory " + f.getAbsolutePath());
460       }
461       if (children != null) {
462         if (children.length > 0 && !recurse) {
463           return false;
464         }
465         for (String child : children) {
466           delete(new File(f, child), true);
467         }
468       } else {
469         logger.debug("Unexpected null listing files in {}", f.getAbsolutePath());
470       }
471     }
472     return f.delete();
473   }
474 
475   /**
476    * Deletes the content of directory <code>dir</code> and, if specified, the directory itself. If <code>dir</code> is a
477    * normal file it will always be deleted.
478    *
479    * @return true everthing was deleted, false otherwise
480    */
481   public static boolean delete(File dir, int mode) {
482     if (dir.isDirectory()) {
483       boolean ok = delete(dir.listFiles(), mode != DELETE_FILES);
484       if (mode == DELETE_ROOT) {
485         ok &= dir.delete();
486       }
487       return ok;
488     } else {
489       return dir.delete();
490     }
491   }
492 
493   /**
494    * Deletes the content of directory <code>dir</code> and, if specified, the directory itself. If <code>dir</code> is a
495    * normal file it will be deleted always.
496    */
497   private static boolean delete(File[] files, boolean deleteDir) {
498     boolean ok = true;
499     for (File f : files) {
500       if (f.isDirectory()) {
501         delete(f.listFiles(), deleteDir);
502         if (deleteDir) {
503           ok &= f.delete();
504         }
505       } else {
506         ok &= f.delete();
507       }
508     }
509     return ok;
510   }
511 
512   /**
513    * Sets the webapp's temporary directory. Make sure that directory exists and has write permissions turned on.
514    *
515    * @param tmpDir
516    *          the new temporary directory
517    * @throws IllegalArgumentException
518    *           if the file object doesn't represent a directory
519    * @throws IllegalStateException
520    *           if the directory is write protected
521    */
522   public static void setTempDirectory(File tmpDir) throws IllegalArgumentException, IllegalStateException {
523     if (tmpDir == null || !tmpDir.isDirectory()) {
524       throw new IllegalArgumentException(tmpDir + " is not a directory");
525     }
526     if (!tmpDir.canWrite()) {
527       throw new IllegalStateException(tmpDir + " is not writable");
528     }
529     FileSupport.tmpDir = tmpDir;
530   }
531 
532   /**
533    * Returns the webapp's temporary work directory.
534    *
535    * @return the temp directory
536    */
537   public static File getTempDirectory() {
538     if (tmpDir == null) {
539       setTempDirectory(new File(System.getProperty(IO_TMPDIR)));
540     }
541     return tmpDir;
542   }
543 
544   /**
545    * Returns a directory <code>subdir</code> inside the webapp's temporary work directory.
546    *
547    * @param subdir
548    *          name of the subdirectory
549    * @return the ready to use temp directory
550    */
551   public static File getTempDirectory(String subdir) {
552     File tmp = new File(getTempDirectory(), subdir);
553     if (!tmp.exists()) {
554       tmp.mkdirs();
555     }
556     if (!tmp.isDirectory()) {
557       throw new IllegalStateException(tmp + " is not a directory!");
558     }
559     if (!tmp.canRead()) {
560       throw new IllegalStateException("Temp directory " + tmp + " is not readable!");
561     }
562     if (!tmp.canWrite()) {
563       throw new IllegalStateException("Temp directory " + tmp + " is not writable!");
564     }
565     return tmp;
566   }
567 
568 }