summaryrefslogtreecommitdiffstats
path: root/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/UnixPath.java
diff options
context:
space:
mode:
Diffstat (limited to 'node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/UnixPath.java')
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/UnixPath.java350
1 files changed, 350 insertions, 0 deletions
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/UnixPath.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/UnixPath.java
new file mode 100644
index 00000000000..1983e94e6f5
--- /dev/null
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/UnixPath.java
@@ -0,0 +1,350 @@
+// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.node.admin.task.util.file;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.FileAlreadyExistsException;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.NotDirectoryException;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.nio.file.StandardOpenOption;
+import java.nio.file.attribute.FileAttribute;
+import java.nio.file.attribute.FileTime;
+import java.nio.file.attribute.GroupPrincipal;
+import java.nio.file.attribute.PosixFileAttributeView;
+import java.nio.file.attribute.PosixFilePermission;
+import java.nio.file.attribute.PosixFilePermissions;
+import java.nio.file.attribute.UserPrincipal;
+import java.nio.file.attribute.UserPrincipalLookupService;
+import java.time.Instant;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Stream;
+
+import static com.yahoo.vespa.hosted.node.admin.task.util.file.IOExceptionUtil.ifExists;
+import static com.yahoo.yolean.Exceptions.uncheck;
+
+/**
+ * Thin wrapper around java.nio.file.Path, especially nice for UNIX-specific features.
+ *
+ * @author hakonhall
+ */
+// @Immutable
+public class UnixPath {
+
+ private static final Set<OpenOption> DEFAULT_OPEN_OPTIONS =
+ Set.of(StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE);
+
+ private final Path path;
+
+ public UnixPath(Path path) { this.path = path; }
+ public UnixPath(String path) { this(Path.of(path)); }
+
+ public Path toPath() { return path; }
+ public UnixPath resolve(String relativeOrAbsolutePath) { return new UnixPath(path.resolve(relativeOrAbsolutePath)); }
+
+ public UnixPath getParent() {
+ Path parentPath = path.getParent();
+ if (parentPath == null) {
+ throw new IllegalStateException("Path has no parent directory: '" + path + "'");
+ }
+
+ return new UnixPath(parentPath);
+ }
+
+ public String getFilename() {
+ Path filename = path.getFileName();
+ if (filename == null) {
+ // E.g. "/".
+ throw new IllegalStateException("Path has no filename: '" + path + "'");
+ }
+
+ return filename.toString();
+ }
+
+ public boolean exists() { return Files.exists(path); }
+
+ public String readUtf8File() {
+ return new String(readBytes(), StandardCharsets.UTF_8);
+ }
+
+ public Optional<String> readUtf8FileIfExists() {
+ try {
+ return Optional.of(Files.readString(path));
+ } catch (NoSuchFileException ignored) {
+ return Optional.empty();
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ public byte[] readBytes() {
+ return uncheck(() -> Files.readAllBytes(path));
+ }
+
+ /** Reads and returns all bytes contained in this path, if any such path exists. */
+ public Optional<byte[]> readBytesIfExists() {
+ try {
+ return Optional.of(Files.readAllBytes(path));
+ } catch (NoSuchFileException ignored) {
+ return Optional.empty();
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ public List<String> readLines() {
+ return uncheck(() -> Files.readAllLines(path));
+ }
+
+ /** Create an empty file and return true, or false if the file already exists (the file may not be regular). */
+ public boolean create() {
+ try {
+ Files.createFile(path);
+ return true;
+ } catch (FileAlreadyExistsException ignored) {
+ return false;
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ public UnixPath writeUtf8File(String content, OpenOption... options) {
+ return writeBytes(content.getBytes(StandardCharsets.UTF_8), options);
+ }
+
+ public UnixPath writeUtf8File(String content, String permissions, OpenOption... options) {
+ return writeBytes(content.getBytes(StandardCharsets.UTF_8), permissions, options);
+ }
+
+ public UnixPath writeBytes(byte[] content, OpenOption... options) {
+ return writeBytes(content, null, options);
+ }
+
+ public UnixPath writeBytes(byte[] content, String permissions, OpenOption... options) {
+ FileAttribute<?>[] attributes = Optional.ofNullable(permissions)
+ .map(this::permissionsAsFileAttributes)
+ .orElseGet(() -> new FileAttribute<?>[0]);
+
+ Set<OpenOption> optionsSet = options.length == 0 ? DEFAULT_OPEN_OPTIONS : Set.of(options);
+
+ try (SeekableByteChannel channel = Files.newByteChannel(path, optionsSet, attributes)) {
+ channel.write(ByteBuffer.wrap(content));
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ return this;
+ }
+
+ /** Write a file to the same dir as this, and then atomically move it to this' path. */
+ public UnixPath atomicWriteBytes(byte[] content) {
+ UnixPath temporaryPath = getParent().resolve(getFilename() + ".10Ia2f4N5");
+ temporaryPath.writeBytes(content);
+ temporaryPath.atomicMove(path);
+ return this;
+ }
+
+ public String getPermissions() {
+ return getAttributes().permissions();
+ }
+
+ /**
+ * @param permissions Example: "rwxr-x---" means rwx for owner, rx for group,
+ * and no permissions for others.
+ */
+ public UnixPath setPermissions(String permissions) {
+ Set<PosixFilePermission> permissionSet = getPosixFilePermissionsFromString(permissions);
+ uncheck(() -> Files.setPosixFilePermissions(path, permissionSet));
+ return this;
+ }
+
+ public int getOwnerId() {
+ return getAttributes().ownerId();
+ }
+
+ public UnixPath setOwner(String user) { return setOwner(user, "user"); }
+ public UnixPath setOwnerId(int uid) { return setOwner(String.valueOf(uid), "uid"); }
+ private UnixPath setOwner(String owner, String type) {
+ UserPrincipalLookupService service = path.getFileSystem().getUserPrincipalLookupService();
+ UserPrincipal principal = uncheck(
+ () -> service.lookupPrincipalByName(owner),
+ "While looking up %s %s", type, owner);
+ uncheck(() -> Files.setOwner(path, principal));
+ return this;
+ }
+
+ public int getGroupId() {
+ return getAttributes().groupId();
+ }
+
+ public UnixPath setGroup(String group) { return setGroup(group, "group"); }
+ public UnixPath setGroupId(int gid) { return setGroup(String.valueOf(gid), "gid"); }
+ private UnixPath setGroup(String group, String type) {
+ UserPrincipalLookupService service = path.getFileSystem().getUserPrincipalLookupService();
+ GroupPrincipal principal = uncheck(
+ () -> service.lookupPrincipalByGroupName(group),
+ "While looking up group %s %s", type, group);
+ uncheck(() -> Files.getFileAttributeView(path, PosixFileAttributeView.class).setGroup(principal));
+ return this;
+ }
+
+ public Instant getLastModifiedTime() {
+ return getAttributes().lastModifiedTime();
+ }
+
+ public UnixPath updateLastModifiedTime() {
+ return setLastModifiedTime(Instant.now());
+ }
+
+ public UnixPath setLastModifiedTime(Instant instant) {
+ uncheck(() -> Files.setLastModifiedTime(path, FileTime.from(instant)));
+ return this;
+ }
+
+ public FileAttributes getAttributes() {
+ return uncheck(() -> FileAttributes.fromAttributes(Files.readAttributes(path, "unix:*")));
+ }
+
+ public Optional<FileAttributes> getAttributesIfExists() {
+ return ifExists(this::getAttributes);
+ }
+
+ public UnixPath createNewFile(String... permissions) {
+ uncheck(() -> Files.createFile(path, permissionsAsFileAttributes(permissions)));
+ return this;
+ }
+
+ public UnixPath createParents(String... permissions) {
+ getParent().createDirectories(permissions);
+ return this;
+ }
+
+ /** Create directory with given permissions and return true, or false if it already exists. */
+ public boolean createDirectory(String... permissions) {
+ try {
+ Files.createDirectory(path, permissionsAsFileAttributes(permissions));
+ } catch (FileAlreadyExistsException ignore) {
+ return false;
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ return true;
+ }
+
+ public UnixPath createDirectories(String... permissions) {
+ uncheck(() -> Files.createDirectories(path, permissionsAsFileAttributes(permissions)));
+ return this;
+ }
+
+ /**
+ * Returns whether this path is a directory. Symlinks are followed, so this returns true for symlinks pointing to a
+ * directory.
+ */
+ public boolean isDirectory() {
+ return uncheck(() -> Files.isDirectory(path));
+ }
+
+ /** Returns whether this is a symlink */
+ public boolean isSymbolicLink() {
+ return Files.isSymbolicLink(path);
+ }
+
+ /**
+ * Similar to rm -rf file:
+ * - It's not an error if file doesn't exist
+ * - If file is a directory, it and all content is removed
+ * - For symlinks: Only the symlink is removed, not what the symlink points to
+ */
+ public boolean deleteRecursively() {
+ if (!isSymbolicLink() && isDirectory()) {
+ try (Stream<UnixPath> paths = listContentsOfDirectory()) {
+ paths.forEach(UnixPath::deleteRecursively);
+ }
+ }
+ return uncheck(() -> Files.deleteIfExists(path));
+ }
+
+ public boolean deleteIfExists() {
+ return uncheck(() -> Files.deleteIfExists(path));
+ }
+
+ /** @return false path does not exist, is not a directory, or has at least one entry. */
+ public boolean isEmptyDirectory() {
+ try (var entryStream = Files.list(path)) {
+ return entryStream.findAny().isEmpty();
+ } catch (NotDirectoryException | NoSuchFileException e) {
+ return false;
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ /** Lists the contents of this as a stream. Callers should use try-with to ensure that the stream is closed */
+ public Stream<UnixPath> listContentsOfDirectory() {
+ try {
+ // Avoid the temptation to collect the stream here as collecting a directory with a high number of entries
+ // can quickly lead to out of memory conditions
+ return Files.list(path).map(UnixPath::new);
+ } catch (NoSuchFileException ignored) {
+ return Stream.empty();
+ } catch (IOException e) {
+ throw new UncheckedIOException("Failed to list contents of directory " + path.toAbsolutePath(), e);
+ }
+ }
+
+ /** This path must be on the same file system as the to-path. Returns UnixPath of 'to'. */
+ public UnixPath atomicMove(Path to) {
+ uncheck(() -> Files.move(path, to, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING));
+ return new UnixPath(to);
+ }
+
+ public boolean moveIfExists(Path to) {
+ try {
+ Files.move(path, to);
+ return true;
+ } catch (NoSuchFileException ignored) {
+ return false;
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ /**
+ * Creates a symbolic link from {@code link} to {@code this} (the target)
+ * @param link the path for the symbolic link
+ * @return the path to the symbolic link
+ */
+ public UnixPath createSymbolicLink(Path link) {
+ uncheck(() -> Files.createSymbolicLink(link, path));
+ return new UnixPath(link);
+ }
+
+ @Override
+ public String toString() {
+ return path.toString();
+ }
+
+ private FileAttribute<?>[] permissionsAsFileAttributes(String... permissions) {
+ if (permissions.length == 0) return new FileAttribute<?>[0];
+ if (permissions.length > 1)
+ throw new IllegalArgumentException("Expected permissions to not be set or be a single string");
+
+ return new FileAttribute<?>[]{PosixFilePermissions.asFileAttribute(getPosixFilePermissionsFromString(permissions[0]))};
+ }
+
+ private Set<PosixFilePermission> getPosixFilePermissionsFromString(String permissions) {
+ try {
+ return PosixFilePermissions.fromString(permissions);
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("Failed to set permissions '" +
+ permissions + "' on path " + path, e);
+ }
+ }
+}