org.opencastproject.util.FileSupport Maven / Gradle / Ivy
/*
* Licensed to The Apereo Foundation under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
*
* The Apereo Foundation licenses this file to you under the Educational
* Community License, Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of the License
* at:
*
* http://opensource.org/licenses/ecl2.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*
*/
package org.opencastproject.util;
import static java.lang.String.format;
import static java.nio.file.Files.createLink;
import static java.nio.file.Files.deleteIfExists;
import static java.nio.file.Files.exists;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
import static java.util.Objects.requireNonNull;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
/** Utility class, dealing with files. */
public final class FileSupport {
/** Only files will be deleted, the directory structure remains untouched. */
public static final int DELETE_FILES = 0;
/** Delete everything including the root directory. */
public static final int DELETE_ROOT = 1;
/** Name of the java environment variable for the temp directory */
private static final String IO_TMPDIR = "java.io.tmpdir";
/** Work directory */
private static File tmpDir = null;
/** Logging facility provided by log4j */
private static final Logger logger = LoggerFactory.getLogger(FileSupport.class);
/** Disable construction of this utility class */
private FileSupport() {
}
/**
* Copies the specified file from sourceLocation
to targetLocation
and returns a reference
* to the newly created file or directory.
*
* If targetLocation
is an existing directory, then the source file or directory will be copied into this
* directory, otherwise the source file will be copied to the file identified by targetLocation
.
*
* Note that existing files and directories will be overwritten.
*
* Also note that if targetLocation
is a directory than the directory itself, not only its content is
* copied.
*
* @param sourceLocation
* the source file or directory
* @param targetLocation
* the directory to copy the source file or directory to
* @return the created copy
* @throws IOException
* if copying of the file or directory failed
*/
public static File copy(File sourceLocation, File targetLocation) throws IOException {
return copy(sourceLocation, targetLocation, true);
}
/**
* Copies the specified sourceLocation
to targetLocation
and returns a reference to the
* newly created file or directory.
*
* If targetLocation
is an existing directory, then the source file or directory will be copied into this
* directory, otherwise the source file will be copied to the file identified by targetLocation
.
*
* If overwrite
is set to false
, this method throws an {@link IOException} if the target
* file already exists.
*
* Note that if targetLocation
is a directory than the directory itself, not only its content is copied.
*
* @param sourceFile
* the source file or directory
* @param targetFile
* the directory to copy the source file or directory to
* @param overwrite
* true
to overwrite existing files
* @return the created copy
* @throws IOException
* if copying of the file or directory failed
*/
public static File copy(File sourceFile, File targetFile, boolean overwrite) throws IOException {
// This variable is used when the channel copy files, and stores the maximum size of the file parts copied from
// source to target
final int chunk = 1024 * 1024 * 512; // 512 MB
// This variable is used when the cannel copy fails completely, as the size of the memory buffer used to copy the
// data from one stream to the other.
final int bufferSize = 1024 * 1024; // 1 MB
File dest = determineDestination(targetFile, sourceFile, overwrite);
// We are copying a directory
if (sourceFile.isDirectory()) {
if (!dest.exists()) {
dest.mkdirs();
}
File[] children = sourceFile.listFiles();
for (File child : children) {
copy(child, dest, overwrite);
}
}
// We are copying a file
else {
// If dest is not an "absolute file", getParentFile may return null, even if there *is* a parent file.
// That's why "getAbsoluteFile" is used here
dest.getAbsoluteFile().getParentFile().mkdirs();
if (dest.exists())
delete(dest);
FileChannel sourceChannel = null;
FileChannel targetChannel = null;
FileInputStream sourceStream = null;
FileOutputStream targetStream = null;
long size = 0;
try {
sourceStream = new FileInputStream(sourceFile);
targetStream = new FileOutputStream(dest);
try {
sourceChannel = sourceStream.getChannel();
targetChannel = targetStream.getChannel();
size = targetChannel.transferFrom(sourceChannel, 0, sourceChannel.size());
} catch (IOException ioe) {
logger.warn("Got IOException using Channels for copying.");
} finally {
// This has to be in "finally", because in 64-bit machines the channel copy may fail to copy the whole file
// without causing a exception
if ((sourceChannel != null) && (targetChannel != null) && (size < sourceFile.length())) {
// Failing back to using FileChannels *but* with chunks and not altogether
logger.info("Trying to copy the file in chunks using Channels");
while (size < sourceFile.length())
size += targetChannel.transferFrom(sourceChannel, size, chunk);
}
}
} catch (IOException ioe) {
if ((sourceStream != null) && (targetStream != null) && (size < sourceFile.length())) {
logger.warn("Got IOException using Channels for copying in chunks. Trying to use stream copy instead...");
int copied = 0;
byte[] buffer = new byte[bufferSize];
while ((copied = sourceStream.read(buffer, 0, buffer.length)) != -1)
targetStream.write(buffer, 0, copied);
} else
throw ioe;
} finally {
if (sourceChannel != null)
sourceChannel.close();
if (sourceStream != null)
sourceStream.close();
if (targetChannel != null)
targetChannel.close();
if (targetStream != null)
targetStream.close();
}
if (sourceFile.length() != dest.length()) {
logger.warn("Source " + sourceFile + " and target " + dest + " do not have the same length");
// TOOD: Why would this happen?
// throw new IOException("Source " + sourceLocation + " and target " +
// dest + " do not have the same length");
}
}
return dest;
}
/**
* Links the specified file or directory from sourceLocation
to targetLocation
. If
* targetLocation
does not exist, it will be created, if the target file already exists, an
* {@link IOException} will be thrown.
*
* If this fails (because linking is not supported on the current filesystem, then a copy is made.
*
*
* @param sourceLocation
* the source file or directory
* @param targetLocation
* the targetLocation
* @return the created link
* @throws IOException
* if linking of the file or directory failed
*/
public static File link(File sourceLocation, File targetLocation) throws IOException {
return link(sourceLocation, targetLocation, false);
}
/**
* Links the specified file or directory from sourceLocation
to targetLocation
. If
* targetLocation
does not exist, it will be created.
*
* If this fails (because linking is not supported on the current filesystem, then a copy is made.
*
* If overwrite
is set to false
, this method throws an {@link IOException} if the target
* file already exists.
*
* @param sourceLocation
* the source file or directory
* @param targetLocation
* the targetLocation
* @param overwrite
* true
to overwrite existing files
* @return the created link
* @throws IOException
* if linking of the file or directory failed
*/
public static File link(final File sourceLocation, final File targetLocation, final boolean overwrite)
throws IOException {
final Path sourcePath = requireNonNull(sourceLocation).toPath();
final Path targetPath = requireNonNull(targetLocation).toPath();
if (exists(sourcePath)) {
if (overwrite) {
deleteIfExists(targetPath);
} else {
if (exists(targetPath)) {
throw new IOException(format("There is already a file/directory at %s", targetPath));
}
}
try {
createLink(targetPath, sourcePath);
targetPath.toFile().length(); // this forces a stat call which is a quickfix for a bug in ceph (https://www.mail-archive.com/[email protected]/msg53368.html)
} catch (UnsupportedOperationException e) {
logger.debug("Copy file because creating hard-links is not supported by the current file system: {}",
ExceptionUtils.getMessage(e));
Files.copy(sourcePath, targetPath);
} catch (IOException e) {
logger.debug("Copy file because creating a hard-link at '{}' for existing file '{}' did not work:",
targetPath, sourcePath, e);
if (overwrite) {
Files.copy(sourcePath, targetPath, REPLACE_EXISTING);
} else {
Files.copy(sourcePath, targetPath);
}
}
} else {
throw new IOException(format("No file/directory found at %s", sourcePath));
}
return targetPath.toFile();
}
/**
* Returns true
if the operating system as well as the disk layout support creating a hard link from
* src
to dest
. Note that this implementation requires two files rather than directories and
* will overwrite any existing file that might already be present at the destination.
*
* @param sourceLocation
* the source file
* @param targetLocation
* the target file
* @return true
if the link was created, false
otherwhise
*/
public static boolean supportsLinking(File sourceLocation, File targetLocation) {
final Path sourcePath = requireNonNull(sourceLocation).toPath();
final Path targetPath = requireNonNull(targetLocation).toPath();
if (!exists(sourcePath))
throw new IllegalArgumentException(format("Source %s does not exist", sourcePath));
logger.debug("Creating hard link from {} to {}", sourcePath, targetPath);
try {
deleteIfExists(targetPath);
createLink(targetPath, sourcePath);
targetPath.toFile().length(); // this forces a stat call which is a quickfix for a bug in ceph (https://www.mail-archive.com/[email protected]/msg53368.html)
} catch (Exception e) {
logger.debug("Unable to create a link from {} to {}: {}", sourcePath, targetPath, e);
return false;
}
return true;
}
private static File determineDestination(File targetLocation, File sourceLocation, boolean overwrite)
throws IOException {
File dest = null;
// Source location exists
if (sourceLocation.exists()) {
// Is the source file/directory readable
if (sourceLocation.canRead()) {
// If a directory...
if (targetLocation.isDirectory()) {
// Create a destination file within it, with the same name of the source target
dest = new File(targetLocation, sourceLocation.getName());
} else {
// targetLocation is either a normal file or doesn't exist
dest = targetLocation;
}
// Source and target locations can not be the same
if (sourceLocation.equals(dest)) {
throw new IOException("Source and target locations must be different");
}
// Search the first existing parent of the target file, to check if it can be written
// getParentFile can return null even though there *is* a parent file, if the file is not absolute
// That's the reason why getAbsoluteFile is used here
for (File iter = dest.getAbsoluteFile(); iter != null; iter = iter.getParentFile()) {
if (iter.exists()) {
if (iter.canWrite()) {
break;
} else {
throw new IOException("Destination " + dest + "cannot be written/modified");
}
}
}
// Check the target file can be overwritten
if (dest.exists() && !dest.isDirectory() && !overwrite) {
throw new IOException("Destination " + dest + " already exists");
}
} else {
throw new IOException(sourceLocation + " cannot be read");
}
} else {
throw new IOException("Source " + sourceLocation + " does not exist");
}
return dest;
}
/**
* Delete all directories from start
up to directory limit
if they are empty. Directory
* limit
is exclusive and will not be deleted.
*
* @return true if the complete hierarchy has been deleted. false in any other case.
*/
public static boolean deleteHierarchyIfEmpty(File limit, File start) {
return limit.isDirectory()
&& start.isDirectory()
&& (isEqual(limit, start) || (isParent(limit, start) && start.list().length == 0 && start.delete() && deleteHierarchyIfEmpty(
limit, start.getParentFile())));
}
/** Compare two files by their canonical paths. */
public static boolean isEqual(File a, File b) {
try {
return a.getCanonicalPath().equals(b.getCanonicalPath());
} catch (IOException e) {
return false;
}
}
/**
* Check if a
is a parent of b
. This can only be the case if a
is a directory
* and a sub path of b
. isParent(a, a) == true
.
*/
public static boolean isParent(File a, File b) {
try {
final String aCanonical = a.getCanonicalPath();
final String bCanonical = b.getCanonicalPath();
return (!aCanonical.equals(bCanonical) && bCanonical.startsWith(aCanonical));
} catch (IOException e) {
return false;
}
}
/**
* Deletes the specified file and returns true
if the file was deleted.
*
* If f
is a directory, it will only be deleted if it doesn't contain any other files or directories. To
* do a recursive delete, you may use {@link #delete(File, boolean)}.
*
* @param f
* the file or directory
* @see #delete(File, boolean)
*/
public static boolean delete(File f) throws IOException {
return delete(f, false);
}
/**
* Like {@link #delete(File)} but does not throw any IO exceptions.
* In case of an IOException it will only be logged at warning level and the method returns false.
*/
public static boolean deleteQuietly(File f) {
return deleteQuietly(f, false);
}
/**
* Like {@link #delete(File, boolean)} but does not throw any IO exceptions.
* In case of an IOException it will only be logged at warning level and the method returns false.
*/
public static boolean deleteQuietly(File f, boolean recurse) {
try {
return delete(f, recurse);
} catch (IOException e) {
logger.warn("Cannot delete " + f.getAbsolutePath() + " because of IOException"
+ (e.getMessage() != null ? " " + e.getMessage() : ""));
return false;
}
}
/**
* Deletes the specified file and returns true
if the file was deleted.
*
* In the case that f
references a directory, it will only be deleted if it doesn't contain other files
* or directories, unless recurse
is set to true
.
*
*
* @param f
* the file or directory
* @param recurse
* true
to do a recursive deletes for directories
*/
public static boolean delete(File f, boolean recurse) throws IOException {
if (f == null)
return false;
if (!f.exists())
return false;
if (f.isDirectory()) {
String[] children = f.list();
if (children == null) {
throw new IOException("Cannot list content of directory " + f.getAbsolutePath());
}
if (children != null) {
if (children.length > 0 && !recurse)
return false;
for (String child : children) {
delete(new File(f, child), true);
}
} else {
logger.debug("Unexpected null listing files in {}", f.getAbsolutePath());
}
}
return f.delete();
}
/**
* Deletes the content of directory dir
and, if specified, the directory itself. If dir
is a
* normal file it will always be deleted.
*
* @return true everthing was deleted, false otherwise
*/
public static boolean delete(File dir, int mode) {
if (dir.isDirectory()) {
boolean ok = delete(dir.listFiles(), mode != DELETE_FILES);
if (mode == DELETE_ROOT) {
ok &= dir.delete();
}
return ok;
} else {
return dir.delete();
}
}
/**
* Deletes the content of directory dir
and, if specified, the directory itself. If dir
is a
* normal file it will be deleted always.
*/
private static boolean delete(File[] files, boolean deleteDir) {
boolean ok = true;
for (File f : files) {
if (f.isDirectory()) {
delete(f.listFiles(), deleteDir);
if (deleteDir) {
ok &= f.delete();
}
} else {
ok &= f.delete();
}
}
return ok;
}
/**
* Sets the webapp's temporary directory. Make sure that directory exists and has write permissions turned on.
*
* @param tmpDir
* the new temporary directory
* @throws IllegalArgumentException
* if the file object doesn't represent a directory
* @throws IllegalStateException
* if the directory is write protected
*/
public static void setTempDirectory(File tmpDir) throws IllegalArgumentException, IllegalStateException {
if (tmpDir == null || !tmpDir.isDirectory())
throw new IllegalArgumentException(tmpDir + " is not a directory");
if (!tmpDir.canWrite())
throw new IllegalStateException(tmpDir + " is not writable");
FileSupport.tmpDir = tmpDir;
}
/**
* Returns the webapp's temporary work directory.
*
* @return the temp directory
*/
public static File getTempDirectory() {
if (tmpDir == null) {
setTempDirectory(new File(System.getProperty(IO_TMPDIR)));
}
return tmpDir;
}
/**
* Returns a directory subdir
inside the webapp's temporary work directory.
*
* @param subdir
* name of the subdirectory
* @return the ready to use temp directory
*/
public static File getTempDirectory(String subdir) {
File tmp = new File(getTempDirectory(), subdir);
if (!tmp.exists())
tmp.mkdirs();
if (!tmp.isDirectory())
throw new IllegalStateException(tmp + " is not a directory!");
if (!tmp.canRead())
throw new IllegalStateException("Temp directory " + tmp + " is not readable!");
if (!tmp.canWrite())
throw new IllegalStateException("Temp directory " + tmp + " is not writable!");
return tmp;
}
}