io.bdeploy.common.util.PathHelper Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of api Show documentation
Show all versions of api Show documentation
Public API including dependencies, ready to be used for integrations and plugins.
package io.bdeploy.common.util;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.PosixFileAttributeView;
import java.util.Comparator;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Stream;
import com.j256.simplemagic.ContentInfo;
import com.j256.simplemagic.ContentInfoUtil;
/**
* Helps in handling different {@link String}s in the context of {@link Path}s.
*/
public class PathHelper {
private static final ContentInfoUtil CIU = loadCIU();
private static ContentInfoUtil loadCIU() {
try (InputStreamReader rdr = new InputStreamReader(PathHelper.class.getClassLoader().getResourceAsStream("bdeploy-magic"),
StandardCharsets.UTF_8)) {
return new ContentInfoUtil(rdr);
} catch (IOException e) {
throw new IllegalStateException("ERROR: Cannot load magic resource", e);
}
}
private PathHelper() {
}
/**
* Returns whether or not the given directory is empty. A non-existing directory is assumed to be empty.
*/
public static boolean isDirEmpty(Path path) {
if (!exists(path)) {
return true;
}
try (DirectoryStream dirStream = Files.newDirectoryStream(path)) {
return !dirStream.iterator().hasNext();
} catch (IOException ioe) {
return false;
}
}
/**
* Converts the given string into a path object.
*
* @param path
* the path to convert.
* @return the path or {@code null} if the input was null or empty
*/
public static Path ofNullableStrig(String path) {
if (path == null || path.trim().isEmpty()) {
return null;
}
return Paths.get(path);
}
/**
* Tests if the given location can be modified.
*/
public static boolean isReadOnly(Path path) {
try {
PathHelper.mkdirs(path);
Path testFile = path.resolve(UuidHelper.randomId());
Files.newOutputStream(testFile).close();
Files.delete(testFile);
return false;
} catch (Exception ioe) {
return true;
}
}
/**
* Tests if the given location can be modified. Additionally the permissions are checked for consistency. Following conditions
* lead to an exception:
*
* - Directory is read-only but we can write some files in there
* - Directory is writable but we cannot modify existing files
* - Directory is writable but we cannot delete files
*
*
* @param directory directory to check permissions. A new file will be created in there to check for write permissions
* @param existingFile file that is already existing. File is tested if it can be opened for writing. The file must exist if
* not the check is skipped.
* @return {@code true} if the root directory is read-only and {@code false} if files can be created / modified
*/
public static boolean isReadOnly(Path directory, Path existingFile) {
boolean canCreate = true;
boolean canDelete = true;
// Check if we can create a new file
Path testFile = directory.resolve(UuidHelper.randomId());
try {
PathHelper.mkdirs(directory);
Files.newOutputStream(testFile).close();
} catch (Exception ioe) {
canCreate = false;
}
// Check if we can delete the file
if (canCreate) {
try {
Files.delete(testFile);
} catch (Exception ioe) {
canDelete = false;
}
}
// Throw if we can create but not delete files
if (canCreate && !canDelete) {
throw new IllegalStateException("Inconsistent file and folder permissions: Missing permission to delete files.");
}
boolean readOnlyDir = !canCreate;
// Check for consistent permissions if possible
if (exists(existingFile)) {
boolean writable = isWritable(existingFile);
if (readOnlyDir && writable) {
throw new IllegalStateException("Inconsistent file and folder permissions: Missing permission to create files.");
}
if (!readOnlyDir && !writable) {
throw new IllegalStateException("Inconsistent file and folder permissions. Missing permission to modify files.");
}
}
return readOnlyDir;
}
/**
* Tests if the given file can be modified.
*
* Implementation note: {@link Files#isWritable} reports wrong results and cannot be used as replacement. When advanced
* permissions are granted where a user can can 'Create Files/Write Data' but cannot
* 'Create Folders/Append Data' then this JAVA API returns 'true' where in reality trying to open the file for
* writing is not denied. Trying to open new stream for writing does work more reliable and reports the correct result.
*
*/
public static boolean isWritable(Path path) {
try {
Files.newOutputStream(path, StandardOpenOption.WRITE).close();
return true;
} catch (Exception ioe) {
return false;
}
}
/**
* Create directories, wrapping {@link IOException} to {@link IllegalStateException}
*
* @param p {@link Path} to create.
*/
public static void mkdirs(Path p) {
try {
Files.createDirectories(p);
} catch (IOException e) {
throw new IllegalStateException("Cannot create " + p, e);
}
}
/**
* Renames the given file or directory and then attempts to delete it.
*/
public static boolean moveAndDelete(Path source, Path target) {
try {
Files.move(source, target, StandardCopyOption.ATOMIC_MOVE);
} catch (IOException e) {
return false;
}
deleteRecursive(target);
return true;
}
/**
* @param path the {@link Path} to delete recursively.
*/
public static void deleteRecursive(Path path) {
if (!exists(path)) {
return;
}
try (Stream walk = Files.walk(path)) {
walk.map(Path::toFile).sorted(Comparator.reverseOrder()).forEach(File::delete);
} catch (IOException e) {
throw new IllegalStateException("Cannot delete " + path, e);
}
}
/**
* Converts all Windows separators ('\\') to the Unix separator ('/').
*/
public static String separatorsToUnix(Path path) {
return path.toString().replace("\\", "/");
}
/**
* Returns the extension of a file.
*/
public static String getExtension(String file) {
int position = file.lastIndexOf('.');
if (position == -1) {
return "";
}
return file.substring(position + 1);
}
/**
* @param zipFile the ZIP file to open (or create)
* @return a {@link FileSystem} which can be used to access (and modify) the ZIP file.
* @throws IOException
*/
public static FileSystem openZip(Path zipFile) throws IOException {
Map env = new TreeMap<>();
env.put("create", "true");
env.put("useTempFile", Boolean.TRUE);
return FileSystems.newFileSystem(URI.create("jar:" + zipFile.toUri()), env);
}
/**
* @param hint the {@link ContentInfo} to check whether it describes something which should be executable.
* @return whether the file described by the given hint should be executable.
*/
public static boolean isExecutable(ContentInfo hint) {
if (hint == null) {
return false;
}
if (hint.getMimeType() != null) {
// match known mime types.
switch (hint.getMimeType()) {
case "application/x-sharedlib":
case "application/x-executable":
case "application/x-dosexec":
case "application/x-mach-binary":
case "text/x-shellscript":
case "text/x-msdos-batch":
return true;
default:
break;
}
}
if (hint.getName() != null) {
switch (hint.getName()) {
case "ELF":
case "32+":
case "Mach-O":
return true;
default:
break;
}
}
if (hint.getMessage() != null && hint.getMessage().toLowerCase().contains("script text executable")) {
// and additionally all with message containing:
// 'script text executable' -> matches all shebangs (#!...) for scripts
// (this is due to https://github.com/j256/simplemagic/issues/59).
return true;
}
return false;
}
/**
* Determine the content type of the given file. A potentially pre-calculated {@link ContentInfo} will be passed through as-is
* if given.
*
* @param child the path to check
* @param hint the potential pre-calculated hint
* @return a {@link ContentInfo} describing the file.
* @throws IOException
*/
public static ContentInfo getContentInfo(Path child, ContentInfo hint) throws IOException {
// hint might have been calculated already while streaming file.
if (hint == null) {
try (InputStream is = Files.newInputStream(child)) {
hint = CIU.findMatch(is);
}
if (hint == null) {
// just any unknown file type.
return null;
}
}
return hint;
}
public static ContentInfoUtil getContentInfoUtil() {
return CIU;
}
/**
* Wrapper around Files.getFileAttributeView as it behaves differently than documented on JDK 17.
*
* @param path the path to get the view for.
* @return a {@link PosixFileAttributeView} or null
if not available.
* @see "https://github.com/adoptium/adoptium-support/issues/363"
*/
public static PosixFileAttributeView getPosixView(Path path) {
try {
return Files.getFileAttributeView(path, PosixFileAttributeView.class);
} catch (Exception e) {
// JDK 17 throws *undeclared* UnsupportedOperationException.
return null;
}
}
/**
* Files.exists is incredible inefficient. Thus this helper does whatever is performance-wise best to determine if a path
* exists.
*
* @param path the path to check
* @return whether the path exists
*/
public static boolean exists(Path path) {
// we only optimize to toFile() in case of the default file-system.
if (path.getFileSystem() != FileSystems.getDefault()) {
return Files.exists(path);
}
return path.toFile().exists();
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy