From 8393818043c3bf8c3b48d93b4affe585423cfc92 Mon Sep 17 00:00:00 2001 From: JeremyStarTM Date: Sun, 15 Dec 2024 01:15:50 +0100 Subject: [PATCH] Add FileAccess class with many exceptions The FileAccess class is an almost 1:1 rewrite of the old FileAccess class from v1-alpha9, just with some method names changed, a set of "verify" methods, no setPosixPermissions method anymore and wrapper exceptions around Java exceptions to avoid direct contact with Java stuff for public API. See the NonKotlinContact annotation for more information. The old FileAccess class (for reference): https://git.staropensource.de/StarOpenSource/Engine/src/commit/1e978e314687109fefa4a0966d772bb0facac338/base/src/main/java/de/staropensource/engine/base/utility/FileAccess.java --- .../de/staropensource/engine/base/Engine.kt | 3 + .../FileOrDirectoryNotFoundException.kt | 29 + .../base/exception/FileTooLargeException.kt | 30 + .../base/exception/IOAccessException.kt | 28 + .../exception/VerificationFailedException.kt | 28 + .../engine/base/exception/package-info.kt | 26 + .../engine/base/utility/FileAccess.kt | 1394 +++++++++++++++++ dist/detekt.yml | 8 + 8 files changed, 1546 insertions(+) create mode 100644 base/src/main/kotlin/de/staropensource/engine/base/exception/FileOrDirectoryNotFoundException.kt create mode 100644 base/src/main/kotlin/de/staropensource/engine/base/exception/FileTooLargeException.kt create mode 100644 base/src/main/kotlin/de/staropensource/engine/base/exception/IOAccessException.kt create mode 100644 base/src/main/kotlin/de/staropensource/engine/base/exception/VerificationFailedException.kt create mode 100644 base/src/main/kotlin/de/staropensource/engine/base/exception/package-info.kt create mode 100644 base/src/main/kotlin/de/staropensource/engine/base/utility/FileAccess.kt diff --git a/base/src/main/kotlin/de/staropensource/engine/base/Engine.kt b/base/src/main/kotlin/de/staropensource/engine/base/Engine.kt index b8241b5..4ae9368 100644 --- a/base/src/main/kotlin/de/staropensource/engine/base/Engine.kt +++ b/base/src/main/kotlin/de/staropensource/engine/base/Engine.kt @@ -21,6 +21,7 @@ package de.staropensource.engine.base import de.staropensource.engine.base.utility.Environment +import de.staropensource.engine.base.utility.FileAccess import de.staropensource.engine.logging.Logger /** @@ -120,6 +121,7 @@ class Engine private constructor() { // Run initialization code Environment.detect() + FileAccess.updateDefaultPaths() state = State.INITIALIZED } @@ -142,6 +144,7 @@ class Engine private constructor() { // Run shutdown code Environment.unset() + FileAccess.unsetDefaultPaths() state = State.SHUT_DOWN } diff --git a/base/src/main/kotlin/de/staropensource/engine/base/exception/FileOrDirectoryNotFoundException.kt b/base/src/main/kotlin/de/staropensource/engine/base/exception/FileOrDirectoryNotFoundException.kt new file mode 100644 index 0000000..c73c110 --- /dev/null +++ b/base/src/main/kotlin/de/staropensource/engine/base/exception/FileOrDirectoryNotFoundException.kt @@ -0,0 +1,29 @@ +/* + * STAROPENSOURCE ENGINE SOURCE FILE + * Copyright (c) 2024 The StarOpenSource Engine Authors + * Licensed under the GNU Affero General Public License v3 + * with an exception allowing classpath linking. + * + * 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.exception + +/** + * Thrown when being unable to + * find a file or directory. + * + * @since v1-alpha10 + */ +class FileOrDirectoryNotFoundException(val path: String, val throwable: Throwable? = null) : RuntimeException("The file or directory '${path}' could not be found", throwable) diff --git a/base/src/main/kotlin/de/staropensource/engine/base/exception/FileTooLargeException.kt b/base/src/main/kotlin/de/staropensource/engine/base/exception/FileTooLargeException.kt new file mode 100644 index 0000000..ec82362 --- /dev/null +++ b/base/src/main/kotlin/de/staropensource/engine/base/exception/FileTooLargeException.kt @@ -0,0 +1,30 @@ +/* + * STAROPENSOURCE ENGINE SOURCE FILE + * Copyright (c) 2024 The StarOpenSource Engine Authors + * Licensed under the GNU Affero General Public License v3 + * with an exception allowing classpath linking. + * + * 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.exception + +/** + * Thrown when reading a file fails + * due to it being larger than the + * configured max heap size. + * + * @since v1-alpha10 + */ +class FileTooLargeException(val path: String, val throwable: Throwable? = null) : RuntimeException("Unable to read file '${path}' as it is larger than the configured heap size", throwable) diff --git a/base/src/main/kotlin/de/staropensource/engine/base/exception/IOAccessException.kt b/base/src/main/kotlin/de/staropensource/engine/base/exception/IOAccessException.kt new file mode 100644 index 0000000..615f38c --- /dev/null +++ b/base/src/main/kotlin/de/staropensource/engine/base/exception/IOAccessException.kt @@ -0,0 +1,28 @@ +/* + * STAROPENSOURCE ENGINE SOURCE FILE + * Copyright (c) 2024 The StarOpenSource Engine Authors + * Licensed under the GNU Affero General Public License v3 + * with an exception allowing classpath linking. + * + * 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.exception + +/** + * Thrown when an IO error occurs. + * + * @since v1-alpha10 + */ +class IOAccessException(val error: String? = null, val throwable: Throwable? = null) : RuntimeException(error, throwable) diff --git a/base/src/main/kotlin/de/staropensource/engine/base/exception/VerificationFailedException.kt b/base/src/main/kotlin/de/staropensource/engine/base/exception/VerificationFailedException.kt new file mode 100644 index 0000000..2518859 --- /dev/null +++ b/base/src/main/kotlin/de/staropensource/engine/base/exception/VerificationFailedException.kt @@ -0,0 +1,28 @@ +/* + * STAROPENSOURCE ENGINE SOURCE FILE + * Copyright (c) 2024 The StarOpenSource Engine Authors + * Licensed under the GNU Affero General Public License v3 + * with an exception allowing classpath linking. + * + * 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.exception + +/** + * Thrown when a verification fails. + * + * @since v1-alpha10 + */ +class VerificationFailedException(val error: String? = null, val throwable: Throwable? = null) : RuntimeException(error, throwable) diff --git a/base/src/main/kotlin/de/staropensource/engine/base/exception/package-info.kt b/base/src/main/kotlin/de/staropensource/engine/base/exception/package-info.kt new file mode 100644 index 0000000..e4d0585 --- /dev/null +++ b/base/src/main/kotlin/de/staropensource/engine/base/exception/package-info.kt @@ -0,0 +1,26 @@ +/* + * STAROPENSOURCE ENGINE SOURCE FILE + * Copyright (c) 2024 The StarOpenSource Engine Authors + * Licensed under the GNU Affero General Public License v3 + * with an exception allowing classpath linking. + * + * 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 . + */ + +/** + * Exceptions thrown by the engine. + * + * @since v1-alpha10 + */ +package de.staropensource.engine.base.exception diff --git a/base/src/main/kotlin/de/staropensource/engine/base/utility/FileAccess.kt b/base/src/main/kotlin/de/staropensource/engine/base/utility/FileAccess.kt new file mode 100644 index 0000000..1aa8153 --- /dev/null +++ b/base/src/main/kotlin/de/staropensource/engine/base/utility/FileAccess.kt @@ -0,0 +1,1394 @@ +/* + * STAROPENSOURCE ENGINE SOURCE FILE + * Copyright (c) 2024 The StarOpenSource Engine Authors + * Licensed under the GNU Affero General Public License v3 + * with an exception allowing classpath linking. + * + * 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.Companion.logger +import de.staropensource.engine.base.annotation.NonKotlinContact +import de.staropensource.engine.base.exception.FileOrDirectoryNotFoundException +import de.staropensource.engine.base.exception.FileTooLargeException +import de.staropensource.engine.base.exception.IOAccessException +import de.staropensource.engine.base.exception.VerificationFailedException +import de.staropensource.engine.base.utility.Environment.OperatingSystem.* +import de.staropensource.engine.base.utility.FileAccess.Companion.configDirectory +import de.staropensource.engine.base.utility.FileAccess.Companion.format +import java.io.File +import java.io.IOException +import java.nio.file.* +import java.nio.file.attribute.BasicFileAttributes +import java.nio.file.attribute.PosixFilePermissions + +/** + * Provides a simplified way of + * accessing files and directories. + * + * @since v1-alpha10 + */ +@Suppress("unused") +class FileAccess { + /** + * Companion object of [FileAccess]. + * + * @since v1-alpha10 + */ + companion object { + // -----> Default paths + /** + * [Path]s which have been scheduled + * for deletion at engine shutdown. + * + * @since v1-alpha10 + */ + @JvmStatic + private var scheduledDeletion: MutableList = mutableListOf() + + /** + * [FileAccess] instance for the temporary + * cache directory. It is created by the + * engine at startup and is pruned at + * engine shutdown. + * + * @since v1-alpha10 + */ + @JvmStatic + var temporaryCacheDirectory: FileAccess? = null + private set + + /** + * [FileAccess] instance for the persistent + * cache directory of the system. + * + * The directory in this variable will + * not be pruned unless the user decides + * to do so. This may involve running the + * system's cleanup tools or the user + * manually finding and deleting the cache. + * + * @since v1-alpha10 + */ + @JvmStatic + var persistentCacheDirectory: FileAccess? = null + private set + + /** + * [FileAccess] instance to the + * user's home directory. + * + * @since v1-alpha10 + */ + @JvmStatic + var homeDirectory: FileAccess? = null + private set + + /** + * [FileAccess] instance to the directory + * in which applications can store their + * configuration files. + * + * @since v1-alpha10 + */ + @JvmStatic + var configDirectory: FileAccess? = null + private set + + /** + * [FileAccess] instance to the directory + * in which applications can store their + * data files. + * + * For storing configuration files, + * see [configDirectory] instead. + * + * @since v1-alpha10 + */ + @JvmStatic + var dataDirectory: FileAccess? = null + private set + + /** + * Unsets all default paths. + * + * @since v1-alpha10 + */ + internal fun unsetDefaultPaths() { + temporaryCacheDirectory = null + persistentCacheDirectory = null + homeDirectory = null + configDirectory = null + dataDirectory = null + } + + /** + * Updates all default paths to + * their platform-specific path. + * + * @since v1-alpha10 + */ + internal fun updateDefaultPaths() { + logger.diag("Updating default paths") + + // Storage + homeDirectory = FileAccess(System.getProperty("user.home")).createDirectory() + configDirectory = FileAccess(when (Environment.operatingSystem) { + LINUX, FREEBSD, NETBSD, OPENBSD -> "${homeDirectory}/.config" + WINDOWS -> "${homeDirectory}/AppData/Roaming/sosengine-config" + else -> "${homeDirectory}/.sosengine/config" + }).createDirectory() + dataDirectory = FileAccess(when (Environment.operatingSystem) { + LINUX, FREEBSD, NETBSD, OPENBSD -> "${homeDirectory}/.local/share" + WINDOWS -> "${homeDirectory}/AppData/Roaming/sosengine-data" + else -> "${homeDirectory}/.sosengine/data" + }).createDirectory() + // Caches + temporaryCacheDirectory = FileAccess( + System.getProperty("java.io.tmpdir") + + "/sosengine-cache-" + + ProcessHandle.current().pid() + ).createDirectory().deleteOnShutdown() + persistentCacheDirectory = FileAccess(when (Environment.operatingSystem) { + LINUX, FREEBSD, NETBSD, OPENBSD -> "${homeDirectory}/.cache" + WINDOWS -> "${homeDirectory}/AppData/Local/Temp" + else -> "${homeDirectory}/.sosengine/persistent-cache" + }).createDirectory() + } + + + // -----> Maintenance + /** + * Deletes all files and directories + * scheduled for deletion. + * + * @since v1-alpha10 + */ + internal fun deleteScheduled() { + logger.verb("Deleting files scheduled for deletion") + + for (path: Path in scheduledDeletion) + try { + Files + .walk(path) + .use { + logger.diag("Deleting file or directory '${unformatFromPath(path)}' scheduled for deletion") + + // Delete all files recursively + // Only applies to directories + it.sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete) + + // Delete file or directory + if (Files.exists(path)) + if (!path.toFile().delete()) + logger.sarn("Unable to delete file or directory '${unformatFromPath(path)}' scheduled for deletion manually") + } + } catch (exception: Exception) { + // TODO add exception printing + logger.sarn("Unable to delete file or directory '${unformatFromPath(path)}' scheduled for deletion.") + } + } + + + // -----> Path formatting + /** + * Formats the specified string path for use + * in conjunction with [Path] and [File]. + * + * @param string string to format + * @return formatted string + * @since v1-alpha10 + */ + fun format(string: String): String { + return string + .replace("\\", "/") + .replace("/./", "/") + .replace("/", File.separator) + } + + /** + * Formats the specified string path + * and returns a matching [Path] instance. + * + * @param string string to format + * @return matching [Path] + * @since v1-alpha10 + */ + fun formatToPath(string: String): @NonKotlinContact Path = Path.of(format(string)) + + /** + * Undoes formatting made by [format] + * and returns a simplified string path. + * + * @param string string path to undo formatting for + * @return unformatted string path + * @since v1-alpha10 + */ + fun unformat(string: String): String = string.replace(File.separator, "/") + + /** + * Converts and undoes formatting for a [Path] + * instance and returns a simplified string path. + * + * @param path [Path] to undo formatting for + * @return unformatted string path + * @since v1-alpha10 + */ + fun unformatFromPath(path: @NonKotlinContact Path): String = unformat(path.toString()) + } + + + // -----> Instance metadata + /** + * Contains the [Path] to the file + * or directory represented by + * this [FileAccess] instance. + * + * Use methods provided by + * [FileAccess] if possible. + * + * @since v1-alpha10 + */ + val path: @NonKotlinContact Path + @JvmName(name = "getJavaPath") + get + + /** + * Contains the [File] to the file + * or directory represented by + * this [FileAccess] instance. + * + * Use methods provided by + * [FileAccess] if possible. + * + * @since v1-alpha10 + */ + private val file: @NonKotlinContact File + @JvmName(name = "getJavaFile") + get + + + // -----> Constructors + /** + * Opens the specified file. + * + * @param path path to the file to open + * @since v1-alpha10 + */ + constructor(path: String) { + this.path = formatToPath(path).toAbsolutePath() + this.file = this.path.toFile() + } + + /** + * Opens the specified file. + * + * @param path path to the file to open + * @since v1-alpha10 + */ + constructor(path: Path) { + this.path = path.toAbsolutePath() + this.file = this.path.toFile() + } + + /** + * Opens the specified file. + * + * @param path path to the file to open + * @throws InvalidPathException if a [Path] cannot be created (see [java.nio.file.FileSystem.getPath]) + * @since v1-alpha10 + */ + constructor(file: File) { + this.path = file.toPath().toAbsolutePath() + this.file = file + } + + + // -----> Getters + /** + * Returns if this file or directory exists. + * + * @return exists? + * @throws IOAccessException on IO error + * @since v1-alpha10 + */ + @Throws(IOAccessException::class) + fun exists(): Boolean { + return Files.exists(path) + } + + /** + * Returns the type of this file or directory. + * + * @return type + * @throws IOAccessException on IO error + * @since v1-alpha10 + */ + @Throws(IOAccessException::class) + fun getType(): Type { + return if (!exists()) Type.VOID + else if (Files.isRegularFile(path)) Type.FILE + else if (Files.isDirectory(path)) Type.DIRECTORY + else Type.UNKNOWN + } + + /** + * Returns if this file or + * directory is a symbolic link. + * + * @return is a symbolic link? + * @throws IOAccessException on IO error + * @since v1-alpha10 + */ + @Throws(IOAccessException::class) + fun isSymbolicLink(): Boolean { + try { + return Files.isSymbolicLink(path) + } catch (exception: Exception) { + throw IOAccessException("Checking if '${unformatFromPath(path)}' is a symbolic link failed", exception) + } + } + + /** + * Returns if this file or + * directory is hidden. + * + * @return is hidden? + * @throws IOAccessException on IO error + * @since v1-alpha10 + */ + @Throws(IOAccessException::class) + fun isHidden(): Boolean { + try { + return Files.isHidden(path) + } catch (exception: Exception) { + throw IOAccessException("Checking if '${unformatFromPath(path)}' is hidden failed", exception) + } + } + + /** + * Returns if this file can be read from. + * + * @return readable? + * @throws IOAccessException on IO error + * @since v1-alpha10 + */ + @Throws(IOAccessException::class) + fun isReadable(): Boolean { + try { + return Files.isReadable(path) + } catch (exception: Exception) { + throw IOAccessException("Checking if '${unformatFromPath(path)}' is readable failed", exception) + } + } + + /** + * Returns if this file can be written to. + * + * @return writable? + * @throws IOAccessException on IO error + * @since v1-alpha10 + */ + @Throws(IOAccessException::class) + fun isWritable(): Boolean { + try { + return Files.isWritable(path) + } catch (exception: Exception) { + throw IOAccessException("Checking if '${unformatFromPath(path)}' is writable failed", exception) + } + } + + /** + * Returns if this file can be executed. + * + * @return executable? + * @throws IOAccessException on IO error + * @since v1-alpha10 + */ + @Throws(IOAccessException::class) + fun isExecutable(): Boolean { + try { + return Files.isExecutable(path) + } catch (exception: Exception) { + throw IOAccessException("Checking if '${unformatFromPath(path)}' is executable failed", exception) + } + } + + /** + * Returns the file's permissions + * in the POSIX `rwxrwxrwx` format. + * + * @param fakeOnUnsupported if the permission format shall be faked on platforms not supporting POSIX file permissions. `null` will be returned if set to `false` + * @return file permissions in the POSIX permission format or `false` if unsupported and [fakeOnUnsupported] is `false` + * @throws IOAccessException on IO error + * @since v1-alpha10 + */ + @Throws(IOAccessException::class) + fun getPosixPermissions(fakeOnUnsupported: Boolean = true): String? { + try { + return PosixFilePermissions.toString(Files.getPosixFilePermissions(path)) + } catch (exception: UnsupportedOperationException) { + if (fakeOnUnsupported) { + val builder: StringBuilder = StringBuilder() + + // Add permissions + if (isReadable()) builder.append("r") + if (isWritable()) builder.append("w") + if (isExecutable()) builder.append("x") + + // Repeat two times to match the format + builder.repeat(2) + + return builder.toString() + } else + return null + } catch (exception: Exception) { + throw IOAccessException(exception.message, exception.cause) + } + } + + + // -----> Path getters + /** + * Returns the absolute path + * of this file or directory. + * + * @return absolute path + * @since v1-alpha10 + */ + override fun toString(): String = unformatFromPath(path) + + /** + * Returns the absolute raw + * path as seen by the JVM. + * + * @return absolute raw path + * @since v1-alpha10 + */ + fun toStringRaw(): String = path.toString() + + /** + * Returns the base name of + * this file or directory. + * + * @param excludeExtension if to exclude the file extension (e.g. `.txt`, `.java`, `.kt`, etc.), if found + * @since v1-alpha10 + */ + fun getBaseName(excludeExtension: Boolean = false): String { + return if (excludeExtension) file.name.replaceFirst("[.][^.]+$", "") + else file.name + } + + /** + * Returns the destination of + * this symbolic or hard link. + * + * @return destination or `null` if not a symbolic or hard link + * @throws IOAccessException on IO error + * @since v1-alpha10 + */ + @Throws(IOAccessException::class) + fun getLinkDestination(): FileAccess? { + return try { + FileAccess(Files.readSymbolicLink(path)) + } catch (exception: Exception) { + when (exception) { + is NotLinkException, is UnsupportedOperationException -> null + else -> throw exception + } + } + } + + + // -----> Filesystem getters + /** + * Returns the file system of this file. + * + * @return filesystem + * @since v1-alpha10 + */ + fun getFileSystem(): @NonKotlinContact FileSystem = path.fileSystem + + /** + * Returns if the filesystem this file + * or directory is on is POSIX-compliant. + * + * @return POSIX-compliant? + * @since v1-alpha10 + */ + fun isFilesystemPosixCompliant(): Boolean { + return path.fileSystem.supportedFileAttributeViews().contains("posix") + } + + /** + * Returns a regular expression matching + * invalid file names on the filesystem + * this file or directory is on. + * + * @return regex matching all invalid file names + * @since v1-alpha10 + */ + fun getFilesystemRestrictedNames(): String { + return when (Environment.operatingSystem) { + WINDOWS -> "(?i)\\|/|:|[*]|[?]|\"|<|>|[|]|CON|PRN|AUX|NUL|COM[0-9]|LPT[0-9]" + else -> "/" + } + } + + + // -----> File creation, moving, copying and deletion + /** + * Creates a file at this location. + * + * If something already exists at this + * location, then nothing will be done. + * + * @return this instance + * @throws IOAccessException on IO error + * @see verifyIsFile + * @since v1-alpha10 + */ + @Throws(IOAccessException::class) + fun createFile(): FileAccess { + if (!exists()) + try { + logger.diag("Creating a file at '${unformatFromPath(path)}'") + file.parentFile.mkdirs() + file.createNewFile() + } catch (exception: Exception) { + throw IOAccessException("Unable to create a new file at '${unformatFromPath(path)}'", exception) + } + + return this + } + + /** + * Creates a directory at this location. + * + * If something already exists at this + * location, then nothing will be done. + * + * @return this instance + * @throws IOAccessException on IO error + * @see verifyIsDirectory + * @since v1-alpha10 + */ + @Throws(IOAccessException::class) + fun createDirectory(): FileAccess { + if (!exists()) + try { + logger.diag("Creating a directory at '${unformatFromPath(path)}'") + file.mkdirs() + } catch (exception: Exception) { + throw IOAccessException("Unable to create a new directory at '${unformatFromPath(path)}'", exception) + } + + return this + } + + /** + * Creates a link at this location. + * + * If something already exists at this + * location, then nothing will be done. + * + * @param destination where to link to + * @param hard if the link should be hard (`true`) or symbolic (`false`). For an explanation on the two, see [symlink(7)](https://man7.org/linux/man-pages/man7/symlink.7.html) + * @return this instance + * @throws IOAccessException on IO error + * @see verifyIsLink + * @see verifyIsSymbolicLink + * @see verifyIsHardLink + * @since v1-alpha10 + */ + @Throws(IOAccessException::class) + fun createLink(destination: String, hard: Boolean): FileAccess { + if (!exists()) + try { + logger.diag("Creating a ${if (hard) "hard" else "symbolic"} link at '${unformatFromPath(path)}'") + if (hard) Files.createLink(path, formatToPath(destination)) + else Files.createSymbolicLink(path, formatToPath(destination)) + } catch (exception: Exception) { + throw IOAccessException("Unable to create a new ${if (hard) "hard" else "symbolic"} link at '${unformatFromPath(path)}'", exception) + } + + return this + } + + /** + * Moves this file or directory. + * + * If you intend to rename a file or directory, + * this method handles renaming as well. + * They are 1:1 the same operation, so why + * create a whole new method just for that? + * + * If something already exists at the + * destination location, it will be forcefully + * deleted and the move operation performed. + * + * @param destination location where to move this file or directory to + * @return this instance + * @throws IOAccessException on IO error + * @since v1-alpha10 + */ + @Throws(IOAccessException::class) + fun move(destination: FileAccess): FileAccess { + try { + logger.diag("Moving '${unformatFromPath(path)}' to '${destination}") + Files.move(path, destination.path, StandardCopyOption.REPLACE_EXISTING) + return this + } catch (exception: Exception) { + throw IOAccessException("Unable to move '${unformatFromPath(path)}' to '${destination}'", exception) + } + } + + /** + * Copies this file or directory. + * + * If something already exists at the + * destination location, it will be forcefully + * deleted and the copy operation performed. + * + * @param destination location where to copy this file or directory to + * @return this instance + * @throws IOAccessException on IO error + * @since v1-alpha10 + */ + @Throws(IOAccessException::class) + fun copy(destination: FileAccess): FileAccess { + try { + logger.diag("Copying '${unformatFromPath(path)}' to '${destination}") + + if (file.isDirectory) { + destination.delete() + Files.walkFileTree(path, CopyDirectoryVisitor(path, destination.path)) + } else + Files.copy(path, destination.path, StandardCopyOption.REPLACE_EXISTING) + + return this + } catch (exception: Exception) { + throw IOAccessException("Unable to copy '${unformatFromPath(path)}' to '${destination}'", exception) + } + } + + /** + * Deletes this file or directory. + * + * If nothing exists at this location, + * then nothing will be done. + * + * @return this instance + * @throws IOAccessException on IO error + * @since v1-alpha10 + */ + @Throws(IOAccessException::class) + fun delete(): FileAccess { + if (exists()) + try { + logger.diag("Deleting '${unformatFromPath(path)}'") + + if (file.isDirectory) + Files.walkFileTree(path, DeleteDirectoryVisitor(path)) + + Files.delete(path) + } catch (exception: Exception) { + throw IOAccessException("Unable to delete '${unformatFromPath(path)}'", exception) + } + + return this + } + + /** + * Marks this file for deletion at engine shutdown. + * + * @return this instance + * @see de.staropensource.engine.base.Engine.shutdown + * @since v1-alpha10 + */ + fun deleteOnShutdown(): FileAccess { + logger.diag("Marking '${path}' for deletion at engine shutdown") + scheduledDeletion.add(path) + return this + } + + + // -----> File access + /** + * Returns the contents of this file. + * + * @return file contents in bytes or `null` if not a file + * @throws IOAccessException on IO error + * @throws FileTooLargeException if the file is larger than the allocated amount of memory + * @since v1-alpha10 + */ + @Throws(IOAccessException::class, FileTooLargeException::class) + fun readBytes(): ByteArray? { + try { + if (getType() != Type.FILE) + return null + + logger.diag("Reading from file '${path}' (bytes)") + return Files.readAllBytes(path) + } catch (error: OutOfMemoryError) { + throw FileTooLargeException(toString(), error) + } catch (exception: Exception) { + throw IOAccessException("Unable to read file '${path}' (bytes)", exception) + } + } + + /** + * Returns the contents of this file. + * + * @return file contents in bytes or `null` if not a file + * @throws IOAccessException on IO error + * @throws FileTooLargeException if the file is larger than the allocated amount of memory + * @since v1-alpha10 + */ + @Throws(IOAccessException::class, FileTooLargeException::class) + fun readLines(): List? { + try { + if (getType() != Type.FILE) + return null + + logger.diag("Reading from file '${path}' (lines)") + return Files.readAllLines(path) + } catch (error: OutOfMemoryError) { + throw FileTooLargeException(toString(), error) + } catch (exception: Exception) { + throw IOAccessException("Unable to read file '${path}' (lines)", exception) + } + } + + /** + * Returns the contents of this file. + * + * @param charset character set used for string encoding + * @return file contents in bytes or `null` if not a file + * @throws IOAccessException on IO error + * @throws FileTooLargeException if the file is larger than the allocated amount of memory + * @since v1-alpha10 + */ + @Throws(IOAccessException::class, FileTooLargeException::class) + fun readString(): String? { + try { + if (getType() != Type.FILE) + return null + + logger.diag("Reading from file '${path}' (string)") + return Files.readString(path) + } catch (error: OutOfMemoryError) { + throw FileTooLargeException(toString(), error) + } catch (exception: Exception) { + throw IOAccessException("Unable to read file '${path}' (string)", exception) + } + } + + /** + * Writes the specified bytes into this file. + * + * A file is automatically created, if missing. + * The request is ignored if something exists + * at this location but is not a file. + * + * @param bytes bytes to write + * @param async allows the operating system to decide when to flush the file to disk if `true`, flushes the data to disk immediately if `false` + * @return this instance + * @throws IOAccessException on IO error + * @see verifyIsFile + * @since v1-alpha10 + */ + @Throws(IOAccessException::class) + fun writeBytes(bytes: ByteArray, async: Boolean = false): FileAccess { + try { + if (getType() != Type.FILE) + return this + + logger.diag("Writing to file '${path}' (bytes, ${if (async) "async" else ""})") + createFile() + Files.write(path, bytes, StandardOpenOption.WRITE, if (async) StandardOpenOption.DSYNC else StandardOpenOption.SYNC) + } catch (exception: Exception) { + throw IOAccessException("Unable to read file '${path}' (bytes, ${if (async) "async" else ""})", exception) + } + + return this + } + + /** + * Writes the specified lines into this file. + * + * A file is automatically created, if missing. + * The request is ignored if something exists + * at this location but is not a file. + * + * @param lines lines to write + * @param async allows the operating system to decide when to flush the file to disk if `true`, flushes the data to disk immediately if `false` + * @return this instance + * @throws IOAccessException on IO error + * @see verifyIsFile + * @since v1-alpha10 + */ + @Throws(IOAccessException::class) + fun writeLines(lines: List, async: Boolean = false): FileAccess { + try { + if (getType() != Type.FILE) + return this + + logger.diag("Writing to file '${path}' (lines, ${if (async) "async" else ""})") + createFile() + Files.write(path, lines, StandardOpenOption.WRITE, if (async) StandardOpenOption.DSYNC else StandardOpenOption.SYNC) + } catch (exception: Exception) { + throw IOAccessException("Unable to read file '${path}' (lines, ${if (async) "async" else ""})", exception) + } + + return this + } + + /** + * Writes the specified string into this file. + * + * A file is automatically created, if missing. + * The request is ignored if something exists + * at this location but is not a file. + * + * @param string string to write + * @param async allows the operating system to decide when to flush the file to disk if `true`, flushes the data to disk immediately if `false` + * @return this instance + * @throws IOAccessException on IO error + * @see verifyIsFile + * @since v1-alpha10 + */ + @Throws(IOAccessException::class) + fun writeString(string: String, async: Boolean = false): FileAccess { + try { + if (getType() != Type.FILE) + return this + + logger.diag("Writing to file '${path}' (string, ${if (async) "async" else ""})") + createFile() + Files.writeString(path, string, StandardOpenOption.WRITE, if (async) StandardOpenOption.DSYNC else StandardOpenOption.SYNC) + } catch (exception: Exception) { + throw IOAccessException("Unable to read file '${path}' (string, ${if (async) "async" else ""})", exception) + } + + return this + } + + /** + * Appends the specified bytes into this file. + * + * A file is automatically created, if missing. + * The request is ignored if something exists + * at this location but is not a file. + * + * @param bytes bytes to append + * @param async allows the operating system to decide when to flush the file to disk if `true`, flushes the data to disk immediately if `false` + * @return this instance + * @throws IOAccessException on IO error + * @see verifyIsFile + * @since v1-alpha10 + */ + @Throws(IOAccessException::class) + fun appendBytes(bytes: ByteArray, async: Boolean = false): FileAccess { + try { + if (getType() != Type.FILE) + return this + + logger.diag("Appending to file '${path}' (bytes, ${if (async) "async" else ""})") + createFile() + Files.write(path, bytes, StandardOpenOption.APPEND, if (async) StandardOpenOption.DSYNC else StandardOpenOption.SYNC) + } catch (exception: Exception) { + throw IOAccessException("Unable to read file '${path}' (bytes, ${if (async) "async" else ""})", exception) + } + + return this + } + + /** + * Appends the specified lines into this file. + * + * A file is automatically created, if missing. + * The request is ignored if something exists + * at this location but is not a file. + * + * @param lines lines to append + * @param async allows the operating system to decide when to flush the file to disk if `true`, flushes the data to disk immediately if `false` + * @return this instance + * @throws IOAccessException on IO error + * @see verifyIsFile + * @since v1-alpha10 + */ + @Throws(IOAccessException::class) + fun appendLines(lines: List, async: Boolean = false): FileAccess { + try { + if (getType() != Type.FILE) + return this + + logger.diag("Appending to file '${path}' (lines, ${if (async) "async" else ""})") + createFile() + Files.write(path, lines, StandardOpenOption.APPEND, if (async) StandardOpenOption.DSYNC else StandardOpenOption.SYNC) + } catch (exception: Exception) { + throw IOAccessException("Unable to read file '${path}' (lines, ${if (async) "async" else ""})", exception) + } + + return this + } + + /** + * Appends the specified string into this file. + * + * A file is automatically created, if missing. + * The request is ignored if something exists + * at this location but is not a file. + * + * @param string string to append + * @param async allows the operating system to decide when to flush the file to disk if `true`, flushes the data to disk immediately if `false` + * @return this instance + * @throws IOAccessException on IO error + * @see verifyIsFile + * @since v1-alpha10 + */ + @Throws(IOAccessException::class) + fun appendString(string: String, async: Boolean = false): FileAccess { + try { + if (getType() != Type.FILE) + return this + + logger.diag("Appending to file '${path}' (string, ${if (async) "async" else ""})") + createFile() + Files.writeString(path, string, StandardOpenOption.APPEND, if (async) StandardOpenOption.DSYNC else StandardOpenOption.SYNC) + } catch (exception: Exception) { + throw IOAccessException("Unable to read file '${path}' (string, ${if (async) "async" else ""})", exception) + } + + return this + } + + + // -----> Directory access + /** + * Returns the names of all files and + * directories contained in this directory. + * + * @return array of file and directory names or `null` if not a directory + * @throws IOException on IO error + * @since v1-alpha10 + */ + fun list(): Array? { + if (getType() != Type.DIRECTORY) + return null + + return file.list() + } + + /** + * Returns the names of all files + * contained in this directory. + * + * @return array of file names or `null` if not a directory + * @throws IOException on IO error + * @since v1-alpha10 + */ + fun listFiles(): Array? { + if (getType() != Type.DIRECTORY) + return null + + val listArray: Array? = file.list() + var list: MutableList = mutableListOf() + + if (listArray == null) + throw IOException("list is 'null' (target is a directory)") + + for (item: String in listArray) + if (path.resolve(item).toFile().isFile) + list.add(item) + + return list.toTypedArray() + } + + /** + * Returns the names of all directories + * contained in this directory. + * + * @return array of directory names or `null` if not a directory + * @throws IOException on IO error + * @since v1-alpha10 + */ + fun listDirectories(): Array? { + if (getType() != Type.DIRECTORY) + return null + + val listArray: Array? = file.list() + var list: MutableList = mutableListOf() + + if (listArray == null) + throw IOException("list is 'null' (target is a directory)") + + for (item: String in listArray) + if (path.resolve(item).toFile().isDirectory) + list.add(item) + + return list.toTypedArray() + } + + + // -----> Directory traversal + /** + * Returns the parent directory. + * + * @return [FileAccess] instance to the parent directory + * @since v1-alpha10 + */ + fun parent(): FileAccess { + return FileAccess(path.parent) + } + + /** + * Traverses through directories and files. + * + * @param path path to traverse to + * @return new [FileAccess] instance + * @since v1-alpha10 + */ + fun traverse(path: String): FileAccess { + return FileAccess(this.path.resolve(formatToPath(path))) + } + + /** + * Traverses through directories and files. + * + * Throws a [FileOrDirectoryNotFoundException] + * if the specific path cannot be accessed. + * + * @param path relative path to traverse to + * @return new [FileAccess] instance + * @throws FileOrDirectoryNotFoundException if the specific relative path does not exist + * @since v1-alpha10 + */ + @Throws(FileOrDirectoryNotFoundException::class) + fun traverseIfExists(path: String): FileAccess { + var pathResolved: Path = this.path.resolve(formatToPath(path)) + + if (!Files.exists(pathResolved)) + throw FileOrDirectoryNotFoundException("Traversal failed as relative path '${path}' in absolute path '${unformatFromPath(this.path)}' does not exist") + + return FileAccess(this.path.resolve(formatToPath(path))) + } + + + // -----> Verification + /** + * Verifies that something + * exists at this location. + * + * @return this instance + * @throws IOAccessException on IO error + * @throws VerificationFailedException if the verification fails + * @since v1-alpha10 + */ + @Throws(IOAccessException::class, VerificationFailedException::class) + fun verifyExists(): FileAccess { + if (!exists()) + throw VerificationFailedException("Expected that something exists at '${unformatFromPath(path)}'") + + return this + } + + /** + * Verifies that something does + * not exist at this location. + * + * @return this instance + * @throws IOAccessException on IO error + * @throws VerificationFailedException if the verification fails + * @since v1-alpha10 + */ + @Throws(IOAccessException::class, VerificationFailedException::class) + fun verifyNotExists(): FileAccess { + if (exists()) + throw VerificationFailedException("Expected that nothing exists at '${unformatFromPath(path)}'") + + return this + } + + /** + * Verifies that a file + * is at this location. + * + * @return this instance + * @throws IOAccessException on IO error + * @throws VerificationFailedException if the verification fails + * @since v1-alpha10 + */ + @Throws(IOAccessException::class, VerificationFailedException::class) + fun verifyIsFile(): FileAccess { + if (getType() != Type.FILE) + throw VerificationFailedException("Expected that '${unformatFromPath(path)}' is a file") + + return this + } + + /** + * Verifies that a file does + * not exist at this location. + * + * @return this instance + * @throws IOAccessException on IO error + * @throws VerificationFailedException if the verification fails + * @since v1-alpha10 + */ + @Throws(IOAccessException::class, VerificationFailedException::class) + fun verifyIsNotFile(): FileAccess { + if (getType() == Type.FILE) + throw VerificationFailedException("Expected that '${unformatFromPath(path)}' is not a file") + + return this + } + + /** + * Verifies that a directory + * is at this location. + * + * @return this instance + * @throws IOAccessException on IO error + * @throws VerificationFailedException if the verification fails + * @since v1-alpha10 + */ + @Throws(IOAccessException::class, VerificationFailedException::class) + fun verifyIsDirectory(): FileAccess { + if (getType() != Type.DIRECTORY) + throw VerificationFailedException("Expected that '${unformatFromPath(path)}' is a directory") + + return this + } + + /** + * Verifies that a directory does + * not exist at this location. + * + * @return this instance + * @throws IOAccessException on IO error + * @throws VerificationFailedException if the verification fails + * @since v1-alpha10 + */ + @Throws(IOAccessException::class, VerificationFailedException::class) + fun verifyIsNotDirectory(): FileAccess { + if (getType() == Type.DIRECTORY) + throw VerificationFailedException("Expected that '${unformatFromPath(path)}' is not a directory") + + return this + } + + /** + * Verifies that a link + * is at this location. + * + * @return this instance + * @throws IOAccessException on IO error + * @throws VerificationFailedException if the verification fails + * @since v1-alpha10 + */ + @Throws(IOAccessException::class, VerificationFailedException::class) + fun verifyIsLink(): FileAccess { + if (exists() && (isSymbolicLink() || getLinkDestination() != null)) + throw VerificationFailedException("Expected that '${unformatFromPath(path)}' is a link") + + return this + } + + /** + * Verifies that a link does + * not exist at this location. + * + * @return this instance + * @throws IOAccessException on IO error + * @throws VerificationFailedException if the verification fails + * @since v1-alpha10 + */ + @Throws(IOAccessException::class, VerificationFailedException::class) + fun verifyIsNotLink(): FileAccess { + if (exists() && !(isSymbolicLink() || getLinkDestination() != null)) + throw VerificationFailedException("Expected that '${unformatFromPath(path)}' is not a link") + + return this + } + + /** + * Verifies that a symbolic + * link is at this location. + * + * @return this instance + * @throws IOAccessException on IO error + * @throws VerificationFailedException if the verification fails + * @since v1-alpha10 + */ + @Throws(IOAccessException::class, VerificationFailedException::class) + fun verifyIsSymbolicLink(): FileAccess { + if (!isSymbolicLink()) + throw VerificationFailedException("Expected that '${unformatFromPath(path)}' is a symbolic link") + + return this + } + + /** + * Verifies that a symbolic link + * does not exist at this location. + * + * @return this instance + * @throws IOAccessException on IO error + * @throws VerificationFailedException if the verification fails + * @since v1-alpha10 + */ + @Throws(IOAccessException::class, VerificationFailedException::class) + fun verifyIsNotSymbolicLink(): FileAccess { + if (isSymbolicLink()) + throw VerificationFailedException("Expected that '${unformatFromPath(path)}' is not a symbolic link") + + return this + } + + /** + * Verifies that a symbolic + * link is at this location. + * + * @return this instance + * @throws IOAccessException on IO error + * @throws VerificationFailedException if the verification fails + * @since v1-alpha10 + */ + @Throws(IOAccessException::class, VerificationFailedException::class) + fun verifyIsHardLink(): FileAccess { + if (exists() && !isSymbolicLink() && getLinkDestination() != null) + throw VerificationFailedException("Expected that '${unformatFromPath(path)}' is a hard link") + + return this + } + + /** + * Verifies that a symbolic link + * does not exist at this location. + * + * @return this instance + * @throws IOAccessException on IO error + * @throws VerificationFailedException if the verification fails + * @since v1-alpha10 + */ + @Throws(IOAccessException::class, VerificationFailedException::class) + fun verifyIsNotHardLink(): FileAccess { + if (exists() && !isSymbolicLink() && getLinkDestination() == null) + throw VerificationFailedException("Expected that '${unformatFromPath(path)}' is not a hard link") + + return this + } + + + // -----> Inner classes + /** + * Represents various types of files. + * + * @since v1-alpha8 + */ + enum class Type { + /** + * Identifies that the path does not exist. + * + * @since v1-alpha8 + */ + VOID, + + /** + * Identifies that the path is a regular file. + * + * @since v1-alpha8 + */ + FILE, + + /** + * Identifies that the path is a directory. + * + * @since v1-alpha8 + */ + DIRECTORY, + + /** + * Identifies that the path is unknown + * to the StarOpenSource Engine. + * + * @since v1-alpha8 + */ + UNKNOWN + } + + /** + * {@link FileVisitor} instance for + * copying directories recursively. + * + * @param source source to copy from + * @param destination destination to copy to + * @since v1-alpha9 + */ + private class CopyDirectoryVisitor(val source: Path, val destination: Path) : FileVisitor { + override fun preVisitDirectory(path: Path, attributes: BasicFileAttributes): FileVisitResult { + Files.createDirectories(destination.resolve(source.relativize(path))) + return FileVisitResult.CONTINUE + } + + override fun visitFile(path: Path, attributes: BasicFileAttributes): FileVisitResult { + Files.copy(path, destination.resolve(source.relativize(path))) + return FileVisitResult.CONTINUE + } + + override fun visitFileFailed(path: Path, exception: IOException): FileVisitResult { + throw exception + } + + override fun postVisitDirectory(path: Path, exception: IOException?): FileVisitResult { + if (exception != null) + throw exception + + return FileVisitResult.CONTINUE + } + } + + /** + * {@link FileVisitor} instance for + * delete directories recursively. + * + * @param directory directory to delete + * @since v1-alpha9 + */ + private class DeleteDirectoryVisitor(directory: Path) : FileVisitor { + override fun preVisitDirectory(path: Path, attributes: BasicFileAttributes): FileVisitResult { + return FileVisitResult.CONTINUE + } + + override fun visitFile(path: Path, attributes: BasicFileAttributes): FileVisitResult { + return FileVisitResult.CONTINUE + } + + override fun visitFileFailed(path: Path, exception: IOException): FileVisitResult { + throw exception + } + + override fun postVisitDirectory(path: Path, exception: IOException?): FileVisitResult { + if (exception != null) + throw exception + + Files.delete(path) + return FileVisitResult.CONTINUE + } + } +} diff --git a/dist/detekt.yml b/dist/detekt.yml index 4fc3eed..95c5c1f 100644 --- a/dist/detekt.yml +++ b/dist/detekt.yml @@ -18,6 +18,10 @@ build: complexity: TooManyFunctions: active: false + NestedBlockDepth: + threshold: 6 + ComplexCondition: + active: false naming: MemberNameEqualsClassName: @@ -38,3 +42,7 @@ style: active: false UnusedParameter: active: false + ReturnCount: + active: false + WildcardImport: + active: false