From ed1c8b9d3e918dd76f6077abb836ba953958e791 Mon Sep 17 00:00:00 2001 From: JeremyStarTM Date: Sun, 3 Nov 2024 18:40:48 +0100 Subject: [PATCH] Add FileAccess class for filesystem access --- .../de/staropensource/engine/base/Engine.java | 3 +- .../engine/base/type/FileType.java | 55 ++ .../engine/base/utility/FileAccess.java | 869 ++++++++++++++++++ 3 files changed, 926 insertions(+), 1 deletion(-) create mode 100644 base/src/main/java/de/staropensource/engine/base/type/FileType.java create mode 100644 base/src/main/java/de/staropensource/engine/base/utility/FileAccess.java diff --git a/base/src/main/java/de/staropensource/engine/base/Engine.java b/base/src/main/java/de/staropensource/engine/base/Engine.java index 7de413c5..20a19c82 100644 --- a/base/src/main/java/de/staropensource/engine/base/Engine.java +++ b/base/src/main/java/de/staropensource/engine/base/Engine.java @@ -49,6 +49,7 @@ import org.reflections.scanners.Scanners; import org.reflections.util.ClasspathHelper; import org.reflections.util.ConfigurationBuilder; +import java.io.IOException; import java.util.*; /** @@ -257,7 +258,7 @@ public final class Engine extends SubsystemClass { * * @since v1-alpha0 */ - private void initializeClasses() { + private void initializeClasses() throws IOException { LOGGER.verb("Initializing engine classes"); // Initialize essential engine classes diff --git a/base/src/main/java/de/staropensource/engine/base/type/FileType.java b/base/src/main/java/de/staropensource/engine/base/type/FileType.java new file mode 100644 index 00000000..501d60d9 --- /dev/null +++ b/base/src/main/java/de/staropensource/engine/base/type/FileType.java @@ -0,0 +1,55 @@ +/* + * STAROPENSOURCE ENGINE SOURCE FILE + * Copyright (c) 2024 The StarOpenSource Engine Authors + * Licensed under the GNU Affero General Public License v3 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.staropensource.engine.base.type; + +/** + * Represents various file types. + * + * @since v1-alpha8 + */ +public enum FileType { + /** + * The path does not exist. + * + * @since v1-alpha8 + */ + VOID, + + /** + * It's a regular file. + * + * @since v1-alpha8 + */ + FILE, + + /** + * It's a directory containing files. + * + * @since v1-alpha8 + */ + DIRECTORY, + + /** + * The file type is unknown to the sos!engine. + * + * @since v1-alpha8 + */ + UNKNOWN +} diff --git a/base/src/main/java/de/staropensource/engine/base/utility/FileAccess.java b/base/src/main/java/de/staropensource/engine/base/utility/FileAccess.java new file mode 100644 index 00000000..3840acd0 --- /dev/null +++ b/base/src/main/java/de/staropensource/engine/base/utility/FileAccess.java @@ -0,0 +1,869 @@ +/* + * STAROPENSOURCE ENGINE SOURCE FILE + * Copyright (c) 2024 The StarOpenSource Engine Authors + * Licensed under the GNU Affero General Public License v3 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.staropensource.engine.base.utility; + +import de.staropensource.engine.base.Engine; +import de.staropensource.engine.base.logging.LoggerInstance; +import de.staropensource.engine.base.type.EngineState; +import de.staropensource.engine.base.type.FileType; +import lombok.Getter; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.URI; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.*; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.*; +import java.util.stream.Stream; + +/** + * Provides a simplified way of + * accessing files and directories. + * + * @since v1-alpha8 + */ +@Getter +@SuppressWarnings({ "JavadocDeclaration", "unused" }) +public final class FileAccess { + // -----> Static variables + /** + * Contains the {@link LoggerInstance} for this instance. + * + * @see LoggerInstance + * @since v1-alpha8 + */ + private static final @NotNull LoggerInstance LOGGER = new LoggerInstance.Builder().setClazz(FileAccess.class).setOrigin("ENGINE").build(); + + /** + * Contains a list of all files and directories + * which should be deleted at shutdown. + *

+ * While this feature is built into Java, in + * our testing it did not seem to work correctly. + * That's why we're implementing it here. + * + * @since v1-alpha8 + */ + private static @NotNull Path[] scheduledDeletion = new Path[0]; + + /** + * Contains a {@link FileAccess} instance to + * a cache directory provided by the engine. + * + * @since v1-alpha8 + * -- GETTER -- + * Returns a {@link FileAccess} instance to + * a cache directory provided by the engine. + * + * @return cache directory + * @since v1-alpha8 + */ + @Getter + private static FileAccess cacheDirectory; + + // -----> Instance variables + /** + * Contains the {@link LoggerInstance} for this instance. + * + * @see LoggerInstance + * @since v1-alpha8 + */ + private final @NotNull LoggerInstance logger; + + /** + * Contains the {@link Path} to this file or directory. + * + * @since v1-alpha8 + * -- GETTER -- + * Returns the {@link Path} to this file or directory. + *

+ * Please only use this method when you have to. + * Use the methods in this class instead, if you can. + * + * @return associated {@link Path} instance + * @since v1-alpha8 + */ + private final @NotNull Path path; + + /** + * Contains the {@link File} to this file or directory. + * + * @since v1-alpha8 + * -- GETTER -- + * Returns the {@link File} to this file or directory. + *

+ * Please only use this method when you have to. + * Use the methods in this class instead, if you can. + * + * @return associated {@link File} instance + * @since v1-alpha8 + */ + private final @NotNull File file; + + // -----> Constructors + /** + * Creates and initializes an instance of this class. + * + * @throws InvalidPathException if a {@link Path} cannot be created (see {@link FileSystem#getPath(String, String...)}) + * @since v1-alpha8 + */ + public FileAccess(@NotNull String pathString) throws InvalidPathException { + this.path = formatPath(pathString).toAbsolutePath(); + this.file = new File(pathString); + logger = new LoggerInstance.Builder().setClazz(getClass()).setOrigin("ENGINE").setMetadata(path.toString()).build(); + } + + /** + * Creates and initializes an instance of this class. + * + * @since v1-alpha8 + */ + public FileAccess(@NotNull Path path) { + this.path = path.toAbsolutePath(); + this.file = this.path.toFile(); + logger = new LoggerInstance.Builder().setClazz(getClass()).setOrigin("ENGINE").setMetadata(path.toString()).build(); + } + + /** + * Creates and initializes an instance of this class. + * + * @throws InvalidPathException if a {@link Path} cannot be created (see {@link FileSystem#getPath(String, String...)}) + * @since v1-alpha8 + */ + public FileAccess(@NotNull File file) throws InvalidPathException { + this.path = file.toPath().toAbsolutePath(); + this.file = file; + logger = new LoggerInstance.Builder().setClazz(getClass()).setOrigin("ENGINE").setMetadata(path.toString()).build(); + } + + /** + * Creates and initializes an instance of this class. + * + * @throws FileSystemNotFoundException if the filesystem is not supported by Java + * @throws IllegalArgumentException if the URI is improperly formatted + * @since v1-alpha8 + */ + public FileAccess(@NotNull URI uri) throws FileSystemNotFoundException, IllegalArgumentException { + this.path = Path.of(uri); + this.file = new File(uri); + logger = new LoggerInstance.Builder().setClazz(getClass()).setOrigin("ENGINE").setMetadata(path.toString()).build(); + } + + + // -----> Static instance initialization + /** + * Initializes all uninitialized static + * {@link FileAccess} instances. + * + * @since v1-alpha8 + */ + public static void initializeInstances() throws IOException { + if (cacheDirectory == null) { + String temp = System.getProperty("os.name").toLowerCase(Locale.ROOT); + + if (temp.contains("nix") || temp.contains("nux") || temp.contains("aix") || temp.contains("bsd")) + temp = "/tmp/"; + else if (temp.contains("win")) + temp = System.getProperty("user.home").replace("\\", "/") + "/AppData/Local/Temp/"; + else if (temp.contains("mac")) + LOGGER.crash("macOS is not supported yet"); + else + LOGGER.crash("The operating system \"" + temp + "\" is not yet supported by the StarOpenSource Engine"); + + cacheDirectory = new FileAccess(temp + "sosengine-cache-" + ProcessHandle.current().pid()).createDirectory().deleteOnShutdown(); + } + } + /** + * Deletes all files scheduled for deletion. + * Only works during engine shutdown. + * + * @since v1-alpha8 + */ + public static void deleteScheduled() { + if (Engine.getInstance().getState() == EngineState.SHUTDOWN || Engine.getInstance().getState() == EngineState.CRASHED) { + LOGGER.verb("Deleting files scheduled for deletion on shutdown"); + + for (Path path : scheduledDeletion) + try (Stream stream = Files.walk(path)) { + LOGGER.diag("Deleting file or directory \"" + path + "\""); + //noinspection ResultOfMethodCallIgnored + stream.sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete); + + if (Files.exists(path)) + LOGGER.error("Deleting file or directory \"" + path + "\" failed"); + } catch (Exception exception) { + LOGGER.error("File or directory \"" + path + "\" could not be deleted\n" + Miscellaneous.getStackTraceHeader(exception) + "\n" + Miscellaneous.getStackTraceAsString(exception, true)); + } + } + } + + + // -----> Path formatting + /** + * Formats the path into a {@link Path} instance. + * + * @since v1-alpha8 + */ + public static @NotNull Path formatPath(@NotNull String path) { + return Path.of( + path + .replace("/./", "/") + .replace("/", File.separator) + ); + } + + /** + * Unformats the a {@link Path} instance into a string. + * + * @since v1-alpha8 + */ + public static @NotNull String unformatPath(@NotNull Path path) { + return path + .toString() + .replace(File.separator, "/"); + } + + + // -----> File getters & setters + /** + * Returns the absolute path of this file. + * + * @return absolute path + * @since v1-alpha8 + */ + public @NotNull String getPath() { + return unformatPath(path); + } + + /** + * Returns the file name. + * + * @param excludeExtension if to remove the extension (e.g. {@code .txt}, {@code .java}) + * @return file name + * @since v1-alpha8 + */ + public @NotNull String getFileName(boolean excludeExtension) { + if (excludeExtension) + return file.getName().replaceFirst("[.][^.]+$", ""); + else + return file.getName(); + } + + /** + * Returns whether or not this file exists. + * + * @return exists? + * @since v1-alpha8 + */ + public boolean exists() { + return Files.exists(path); + } + + /** + * Returns the type of this file. + * + * @return file type + * @since v1-alpha8 + */ + public @NotNull FileType getType() { + if (!exists()) + return FileType.VOID; + else if (Files.isRegularFile(path)) + return FileType.FILE; + else if (Files.isDirectory(path)) + return FileType.DIRECTORY; + else + return FileType.UNKNOWN; + } + + /** + * Returns whether or not this file is a symbolic link. + * + * @return symbolic link? + * @since v1-alpha8 + */ + public boolean isSymbolicLink() { + return Files.isSymbolicLink(path); + } + + /** + * Returns whether or not the file is hidden. + * + * @return is hidden? + * @throws IOException on an IO error + * @since v1-alpha8 + */ + public boolean isHidden() throws IOException { + return Files.isHidden(path); + } + + /** + * Returns the names of all files and + * directories in this directory. + * + * @return array of file and directory names + * @throws UnsupportedOperationException if the file is not a directory + * @throws IOException on an IO error + * @since v1-alpha8 + */ + public @NotNull String @NotNull [] listContents() throws UnsupportedOperationException, IOException { + if (getType() != FileType.DIRECTORY) + throw new UnsupportedOperationException("The file \"" + path + "\" is not a directory"); + + String[] list = file.list(); + + if (list == null) + throw new IOException("list is null"); + else + return list; + } + + /** + * Returns the destination of the symbolic link. + * + * @return file type + * @throws UnsupportedOperationException if the file is not a symbolic link + * @throws IOException on an IO error + * @since v1-alpha8 + */ + public @NotNull String getLinkDestination() throws IOException { + if (!isSymbolicLink()) + throw new UnsupportedOperationException("The file \"" + path + "\" is not a symbolic link"); + return unformatPath(Files.readSymbolicLink(path)); + } + + // -----> Permissions + /** + * Returns whether or not the file can be read from. + * + * @return can be read? + * @since v1-alpha8 + */ + public boolean isReadable() { + return Files.isReadable(path); + } + + /** + * Returns whether or not the file can be written to. + * + * @return can be written? + * @since v1-alpha8 + */ + public boolean isWritable() { + return Files.isWritable(path); + } + + /** + * Returns whether or not the file can be executed. + * + * @return can be executed? + * @since v1-alpha8 + */ + public boolean isExecutable() { + return Files.isExecutable(path); + } + + /** + * Returns the file's permissions the + * POSIX {@code rwxrwxrwx} format. + * + * @return POSIX permissions format + * @since v1-alpha8 + */ + public @NotNull String getPosixPermissions() throws IOException { + try { + return PosixFilePermissions.toString(Files.getPosixFilePermissions(path)); + } catch (UnsupportedOperationException exception) { + // POSIX permissions are not supported + // For the Macrohard Windoze users under us + StringBuilder output = new StringBuilder(); + + if (isReadable()) + output.append("r"); + if (isWritable()) + output.append("w"); + if (isExecutable()) + output.append("x"); + + // Repeat the same thing two times + output.repeat(output, 2); + + return output.toString(); + } + } + + /** + * Returns the file's permissions the + * POSIX {@code rwxrwxrwx} format. + * + * @param permissions POSIX {@code rwxrwxrwx} permission format + * @return this instance + * @throws IllegalArgumentException if the format of the {@code permissions} argument is invalid + * @throws IOException on an IO error + * @since v1-alpha8 + */ + @SuppressWarnings({ "RegExpSingleCharAlternation", "ResultOfMethodCallIgnored" }) + public @NotNull FileAccess setPosixPermissions(@NotNull String permissions) throws IllegalArgumentException, IOException { + if ( + permissions.length() != 9 + || !permissions.matches("^(r|-)(w|-)(x|-)(r|-)(w|-)(x|-)(r|-)(w|-)(x|-)$") + ) + throw new IllegalArgumentException("Invalid permission format: " + permissions); + + try { + logger.diag("Setting POSIX file permissions for \"" + path + "\" to '" + permissions + "'"); + Files.setPosixFilePermissions(path, PosixFilePermissions.fromString(permissions)); + } catch (UnsupportedOperationException exception) { + logger.diag("Setting POSIX file permissions for \"" + path + "\" to '" + permissions.substring(0, 2) + "'"); + char @NotNull [] chars = permissions.toCharArray(); + + for (int permission = 0; permission < 3; permission++) { + boolean enabled = chars[permission] != '-'; + + switch (permission) { + case 0 -> file.setReadable(enabled); + case 1 -> file.setWritable(enabled); + case 2 -> file.setExecutable(enabled); + } + } + } + + return this; + } + + + // -----> Filesystem information + /** + * Returns the filesystem of this file. + * + * @return filesystem + * @since v1-alpha8 + */ + public @NotNull FileSystem getFilesystem() { + return path.getFileSystem(); + } + /** + * Returns whether or not the filesystem is POSIX-compliant. + * + * @return POSIX compliant? + * @since v1-alpha8 + */ + public boolean isFilesystemPosixCompliant() { + return path.getFileSystem().supportedFileAttributeViews().contains("posix"); + } + + /** + * Returns all forbidden file names. + *

+ * The required functionality is not yet + * implemented. As such, this method + * will just return an empty array. + * + * @return forbidden file names + * @since v1-alpha8 + */ + @ApiStatus.Experimental + public @NotNull String @NotNull [] getRestrictedFileNames() { + return new String[0]; + } + + /** + * Returns all forbidden characters in file names. + *

+ * The required functionality is not yet + * implemented. As such, this method + * will just return an empty array. + * + * @return forbidden characters + * @since v1-alpha8 + */ + @ApiStatus.Experimental + public char @NotNull [] getRestrictedCharacters() { + return new char[0]; + } + + + // -----> Directory traversal + /** + * Returns the parent directory. + * + * @return new {@link FileAccess} instance to the parent directory + * @since v1-alpha8 + */ + public @NotNull FileAccess parent() { + return new FileAccess(path.getParent()); + } + + /** + * Traverses through directories and files. + * + * @param path path to traverse + * @return new {@link FileAccess} instance + * @since v1-alpha8 + */ + public @NotNull FileAccess traverse(@NotNull String path) { + return new FileAccess(this.path.resolve(formatPath(path))); + } + + /** + * Traverses through directories and files. + * + * @param path path to traverse + * @return new {@link FileAccess} instance + * @throws FileNotFoundException if the specified path does not exist + * @since v1-alpha8 + */ + public @NotNull FileAccess traverseIfExists(@NotNull String path) throws FileNotFoundException { + Path pathResolved = this.path.resolve(formatPath(path)); + + if (!Files.exists(pathResolved)) + throw new FileNotFoundException("Traversal failed as relative path \"" + path + "\" does not exist from absolute path \"" + path + "\""); + + return new FileAccess(pathResolved); + } + + + // -----> File/Directory creation and deletion + /** + * Deletes the file. + * If it doesn't exist, nothing will be done. + * + * @return this instance + * @since v1-alpha8 + */ + public @NotNull FileAccess delete() { + if (exists()) { + logger.diag("Deleting \"" + path + "\""); + //noinspection ResultOfMethodCallIgnored + file.delete(); + } + return this; + } + + /** + * Marks this file for deletion at engine shutdown. + * + * @return this instance + * @see Engine#shutdown() + * @see Engine#shutdown(int) + * @since v1-alpha8 + */ + public @NotNull FileAccess deleteOnShutdown() { + logger.diag("Marking \"" + path + "\" for deletion at engine shutdown"); + + // Append path to scheduledDeletion array + List<@NotNull Path> scheduledDeletionList = new ArrayList<>(Arrays.stream(scheduledDeletion).toList()); + scheduledDeletionList.add(path); + scheduledDeletion = scheduledDeletionList.toArray(new Path[0]); + + return this; + } + + /** + * Creates the file. + * If it already exists, nothing will be done. + * + * @return this instance + * @throws IOException on an IO error + * @since v1-alpha8 + */ + @SuppressWarnings({"UnusedReturnValue", "ResultOfMethodCallIgnored"}) + public @NotNull FileAccess createFile() throws IOException { + if (!exists()) { + logger.diag("Creating a file at \"" + path + "\""); + file.getParentFile().mkdirs(); + file.createNewFile(); + } + + return this; + } + + /** + * Creates the directory recursively. + * If it already exists, nothing will be done. + * + * @return this instance + * @throws IOException on an IO error + * @since v1-alpha8 + */ + public @NotNull FileAccess createDirectory() throws IOException { + if (!exists()) { + logger.diag("Creating a directory at \"" + path + "\""); + if (!file.mkdirs()) + throw new IOException("Creating directory \"" + path + "\" recursively failed"); + } + + return this; + } + + /** + * Creates a symbolic link at this location. + * If it already exists, nothing will be done. + * + * @param hard creates a hard link if {@code true} or a symbolic link if {@code false} + * @return this instance + * @throws IOException on an IO error + * @since v1-alpha8 + */ + @SuppressWarnings("UnusedReturnValue") + public @NotNull FileAccess createLink(boolean hard, @NotNull String destination) throws IOException { + if (!exists()) { + logger.diag("Creating a " + (hard ? "hard" : "symbolic") + " link at \"" + path + "\""); + + if (hard) + Files.createLink(path, formatPath(destination)); + else + Files.createSymbolicLink(path, formatPath(destination)); + } + + return this; + } + + + // -----> File locking + /** + * Returns whether or not this file is locked. + * + * @return is locked? + * @since v1-alpha8 + */ + public boolean isLocked() { + return false; + } + + /** + * Locks this file. + * + * @return this instance + * @since v1-alpha8 + */ + public @NotNull FileAccess lock() { + return this; + } + + /** + * Unlocks this file. + * + * @return this instance + * @since v1-alpha8 + */ + public @NotNull FileAccess unlock() { + return this; + } + + + // -----> Content reading + /** + * Returns the contents of this file. + *

+ * Returns an empty array if this file + * is not of type {@link FileType#FILE}. + * + * @return file contents in bytes + * @throws IOException on an IO error + * @throws OutOfMemoryError if the file is larger than the allocated amount of memory + * @since v1-alpha8 + */ + public byte @NotNull [] readBytes() throws IOException, OutOfMemoryError { + if (getType() != FileType.FILE) + return new byte[0]; + + logger.diag("Reading file \"" + path + "\" (bytes)"); + return Files.readAllBytes(path); + } + + /** + * Returns the contents of this file. + *

+ * Returns an empty list if this file + * is not of type {@link FileType#FILE}. + * + * @return file contents in bytes + * @throws IOException on an IO error + * @throws OutOfMemoryError if the file is larger than the allocated amount of memory + * @since v1-alpha8 + */ + public @NotNull List<@NotNull String> readLines() throws IOException, OutOfMemoryError { + if (getType() != FileType.FILE) + return new ArrayList<>(); + + logger.diag("Reading file \"" + path + "\" (lines)"); + return Files.readAllLines(path); + } + + /** + * Returns the contents of this file. + * This method will decode the bytes using the + * {@link StandardCharsets#UTF_8} character set. + *

+ * Returns an empty string if this file + * is not of type {@link FileType#FILE}. + * + * @return file contents as a string + * @throws IOException on an IO error + * @throws OutOfMemoryError if the file is larger than the allocated amount of memory + * @since v1-alpha8 + */ + public @NotNull String readContent() throws IOException, OutOfMemoryError { + return readContent(StandardCharsets.UTF_8); + } + + /** + * Returns the contents of this file. + *

+ * Returns an empty string if this file + * is not of type {@link FileType#FILE}. + * + * @param charset charset to decode the bytes with + * @return file contents as a string + * @throws IOException on an IO error + * @throws OutOfMemoryError if the file is larger than the allocated amount of memory + * @since v1-alpha8 + */ + public @NotNull String readContent(@NotNull Charset charset) throws IOException, OutOfMemoryError { + if (getType() != FileType.FILE) + return ""; + + logger.diag("Reading file \"" + path + "\" (string)"); + return Files.readString(path, charset); + } + + + // -----> Content writing + /** + * Writes the specified bytes into this file. + * + * @param bytes bytes to write + * @param async allows the operating system to decide when to flush the file to disk if {@code true}, flushes the data to disk immediately if {@code false} + * @throws UnsupportedOperationException if the type of this file is neither {@link FileType#VOID} or {@link FileType#FILE} + * @throws IOException on an IO error + * @return this instance + */ + public @NotNull FileAccess writeBytes(byte @NotNull [] bytes, boolean async) throws UnsupportedOperationException, IOException { + if (getType() == FileType.VOID) + createFile(); + else if (getType() != FileType.FILE) + throw new UnsupportedOperationException("File \"" + path + "\" is not of type FileType.VOID or FileType.FILE"); + + createFile(); + logger.diag("Writing file \"" + path + "\" (bytes, " + (async ? "async" : "dsync") + ")"); + Files.write(path, bytes, StandardOpenOption.WRITE, async ? StandardOpenOption.DSYNC : StandardOpenOption.SYNC); + return this; + } + + /** + * Writes the specified bytes into this file. + * This method will encode the string using the + * {@link StandardCharsets#UTF_8} character set. + * + * @param string string to write + * @param async allows the operating system to decide when to flush the file to disk if {@code true}, flushes the data to disk immediately if {@code false} + * @throws UnsupportedOperationException if the type of this file is neither {@link FileType#VOID} or {@link FileType#FILE} + * @throws IOException on an IO error + * @return this instance + */ + public @NotNull FileAccess writeString(@NotNull String string, boolean async) throws UnsupportedOperationException, IOException { + return writeString(string, StandardCharsets.UTF_8, async); + } + + /** + * Writes the specified bytes into this file. + * + * @param string string to write + * @param charset charset to encode the string in + * @param async allows the operating system to decide when to flush the file to disk if {@code true}, flushes the data to disk immediately if {@code false} + * @throws UnsupportedOperationException if the type of this file is neither {@link FileType#VOID} or {@link FileType#FILE} + * @throws IOException on an IO error + * @return this instance + */ + public @NotNull FileAccess writeString(@NotNull String string, @NotNull Charset charset, boolean async) throws UnsupportedOperationException, IOException { + if (getType() == FileType.VOID) + createFile(); + else if (getType() != FileType.FILE) + throw new UnsupportedOperationException("File \"" + path + "\" is not of type FileType.VOID or FileType.FILE"); + + logger.diag("Writing file \"" + path + "\" (string, " + (async ? "async" : "dsync") + ")"); + Files.writeString(path, string, charset, StandardOpenOption.WRITE, async ? StandardOpenOption.DSYNC : StandardOpenOption.SYNC); + return this; + } + + + // -----> Content appending + /** + * Appends the specified bytes to this file. + * + * @param bytes bytes to append + * @param async allows the operating system to decide when to flush the file to disk if {@code true}, flushes the data to disk immediately if {@code false} + * @throws UnsupportedOperationException if the type of this file is neither {@link FileType#VOID} or {@link FileType#FILE} + * @throws IOException on an IO error + * @return this instance + */ + public @NotNull FileAccess appendBytes(byte @NotNull [] bytes, boolean async) throws UnsupportedOperationException, IOException { + if (getType() == FileType.VOID) + createFile(); + else if (getType() != FileType.FILE) + throw new UnsupportedOperationException("File \"" + path + "\" is not of type FileType.VOID or FileType.FILE"); + + logger.diag("Appending file \"" + path + "\" (bytes, " + (async ? "async" : "dsync") + ")"); + Files.write(path, bytes, StandardOpenOption.APPEND, async ? StandardOpenOption.DSYNC : StandardOpenOption.SYNC); + return this; + } + + /** + * Appends the specified string to this file. + * This method will encode the string using the + * {@link StandardCharsets#UTF_8} character set. + * + * @param string string to append + * @param async allows the operating system to decide when to flush the file to disk if {@code true}, flushes the data to disk immediately if {@code false} + * @throws UnsupportedOperationException if the type of this file is neither {@link FileType#VOID} or {@link FileType#FILE} + * @throws IOException on an IO error + * @return this instance + */ + public @NotNull FileAccess appendString(@NotNull String string, boolean async) throws UnsupportedOperationException, IOException { + return appendString(string, StandardCharsets.UTF_8, async); + } + + /** + * Appends the specified string to this file. + * + * @param string string to append + * @param charset charset to encode the string in + * @param async allows the operating system to decide when to flush the file to disk if {@code true}, flushes the data to disk immediately if {@code false} + * @throws UnsupportedOperationException if the type of this file is neither {@link FileType#VOID} or {@link FileType#FILE} + * @throws IOException on an IO error + * @return this instance + */ + public @NotNull FileAccess appendString(@NotNull String string, @NotNull Charset charset, boolean async) throws UnsupportedOperationException, IOException { + if (getType() == FileType.VOID) + createFile(); + else if (getType() != FileType.FILE) + throw new UnsupportedOperationException("File \"" + path + "\" is not of type FileType.VOID or FileType.FILE"); + + logger.diag("Appending file \"" + path + "\" (string, " + (async ? "async" : "dsync") + ")"); + Files.writeString(path, string, charset, StandardOpenOption.APPEND, async ? StandardOpenOption.DSYNC : StandardOpenOption.SYNC); + return this; + } +}