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