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       FileChannel sourceChannel = null;
142       FileChannel targetChannel = null;
143       FileInputStream sourceStream = null;
144       FileOutputStream targetStream = null;
145       long size = 0;
146 
147       try {
148         sourceStream = new FileInputStream(sourceFile);
149         targetStream = new FileOutputStream(dest);
150         try {
151           sourceChannel = sourceStream.getChannel();
152           targetChannel = targetStream.getChannel();
153           size = targetChannel.transferFrom(sourceChannel, 0, sourceChannel.size());
154         } catch (IOException ioe) {
155           logger.warn("Got IOException using Channels for copying.");
156         } finally {
157           // This has to be in "finally", because in 64-bit machines the channel copy may fail to copy the whole file
158           // without causing a exception
159           if ((sourceChannel != null) && (targetChannel != null) && (size < sourceFile.length())) {
160             // Failing back to using FileChannels *but* with chunks and not altogether
161             logger.info("Trying to copy the file in chunks using Channels");
162             while (size < sourceFile.length())
163               size += targetChannel.transferFrom(sourceChannel, size, chunk);
164           }
165         }
166       } catch (IOException ioe) {
167         if ((sourceStream != null) && (targetStream != null) && (size < sourceFile.length())) {
168           logger.warn("Got IOException using Channels for copying in chunks. Trying to use stream copy instead...");
169           int copied = 0;
170           byte[] buffer = new byte[bufferSize];
171           while ((copied = sourceStream.read(buffer, 0, buffer.length)) != -1)
172             targetStream.write(buffer, 0, copied);
173         } else
174           throw ioe;
175       } finally {
176         if (sourceChannel != null)
177           sourceChannel.close();
178         if (sourceStream != null)
179           sourceStream.close();
180         if (targetChannel != null)
181           targetChannel.close();
182         if (targetStream != null)
183           targetStream.close();
184       }
185 
186       if (sourceFile.length() != dest.length()) {
187         logger.warn("Source " + sourceFile + " and target " + dest + " do not have the same length");
188         // TOOD: Why would this happen?
189         // throw new IOException("Source " + sourceLocation + " and target " +
190         // dest + " do not have the same length");
191       }
192     }
193     return dest;
194   }
195 
196   /**
197    * Links the specified file or directory from <code>sourceLocation</code> to <code>targetLocation</code>. If
198    * <code>targetLocation</code> does not exist, it will be created, if the target file already exists, an
199    * {@link IOException} will be thrown.
200    * <p>
201    * If this fails (because linking is not supported on the current filesystem, then a copy is made.
202    * </p>
203    *
204    * @param sourceLocation
205    *          the source file or directory
206    * @param targetLocation
207    *          the targetLocation
208    * @return the created link
209    * @throws IOException
210    *           if linking of the file or directory failed
211    */
212   public static File link(File sourceLocation, File targetLocation) throws IOException {
213     return link(sourceLocation, targetLocation, false);
214   }
215 
216   /**
217    * Links the specified file or directory from <code>sourceLocation</code> to <code>targetLocation</code>. If
218    * <code>targetLocation</code> does not exist, it will be created.
219    * <p>
220    * If this fails (because linking is not supported on the current filesystem, then a copy is made.
221    * </p>
222    * If <code>overwrite</code> is set to <code>false</code>, this method throws an {@link IOException} if the target
223    * file already exists.
224    *
225    * @param sourceLocation
226    *          the source file or directory
227    * @param targetLocation
228    *          the targetLocation
229    * @param overwrite
230    *          <code>true</code> to overwrite existing files
231    * @return the created link
232    * @throws IOException
233    *           if linking of the file or directory failed
234    */
235   public static File link(final File sourceLocation, final File targetLocation, final boolean overwrite)
236           throws IOException {
237     final Path sourcePath = requireNonNull(sourceLocation).toPath();
238     final Path targetPath = requireNonNull(targetLocation).toPath();
239 
240     if (exists(sourcePath)) {
241       if (overwrite) {
242         deleteIfExists(targetPath);
243       } else {
244         if (exists(targetPath)) {
245           throw new IOException(format("There is already a file/directory at %s", targetPath));
246         }
247       }
248 
249       try {
250         createLink(targetPath, sourcePath);
251         targetPath.toFile().length(); // this forces a stat call which is a quickfix for a bug in ceph (https://www.mail-archive.com/ceph-users@lists.ceph.com/msg53368.html)
252       } catch (UnsupportedOperationException e) {
253         logger.debug("Copy file because creating hard-links is not supported by the current file system: {}",
254                 ExceptionUtils.getMessage(e));
255         Files.copy(sourcePath, targetPath);
256       } catch (IOException e) {
257         logger.debug("Copy file because creating a hard-link at '{}' for existing file '{}' did not work:",
258                 targetPath, sourcePath, e);
259         if (overwrite) {
260           Files.copy(sourcePath, targetPath, REPLACE_EXISTING);
261         } else {
262           Files.copy(sourcePath, targetPath);
263         }
264       }
265     } else {
266       throw new IOException(format("No file/directory found at %s", sourcePath));
267     }
268 
269     return targetPath.toFile();
270   }
271 
272   /**
273    * Returns <code>true</code> if the operating system as well as the disk layout support creating a hard link from
274    * <code>src</code> to <code>dest</code>. Note that this implementation requires two files rather than directories and
275    * will overwrite any existing file that might already be present at the destination.
276    *
277    * @param sourceLocation
278    *          the source file
279    * @param targetLocation
280    *          the target file
281    * @return <code>true</code> if the link was created, <code>false</code> otherwhise
282    */
283   public static boolean supportsLinking(File sourceLocation, File targetLocation) {
284     final Path sourcePath = requireNonNull(sourceLocation).toPath();
285     final Path targetPath = requireNonNull(targetLocation).toPath();
286 
287     if (!exists(sourcePath))
288       throw new IllegalArgumentException(format("Source %s does not exist", sourcePath));
289 
290     logger.debug("Creating hard link from {} to {}", sourcePath, targetPath);
291     try {
292       deleteIfExists(targetPath);
293       createLink(targetPath, sourcePath);
294       targetPath.toFile().length(); // this forces a stat call which is a quickfix for a bug in ceph (https://www.mail-archive.com/ceph-users@lists.ceph.com/msg53368.html)
295     } catch (Exception e) {
296       logger.debug("Unable to create a link from {} to {}", sourcePath, targetPath, e);
297       return false;
298     }
299 
300     return true;
301   }
302 
303   private static File determineDestination(File targetLocation, File sourceLocation, boolean overwrite)
304           throws IOException {
305     File dest = null;
306 
307     // Source location exists
308     if (sourceLocation.exists()) {
309       // Is the source file/directory readable
310       if (sourceLocation.canRead()) {
311         // If a directory...
312         if (targetLocation.isDirectory()) {
313           // Create a destination file within it, with the same name of the source target
314           dest = new File(targetLocation, sourceLocation.getName());
315         } else {
316           // targetLocation is either a normal file or doesn't exist
317           dest = targetLocation;
318         }
319 
320         // Source and target locations can not be the same
321         if (sourceLocation.equals(dest)) {
322           throw new IOException("Source and target locations must be different");
323         }
324 
325         // Search the first existing parent of the target file, to check if it can be written
326         // getParentFile can return null even though there *is* a parent file, if the file is not absolute
327         // That's the reason why getAbsoluteFile is used here
328         for (File iter = dest.getAbsoluteFile(); iter != null; iter = iter.getParentFile()) {
329           if (iter.exists()) {
330             if (iter.canWrite()) {
331               break;
332             } else {
333               throw new IOException("Destination " + dest + "cannot be written/modified");
334             }
335           }
336         }
337 
338         // Check the target file can be overwritten
339         if (dest.exists() && !dest.isDirectory() && !overwrite) {
340           throw new IOException("Destination " + dest + " already exists");
341         }
342 
343       } else {
344         throw new IOException(sourceLocation + " cannot be read");
345       }
346     } else {
347       throw new IOException("Source " + sourceLocation + " does not exist");
348     }
349 
350     return dest;
351   }
352 
353   /**
354    * Delete all directories from <code>start</code> up to directory <code>limit</code> if they are empty. Directory
355    * <code>limit</code> is <em>exclusive</em> and will not be deleted.
356    *
357    * @return true if the <em>complete</em> hierarchy has been deleted. false in any other case.
358    */
359   public static boolean deleteHierarchyIfEmpty(File limit, File start) {
360     return limit.isDirectory()
361             && start.isDirectory()
362             && (isEqual(limit, start) || (isParent(limit, start) && start.list().length == 0 && start.delete() && deleteHierarchyIfEmpty(
363                     limit, start.getParentFile())));
364   }
365 
366   /** Compare two files by their canonical paths. */
367   public static boolean isEqual(File a, File b) {
368     try {
369       return a.getCanonicalPath().equals(b.getCanonicalPath());
370     } catch (IOException e) {
371       return false;
372     }
373   }
374 
375   /**
376    * 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
377    * and a sub path of <code>b</code>. <code>isParent(a, a) == true</code>.
378    */
379   public static boolean isParent(File a, File b) {
380     try {
381       final String aCanonical = a.getCanonicalPath();
382       final String bCanonical = b.getCanonicalPath();
383       return (!aCanonical.equals(bCanonical) && bCanonical.startsWith(aCanonical));
384     } catch (IOException e) {
385       return false;
386     }
387   }
388 
389   /**
390    * Deletes the specified file and returns <code>true</code> if the file was deleted.
391    * <p>
392    * If <code>f</code> is a directory, it will only be deleted if it doesn't contain any other files or directories. To
393    * do a recursive delete, you may use {@link #delete(File, boolean)}.
394    *
395    * @param f
396    *          the file or directory
397    * @see #delete(File, boolean)
398    */
399   public static boolean delete(File f) throws IOException {
400     return delete(f, false);
401   }
402 
403   /**
404    * Like {@link #delete(File)} but does not throw any IO exceptions.
405    * In case of an IOException it will only be logged at warning level and the method returns false.
406    */
407   public static boolean deleteQuietly(File f) {
408     return deleteQuietly(f, false);
409   }
410 
411   /**
412    * Like {@link #delete(File, boolean)} but does not throw any IO exceptions.
413    * In case of an IOException it will only be logged at warning level and the method returns false.
414    */
415   public static boolean deleteQuietly(File f, boolean recurse) {
416     try {
417       return delete(f, recurse);
418     } catch (IOException e) {
419       logger.warn("Cannot delete " + f.getAbsolutePath() + " because of IOException"
420                        + (e.getMessage() != null ? " " + e.getMessage() : ""));
421       return false;
422     }
423   }
424 
425   /**
426    * Deletes the specified file and returns <code>true</code> if the file was deleted.
427    * <p>
428    * In the case that <code>f</code> references a directory, it will only be deleted if it doesn't contain other files
429    * or directories, unless <code>recurse</code> is set to <code>true</code>.
430    * </p>
431    *
432    * @param f
433    *          the file or directory
434    * @param recurse
435    *          <code>true</code> to do a recursive deletes for directories
436    */
437   public static boolean delete(File f, boolean recurse) throws IOException {
438     if (f == null)
439       return false;
440     if (!f.exists())
441       return false;
442     if (f.isDirectory()) {
443       String[] children = f.list();
444       if (children == null) {
445         throw new IOException("Cannot list content of directory " + f.getAbsolutePath());
446       }
447       if (children != null) {
448         if (children.length > 0 && !recurse)
449           return false;
450         for (String child : children) {
451           delete(new File(f, child), true);
452         }
453       } else {
454         logger.debug("Unexpected null listing files in {}", f.getAbsolutePath());
455       }
456     }
457     return f.delete();
458   }
459 
460   /**
461    * Deletes the content of directory <code>dir</code> and, if specified, the directory itself. If <code>dir</code> is a
462    * normal file it will always be deleted.
463    *
464    * @return true everthing was deleted, false otherwise
465    */
466   public static boolean delete(File dir, int mode) {
467     if (dir.isDirectory()) {
468       boolean ok = delete(dir.listFiles(), mode != DELETE_FILES);
469       if (mode == DELETE_ROOT) {
470         ok &= dir.delete();
471       }
472       return ok;
473     } else {
474       return dir.delete();
475     }
476   }
477 
478   /**
479    * Deletes the content of directory <code>dir</code> and, if specified, the directory itself. If <code>dir</code> is a
480    * normal file it will be deleted always.
481    */
482   private static boolean delete(File[] files, boolean deleteDir) {
483     boolean ok = true;
484     for (File f : files) {
485       if (f.isDirectory()) {
486         delete(f.listFiles(), deleteDir);
487         if (deleteDir) {
488           ok &= f.delete();
489         }
490       } else {
491         ok &= f.delete();
492       }
493     }
494     return ok;
495   }
496 
497   /**
498    * Sets the webapp's temporary directory. Make sure that directory exists and has write permissions turned on.
499    *
500    * @param tmpDir
501    *          the new temporary directory
502    * @throws IllegalArgumentException
503    *           if the file object doesn't represent a directory
504    * @throws IllegalStateException
505    *           if the directory is write protected
506    */
507   public static void setTempDirectory(File tmpDir) throws IllegalArgumentException, IllegalStateException {
508     if (tmpDir == null || !tmpDir.isDirectory())
509       throw new IllegalArgumentException(tmpDir + " is not a directory");
510     if (!tmpDir.canWrite())
511       throw new IllegalStateException(tmpDir + " is not writable");
512     FileSupport.tmpDir = tmpDir;
513   }
514 
515   /**
516    * Returns the webapp's temporary work directory.
517    *
518    * @return the temp directory
519    */
520   public static File getTempDirectory() {
521     if (tmpDir == null) {
522       setTempDirectory(new File(System.getProperty(IO_TMPDIR)));
523     }
524     return tmpDir;
525   }
526 
527   /**
528    * Returns a directory <code>subdir</code> inside the webapp's temporary work directory.
529    *
530    * @param subdir
531    *          name of the subdirectory
532    * @return the ready to use temp directory
533    */
534   public static File getTempDirectory(String subdir) {
535     File tmp = new File(getTempDirectory(), subdir);
536     if (!tmp.exists())
537       tmp.mkdirs();
538     if (!tmp.isDirectory())
539       throw new IllegalStateException(tmp + " is not a directory!");
540     if (!tmp.canRead())
541       throw new IllegalStateException("Temp directory " + tmp + " is not readable!");
542     if (!tmp.canWrite())
543       throw new IllegalStateException("Temp directory " + tmp + " is not writable!");
544     return tmp;
545   }
546 
547 }