/*
* Copyright 2013 Robert von Burg <eitch@eitchnet.ch>
*
* Licensed under the Apache 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://www.apache.org/licenses/LICENSE-2.0
*
* 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 li.strolch.utils.helper;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.MessageDigest;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Helper class for dealing with files
*
* @author Robert von Burg <eitch@eitchnet.ch>
*/
public class FileHelper {
private static final int MAX_FILE_SIZE = 50 * 1024 * 1024;
private static final Logger logger = LoggerFactory.getLogger(FileHelper.class);
/**
* Reads the contents of a file into a byte array.
*
* @param file
* the file to read
*
* @return the contents of a file as a string
*/
public static final byte[] readFile(File file) {
if (file.length() > MAX_FILE_SIZE)
throw new RuntimeException(String.format("Only allowed to read files up to %s. File too large: %s", //$NON-NLS-1$
humanizeFileSize(MAX_FILE_SIZE), humanizeFileSize(file.length())));
byte[] data = new byte[(int) file.length()];
int pos = 0;
try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(file));) {
byte[] bytes = new byte[8192];
int read;
while ((read = in.read(bytes)) != -1) {
System.arraycopy(bytes, 0, data, pos, read);
pos += read;
}
} catch (FileNotFoundException e) {
throw new RuntimeException("Filed does not exist " + file.getAbsolutePath()); //$NON-NLS-1$
} catch (IOException e) {
throw new RuntimeException("Could not read file " + file.getAbsolutePath()); //$NON-NLS-1$
}
return data;
}
/**
* Reads the contents of a file into a string. Note, no encoding is checked. It is expected to be UTF-8
*
* @param file
* the file to read
*
* @return the contents of a file as a string
*/
public static final String readFileToString(File file) {
try (BufferedReader bufferedReader = new BufferedReader(new FileReader(file));) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = bufferedReader.readLine()) != null) {
sb.append(line + "\n"); //$NON-NLS-1$
}
return sb.toString();
} catch (FileNotFoundException e) {
throw new RuntimeException("File does not exist " + file.getAbsolutePath()); //$NON-NLS-1$
} catch (IOException e) {
throw new RuntimeException("Could not read file " + file.getAbsolutePath()); //$NON-NLS-1$
}
}
/**
* Reads the contents of a {@link InputStream} into a string. Note, no encoding is checked. It is expected to be
* UTF-8
*
* @param stream
* the stream to read
*
* @return the contents of a file as a string
*/
public static final String readStreamToString(InputStream stream) {
try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(stream));) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = bufferedReader.readLine()) != null) {
sb.append(line + "\n"); //$NON-NLS-1$
}
return sb.toString();
} catch (IOException e) {
throw new RuntimeException("Could not read strean " + stream); //$NON-NLS-1$
}
}
/**
* Writes the given byte array to the given file
*
* @param bytes
* the data to write to the file
* @param dstFile
* the path to which to write the data
*/
public static final void writeToFile(byte[] bytes, File dstFile) {
try (BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(dstFile));) {
out.write(bytes);
} catch (FileNotFoundException e) {
throw new RuntimeException("Filed does not exist " + dstFile.getAbsolutePath()); //$NON-NLS-1$
} catch (IOException e) {
throw new RuntimeException("Could not write to file " + dstFile.getAbsolutePath()); //$NON-NLS-1$
}
}
/**
* Writes the string to dstFile
*
* @param string
* string to write to file
* @param dstFile
* the file to write to
*/
public static final void writeStringToFile(String string, File dstFile) {
try (BufferedWriter bufferedwriter = new BufferedWriter(new FileWriter(dstFile));) {
bufferedwriter.write(string);
} catch (FileNotFoundException e) {
throw new RuntimeException("Filed does not exist " + dstFile.getAbsolutePath()); //$NON-NLS-1$
} catch (IOException e) {
throw new RuntimeException("Could not write to file " + dstFile.getAbsolutePath()); //$NON-NLS-1$
}
}
/**
* Deletes files recursively. No question asked, but logging is done in case of problems
*
* @param file
* the file to delete
* @param log
* @return true if all went well, and false if it did not work. The log will contain the problems encountered
*/
public final static boolean deleteFile(File file, boolean log) {
return FileHelper.deleteFiles(new File[] { file }, log);
}
/**
* Deletes files recursively. No question asked, but logging is done in case of problems
*
* @param files
* the files to delete
* @param log
* @return true if all went well, and false if it did not work. The log will contain the problems encountered
*/
public final static boolean deleteFiles(File[] files, boolean log) {
boolean worked = true;
for (int i = 0; i < files.length; i++) {
File file = files[i];
if (file.isDirectory()) {
boolean done = FileHelper.deleteFiles(file.listFiles(), log);
if (!done) {
worked = false;
FileHelper.logger.warn("Could not empty the directory: " + file.getAbsolutePath()); //$NON-NLS-1$
} else {
done = file.delete();
if (done) {
if (log)
FileHelper.logger.info("Deleted DIR " + file.getAbsolutePath()); //$NON-NLS-1$
} else {
worked = false;
FileHelper.logger.warn("Could not delete the directory: " + file.getAbsolutePath()); //$NON-NLS-1$
}
}
} else {
boolean done = file.delete();
if (done) {
if (log)
FileHelper.logger.info("Deleted FILE " + file.getAbsolutePath()); //$NON-NLS-1$
} else {
worked = false;
FileHelper.logger.warn(("Could not delete the file: " + file.getAbsolutePath())); //$NON-NLS-1$
}
}
}
return worked;
}
/**
* <p>
* Copy a given list of {@link File Files}. Recursively copies the files and directories to the destination.
* </p>
*
* @param srcFiles
* The source files to copy
* @param dstDirectory
* The destination where to copy the files
* @param checksum
* if true, then a MD5 checksum is made to validate copying
* @return <b>true</b> if and only if the copying succeeded; <b>false</b> otherwise
*/
public final static boolean copy(File[] srcFiles, File dstDirectory, boolean checksum) {
if (!dstDirectory.isDirectory() || !dstDirectory.canWrite()) {
String msg = "Destination is not a directory or is not writeable: {0}"; //$NON-NLS-1$
throw new IllegalArgumentException(MessageFormat.format(msg, dstDirectory.getAbsolutePath()));
}
for (File srcFile : srcFiles) {
File dstFile = new File(dstDirectory, srcFile.getName());
if (srcFile.isDirectory()) {
dstFile.mkdir();
if (!copy(srcFile.listFiles(), dstFile, checksum)) {
String msg = "Failed to copy contents of {0} to {1}"; //$NON-NLS-1$
msg = MessageFormat.format(msg, srcFile.getAbsolutePath(), dstFile.getAbsolutePath());
logger.error(msg);
return false;
}
} else {
if (!copy(srcFile, dstFile, checksum)) {
return false;
}
}
}
return true;
}
/**
* Copy a {@link File} The renameTo method does not allow action across NFS mounted filesystems this method is the
* workaround
*
* @param fromFile
* The existing File
* @param toFile
* The new File
* @param checksum
* if true, then a MD5 checksum is made to validate copying
* @return <b>true</b> if and only if the renaming succeeded; <b>false</b> otherwise
*/
public final static boolean copy(File fromFile, File toFile, boolean checksum) {
try (BufferedInputStream inBuffer = new BufferedInputStream(new FileInputStream(fromFile));
BufferedOutputStream outBuffer = new BufferedOutputStream(new FileOutputStream(toFile));) {
int theByte = 0;
while ((theByte = inBuffer.read()) > -1) {
outBuffer.write(theByte);
}
outBuffer.flush();
if (checksum) {
String fromFileMD5 = StringHelper.getHexString(FileHelper.hashFileMd5(fromFile));
String toFileMD5 = StringHelper.getHexString(FileHelper.hashFileMd5(toFile));
if (!fromFileMD5.equals(toFileMD5)) {
FileHelper.logger.error(MessageFormat.format(
"Copying failed, as MD5 sums are not equal: {0} / {1}", fromFileMD5, toFileMD5)); //$NON-NLS-1$
toFile.delete();
return false;
}
}
// cleanup if files are not the same length
if (fromFile.length() != toFile.length()) {
String msg = "Copying failed, as new files are not the same length: {0} / {1}"; //$NON-NLS-1$
msg = MessageFormat.format(msg, fromFile.length(), toFile.length());
FileHelper.logger.error(msg);
toFile.delete();
return false;
}
} catch (Exception e) {
String msg = MessageFormat.format("Failed to copy path from {0} to + {1} due to:", fromFile, toFile); //$NON-NLS-1$
FileHelper.logger.error(msg, e);
return false;
}
return true;
}
/**
* Move a File The renameTo method does not allow action across NFS mounted filesystems this method is the
* workaround
*
* @param fromFile
* The existing File
* @param toFile
* The new File
* @return <b>true</b> if and only if the renaming succeeded; <b>false</b> otherwise
*/
public final static boolean move(File fromFile, File toFile) {
if (fromFile.renameTo(toFile)) {
return true;
}
FileHelper.logger.warn("Simple File.renameTo failed, trying copy/delete..."); //$NON-NLS-1$
// delete if copy was successful, otherwise move will fail
if (FileHelper.copy(fromFile, toFile, true)) {
FileHelper.logger.info("Deleting fromFile: " + fromFile.getAbsolutePath()); //$NON-NLS-1$
return fromFile.delete();
}
return false;
}
/**
* Finds the common parent for the files in the given list
*
* @param files
* the files to find the common parent for
*
* @return the {@link File} representing the common parent, or null if there is none
*/
public static File findCommonParent(List<File> files) {
// be gentle with bad input data
if (files.size() == 0)
return null;
if (files.size() == 1)
return files.get(0).getParentFile();
File commonParent = null;
int commonParentDepth = -1;
// find the common parent among all the files
for (int i = 0; i < files.size() - 1; i++) {
// get first file
File file = files.get(i);
// get list of parents for this file
List<File> parents = new ArrayList<>();
File parent = file.getParentFile();
while (parent != null) {
parents.add(parent);
parent = parent.getParentFile();
}
// reverse
Collections.reverse(parents);
// and now the same for the next file
File fileNext = files.get(i + 1);
List<File> parentsNext = new ArrayList<>();
File parentNext = fileNext.getParentFile();
while (parentNext != null) {
parentsNext.add(parentNext);
parentNext = parentNext.getParentFile();
}
// reverse
Collections.reverse(parentsNext);
//logger.info("Files: " + file + " / " + fileNext);
// now find the common parent
File newCommonParent = null;
int newCommonParentDepth = -1;
for (int j = 0; j < (parents.size()); j++) {
// don't overflow the size of the next list of parents
if (j >= parentsNext.size())
break;
// if we once found a common parent, and our current depth is
// greater than the one for the common parent, then stop as
// there can't be a deeper parent
if (commonParent != null && j > commonParentDepth)
break;
// get the next parents to compare
File aParent = parents.get(j);
File bParent = parentsNext.get(j);
//logger.info("Comparing " + aParent + " |||| " + bParent);
// if they parent are the same, then break, as we won't
// have another match
if (!aParent.equals(bParent))
break;
// save the parent and the depth where we found the parent
newCommonParent = aParent;
newCommonParentDepth = j;
}
// if no common parent was found, then break as there won't be one
if (commonParent == null && newCommonParent == null)
break;
// if there is no new common parent, then check the next file
if (newCommonParent == null)
continue;
// store the common parent
commonParent = newCommonParent;
commonParentDepth = newCommonParentDepth;
//logger.info("Temporary common parent: (" + commonParentDepth + ") " + commonParent);
}
//logger.info("Common parent: " + commonParent);
return commonParent;
}
/**
* Returns the size of the file in a human readable form. Everything smaller than 1024 bytes is returned as x bytes,
* next is KB, then MB and then GB
*
* @param file
* the file for which the humanized size is to be returned
*
* @return the humanized form of the files size
*/
public final static String humanizeFileSize(File file) {
return FileHelper.humanizeFileSize(file.length());
}
/**
* Returns the size of the file in a human readable form. Everything smaller than 1024 bytes is returned as x bytes,
* next is KB, then MB and then GB
*
* @param fileSize
* the size of a file for which the humanized size is to be returned
*
* @return the humanized form of the files size
*/
@SuppressWarnings("nls")
public final static String humanizeFileSize(long fileSize) {
if (fileSize < 1024)
return String.format("%d bytes", fileSize);
if (fileSize < 1048576)
return String.format("%.1f KB", (fileSize / 1024.0d));
if (fileSize < 1073741824)
return String.format("%.1f MB", (fileSize / 1048576.0d));
return String.format("%.1f GB", (fileSize / 1073741824.0d));
}
/**
* Creates the MD5 hash of the given file, returning the hash as a byte array. Use
* {@link StringHelper#getHexString(byte[])} to create a HEX string of the bytes
*
* @param file
* the file to hash
*
* @return the hash as a byte array
*/
public static byte[] hashFileMd5(File file) {
return FileHelper.hashFile(file, "MD5"); //$NON-NLS-1$
}
/**
* Creates the SHA1 hash of the given file, returning the hash as a byte array. Use
* {@link StringHelper#getHexString(byte[])} to create a HEX string of the bytes
*
* @param file
* the file to hash
*
* @return the hash as a byte array
*/
public static byte[] hashFileSha1(File file) {
return FileHelper.hashFile(file, "SHA-1"); //$NON-NLS-1$
}
/**
* Creates the SHA256 hash of the given file, returning the hash as a byte array. Use
* {@link StringHelper#getHexString(byte[])} to create a HEX string of the bytes
*
* @param file
* the file to hash
*
* @return the hash as a byte array
*/
public static byte[] hashFileSha256(File file) {
return FileHelper.hashFile(file, "SHA-256"); //$NON-NLS-1$
}
/**
* Creates the hash of the given file with the given algorithm, returning the hash as a byte array. Use
* {@link StringHelper#getHexString(byte[])} to create a HEX string of the bytes
*
* @param file
* the file to hash
* @param algorithm
* the hashing algorithm to use
*
* @return the hash as a byte array
*/
public static byte[] hashFile(File file, String algorithm) {
try (InputStream fis = new FileInputStream(file);) {
byte[] buffer = new byte[1024];
MessageDigest complete = MessageDigest.getInstance(algorithm);
int numRead;
do {
numRead = fis.read(buffer);
if (numRead > 0) {
complete.update(buffer, 0, numRead);
}
} while (numRead != -1);
return complete.digest();
} catch (Exception e) {
throw new RuntimeException("Something went wrong while hashing file: " + file.getAbsolutePath()); //$NON-NLS-1$
}
}
/**
* Helper method to append bytes to a specified file. The file is created if it does not exist otherwise the bytes
* are simply appended
*
* @param dstFile
* the file to append to
* @param bytes
* the bytes to append
*/
public static void appendFilePart(File dstFile, byte[] bytes) {
try (FileOutputStream outputStream = new FileOutputStream(dstFile, true);) {
outputStream.write(bytes);
outputStream.flush();
} catch (IOException e) {
throw new RuntimeException("Could not create and append the bytes to the file " + dstFile.getAbsolutePath()); //$NON-NLS-1$
}
}
}