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 }