All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.bdeploy.common.util.PathHelper Maven / Gradle / Ivy

Go to download

Public API including dependencies, ready to be used for integrations and plugins.

There is a newer version: 7.3.6
Show newest version
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