413 lines
13 KiB
Java
413 lines
13 KiB
Java
/*
|
|
* 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 li.strolch.utils.DataUnit;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
import java.io.*;
|
|
import java.nio.file.Files;
|
|
import java.security.MessageDigest;
|
|
import java.text.MessageFormat;
|
|
import java.time.LocalDate;
|
|
import java.time.LocalDateTime;
|
|
import java.time.LocalTime;
|
|
import java.time.format.DateTimeFormatter;
|
|
import java.util.Set;
|
|
|
|
import static java.util.Collections.emptySet;
|
|
import static li.strolch.utils.helper.StringHelper.isEmpty;
|
|
import static li.strolch.utils.helper.StringHelper.normalizeLength;
|
|
import static li.strolch.utils.helper.TempFileOptions.*;
|
|
|
|
/**
|
|
* Helper class for dealing with files
|
|
*
|
|
* @author Robert von Burg <eitch@eitchnet.ch>
|
|
*/
|
|
public class FileHelper {
|
|
|
|
private static final Logger logger = LoggerFactory.getLogger(FileHelper.class);
|
|
|
|
private static final DateTimeFormatter separatedDtf = DateTimeFormatter.ofPattern("yyyy/MM/dd");
|
|
private static final DateTimeFormatter nonSeparatedDtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
|
|
|
/**
|
|
* <p>Generates a path to the given temporary path to write a file separated in sub folders by day using current
|
|
* date without any options enabled</p>
|
|
*
|
|
* @param tempPath the directory to which to write the file
|
|
*
|
|
* @return the temporary file
|
|
*/
|
|
public static File getTempFile(File tempPath) {
|
|
return getTempFile(tempPath, "", "", emptySet());
|
|
}
|
|
|
|
/**
|
|
* <p>Generates a path to the given temporary path to write a file separated in sub folders by day using current
|
|
* date without any options enabled</p>
|
|
*
|
|
* @param tempPath the directory to which to write the file
|
|
* @param prefix the prefix of the file name
|
|
* @param suffix the prefix of the file name
|
|
*
|
|
* @return the temporary file
|
|
*/
|
|
public static File getTempFile(File tempPath, String prefix, String suffix) {
|
|
return getTempFile(tempPath, prefix, suffix, emptySet());
|
|
}
|
|
|
|
/**
|
|
* <p>Generates a path to the given temporary path to write a file separated in sub folders by day using current
|
|
* date</p>
|
|
*
|
|
* <p>The options can be one of:</p>
|
|
* <ul>
|
|
* <li>{@link TempFileOptions#APPEND_MILLIS}</li>
|
|
* <li>{@link TempFileOptions#SEPARATE_DATE_SEGMENTS}</li>
|
|
* <li>{@link TempFileOptions#SEPARATE_HOURS}</li>
|
|
* <li>{@link TempFileOptions#WITH_HOURS}</li>
|
|
* <li>{@link TempFileOptions#MILLIS_FIRST}</li>
|
|
* </ul>
|
|
*
|
|
* @param tempPath the directory to which to write the file
|
|
* @param prefix the prefix of the file name
|
|
* @param suffix the prefix of the file name
|
|
* @param options the option bits
|
|
*
|
|
* @return the temporary file
|
|
*/
|
|
public static File getTempFile(File tempPath, String prefix, String suffix, Set<TempFileOptions> options) {
|
|
return getTempFile(tempPath, prefix, suffix, options, LocalDateTime.now(), System.currentTimeMillis());
|
|
}
|
|
|
|
static File getTempFile(File tempPath, String prefix, String suffix, Set<TempFileOptions> options,
|
|
LocalDateTime dateTime, long timestamp) {
|
|
prefix = StringHelper.trimOrEmpty(prefix);
|
|
suffix = StringHelper.trimOrEmpty(suffix);
|
|
|
|
LocalDate localDate = dateTime.toLocalDate();
|
|
|
|
boolean separateDateSegments = options.contains(SEPARATE_DATE_SEGMENTS);
|
|
String date = separateDateSegments ? localDate.format(separatedDtf) : localDate.format(nonSeparatedDtf);
|
|
|
|
String pathS;
|
|
if (options.contains(WITH_HOURS)) {
|
|
boolean separateHours = options.contains(SEPARATE_HOURS);
|
|
String currentHour = normalizeLength(String.valueOf(LocalTime.now().getHour()), 2, true, '0');
|
|
pathS = date + (separateHours ? "/" : "_") + currentHour;
|
|
} else {
|
|
pathS = date;
|
|
}
|
|
|
|
File path = new File(tempPath, pathS);
|
|
if (!path.exists() && !path.mkdirs())
|
|
throw new IllegalStateException("Failed to create path " + path.getAbsolutePath());
|
|
|
|
boolean appendMillis = options.contains(APPEND_MILLIS);
|
|
boolean millisFirst = appendMillis && options.contains(MILLIS_FIRST);
|
|
if (appendMillis) {
|
|
if (isEmpty(prefix)) {
|
|
prefix = String.valueOf(timestamp);
|
|
} else {
|
|
if (millisFirst)
|
|
prefix = timestamp + "_" + prefix;
|
|
else
|
|
prefix = prefix + "_" + timestamp;
|
|
}
|
|
|
|
if (!suffix.startsWith("."))
|
|
prefix = prefix + "_";
|
|
}
|
|
|
|
String fileName = prefix + suffix;
|
|
return new File(path, fileName);
|
|
}
|
|
|
|
/**
|
|
* 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 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).append("\n");
|
|
}
|
|
|
|
return sb.toString();
|
|
|
|
} catch (IOException e) {
|
|
throw new RuntimeException("Could not read strean " + stream);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deletes files recursively. No question asked, but logging is done in case of problems
|
|
*
|
|
* @param file the file to delete
|
|
* @param log true to enable logging
|
|
*
|
|
* @return true if all went well, and false if it did not work. The log will contain the problems encountered
|
|
*/
|
|
public 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 true to enable logging
|
|
*
|
|
* @return true if all went well, and false if it did not work. The log will contain the problems encountered
|
|
*/
|
|
public static boolean deleteFiles(File[] files, boolean log) {
|
|
boolean worked = true;
|
|
for (File file : files) {
|
|
|
|
if (file.isDirectory()) {
|
|
File[] children = file.listFiles();
|
|
if (children == null)
|
|
continue;
|
|
|
|
boolean done = FileHelper.deleteFiles(children, log);
|
|
if (!done) {
|
|
worked = false;
|
|
logger.warn("Could not empty the directory: " + file.getAbsolutePath());
|
|
} else {
|
|
done = file.delete();
|
|
if (done) {
|
|
if (log)
|
|
logger.info("Deleted DIR " + file.getAbsolutePath());
|
|
} else {
|
|
worked = false;
|
|
logger.warn("Could not delete the directory: " + file.getAbsolutePath());
|
|
}
|
|
}
|
|
} else {
|
|
boolean done = file.delete();
|
|
if (done) {
|
|
if (log)
|
|
FileHelper.logger.info("Deleted FILE " + file.getAbsolutePath());
|
|
} else {
|
|
worked = false;
|
|
FileHelper.logger.warn(("Could not delete the file: " + file.getAbsolutePath()));
|
|
}
|
|
}
|
|
}
|
|
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 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}";
|
|
throw new IllegalArgumentException(MessageFormat.format(msg, dstDirectory.getAbsolutePath()));
|
|
}
|
|
|
|
for (File srcFile : srcFiles) {
|
|
|
|
File dstFile = new File(dstDirectory, srcFile.getName());
|
|
if (srcFile.isDirectory()) {
|
|
if (!dstFile.isDirectory() && !dstFile.mkdir())
|
|
throw new IllegalStateException("Failed to create directory " + dstFile);
|
|
if (!copy(srcFile.listFiles(), dstFile, checksum)) {
|
|
String msg = "Failed to copy contents of {0} to {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 static boolean copy(File fromFile, File toFile, boolean checksum) {
|
|
|
|
try (BufferedInputStream inBuffer = new BufferedInputStream(Files.newInputStream(fromFile.toPath()));
|
|
BufferedOutputStream outBuffer = new BufferedOutputStream(Files.newOutputStream(toFile.toPath()))) {
|
|
|
|
int theByte;
|
|
|
|
while ((theByte = inBuffer.read()) > -1) {
|
|
outBuffer.write(theByte);
|
|
}
|
|
|
|
outBuffer.flush();
|
|
|
|
if (checksum) {
|
|
String fromFileSha256 = StringHelper.toHexString(FileHelper.hashFileSha256(fromFile));
|
|
String toFileSha256 = StringHelper.toHexString(FileHelper.hashFileSha256(toFile));
|
|
if (!fromFileSha256.equals(toFileSha256)) {
|
|
FileHelper.logger.error(
|
|
MessageFormat.format("Copying failed, as MD5 sums are not equal: {0} / {1}", fromFileSha256,
|
|
toFileSha256));
|
|
if (!toFile.delete())
|
|
throw new IllegalStateException("Failed to delete file " + toFile);
|
|
|
|
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}";
|
|
msg = MessageFormat.format(msg, fromFile.length(), toFile.length());
|
|
FileHelper.logger.error(msg);
|
|
if (!toFile.delete())
|
|
throw new IllegalStateException("Failed to delete file " + toFile);
|
|
|
|
return false;
|
|
}
|
|
|
|
} catch (Exception e) {
|
|
String msg = MessageFormat.format("Failed to copy path from {0} to + {1} due to:", fromFile, toFile);
|
|
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 static boolean move(File fromFile, File toFile) {
|
|
|
|
if (fromFile.renameTo(toFile)) {
|
|
return true;
|
|
}
|
|
|
|
FileHelper.logger.warn("Simple File.renameTo failed, trying copy/delete...");
|
|
|
|
// delete if copy was successful, otherwise move will fail
|
|
if (FileHelper.copy(fromFile, toFile, true)) {
|
|
FileHelper.logger.info("Deleting fromFile: " + fromFile.getAbsolutePath());
|
|
return fromFile.delete();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 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 static String humanizeFileSize(File file) {
|
|
return 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
|
|
*/
|
|
public static String humanizeFileSize(long fileSize) {
|
|
return DataUnit.Bytes.humanizeBytesValue(fileSize);
|
|
}
|
|
|
|
/**
|
|
* Creates the SHA256 hash of the given file, returning the hash as a byte array. Use
|
|
* {@link StringHelper#toHexString(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");
|
|
}
|
|
|
|
/**
|
|
* Creates the hash of the given file with the given algorithm, returning the hash as a byte array. Use
|
|
* {@link StringHelper#toHexString(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 = Files.newInputStream(file.toPath())) {
|
|
|
|
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());
|
|
}
|
|
}
|
|
}
|