diff --git a/admin/src/main/kotlin/gui/components/InstanceDirectoryInputField.kt b/admin/src/main/kotlin/gui/components/InstanceDirectoryInputField.kt index 0eec8d9..88c4bfa 100644 --- a/admin/src/main/kotlin/gui/components/InstanceDirectoryInputField.kt +++ b/admin/src/main/kotlin/gui/components/InstanceDirectoryInputField.kt @@ -24,6 +24,7 @@ import javax.swing.JComponent import javax.swing.JFileChooser import javax.swing.JTextField import javax.swing.plaf.basic.BasicComboBoxEditor +import kotlin.io.path.absolutePathString fun instanceDirectoryInputField( inputComboBox: JComboBox>, @@ -106,7 +107,7 @@ fun instanceDirectoryInputField( // The second which is changed in this lambda, which is why we need to check to avoid // unexpected error on the second change. The (item) will be null if the selected item // is null or is different from the JComboBox type of the elements - selectedItem = item.value.launcherInstanceDirectory.absolutePath + selectedItem = item.value.launcherInstanceDirectoryPath.absolutePathString() } }, JButton("Browse").onClick { @@ -116,7 +117,7 @@ fun instanceDirectoryInputField( fileSelectionMode = JFileChooser.DIRECTORIES_ONLY } val result = fileChooser.showOpenDialog(parentComponent) - val selectedFile = + val selectedFilePath = fileChooser.handleResult( result = result, onErrorWhileChoosingFile = { @@ -127,7 +128,7 @@ fun instanceDirectoryInputField( ) }, ) ?: return@onClick - inputComboBox.selectedItem = selectedFile.absolutePath + inputComboBox.selectedItem = selectedFilePath.absolutePathString() }, ), preferredLabelWidth = preferredLabelWidth, diff --git a/admin/src/main/kotlin/gui/tabs/ModsConverterTab.kt b/admin/src/main/kotlin/gui/tabs/ModsConverterTab.kt index c0bfacd..1ca3720 100644 --- a/admin/src/main/kotlin/gui/tabs/ModsConverterTab.kt +++ b/admin/src/main/kotlin/gui/tabs/ModsConverterTab.kt @@ -31,6 +31,7 @@ import javax.swing.JComboBox import javax.swing.JFileChooser import javax.swing.JPanel import javax.swing.filechooser.FileNameExtensionFilter +import kotlin.io.path.writeText class ModsConverterTab : Tab() { private val coroutineScope = CoroutineScope(Dispatchers.IO) @@ -126,7 +127,7 @@ class ModsConverterTab : Tab() { val result = ModsConverterInstance.convertMods( launcher = launcherComboBox.getSelectedItemOrThrow(), - launcherInstanceDirectoryPath = + launcherInstanceDirectoryPathString = (launcherInstanceDirectoryComboBox.selectedItem as? String) ?: throw IllegalStateException( "The selected item of ${::launcherInstanceDirectoryComboBox.name} is null", ), @@ -209,7 +210,7 @@ class ModsConverterTab : Tab() { "Some launchers might save the changes after closing the launcher/app.", ) newLine() - text("If you created the instance/profile recently, try closing the launcher and try again.") + text("If you created the instance/profile recently, close the launcher and try again.") }.buildBodyAsText(), parentComponent = this@ModsConverterTab, ) @@ -299,13 +300,13 @@ class ModsConverterTab : Tab() { val filePickResult = outputFileChooser.showSaveDialog(this@ModsConverterTab) - val outputFile = + val outputFilePath = outputFileChooser.handleResult( result = filePickResult, onErrorWhileChoosingFile = {}, ) ?: return try { - outputFile.writeText(result.modsOutputText) + outputFilePath.writeText(result.modsOutputText) } catch (e: Exception) { e.printStackTrace() GuiUtils.showErrorMessage( diff --git a/admin/src/main/kotlin/gui/tabs/SyncScriptInstallerTab.kt b/admin/src/main/kotlin/gui/tabs/SyncScriptInstallerTab.kt index 76b99a4..4f015fa 100644 --- a/admin/src/main/kotlin/gui/tabs/SyncScriptInstallerTab.kt +++ b/admin/src/main/kotlin/gui/tabs/SyncScriptInstallerTab.kt @@ -29,6 +29,7 @@ import javax.swing.JComponent import javax.swing.JFileChooser import javax.swing.JLabel import javax.swing.filechooser.FileNameExtensionFilter +import kotlin.io.path.absolutePathString class SyncScriptInstallerTab : Tab() { private val coroutineScope = CoroutineScope(Dispatchers.IO) @@ -78,7 +79,7 @@ class SyncScriptInstallerTab : Tab() { configureInstallation( installationConfig = SyncScriptInstallationConfig.Install( - getSyncScriptJarFilePath = { + getSyncScriptJarFilePathString = { val fileChooser = JFileChooser().apply { dialogTitle = "Choose the JAR File for the sync script." @@ -86,12 +87,12 @@ class SyncScriptInstallerTab : Tab() { fileFilter = FileNameExtensionFilter("JAR Files", "jar") } val result = fileChooser.showOpenDialog(this@SyncScriptInstallerTab) - val selectedFile = + val selectedFilePath = fileChooser.handleResult( result = result, onErrorWhileChoosingFile = {}, ) ?: return@Install null - selectedFile.path + selectedFilePath.absolutePathString() }, ), confirmReplaceExistingPreLaunchCommand = false, @@ -116,7 +117,7 @@ class SyncScriptInstallerTab : Tab() { val result = SyncScriptInstallerInstance.configureInstallation( installationConfig = installationConfig, - launcherInstanceDirectoryPath = + launcherInstanceDirectoryPathString = (launcherInstanceDirectoryComboBox.selectedItem as? String) ?: throw IllegalStateException( "The selected item of ${::launcherInstanceDirectoryComboBox.name} is null", ), @@ -167,18 +168,18 @@ class SyncScriptInstallerTab : Tab() { ) } - SyncScriptInstallationError.CouldNotDeleteSyncScriptJarFileWhileUninstall -> { + is SyncScriptInstallationError.CouldNotDeleteSyncScriptJarFileWhileUninstall -> { GuiUtils.showErrorMessage( title = "❌ Unexpected error", - message = "An error occurred while deleting the sync script JAR file.", + message = "An error occurred while deleting the sync script JAR file: ${result.error.message}", parentComponent = this@SyncScriptInstallerTab, ) } - SyncScriptInstallationError.CouldNotDeleteSyncScriptDataWhileUninstall -> { + is SyncScriptInstallationError.CouldNotDeleteSyncScriptDataWhileUninstall -> { GuiUtils.showErrorMessage( title = "❌ Unexpected error", - message = "An error occurred while deleting the sync script data \uD83D\uDCC1.", + message = "An error occurred while deleting the sync script data \uD83D\uDCC1: ${result.error.message}", parentComponent = this@SyncScriptInstallerTab, ) } @@ -187,7 +188,7 @@ class SyncScriptInstallerTab : Tab() { GuiUtils.showErrorMessage( title = "❌ Unexpected error", message = - "An error occurred while updating the Pre Launch command/hook: \uD83D\uDEE0: ${result.error.message}", + "An error occurred while updating the Pre-Launch command/hook: \uD83D\uDEE0: ${result.error.message}", parentComponent = this@SyncScriptInstallerTab, ) } diff --git a/admin/src/main/kotlin/gui/utils/FileChooserExtensions.kt b/admin/src/main/kotlin/gui/utils/FileChooserExtensions.kt index b7085d2..aae34a8 100644 --- a/admin/src/main/kotlin/gui/utils/FileChooserExtensions.kt +++ b/admin/src/main/kotlin/gui/utils/FileChooserExtensions.kt @@ -1,6 +1,6 @@ package gui.utils -import java.io.File +import java.nio.file.Path import javax.swing.JFileChooser /** @@ -9,7 +9,7 @@ import javax.swing.JFileChooser fun JFileChooser.handleResult( result: Int, onErrorWhileChoosingFile: () -> Unit, -): File? { +): Path? { when (result) { JFileChooser.CANCEL_OPTION -> { return null @@ -21,7 +21,7 @@ fun JFileChooser.handleResult( } JFileChooser.APPROVE_OPTION -> { - return selectedFile + return selectedFile.toPath() } } return null diff --git a/admin/src/main/kotlin/launchers/Instance.kt b/admin/src/main/kotlin/launchers/Instance.kt index 32e4048..aab4f60 100644 --- a/admin/src/main/kotlin/launchers/Instance.kt +++ b/admin/src/main/kotlin/launchers/Instance.kt @@ -1,6 +1,6 @@ package launchers -import java.io.File +import java.nio.file.Path /** * An abstraction of Instance/Profile for all Minecraft launchers that @@ -12,6 +12,6 @@ import java.io.File * them as dropdown options for the text input field that request the instance directory path. * */ data class Instance( - val launcherInstanceDirectory: File, + val launcherInstanceDirectoryPath: Path, val instanceName: String, ) diff --git a/admin/src/main/kotlin/launchers/LauncherDataSource.kt b/admin/src/main/kotlin/launchers/LauncherDataSource.kt index a5726a5..f00c15e 100644 --- a/admin/src/main/kotlin/launchers/LauncherDataSource.kt +++ b/admin/src/main/kotlin/launchers/LauncherDataSource.kt @@ -2,7 +2,10 @@ package launchers import minecraftAssetProviders.MinecraftAssetProvider import syncInfo.models.Mod -import java.io.File +import java.nio.file.Path + +// TODO: Rename launcherInstanceDirectoryPath to dotMinecraftDirectoryPath, +// review other usages such as launcherInstanceDirectory /** * An interface that abstract dealing with the launcher data, like converting mods from the launcher data format @@ -21,7 +24,7 @@ interface LauncherDataSource { * with an exception. * @return [Result.success] if valid, otherwise [Result.failure] * */ - suspend fun validateInstanceDirectory(launcherInstanceDirectory: File): Result + suspend fun validateInstanceDirectory(launcherInstanceDirectoryPath: Path): Result /** * If an HTTP get request is needed to convert all the mods, this will be true if @@ -31,19 +34,19 @@ interface LauncherDataSource { * 3. Other reasons that are specific to the launcher implementation, if the download url or related info wasn't available * for some reason and a request to Curse Forge API is needed to get the info, this should return true * */ - suspend fun isCurseForgeApiRequestNeededForConvertingMods(launcherInstanceDirectory: File): Result + suspend fun isCurseForgeApiRequestNeededForConvertingMods(launcherInstanceDirectoryPath: Path): Result /** * Check if the instance has mods installed (not empty). * */ - suspend fun hasMods(launcherInstanceDirectory: File): Result + suspend fun hasMods(launcherInstanceDirectoryPath: Path): Result /** * @return A list of [Mod] which contains the information about the mod, converting it from the specified launcher * into the script data format * */ suspend fun getLauncherInstanceMods( - launcherInstanceDirectory: File, + launcherInstanceDirectoryPath: Path, overrideCurseForgeApiKey: String?, ): Result> @@ -53,7 +56,7 @@ interface LauncherDataSource { * * @return Null if it's not set * */ - suspend fun getPreLaunchCommand(launcherInstanceDirectory: File): Result + suspend fun getPreLaunchCommand(launcherInstanceDirectoryPath: Path): Result /** * Update the Pre Launch command. @@ -65,7 +68,7 @@ interface LauncherDataSource { * */ suspend fun setPreLaunchCommand( command: String?, - launcherInstanceDirectory: File, + launcherInstanceDirectoryPath: Path, ): Result /** diff --git a/admin/src/main/kotlin/launchers/atLauncher/ATLauncherDataSource.kt b/admin/src/main/kotlin/launchers/atLauncher/ATLauncherDataSource.kt index 91c9183..32b2848 100644 --- a/admin/src/main/kotlin/launchers/atLauncher/ATLauncherDataSource.kt +++ b/admin/src/main/kotlin/launchers/atLauncher/ATLauncherDataSource.kt @@ -1,8 +1,6 @@ package launchers.atLauncher import curseForgeDataSource -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive @@ -14,13 +12,16 @@ import syncInfo.models.Mod import utils.JsonIgnoreUnknownKeys import utils.JsonPrettyPrint import utils.SystemFileProvider -import java.io.File -import java.nio.file.Files -import java.nio.file.Paths +import utils.listFilteredPaths +import java.nio.file.Path +import kotlin.io.path.absolutePathString +import kotlin.io.path.exists import kotlin.io.path.isDirectory import kotlin.io.path.isHidden +import kotlin.io.path.isRegularFile import kotlin.io.path.name -import kotlin.streams.toList +import kotlin.io.path.readText +import kotlin.io.path.writeText class ATLauncherDataSource : LauncherDataSource { companion object { @@ -30,13 +31,14 @@ class ATLauncherDataSource : LauncherDataSource { const val INSTANCE_FILE_NAME = "instance.json" } - private fun getInstanceConfigFile(launcherInstanceDirectory: File): File = - Paths.get(launcherInstanceDirectory.path, INSTANCE_FILE_NAME).toFile() + private fun getInstanceConfigFilePath(launcherInstanceDirectoryPath: Path): Path = + launcherInstanceDirectoryPath.resolve(INSTANCE_FILE_NAME) - private fun getInstance(launcherInstanceDirectory: File): Result { + private fun getInstance(launcherInstanceDirectoryPath: Path): Result { return try { - val instanceConfigFile = getInstanceConfigFile(launcherInstanceDirectory = launcherInstanceDirectory) - val instance = JsonIgnoreUnknownKeys.decodeFromString(instanceConfigFile.readText()) + val instanceConfigFilePath = + getInstanceConfigFilePath(launcherInstanceDirectoryPath = launcherInstanceDirectoryPath) + val instance = JsonIgnoreUnknownKeys.decodeFromString(instanceConfigFilePath.readText()) return Result.success(instance) } catch (e: Exception) { e.printStackTrace() @@ -54,14 +56,17 @@ class ATLauncherDataSource : LauncherDataSource { instance.launcher.mods .filter { it.type == ATLauncherInstance.Launcher.Mod.Type.Mods } - override suspend fun validateInstanceDirectory(launcherInstanceDirectory: File): Result { + override suspend fun validateInstanceDirectory(launcherInstanceDirectoryPath: Path): Result { return try { - val instanceConfigFile = getInstanceConfigFile(launcherInstanceDirectory = launcherInstanceDirectory) - if (!instanceConfigFile.exists()) { - return Result.failure(IllegalArgumentException("The file (${instanceConfigFile.absolutePath}) does not exist.")) + val instanceConfigFilePath = + getInstanceConfigFilePath(launcherInstanceDirectoryPath = launcherInstanceDirectoryPath) + if (!instanceConfigFilePath.exists()) { + return Result.failure(IllegalArgumentException("The file (${instanceConfigFilePath.absolutePathString()}) does not exist.")) } - if (!instanceConfigFile.isFile) { - return Result.failure(IllegalArgumentException("The file (${instanceConfigFile.absolutePath}) should be a file.")) + if (!instanceConfigFilePath.isRegularFile()) { + return Result.failure( + IllegalArgumentException("The file (${instanceConfigFilePath.absolutePathString()}) should be a file."), + ) } Result.success(Unit) } catch (e: Exception) { @@ -75,9 +80,9 @@ class ATLauncherDataSource : LauncherDataSource { return modrinthFile == null && (mod.curseForgeProjectId != null && mod.curseForgeFileId != null) } - override suspend fun isCurseForgeApiRequestNeededForConvertingMods(launcherInstanceDirectory: File): Result = + override suspend fun isCurseForgeApiRequestNeededForConvertingMods(launcherInstanceDirectoryPath: Path): Result = try { - val instance = getInstance(launcherInstanceDirectory = launcherInstanceDirectory).getOrThrow() + val instance = getInstance(launcherInstanceDirectoryPath = launcherInstanceDirectoryPath).getOrThrow() val isCurseForgeApiRequestNeeded = getMods(instance).any { atLauncherMod -> isCurseForgeApiRequestNeededForMod(mod = atLauncherMod) @@ -88,11 +93,11 @@ class ATLauncherDataSource : LauncherDataSource { Result.failure(e) } - override suspend fun hasMods(launcherInstanceDirectory: File): Result = + override suspend fun hasMods(launcherInstanceDirectoryPath: Path): Result = try { val mods = getMods( - instance = getInstance(launcherInstanceDirectory = launcherInstanceDirectory).getOrThrow(), + instance = getInstance(launcherInstanceDirectoryPath = launcherInstanceDirectoryPath).getOrThrow(), ) Result.success(mods.isNotEmpty()) } catch (e: Exception) { @@ -100,11 +105,11 @@ class ATLauncherDataSource : LauncherDataSource { } override suspend fun getLauncherInstanceMods( - launcherInstanceDirectory: File, + launcherInstanceDirectoryPath: Path, overrideCurseForgeApiKey: String?, ): Result> = try { - val instance = getInstance(launcherInstanceDirectory = launcherInstanceDirectory).getOrThrow() + val instance = getInstance(launcherInstanceDirectoryPath = launcherInstanceDirectoryPath).getOrThrow() val mods = getMods(instance = instance) @@ -151,9 +156,9 @@ class ATLauncherDataSource : LauncherDataSource { Result.failure(e) } - override suspend fun getPreLaunchCommand(launcherInstanceDirectory: File): Result = + override suspend fun getPreLaunchCommand(launcherInstanceDirectoryPath: Path): Result = try { - val instance = getInstance(launcherInstanceDirectory = launcherInstanceDirectory).getOrThrow() + val instance = getInstance(launcherInstanceDirectoryPath = launcherInstanceDirectoryPath).getOrThrow() Result.success(instance.launcher.preLaunchCommand) } catch (e: Exception) { Result.failure(e) @@ -161,12 +166,13 @@ class ATLauncherDataSource : LauncherDataSource { override suspend fun setPreLaunchCommand( command: String?, - launcherInstanceDirectory: File, + launcherInstanceDirectoryPath: Path, ): Result = try { - val instanceConfigFile = getInstanceConfigFile(launcherInstanceDirectory = launcherInstanceDirectory) + val instanceConfigFilePath = + getInstanceConfigFilePath(launcherInstanceDirectoryPath = launcherInstanceDirectoryPath) - val instanceJsonObject: JsonObject = Json.parseToJsonElement(instanceConfigFile.readText()).jsonObject + val instanceJsonObject: JsonObject = Json.parseToJsonElement(instanceConfigFilePath.readText()).jsonObject val launcherJsonKey = ATLauncherInstance::launcher.name val preLaunchCommandJsonKey = ATLauncherInstance.Launcher::preLaunchCommand.name @@ -199,7 +205,7 @@ class ATLauncherDataSource : LauncherDataSource { this[launcherJsonKey] = updatedLauncher }.let { JsonObject(it) } - instanceConfigFile.writeText( + instanceConfigFilePath.writeText( text = JsonPrettyPrint.encodeToString( JsonObject.serializer(), @@ -213,27 +219,24 @@ class ATLauncherDataSource : LauncherDataSource { override suspend fun getInstances(): Result?> = try { - val (directory, _) = + val (directoryPath, _) = SystemFileProvider .getUserApplicationDataDirectoryWithFlatpakSupport( applicationDirectoryName = "ATLauncher", flatpakApplicationId = "com.atlauncher.ATLauncher", ).getOrThrow() - val instancesDirectory = directory?.resolve("instances") val instances = - instancesDirectory?.let { - withContext(Dispatchers.IO) { - Files - .list(it.toPath()) - .filter { it.isDirectory() && !it.isHidden() } - .toList() - }.map { + directoryPath + ?.resolve("instances") + ?.listFilteredPaths { path -> + path.isDirectory() && !path.isHidden() + }?.getOrThrow() + ?.map { Instance( - launcherInstanceDirectory = it.toFile(), + launcherInstanceDirectoryPath = it, instanceName = it.name, ) } - } Result.success(instances) } catch (e: Exception) { Result.failure(e) diff --git a/admin/src/main/kotlin/launchers/modrinth/ModrinthLauncherDataSource.kt b/admin/src/main/kotlin/launchers/modrinth/ModrinthLauncherDataSource.kt index 0ce3034..867c99b 100644 --- a/admin/src/main/kotlin/launchers/modrinth/ModrinthLauncherDataSource.kt +++ b/admin/src/main/kotlin/launchers/modrinth/ModrinthLauncherDataSource.kt @@ -1,7 +1,5 @@ package launchers.modrinth -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject @@ -14,14 +12,17 @@ import launchers.modrinth.ModrinthLauncherInstance.ModrinthLauncherProject import syncInfo.models.Mod import utils.JsonIgnoreUnknownKeys import utils.SystemFileProvider +import utils.listFilteredPaths import utils.simpleMergeJsonObjects -import java.io.File -import java.nio.file.Files -import java.nio.file.Paths +import java.nio.file.Path +import kotlin.io.path.absolutePathString +import kotlin.io.path.exists import kotlin.io.path.isDirectory import kotlin.io.path.isHidden +import kotlin.io.path.isRegularFile import kotlin.io.path.name -import kotlin.streams.toList +import kotlin.io.path.readText +import kotlin.io.path.writeText class ModrinthLauncherDataSource : LauncherDataSource { companion object { @@ -31,27 +32,33 @@ class ModrinthLauncherDataSource : LauncherDataSource { const val INSTANCE_FILE_NAME = "profile.json" } - private fun getInstanceConfigFile(launcherInstanceDirectory: File) = - Paths.get(launcherInstanceDirectory.path, INSTANCE_FILE_NAME).toFile() + private fun getInstanceConfigFilePath(launcherInstanceDirectoryPath: Path): Path = + launcherInstanceDirectoryPath.resolve(INSTANCE_FILE_NAME) - private fun getInstance(launcherInstanceDirectory: File): Result = + private fun getInstance(launcherInstanceDirectoryPath: Path): Result = try { - val instanceConfigFile = getInstanceConfigFile(launcherInstanceDirectory = launcherInstanceDirectory) + val instanceConfigFilePath = + getInstanceConfigFilePath(launcherInstanceDirectoryPath = launcherInstanceDirectoryPath) val modrinthLauncherInstance = - JsonIgnoreUnknownKeys.decodeFromString(instanceConfigFile.readText()) + JsonIgnoreUnknownKeys.decodeFromString(instanceConfigFilePath.readText()) Result.success(modrinthLauncherInstance) } catch (e: Exception) { Result.failure(e) } - override suspend fun validateInstanceDirectory(launcherInstanceDirectory: File): Result { + override suspend fun validateInstanceDirectory(launcherInstanceDirectoryPath: Path): Result { return try { - val instanceConfigFile = getInstanceConfigFile(launcherInstanceDirectory = launcherInstanceDirectory) - if (!instanceConfigFile.exists()) { - return Result.failure(IllegalArgumentException("The file (${instanceConfigFile.absolutePath}) does not exist.")) + val instanceConfigFilePath = + getInstanceConfigFilePath(launcherInstanceDirectoryPath = launcherInstanceDirectoryPath) + if (!instanceConfigFilePath.exists()) { + return Result.failure( + IllegalArgumentException("The file (${instanceConfigFilePath.absolutePathString()}) does not exist."), + ) } - if (!instanceConfigFile.isFile) { - return Result.failure(IllegalArgumentException("The file (${instanceConfigFile.absolutePath}) should be a file.")) + if (!instanceConfigFilePath.isRegularFile()) { + return Result.failure( + IllegalArgumentException("The file (${instanceConfigFilePath.absolutePathString()}) should be a file."), + ) } Result.success(Unit) } catch (e: Exception) { @@ -59,25 +66,30 @@ class ModrinthLauncherDataSource : LauncherDataSource { } } - override suspend fun isCurseForgeApiRequestNeededForConvertingMods(launcherInstanceDirectory: File): Result = + override suspend fun isCurseForgeApiRequestNeededForConvertingMods(launcherInstanceDirectoryPath: Path): Result = Result.success(false) private fun getModrinthLauncherProjects(instance: ModrinthLauncherInstance): Map = instance.projects - override suspend fun hasMods(launcherInstanceDirectory: File): Result { + override suspend fun hasMods(launcherInstanceDirectoryPath: Path): Result { return try { - val instanceConfigFile = getInstanceConfigFile(launcherInstanceDirectory = launcherInstanceDirectory) - if (!instanceConfigFile.exists()) { - return Result.failure(IllegalArgumentException("The file (${instanceConfigFile.absolutePath}) does not exist.")) + val instanceConfigFilePath = + getInstanceConfigFilePath(launcherInstanceDirectoryPath = launcherInstanceDirectoryPath) + if (!instanceConfigFilePath.exists()) { + return Result.failure( + IllegalArgumentException("The file (${instanceConfigFilePath.absolutePathString()}) does not exist."), + ) } - if (!instanceConfigFile.isFile) { - return Result.failure(IllegalArgumentException("The file (${instanceConfigFile.absolutePath}) should be a file.")) + if (!instanceConfigFilePath.isRegularFile()) { + return Result.failure( + IllegalArgumentException("The file (${instanceConfigFilePath.absolutePathString()}) should be a file."), + ) } val mods = getModrinthLauncherProjects( instance = getInstance( - launcherInstanceDirectory = launcherInstanceDirectory, + launcherInstanceDirectoryPath = launcherInstanceDirectoryPath, ).getOrThrow(), ) Result.success(mods.isNotEmpty()) @@ -87,11 +99,11 @@ class ModrinthLauncherDataSource : LauncherDataSource { } override suspend fun getLauncherInstanceMods( - launcherInstanceDirectory: File, + launcherInstanceDirectoryPath: Path, overrideCurseForgeApiKey: String?, ): Result> = try { - val instance = getInstance(launcherInstanceDirectory = launcherInstanceDirectory).getOrThrow() + val instance = getInstance(launcherInstanceDirectoryPath = launcherInstanceDirectoryPath).getOrThrow() val mods = getModrinthLauncherProjects(instance = instance).map { (_, project) -> val metadata = project.metadata @@ -111,9 +123,9 @@ class ModrinthLauncherDataSource : LauncherDataSource { Result.failure(e) } - override suspend fun getPreLaunchCommand(launcherInstanceDirectory: File): Result = + override suspend fun getPreLaunchCommand(launcherInstanceDirectoryPath: Path): Result = try { - val instance = getInstance(launcherInstanceDirectory = launcherInstanceDirectory).getOrThrow() + val instance = getInstance(launcherInstanceDirectoryPath = launcherInstanceDirectoryPath).getOrThrow() val command = instance.hooks?.preLaunch Result.success(command) } catch (e: Exception) { @@ -122,14 +134,15 @@ class ModrinthLauncherDataSource : LauncherDataSource { override suspend fun setPreLaunchCommand( command: String?, - launcherInstanceDirectory: File, + launcherInstanceDirectoryPath: Path, ): Result = try { - val instanceConfigFile = getInstanceConfigFile(launcherInstanceDirectory = launcherInstanceDirectory) + val instanceConfigFilePath = + getInstanceConfigFilePath(launcherInstanceDirectoryPath = launcherInstanceDirectoryPath) // Since the data class does not have the full properties, and to avoid removing any other properties, // we will load it using a json element and modify it using the data class then merge it - val instanceJsonElement: JsonElement = Json.parseToJsonElement(instanceConfigFile.readText()) + val instanceJsonElement: JsonElement = Json.parseToJsonElement(instanceConfigFilePath.readText()) val instance: ModrinthLauncherInstance = JsonIgnoreUnknownKeys.decodeFromJsonElement(instanceJsonElement) // TODO The current implementation does not disable @@ -152,7 +165,7 @@ class ModrinthLauncherDataSource : LauncherDataSource { updates = Json.encodeToJsonElement(newInstance).jsonObject, ) - instanceConfigFile.writeText( + instanceConfigFilePath.writeText( text = Json.encodeToString( JsonObject.serializer(), @@ -166,24 +179,20 @@ class ModrinthLauncherDataSource : LauncherDataSource { override suspend fun getInstances(): Result?> = try { - val directory = + val directoryPath = SystemFileProvider .getUserApplicationDataDirectory( applicationDirectoryName = "com.modrinth.theseus", ).getOrThrow() - val instancesDirectory = directory?.resolve("profiles") val instances = - instancesDirectory - ?.let { - withContext(Dispatchers.IO) { - Files - .list(it.toPath()) - .filter { it.isDirectory() && !it.isHidden() } - .toList() - } - }?.map { + directoryPath + ?.resolve("profiles") + ?.listFilteredPaths { path -> + path.isDirectory() && !path.isHidden() + }?.getOrThrow() + ?.map { Instance( - launcherInstanceDirectory = it.toFile(), + launcherInstanceDirectoryPath = it, instanceName = it.name, ) } diff --git a/admin/src/main/kotlin/launchers/prismLauncher/PrismLauncherDataSource.kt b/admin/src/main/kotlin/launchers/prismLauncher/PrismLauncherDataSource.kt index ea96b01..0e975fa 100644 --- a/admin/src/main/kotlin/launchers/prismLauncher/PrismLauncherDataSource.kt +++ b/admin/src/main/kotlin/launchers/prismLauncher/PrismLauncherDataSource.kt @@ -3,19 +3,24 @@ package launchers.prismLauncher import com.akuleshov7.ktoml.Toml import constants.DotMinecraftFileNames import curseForgeDataSource -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext import launchers.Instance import launchers.LauncherDataSource +import okio.IOException import syncInfo.models.Mod import utils.SystemFileProvider -import java.io.File -import java.nio.file.Files -import java.nio.file.Paths +import utils.listFilteredPaths +import java.nio.file.Path +import kotlin.io.path.absolutePathString +import kotlin.io.path.exists +import kotlin.io.path.extension import kotlin.io.path.isDirectory import kotlin.io.path.isHidden +import kotlin.io.path.isRegularFile import kotlin.io.path.name -import kotlin.streams.toList +import kotlin.io.path.pathString +import kotlin.io.path.readLines +import kotlin.io.path.readText +import kotlin.io.path.writeText class PrismLauncherDataSource : LauncherDataSource { companion object { @@ -43,7 +48,8 @@ class PrismLauncherDataSource : LauncherDataSource { } } - private fun getInstanceConfigFile(launcherInstanceDirectory: File): File = launcherInstanceDirectory.parentFile.resolve("instance.cfg") + private fun getInstanceConfigFilePath(launcherInstanceDirectoryPath: Path): Path = + launcherInstanceDirectoryPath.parent.resolve("instance.cfg") // TODO: We have a issue in the naming of launcherInstanceDirectory and similar names everywhere in the admin module // by this, we mean the root instance folder as some launchers might store minecraft specific folders @@ -54,28 +60,31 @@ class PrismLauncherDataSource : LauncherDataSource { // by the root instance folder (launcherInstanceDirectory), currently for this implementation, we're asking // for the `.minecraft` folder, also update the GUI instructions (InstanceDirectoryInputField.kt), and the docs if there are any references - override suspend fun validateInstanceDirectory(launcherInstanceDirectory: File): Result { - val dotMinecraftDirectory = File(launcherInstanceDirectory.parentFile, DOT_MINECRAFT_DIRECTORY_NAME) + override suspend fun validateInstanceDirectory(launcherInstanceDirectoryPath: Path): Result { + val dotMinecraftDirectoryPath = launcherInstanceDirectoryPath.parent.resolve(DOT_MINECRAFT_DIRECTORY_NAME) - if (!dotMinecraftDirectory.exists()) { + if (!dotMinecraftDirectoryPath.exists()) { return Result.failure( IllegalArgumentException( - "The file (${dotMinecraftDirectory.absolutePath}) does not exist. If this " + + "The file (${dotMinecraftDirectoryPath.absolutePathString()}) does not exist. If this " + "is the root instance folder for Prism Launcher, the path should be to '.minecraft' folder.", ), ) } - if (!dotMinecraftDirectory.isDirectory) { - return Result.failure(IllegalArgumentException("The file (${dotMinecraftDirectory.absolutePath}) should be a folder.")) + if (!dotMinecraftDirectoryPath.isDirectory()) { + return Result.failure( + IllegalArgumentException("The file (${dotMinecraftDirectoryPath.absolutePathString()}) should be a folder."), + ) } - val instanceConfigFile = getInstanceConfigFile(launcherInstanceDirectory = launcherInstanceDirectory) + val instanceConfigFilePath = + getInstanceConfigFilePath(launcherInstanceDirectoryPath = launcherInstanceDirectoryPath) - if (!instanceConfigFile.exists()) { - return Result.failure(IllegalArgumentException("The file (${instanceConfigFile.absolutePath}) does not exist.")) + if (!instanceConfigFilePath.exists()) { + return Result.failure(IllegalArgumentException("The file (${instanceConfigFilePath.absolutePathString()}) does not exist.")) } - if (!instanceConfigFile.isFile) { - return Result.failure(IllegalArgumentException("The file (${instanceConfigFile.absolutePath}) should be a file.")) + if (!instanceConfigFilePath.isRegularFile()) { + return Result.failure(IllegalArgumentException("The file (${instanceConfigFilePath.absolutePathString()}) should be a file.")) } return Result.success(Unit) @@ -84,36 +93,41 @@ class PrismLauncherDataSource : LauncherDataSource { private fun isCurseForgeApiRequestNeededForMod(modMetadata: PrismLauncherModMetadata): Boolean = modMetadata.download.url.isBlank() && modMetadata.update.curseForge != null - private fun getModsMetaDataDirectory(launcherInstanceDirectory: File): File = - File( - Paths.get(launcherInstanceDirectory.path, DotMinecraftFileNames.MODS_DIRECTORY).toFile(), - MODS_METADATA_DIRECTORY_NAME, - ) + private fun getModsMetaDataDirectoryPath(launcherInstanceDirectoryPath: Path): Path = + launcherInstanceDirectoryPath + .resolve(launcherInstanceDirectoryPath) + .resolve(DotMinecraftFileNames.MODS_DIRECTORY) + .resolve(MODS_METADATA_DIRECTORY_NAME) - private fun getModMetadataFiles(launcherInstanceDirectory: File): Result> { + private suspend fun getModMetadataFilePaths(launcherInstanceDirectoryPath: Path): Result> { return try { - val modsMetaDataDirectory = getModsMetaDataDirectory(launcherInstanceDirectory = launcherInstanceDirectory) - val modMetadataFiles = - modsMetaDataDirectory.listFiles()?.filter { - it.isFile && it.extension == MOD_METADATA_FILE_EXTENSION + val modsMetaDataDirectoryPath = + getModsMetaDataDirectoryPath(launcherInstanceDirectoryPath = launcherInstanceDirectoryPath) + + val modMetadataFilePaths = + try { + modsMetaDataDirectoryPath + .listFilteredPaths { + it.isRegularFile() && !it.isHidden() && it.extension == MOD_METADATA_FILE_EXTENSION + }.getOrThrow() + } catch (e: Exception) { + e.printStackTrace() + return Result.failure( + IOException("Failed to list the files in (${modsMetaDataDirectoryPath.pathString}): ${e.message}"), + ) } - if (modMetadataFiles == null) { - return Result.failure( - IllegalArgumentException("(${modsMetaDataDirectory.absolutePath}) might not be a directory or an I/O error occurred."), - ) - } - Result.success(modMetadataFiles) + Result.success(modMetadataFilePaths) } catch (e: Exception) { Result.failure(e) } } - private fun getModsMetadata(launcherInstanceDirectory: File): Result> = + private suspend fun getModsMetadata(launcherInstanceDirectoryPath: Path): Result> = try { - val modMetadataFiles = - getModMetadataFiles(launcherInstanceDirectory = launcherInstanceDirectory).getOrThrow() + val modMetadataFilePaths = + getModMetadataFilePaths(launcherInstanceDirectoryPath = launcherInstanceDirectoryPath).getOrThrow() val modsMetadata = - modMetadataFiles.map { + modMetadataFilePaths.map { val fileText = it.readText() Toml.decodeFromString(PrismLauncherModMetadata.serializer(), fileText) } @@ -123,10 +137,10 @@ class PrismLauncherDataSource : LauncherDataSource { Result.failure(e) } - override suspend fun isCurseForgeApiRequestNeededForConvertingMods(launcherInstanceDirectory: File): Result = + override suspend fun isCurseForgeApiRequestNeededForConvertingMods(launcherInstanceDirectoryPath: Path): Result = try { val modsMetadata = - getModsMetadata(launcherInstanceDirectory = launcherInstanceDirectory).getOrThrow() + getModsMetadata(launcherInstanceDirectoryPath = launcherInstanceDirectoryPath).getOrThrow() val isCurseForgeApiRequestNeeded = modsMetadata.any { modMetadata -> isCurseForgeApiRequestNeededForMod(modMetadata = modMetadata) @@ -136,35 +150,35 @@ class PrismLauncherDataSource : LauncherDataSource { Result.failure(e) } - override suspend fun hasMods(launcherInstanceDirectory: File): Result { + override suspend fun hasMods(launcherInstanceDirectoryPath: Path): Result { return try { - val modsMetaDataDirectory = - getModsMetaDataDirectory( - launcherInstanceDirectory = launcherInstanceDirectory, + val modsMetaDataDirectoryPath = + getModsMetaDataDirectoryPath( + launcherInstanceDirectoryPath = launcherInstanceDirectoryPath, ) - if (!modsMetaDataDirectory.exists()) { + if (!modsMetaDataDirectoryPath.exists()) { return Result.success(false) } - if (!modsMetaDataDirectory.isDirectory) { + if (!modsMetaDataDirectoryPath.isDirectory()) { return Result.failure( - IllegalArgumentException("The file (${modsMetaDataDirectory.absolutePath} should be a folder/directory."), + IllegalArgumentException("The file (${modsMetaDataDirectoryPath.absolutePathString()} should be a folder/directory."), ) } - val modMetadataFiles = - getModMetadataFiles(launcherInstanceDirectory = launcherInstanceDirectory).getOrThrow() - Result.success(modMetadataFiles.isNotEmpty()) + val modMetadataFilePaths = + getModMetadataFilePaths(launcherInstanceDirectoryPath = launcherInstanceDirectoryPath).getOrThrow() + Result.success(modMetadataFilePaths.isNotEmpty()) } catch (e: Exception) { Result.failure(e) } } override suspend fun getLauncherInstanceMods( - launcherInstanceDirectory: File, + launcherInstanceDirectoryPath: Path, overrideCurseForgeApiKey: String?, ): Result> = try { val modsMetadata = - getModsMetadata(launcherInstanceDirectory = launcherInstanceDirectory).getOrThrow() + getModsMetadata(launcherInstanceDirectoryPath = launcherInstanceDirectoryPath).getOrThrow() val mods = modsMetadata.map { modMetadata -> var modDownloadUrl = modMetadata.download.url @@ -210,12 +224,13 @@ class PrismLauncherDataSource : LauncherDataSource { Result.failure(e) } - override suspend fun getPreLaunchCommand(launcherInstanceDirectory: File): Result { + override suspend fun getPreLaunchCommand(launcherInstanceDirectoryPath: Path): Result { return try { val preLaunchCommand = readInstanceProperty( propertyKey = PropertyKey.PRE_LAUNCH_COMMAND, - instanceFileLines = getInstanceConfigFile(launcherInstanceDirectory = launcherInstanceDirectory).readLines(), + instanceFileLines = + getInstanceConfigFilePath(launcherInstanceDirectoryPath = launcherInstanceDirectoryPath).readLines(), ) return Result.success(preLaunchCommand) } catch (e: Exception) { @@ -274,12 +289,13 @@ class PrismLauncherDataSource : LauncherDataSource { override suspend fun setPreLaunchCommand( command: String?, - launcherInstanceDirectory: File, + launcherInstanceDirectoryPath: Path, ): Result = try { // Manually update the file instead of Properties - val instanceConfigFile = getInstanceConfigFile(launcherInstanceDirectory = launcherInstanceDirectory) - val instanceConfigFileLines = instanceConfigFile.readLines().toMutableList() + val instanceConfigFilePath = + getInstanceConfigFilePath(launcherInstanceDirectoryPath = launcherInstanceDirectoryPath) + val instanceConfigFileLines = instanceConfigFilePath.readLines().toMutableList() setInstancePropertyInGeneralSection( propertyKey = PropertyKey.PRE_LAUNCH_COMMAND, @@ -316,7 +332,7 @@ class PrismLauncherDataSource : LauncherDataSource { } } - instanceConfigFile.writeText(text = instanceConfigFileLines.joinToString("\n")) + instanceConfigFilePath.writeText(text = instanceConfigFileLines.joinToString("\n")) Result.success(Unit) } catch (e: Exception) { @@ -325,34 +341,29 @@ class PrismLauncherDataSource : LauncherDataSource { override suspend fun getInstances(): Result?> = try { - val (directory, isFlatpak) = + val (directoryPath, isFlatpak) = SystemFileProvider .getUserApplicationDataDirectoryWithFlatpakSupport( applicationDirectoryName = "PrismLauncher", flatpakApplicationId = "org.prismlauncher.PrismLauncher", ).getOrThrow() - val instancesDirectory = - (if (isFlatpak) directory?.resolve("PrismLauncher") else directory) + val instancesDirectoryPath = + (if (isFlatpak) directoryPath?.resolve("PrismLauncher") else directoryPath) ?.resolve("instances") val instances = - instancesDirectory - ?.let { - withContext(Dispatchers.IO) { - Files - .list(it.toPath()) - .filter { - it.isDirectory() && - !it.isHidden() && - it.name !in - listOf( - ".LAUNCHER_TEMP", - ".tmp", - ) - }.toList() - } - }?.map { + instancesDirectoryPath + ?.listFilteredPaths { path -> + path.isDirectory() && + !path.isHidden() && + path.name !in + listOf( + ".LAUNCHER_TEMP", + ".tmp", + ) + }?.getOrThrow() + ?.map { Instance( - launcherInstanceDirectory = it.resolve(DOT_MINECRAFT_DIRECTORY_NAME).toFile(), + launcherInstanceDirectoryPath = it.resolve(DOT_MINECRAFT_DIRECTORY_NAME), instanceName = it.name, ) } diff --git a/admin/src/main/kotlin/services/modsConverter/ModsConverter.kt b/admin/src/main/kotlin/services/modsConverter/ModsConverter.kt index 75589bc..9c4eb49 100644 --- a/admin/src/main/kotlin/services/modsConverter/ModsConverter.kt +++ b/admin/src/main/kotlin/services/modsConverter/ModsConverter.kt @@ -10,7 +10,7 @@ interface ModsConverter { * */ suspend fun convertMods( launcher: MinecraftLauncher, - launcherInstanceDirectoryPath: String, + launcherInstanceDirectoryPathString: String, convertMode: ModsConvertMode, prettyFormat: Boolean, overrideCurseForgeApiKey: String?, diff --git a/admin/src/main/kotlin/services/modsConverter/ModsConverterImpl.kt b/admin/src/main/kotlin/services/modsConverter/ModsConverterImpl.kt index ffa21ce..38d7f75 100644 --- a/admin/src/main/kotlin/services/modsConverter/ModsConverterImpl.kt +++ b/admin/src/main/kotlin/services/modsConverter/ModsConverterImpl.kt @@ -8,25 +8,26 @@ import launchers.MinecraftLauncher import services.modsConverter.models.ModsConvertMode import syncInfo.models.SyncInfo import utils.JsonPrettyPrint -import java.io.File +import java.nio.file.Paths +import kotlin.io.path.exists class ModsConverterImpl : ModsConverter { override suspend fun convertMods( launcher: MinecraftLauncher, - launcherInstanceDirectoryPath: String, + launcherInstanceDirectoryPathString: String, convertMode: ModsConvertMode, prettyFormat: Boolean, overrideCurseForgeApiKey: String?, isCurseForgeForStudiosTermsOfServiceAccepted: Boolean, ): ModsConvertResult { return try { - if (launcherInstanceDirectoryPath.isBlank()) { + if (launcherInstanceDirectoryPathString.isBlank()) { return ModsConvertResult.Failure( error = ModsConvertError.EmptyLauncherInstanceDirectoryPath, ) } - val launcherInstanceDirectory = File(launcherInstanceDirectoryPath) - if (!launcherInstanceDirectory.exists()) { + val launcherInstanceDirectoryPath = Paths.get(launcherInstanceDirectoryPathString) + if (!launcherInstanceDirectoryPath.exists()) { return ModsConvertResult.Failure( error = ModsConvertError.LauncherInstanceDirectoryNotFound, ) @@ -34,23 +35,23 @@ class ModsConverterImpl : ModsConverter { val launcherDataSource: LauncherDataSource = LauncherDataSourceFactory.getHandler(launcher) launcherDataSource - .validateInstanceDirectory(launcherInstanceDirectory = launcherInstanceDirectory) + .validateInstanceDirectory(launcherInstanceDirectoryPath = launcherInstanceDirectoryPath) .getOrElse { return ModsConvertResult.Failure( error = ModsConvertError.InvalidLauncherInstanceDirectory( - message = it.message.toString(), + message = it.toString(), exception = it, ), ) } val hasMods = - launcherDataSource.hasMods(launcherInstanceDirectory = launcherInstanceDirectory).getOrElse { + launcherDataSource.hasMods(launcherInstanceDirectoryPath = launcherInstanceDirectoryPath).getOrElse { return ModsConvertResult.Failure( error = ModsConvertError.ModsAvailabilityCheckError( - message = it.message.toString(), + message = it.toString(), exception = it, ), ) @@ -67,12 +68,12 @@ class ModsConverterImpl : ModsConverter { val isCurseForgeApiRequestNeeded = launcherDataSource - .isCurseForgeApiRequestNeededForConvertingMods(launcherInstanceDirectory) + .isCurseForgeApiRequestNeededForConvertingMods(launcherInstanceDirectoryPath) .getOrElse { return ModsConvertResult.Failure( error = ModsConvertError.CurseForgeApiCheckError( - message = it.message.toString(), + message = it.toString(), exception = it, ), ) @@ -83,13 +84,13 @@ class ModsConverterImpl : ModsConverter { val mods = launcherDataSource .getLauncherInstanceMods( - launcherInstanceDirectory = launcherInstanceDirectory, + launcherInstanceDirectoryPath = launcherInstanceDirectoryPath, overrideCurseForgeApiKey = overrideCurseForgeApiKey?.ifBlank { null }, ).getOrElse { return ModsConvertResult.Failure( error = ModsConvertError.CouldNotConvertMods( - message = it.message.toString(), + message = it.toString(), exception = it, ), ) @@ -116,7 +117,9 @@ class ModsConverterImpl : ModsConverter { modsOutputText = modsOutputText, ) } catch (e: Exception) { - ModsConvertResult.Failure(error = ModsConvertError.UnknownError(e.message.toString(), e)) + ModsConvertResult.Failure( + error = ModsConvertError.UnknownError(e.toString(), e), + ) } } } diff --git a/admin/src/main/kotlin/services/syncScriptInstaller/SyncScriptInstallationConfig.kt b/admin/src/main/kotlin/services/syncScriptInstaller/SyncScriptInstallationConfig.kt index 1adb2d8..b308caf 100644 --- a/admin/src/main/kotlin/services/syncScriptInstaller/SyncScriptInstallationConfig.kt +++ b/admin/src/main/kotlin/services/syncScriptInstaller/SyncScriptInstallationConfig.kt @@ -8,7 +8,7 @@ sealed class SyncScriptInstallationConfig { /** * Null if the user canceled the operation * */ - val getSyncScriptJarFilePath: () -> String?, + val getSyncScriptJarFilePathString: () -> String?, ) : SyncScriptInstallationConfig() /** diff --git a/admin/src/main/kotlin/services/syncScriptInstaller/SyncScriptInstallationError.kt b/admin/src/main/kotlin/services/syncScriptInstaller/SyncScriptInstallationError.kt index a2f8ba1..f8590cf 100644 --- a/admin/src/main/kotlin/services/syncScriptInstaller/SyncScriptInstallationError.kt +++ b/admin/src/main/kotlin/services/syncScriptInstaller/SyncScriptInstallationError.kt @@ -14,9 +14,15 @@ sealed class SyncScriptInstallationError { data object SyncScriptJarFileNotFound : SyncScriptInstallationError() - data object CouldNotDeleteSyncScriptJarFileWhileUninstall : SyncScriptInstallationError() + data class CouldNotDeleteSyncScriptJarFileWhileUninstall( + val message: String, + val exception: Exception, + ) : SyncScriptInstallationError() - data object CouldNotDeleteSyncScriptDataWhileUninstall : SyncScriptInstallationError() + data class CouldNotDeleteSyncScriptDataWhileUninstall( + val message: String, + val exception: Exception, + ) : SyncScriptInstallationError() data class CouldNotSetPreLaunchCommand( val message: String, diff --git a/admin/src/main/kotlin/services/syncScriptInstaller/SyncScriptInstaller.kt b/admin/src/main/kotlin/services/syncScriptInstaller/SyncScriptInstaller.kt index 0ac3c38..30f3e5f 100644 --- a/admin/src/main/kotlin/services/syncScriptInstaller/SyncScriptInstaller.kt +++ b/admin/src/main/kotlin/services/syncScriptInstaller/SyncScriptInstaller.kt @@ -9,7 +9,7 @@ interface SyncScriptInstaller { suspend fun configureInstallation( installationConfig: SyncScriptInstallationConfig, launcher: MinecraftLauncher, - launcherInstanceDirectoryPath: String, + launcherInstanceDirectoryPathString: String, confirmReplaceExistingPreLaunchCommand: Boolean, ): SyncScriptInstallationResult } diff --git a/admin/src/main/kotlin/services/syncScriptInstaller/SyncScriptInstallerImpl.kt b/admin/src/main/kotlin/services/syncScriptInstaller/SyncScriptInstallerImpl.kt index 09ef2f4..48b9871 100644 --- a/admin/src/main/kotlin/services/syncScriptInstaller/SyncScriptInstallerImpl.kt +++ b/admin/src/main/kotlin/services/syncScriptInstaller/SyncScriptInstallerImpl.kt @@ -5,23 +5,27 @@ import constants.ProjectInfoConstants import launchers.LauncherDataSource import launchers.LauncherDataSourceFactory import launchers.MinecraftLauncher -import java.io.File +import utils.deleteRecursivelyWithLegacyJavaIo +import java.nio.file.Paths +import kotlin.io.path.copyTo +import kotlin.io.path.deleteIfExists +import kotlin.io.path.exists class SyncScriptInstallerImpl : SyncScriptInstaller { override suspend fun configureInstallation( installationConfig: SyncScriptInstallationConfig, launcher: MinecraftLauncher, - launcherInstanceDirectoryPath: String, + launcherInstanceDirectoryPathString: String, confirmReplaceExistingPreLaunchCommand: Boolean, ): SyncScriptInstallationResult { return try { - if (launcherInstanceDirectoryPath.isBlank()) { + if (launcherInstanceDirectoryPathString.isBlank()) { return SyncScriptInstallationResult.Failure( error = SyncScriptInstallationError.EmptyLauncherInstanceDirectoryPath, ) } - val launcherInstanceDirectory = File(launcherInstanceDirectoryPath) - if (!launcherInstanceDirectory.exists()) { + val launcherInstanceDirectoryPath = Paths.get(launcherInstanceDirectoryPathString) + if (!launcherInstanceDirectoryPath.exists()) { return SyncScriptInstallationResult.Failure( error = SyncScriptInstallationError.LauncherInstanceDirectoryNotFound, ) @@ -30,53 +34,66 @@ class SyncScriptInstallerImpl : SyncScriptInstaller { launcherDataSource .validateInstanceDirectory( - launcherInstanceDirectory = launcherInstanceDirectory, + launcherInstanceDirectoryPath = launcherInstanceDirectoryPath, ).getOrElse { return SyncScriptInstallationResult.Failure( error = SyncScriptInstallationError.InvalidLauncherInstanceDirectory( - message = it.message.toString(), + message = it.toString(), exception = it, ), ) } val newSyncScriptJarFileName = "${ProjectInfoConstants.NORMALIZED_NAME}.jar" - val syncScriptJarFile = launcherInstanceDirectory.resolve(newSyncScriptJarFileName) + val newSyncScriptJarFilePath = launcherInstanceDirectoryPath.resolve(newSyncScriptJarFileName) when (installationConfig) { is SyncScriptInstallationConfig.Install -> { - val providedSyncScriptJarFilePath = - installationConfig.getSyncScriptJarFilePath() ?: return SyncScriptInstallationResult.Cancelled - if (providedSyncScriptJarFilePath.isBlank()) { + val syncScriptJarFilePathString = + installationConfig.getSyncScriptJarFilePathString() + ?: return SyncScriptInstallationResult.Cancelled + if (syncScriptJarFilePathString.isBlank()) { return SyncScriptInstallationResult.Failure( error = SyncScriptInstallationError.EmptySyncScriptJarFilePath, ) } - val providedSyncScriptJarFile = File(providedSyncScriptJarFilePath) - if (!providedSyncScriptJarFile.exists()) { + val providedSyncScriptJarFilePath = Paths.get(syncScriptJarFilePathString) + if (!providedSyncScriptJarFilePath.exists()) { return SyncScriptInstallationResult.Failure( error = SyncScriptInstallationError.SyncScriptJarFileNotFound, ) } - providedSyncScriptJarFile.copyTo( - syncScriptJarFile, + providedSyncScriptJarFilePath.copyTo( + newSyncScriptJarFilePath, overwrite = true, ) } SyncScriptInstallationConfig.UnInstall -> { - val isSyncScriptJarFileDeleted = syncScriptJarFile.delete() - if (!isSyncScriptJarFileDeleted && syncScriptJarFile.exists()) { + try { + newSyncScriptJarFilePath.deleteIfExists() + } catch (e: Exception) { return SyncScriptInstallationResult.Failure( - error = SyncScriptInstallationError.CouldNotDeleteSyncScriptJarFileWhileUninstall, + error = + SyncScriptInstallationError.CouldNotDeleteSyncScriptJarFileWhileUninstall( + message = e.toString(), + exception = e, + ), ) } - val isSyncScriptDataExist = - File(launcherInstanceDirectory, DotMinecraftFileNames.SYNC_SCRIPT_DIRECTORY).deleteRecursively() - if (!isSyncScriptDataExist) { + + try { + launcherInstanceDirectoryPath + .resolve(DotMinecraftFileNames.SYNC_SCRIPT_DIRECTORY) + .deleteRecursivelyWithLegacyJavaIo() + } catch (e: Exception) { return SyncScriptInstallationResult.Failure( - error = SyncScriptInstallationError.CouldNotDeleteSyncScriptDataWhileUninstall, + error = + SyncScriptInstallationError.CouldNotDeleteSyncScriptDataWhileUninstall( + message = e.toString(), + exception = e, + ), ) } } @@ -96,7 +113,7 @@ class SyncScriptInstallerImpl : SyncScriptInstaller { val currentCommand = launcherDataSource - .getPreLaunchCommand(launcherInstanceDirectory = launcherInstanceDirectory) + .getPreLaunchCommand(launcherInstanceDirectoryPath = launcherInstanceDirectoryPath) .getOrThrow() if ((currentCommand != null && currentCommand != newCommand) && !confirmReplaceExistingPreLaunchCommand) { @@ -109,12 +126,12 @@ class SyncScriptInstallerImpl : SyncScriptInstaller { launcherDataSource .setPreLaunchCommand( command = newCommandToSet, - launcherInstanceDirectory = launcherInstanceDirectory, + launcherInstanceDirectoryPath = launcherInstanceDirectoryPath, ).getOrElse { return SyncScriptInstallationResult.Failure( error = SyncScriptInstallationError.CouldNotSetPreLaunchCommand( - message = it.message.toString(), + message = it.toString(), exception = it, ), ) @@ -124,7 +141,7 @@ class SyncScriptInstallerImpl : SyncScriptInstaller { SyncScriptInstallationResult.Failure( error = SyncScriptInstallationError.UnknownError( - message = e.message.toString(), + message = e.toString(), exception = e, ), ) diff --git a/admin/src/main/kotlin/utils/SystemFileProvider.kt b/admin/src/main/kotlin/utils/SystemFileProvider.kt index 37bc5dc..f3994a8 100644 --- a/admin/src/main/kotlin/utils/SystemFileProvider.kt +++ b/admin/src/main/kotlin/utils/SystemFileProvider.kt @@ -1,14 +1,15 @@ package utils import utils.os.OperatingSystem -import java.io.File +import java.nio.file.Path import java.nio.file.Paths +import kotlin.io.path.exists object SystemFileProvider { /** * @return Get the directory where the applications store their data for the current [OperatingSystem] * */ - private fun getUserApplicationDataRootDirectory(): Result { + private fun getUserApplicationDataRootDirectory(): Result { return try { val directory = when (OperatingSystem.current) { @@ -18,7 +19,7 @@ object SystemFileProvider { SystemInfoProvider.getUserHomeDirectoryPath(), ".local", "share", - ).toFile() + ) OperatingSystem.MacOS -> Paths @@ -26,14 +27,14 @@ object SystemFileProvider { SystemInfoProvider.getUserHomeDirectoryPath(), "Library", "Application Support", - ).toFile() + ) OperatingSystem.Windows -> SystemInfoProvider.getWindowsAppDataDirectory()?.let { Paths .get( it, - ).toFile() + ) } ?: throw IllegalStateException("Windows APPDATA environment variable is not set") OperatingSystem.Unknown -> { @@ -59,7 +60,7 @@ object SystemFileProvider { * @param applicationDirectoryName The name of the application directory, it depends on the application * and might not be the application name, or it could with a slightly different name * */ - fun getUserApplicationDataDirectory(applicationDirectoryName: String): Result { + fun getUserApplicationDataDirectory(applicationDirectoryName: String): Result { return try { val directory = getUserApplicationDataRootDirectory().getOrThrow()?.resolve(applicationDirectoryName) if (directory?.exists() == false) { @@ -71,7 +72,7 @@ object SystemFileProvider { } } - private fun getFlatpakApplicationDataDirectory(flatpakApplicationId: String): Result { + private fun getFlatpakApplicationDataDirectory(flatpakApplicationId: String): Result { if (!OperatingSystem.current.isLinux()) { return Result.failure( UnsupportedOperationException( @@ -82,8 +83,7 @@ object SystemFileProvider { } return Result.success( Paths - .get(SystemInfoProvider.getUserHomeDirectoryPath(), ".var", "app", flatpakApplicationId, "data") - .toFile(), + .get(SystemInfoProvider.getUserHomeDirectoryPath(), ".var", "app", flatpakApplicationId, "data"), ) } @@ -94,7 +94,7 @@ object SystemFileProvider { fun getUserApplicationDataDirectoryWithFlatpakSupport( applicationDirectoryName: String, flatpakApplicationId: String, - ): Result> { + ): Result> { return try { val directory = getUserApplicationDataDirectory(applicationDirectoryName = applicationDirectoryName).getOrThrow() diff --git a/common/src/main/kotlin/gui/utils/ThemeDetector.kt b/common/src/main/kotlin/gui/utils/ThemeDetector.kt index 9b4820f..6bd8723 100644 --- a/common/src/main/kotlin/gui/utils/ThemeDetector.kt +++ b/common/src/main/kotlin/gui/utils/ThemeDetector.kt @@ -6,6 +6,9 @@ import utils.os.LinuxDesktopEnvironment import utils.os.OperatingSystem import utils.powerShellCommandLine import java.nio.file.Paths +import kotlin.io.path.bufferedReader +import kotlin.io.path.name +import kotlin.io.path.pathString object ThemeDetector { private fun LinuxDesktopEnvironment.Companion.isSystemInDarkMode(): Boolean { @@ -45,10 +48,10 @@ object ThemeDetector { Paths.get( SystemInfoProvider.getUserHomeDirectoryPath(), ".config/kdeglobals", - ).toFile() + ) println( "\uD83D\uDCC4 Reading the following file to check if the KDE Plasma desktop environment " + - "is in dark mode: ${kdeGlobalsFile.path}", + "is in dark mode: ${kdeGlobalsFile.pathString}", ) return try { val lookAndFeelPackageName = @@ -143,8 +146,7 @@ object ThemeDetector { "-g", "AppleInterfaceStyle", reasonOfRunningTheCommand = "to check if the macOS system is in dark mode", - ) - .getOrDefault("") + ).getOrDefault("") if (appleInterfaceStyleOutput.contains("dark", ignoreCase = true)) { return true } diff --git a/common/src/main/kotlin/utils/CommandLine.kt b/common/src/main/kotlin/utils/CommandLine.kt index c86d522..822283f 100644 --- a/common/src/main/kotlin/utils/CommandLine.kt +++ b/common/src/main/kotlin/utils/CommandLine.kt @@ -1,8 +1,9 @@ package utils import utils.os.OperatingSystem -import java.io.File +import java.nio.file.Path import java.util.concurrent.TimeUnit +import kotlin.io.path.absolutePathString /** * Run a command in the command line of the system @@ -81,9 +82,9 @@ fun powerShellCommandLine( * the code execution of the bat script will continue to work even if the application has been closed * @throws IllegalStateException If the current operating system is not [OperatingSystem.Windows] * */ -fun executeBatchScriptInSeparateWindow(batScriptFile: File) { +fun executeBatchScriptInSeparateWindow(batScriptFilePath: Path) { if (!OperatingSystem.current.isWindows()) { throw IllegalStateException("Bat script can be only executed on Windows.") } - ProcessBuilder("cmd", "/c", "start", batScriptFile.absolutePath).start() + ProcessBuilder("cmd", "/c", "start", batScriptFilePath.absolutePathString()).start() } diff --git a/common/src/main/kotlin/utils/SharedFilePathUtils.kt b/common/src/main/kotlin/utils/SharedFilePathUtils.kt new file mode 100644 index 0000000..7f0fca2 --- /dev/null +++ b/common/src/main/kotlin/utils/SharedFilePathUtils.kt @@ -0,0 +1,33 @@ +package utils + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.nio.file.FileSystemException +import java.nio.file.Files +import java.nio.file.Path +import kotlin.io.path.name +import kotlin.streams.toList + +fun Path.deleteRecursivelyWithLegacyJavaIo() { + if (!toFile().deleteRecursively()) { + throw FileSystemException("Could not delete the directory '$name' recursively.") + } +} + +suspend fun Path.listFilteredPaths(filter: (path: Path) -> Boolean): Result> = + try { + val paths = + withContext(Dispatchers.IO) { + Files + .list(this@listFilteredPaths) + .use { stream -> + stream + .filter { path -> + filter(path) + }.toList() + } + } + Result.success(paths) + } catch (e: Exception) { + Result.failure(e) + } diff --git a/common/src/main/kotlin/utils/SharedUtils.kt b/common/src/main/kotlin/utils/SharedUtils.kt index 1cc186b..4b99425 100644 --- a/common/src/main/kotlin/utils/SharedUtils.kt +++ b/common/src/main/kotlin/utils/SharedUtils.kt @@ -1,10 +1,10 @@ package utils import kotlinx.serialization.json.Json -import java.io.File -import java.io.InputStream import java.net.URL import java.net.URLDecoder +import java.nio.file.Path +import kotlin.io.path.toPath /** * Extracts the file name from a public download URL. @@ -35,25 +35,14 @@ fun getFileNameFromUrl(url: String): Result { } } -fun getClassLoader(): ClassLoader { - return object {}.javaClass.classLoader -} +fun getClassLoader(): ClassLoader = object {}.javaClass.classLoader -fun getResourceAsURLOrThrow(resourceName: String): URL { - return getClassLoader().getResource(resourceName) ?: throw IllegalStateException( +fun getResourceAsURLOrThrow(resourceName: String): URL = + getClassLoader().getResource(resourceName) ?: throw IllegalStateException( "Could not get the resource with name $resourceName from the resources, double check if it exist.", ) -} - -fun getResourceAsFileOrThrow(resourceName: String): File { - return File(getResourceAsURLOrThrow(resourceName).toURI()) -} -fun getResourceAsStreamOrThrow(resourceName: String): InputStream { - return getClassLoader().getResourceAsStream(resourceName) ?: throw IllegalStateException( - "Could not get the resource with name $resourceName from the resources, double check if it exist.", - ) -} +fun getResourceAsPathOrThrow(resourceName: String): Path = getResourceAsURLOrThrow(resourceName).toURI().toPath() fun String.isValidUrl(): Boolean { val urlPattern = Regex("""^(https?://)?([\w\-.]+)+(:\d+)?(/[-._~/\w:@?=%&$+,()!*]*)?""") diff --git a/common/src/test/kotlin/utils/SharedUtilsTest.kt b/common/src/test/kotlin/utils/SharedUtilsTest.kt index 2591466..44fee8d 100644 --- a/common/src/test/kotlin/utils/SharedUtilsTest.kt +++ b/common/src/test/kotlin/utils/SharedUtilsTest.kt @@ -3,6 +3,7 @@ package utils import constants.SharedAssetConstants import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows +import kotlin.io.path.exists import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -73,12 +74,12 @@ class SharedUtilsTest { } @Test - fun `test getResourceAsFile`() { + fun `test getResourceAsFilePathOrThrow`() { assertTrue( - getResourceAsFileOrThrow(SharedAssetConstants.PROJECT_ICON_FILE_NAME).exists(), + getResourceAsPathOrThrow(SharedAssetConstants.PROJECT_ICON_FILE_NAME).exists(), ) assertThrows { - getResourceAsFileOrThrow("file-does-not-exist").exists() + getResourceAsPathOrThrow("file-does-not-exist").exists() } } } diff --git a/dev-local-server/src/main/kotlin/Main.kt b/dev-local-server/src/main/kotlin/Main.kt index 7779479..844d63d 100644 --- a/dev-local-server/src/main/kotlin/Main.kt +++ b/dev-local-server/src/main/kotlin/Main.kt @@ -1,12 +1,13 @@ import com.sun.net.httpserver.HttpServer -import java.io.File import java.io.OutputStream import java.net.InetSocketAddress import java.net.NetworkInterface import java.nio.file.Files import java.nio.file.Paths import kotlin.concurrent.thread +import kotlin.io.path.exists import kotlin.io.path.isRegularFile +import kotlin.io.path.readText import kotlin.streams.toList fun main() { @@ -19,9 +20,9 @@ fun main() { when { exchange.requestMethod.equals("GET", ignoreCase = true) -> { val path = exchange.requestURI.path - val requestedFile = File(path.substring(1)) + val requestedFile = Paths.get(path.substring(1)) - if (requestedFile.exists() && requestedFile.isFile) { + if (requestedFile.exists() && requestedFile.isRegularFile()) { val responseText = requestedFile.readText() exchange.sendResponseHeaders(200, responseText.toByteArray().size.toLong()) val outputStream: OutputStream = exchange.responseBody diff --git a/sync-script/build.gradle.kts b/sync-script/build.gradle.kts index e47f2b2..ada7b0c 100644 --- a/sync-script/build.gradle.kts +++ b/sync-script/build.gradle.kts @@ -1,6 +1,7 @@ -import java.io.FileNotFoundException +import java.nio.file.NoSuchFileException import java.nio.file.Paths import java.util.jar.JarFile +import kotlin.io.path.exists import kotlin.math.abs as kotlinMathAbs plugins { @@ -125,10 +126,9 @@ val minimizedJar = // Starting from Java 9, runtime classes are packaged in modular JMOD files. fun includeModuleFromJdk(jModFileNameWithoutExtension: String) { - val jModFilePath = Paths.get(javaHome, "jmods", "$jModFileNameWithoutExtension.jmod").toString() - val jModFile = File(jModFilePath) - if (!jModFile.exists()) { - throw FileNotFoundException("The '$jModFileNameWithoutExtension' at '$jModFilePath' doesn't exist.") + val jModFilePath = Paths.get(javaHome, "jmods", "$jModFileNameWithoutExtension.jmod") + if (!jModFilePath.exists()) { + throw NoSuchFileException("The '$jModFileNameWithoutExtension' at '$jModFilePath' doesn't exist.") } libraryjars( mapOf("jarfilter" to "!**.jar", "filter" to "!module-info.class"), diff --git a/sync-script/src/main/kotlin/JarAutoUpdater.kt b/sync-script/src/main/kotlin/JarAutoUpdater.kt index cc947a4..4e97d52 100644 --- a/sync-script/src/main/kotlin/JarAutoUpdater.kt +++ b/sync-script/src/main/kotlin/JarAutoUpdater.kt @@ -7,26 +7,35 @@ import okhttp3.Request import utils.FileDownloader import utils.HttpService import utils.SystemInfoProvider +import utils.createFileWithParentDirectoriesOrTerminate +import utils.deleteExistingOrTerminate import utils.executeAsync import utils.executeBatchScriptInSeparateWindow import utils.getBodyOrThrow -import utils.getRunningJarFileAsUrl +import utils.getRunningJarFilePath +import utils.moveToOrTerminate import utils.os.OperatingSystem import utils.terminateWithOrWithoutError -import java.io.File +import java.nio.file.Path +import kotlin.io.path.absolutePathString +import kotlin.io.path.exists +import kotlin.io.path.writeText object JarAutoUpdater { - private suspend fun downloadLatestJarFile(): Result = + private suspend fun downloadLatestJarFile(): Result = try { val newJarFile = - SyncScriptDotMinecraftFiles.SyncScriptData.Temp.file + SyncScriptDotMinecraftFiles.SyncScriptData.Temp.path .resolve("${ProjectInfoConstants.NORMALIZED_NAME}-new.jar") if (newJarFile.exists()) { - newJarFile.delete() + newJarFile.deleteExistingOrTerminate( + fileEntityType = "JAR", + reasonOfDelete = "the script is downloading the new update", + ) } FileDownloader( downloadUrl = ProjectInfoConstants.LATEST_SYNC_SCRIPT_JAR_FILE_URL, - targetFile = newJarFile, + targetFilePath = newJarFile, progressListener = { _, _, _ -> }, ).downloadFile() Result.success(newJarFile) @@ -63,14 +72,12 @@ object JarAutoUpdater { } suspend fun updateIfAvailable() { - val currentRunningJarFile = - File( - getRunningJarFileAsUrl() - .getOrElse { - println("⚠\uFE0F Auto update feature is only supported when running using JAR.") - return - }.file, - ) + val currentRunningJarFilePath = + getRunningJarFilePath() + .getOrElse { + println("⚠\uFE0F Auto update feature is only supported when running using JAR.") + return + } val latestProjectVersion = getLatestProjectVersion().getOrElse { println("❌ We couldn't get the latest project version: ${it.message}") @@ -94,21 +101,28 @@ object JarAutoUpdater { } println("ℹ\uFE0F The new update has been downloaded, will close the application.") updateApplication( - currentRunningJarFile = currentRunningJarFile, - newJarFile = newJarFile, + currentRunningJarFilePath = currentRunningJarFilePath, + newJarFilePath = newJarFile, ) } private suspend fun updateApplication( - currentRunningJarFile: File, - newJarFile: File, + currentRunningJarFilePath: Path, + newJarFilePath: Path, ) { when (OperatingSystem.current) { OperatingSystem.Linux, OperatingSystem.MacOS -> { Runtime.getRuntime().addShutdownHook( Thread { - currentRunningJarFile.delete() - newJarFile.renameTo(currentRunningJarFile) + currentRunningJarFilePath.deleteExistingOrTerminate( + fileEntityType = "JAR", + reasonOfDelete = "the script is deleting is deleting the current JAR file to use the updated one", + ) + newJarFilePath.moveToOrTerminate( + target = currentRunningJarFilePath, + overwrite = true, + fileEntityType = "JAR", + ) }, ) } @@ -116,24 +130,23 @@ object JarAutoUpdater { OperatingSystem.Windows -> { // On Windows, we can't rename, delete or modify the current running JAR file due to file locking val updateBatScriptFile = - SyncScriptDotMinecraftFiles.SyncScriptData.Temp.file + SyncScriptDotMinecraftFiles.SyncScriptData.Temp.path .resolve("update.bat") withContext(Dispatchers.IO) { - updateBatScriptFile.parentFile.mkdirs() - updateBatScriptFile.createNewFile() + updateBatScriptFile.createFileWithParentDirectoriesOrTerminate() } updateBatScriptFile.writeText( """ @echo off echo Waiting for 2 seconds to ensure application closure... timeout /t 2 > nul - del "${currentRunningJarFile.absolutePath}" - move "${newJarFile.absolutePath}" "${currentRunningJarFile.absolutePath}" + del "${currentRunningJarFilePath.absolutePathString()}" + move "${newJarFilePath.absolutePathString()}" "${currentRunningJarFilePath.absolutePathString()}" exit """.trimIndent(), ) executeBatchScriptInSeparateWindow( - batScriptFile = updateBatScriptFile, + batScriptFilePath = updateBatScriptFile, ) } diff --git a/sync-script/src/main/kotlin/Main.kt b/sync-script/src/main/kotlin/Main.kt index 1df6018..b198811 100644 --- a/sync-script/src/main/kotlin/Main.kt +++ b/sync-script/src/main/kotlin/Main.kt @@ -23,11 +23,15 @@ import syncService.SyncService import utils.ExecutionTimer import utils.HttpService import utils.SystemInfoProvider +import utils.createFileWithParentDirectoriesOrTerminate +import utils.deleteRecursivelyWithLegacyJavaIo import utils.os.LinuxDesktopEnvironment import utils.os.OperatingSystem import utils.showErrorMessageAndTerminate import utils.terminateWithOrWithoutError import java.awt.GraphicsEnvironment +import kotlin.io.path.exists +import kotlin.io.path.pathString import kotlin.system.exitProcess val scriptConfigDataSource: ScriptConfigDataSource = LocalJsonScriptConfigDataSource() @@ -59,13 +63,13 @@ suspend fun main(args: Array) { }.also { println(it) } } - SyncScriptDotMinecraftFiles.SyncScriptData.Temp.file.apply { + SyncScriptDotMinecraftFiles.SyncScriptData.Temp.path.apply { if (exists()) { println( - "ℹ\uFE0F The temporary folder: $path exist. " + + "ℹ\uFE0F The temporary folder: $pathString exist. " + "The script might not finished last time. Removing the folder.", ) - deleteRecursively() + deleteRecursivelyWithLegacyJavaIo() } } @@ -88,7 +92,7 @@ suspend fun main(args: Array) { // Loading the script config file from json file - val scriptConfigFile = SyncScriptDotMinecraftFiles.SyncScriptData.ScriptConfig.file + val scriptConfigFile = SyncScriptDotMinecraftFiles.SyncScriptData.ScriptConfig.path if (!scriptConfigFile.exists()) { if (GuiState.isGuiEnabled) { println( @@ -110,7 +114,7 @@ suspend fun main(args: Array) { title = "Configuration Missing! ⚠\uFE0F", message = """ - The script configuration file, `${scriptConfigFile.path}`, couldn't be found. + The script configuration file `${scriptConfigFile.pathString}` couldn't be found. To get started, create this file in the same directory where you're running the script or in the working directory. @@ -128,7 +132,7 @@ suspend fun main(args: Array) { title = "Configuration Error ⚠\uFE0F", message = buildString { - append("An error occurred while parsing your script configuration file (${scriptConfigFile.path})\n\n") + append("An error occurred while parsing your script configuration file (${scriptConfigFile.pathString})\n\n") append("Ensure it's valid JSON format.\n\n") append("Error details: ${it.message?.trim()}") }, @@ -157,11 +161,8 @@ suspend fun main(args: Array) { } // TODO: Plan if we should implement this in non GUI mode - val isPreferencesConfiguredFile = SyncScriptDotMinecraftFiles.SyncScriptData.IsPreferencesConfigured.file - if (GuiState.isGuiEnabled && - !isPreferencesConfiguredFile - .exists() - ) { + val isPreferencesConfiguredFilePath = SyncScriptDotMinecraftFiles.SyncScriptData.IsPreferencesConfigured.path + if (GuiState.isGuiEnabled && !isPreferencesConfiguredFilePath.exists()) { val newScriptConfig = QuickPreferencesDialog().showDialog() scriptConfigDataSource.replaceConfig(newScriptConfig).getOrElse { @@ -178,17 +179,7 @@ suspend fun main(args: Array) { GuiState.updateIsGuiEnabled() withContext(Dispatchers.IO) { - val wasFileCreated = - isPreferencesConfiguredFile - .createNewFile() - if (!wasFileCreated) { - showErrorMessageAndTerminate( - title = "📄 File Already Exists", - message = - "⚠️ The file '${isPreferencesConfiguredFile.name}' already exists. We're unable to create it. " + - "This might be a bug, a workaround is to delete '${isPreferencesConfiguredFile.path}'.", - ) - } + isPreferencesConfiguredFilePath.createFileWithParentDirectoriesOrTerminate() } } @@ -261,10 +252,10 @@ suspend fun main(args: Array) { // The temporary folder usually contains the downloaded files which will be moved once finished // after finish syncing the contents successfully, we don't need it anymore. - SyncScriptDotMinecraftFiles.SyncScriptData.Temp.file.apply { + SyncScriptDotMinecraftFiles.SyncScriptData.Temp.path.apply { if (exists()) { - println("\uD83D\uDEAB Deleting the temporary folder: '$path' (no longer needed).") - deleteRecursively() + println("\uD83D\uDEAB Deleting the temporary folder: '$pathString' (no longer needed).") + deleteRecursivelyWithLegacyJavaIo() } } diff --git a/sync-script/src/main/kotlin/config/data/LocalJsonScriptConfigDataSource.kt b/sync-script/src/main/kotlin/config/data/LocalJsonScriptConfigDataSource.kt index 934c731..a90b88f 100644 --- a/sync-script/src/main/kotlin/config/data/LocalJsonScriptConfigDataSource.kt +++ b/sync-script/src/main/kotlin/config/data/LocalJsonScriptConfigDataSource.kt @@ -6,6 +6,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import utils.JsonPrettyPrint +import utils.createParentDirectoriesIfDoesNotExist +import kotlin.io.path.readText +import kotlin.io.path.writeText class LocalJsonScriptConfigDataSource : ScriptConfigDataSource { override suspend fun getConfig(): Result = @@ -13,7 +16,7 @@ class LocalJsonScriptConfigDataSource : ScriptConfigDataSource { val scriptConfig = withContext(Dispatchers.IO) { Json.decodeFromString( - SyncScriptDotMinecraftFiles.SyncScriptData.ScriptConfig.file + SyncScriptDotMinecraftFiles.SyncScriptData.ScriptConfig.path .readText(), ) } @@ -25,10 +28,8 @@ class LocalJsonScriptConfigDataSource : ScriptConfigDataSource { override suspend fun replaceConfig(scriptConfig: ScriptConfig): Result = try { withContext(Dispatchers.IO) { - val configFile = SyncScriptDotMinecraftFiles.SyncScriptData.ScriptConfig.file - if (!configFile.parentFile.exists()) { - configFile.parentFile.mkdirs() - } + val configFile = SyncScriptDotMinecraftFiles.SyncScriptData.ScriptConfig.path + configFile.createParentDirectoriesIfDoesNotExist() configFile.writeText(JsonPrettyPrint.encodeToString(ScriptConfig.serializer(), scriptConfig)) } Result.success(Unit) diff --git a/sync-script/src/main/kotlin/constants/SyncScriptDotMinecraftFiles.kt b/sync-script/src/main/kotlin/constants/SyncScriptDotMinecraftFiles.kt index 6cc534f..ebbfd32 100644 --- a/sync-script/src/main/kotlin/constants/SyncScriptDotMinecraftFiles.kt +++ b/sync-script/src/main/kotlin/constants/SyncScriptDotMinecraftFiles.kt @@ -1,6 +1,7 @@ package constants -import java.io.File +import java.nio.file.Path +import java.nio.file.Paths /** * Files of the minecraft instance (e.g.,`.minecraft`) where it has the mods, resource-packs and shaders @@ -9,44 +10,44 @@ import java.io.File * This should be only used by the script that syncs the content * */ sealed class SyncScriptDotMinecraftFiles( - val file: File, + val path: Path, ) { - data object Mods : SyncScriptDotMinecraftFiles(File(DotMinecraftFileNames.MODS_DIRECTORY)) + data object Mods : SyncScriptDotMinecraftFiles(Paths.get(DotMinecraftFileNames.MODS_DIRECTORY)) - data object ResourcePacks : SyncScriptDotMinecraftFiles(File(DotMinecraftFileNames.RESOURCE_PACKS_DIRECTORY)) + data object ResourcePacks : SyncScriptDotMinecraftFiles(Paths.get(DotMinecraftFileNames.RESOURCE_PACKS_DIRECTORY)) - data object ShaderPacks : SyncScriptDotMinecraftFiles(File(DotMinecraftFileNames.SHADER_PACKS_DIRECTORY)) + data object ShaderPacks : SyncScriptDotMinecraftFiles(Paths.get(DotMinecraftFileNames.SHADER_PACKS_DIRECTORY)) - data object Config : SyncScriptDotMinecraftFiles(File(DotMinecraftFileNames.CONFIG_DIRECTORY)) + data object Config : SyncScriptDotMinecraftFiles(Paths.get(DotMinecraftFileNames.CONFIG_DIRECTORY)) - data object Saves : SyncScriptDotMinecraftFiles(File(DotMinecraftFileNames.SAVES_DIRECTORY)) + data object Saves : SyncScriptDotMinecraftFiles(Paths.get(DotMinecraftFileNames.SAVES_DIRECTORY)) /** * A txt file contains the minecraft options/settings or data like the key bindings, sound settings * enabled resource-packs, language and the minecraft video settings, and more * */ - data object Options : SyncScriptDotMinecraftFiles(File(DotMinecraftFileNames.OPTIONS_FILE)) + data object Options : SyncScriptDotMinecraftFiles(Paths.get(DotMinecraftFileNames.OPTIONS_FILE)) /** * A file that contains all the information about the list of servers * */ - data object ServersDat : SyncScriptDotMinecraftFiles(File(DotMinecraftFileNames.SERVERS_FILE)) + data object ServersDat : SyncScriptDotMinecraftFiles(Paths.get(DotMinecraftFileNames.SERVERS_FILE)) /** * This directory will be created and managed by the script, it's specific to it * */ - data object SyncScriptData : SyncScriptDotMinecraftFiles(File(DotMinecraftFileNames.SYNC_SCRIPT_DIRECTORY)) { + data object SyncScriptData : SyncScriptDotMinecraftFiles(Paths.get(DotMinecraftFileNames.SYNC_SCRIPT_DIRECTORY)) { /** * The script needs a config file that contains required data and other optional to configure it * */ - data object ScriptConfig : SyncScriptDotMinecraftFiles(File(SyncScriptData.file, "config.json")) + data object ScriptConfig : SyncScriptDotMinecraftFiles(SyncScriptData.path.resolve("config.json")) /** * A directory that contain the temporary files used by the script, * for example, when downloading a file, the content will be saved to this directory * and after finish downloading, the file will be moved into where it should. * */ - data object Temp : SyncScriptDotMinecraftFiles(File(SyncScriptData.file, "temp")) + data object Temp : SyncScriptDotMinecraftFiles(SyncScriptData.path.resolve("temp")) /** * A file will be used to indicate if the user did set up the preferences for using the script, and it will @@ -56,6 +57,6 @@ sealed class SyncScriptDotMinecraftFiles( * it later and launch the dialog again. * */ data object IsPreferencesConfigured : - SyncScriptDotMinecraftFiles(File(SyncScriptData.file, "isPreferencesConfigured")) + SyncScriptDotMinecraftFiles(SyncScriptData.path.resolve("isPreferencesConfigured")) } } diff --git a/sync-script/src/main/kotlin/gui/dialogs/LoadingIndicatorDialog.kt b/sync-script/src/main/kotlin/gui/dialogs/LoadingIndicatorDialog.kt index ddd5d18..64be858 100644 --- a/sync-script/src/main/kotlin/gui/dialogs/LoadingIndicatorDialog.kt +++ b/sync-script/src/main/kotlin/gui/dialogs/LoadingIndicatorDialog.kt @@ -65,7 +65,7 @@ class LoadingIndicatorDialog( preferredSize = Dimension(200, 20) padding(right = 12, left = 12, top = 12) }, - JLabel("Loading...").apply { + detailsLabel.apply { padding(top = 12, bottom = 12) }, JButton("Cancel").onClick { diff --git a/sync-script/src/main/kotlin/services/hashGenerator/HashGenerator.kt b/sync-script/src/main/kotlin/services/hashGenerator/HashGenerator.kt index c5af182..ad3c022 100644 --- a/sync-script/src/main/kotlin/services/hashGenerator/HashGenerator.kt +++ b/sync-script/src/main/kotlin/services/hashGenerator/HashGenerator.kt @@ -1,6 +1,6 @@ package services.hashGenerator -import java.io.File +import java.nio.file.Path interface HashGenerator { /** @@ -17,11 +17,11 @@ interface HashGenerator { * Calculates the MD5 checksum of a file and returns it as a hexadecimal string. * * - * @param file The file to calculate the SHA-1 checksum from. + * @param filePath The file to calculate the SHA-1 checksum from. * @return The MD5 checksum of the file as a hexadecimal string * @throws SecurityException If there is a security error during the process. */ - suspend fun generateMD5(file: File): Result + suspend fun generateMD5(filePath: Path): Result /** * Calculates the SHA-1 checksum of a file and returns it as a hexadecimal string. @@ -30,11 +30,11 @@ interface HashGenerator { * Java Cryptography Architecture (JCA), and returns the hash as a hexadecimal string. * If the file doesn't exist, the function returns null. * - * @param file The file to calculate the SHA-1 checksum from. + * @param filePath The file to calculate the SHA-1 checksum from. * @return The SHA-1 checksum of the file as a hexadecimal string * @throws SecurityException If there is a security error during the process. */ - suspend fun generateSHA1(file: File): Result + suspend fun generateSHA1(filePath: Path): Result /** * Calculates the SHA-256 checksum of a file and returns it as a hexadecimal string. @@ -43,12 +43,12 @@ interface HashGenerator { * Java Cryptography Architecture (JCA), and returns the hash as a hexadecimal string. * If the file doesn't exist, the function returns null. * - * @param file The file to calculate the SHA-256 checksum from. + * @param filePath The file to calculate the SHA-256 checksum from. * @return The SHA-256 checksum of the file as a hexadecimal string * @throws SecurityException If there is a security error during the process. * */ - suspend fun generateSHA256(file: File): Result + suspend fun generateSHA256(filePath: Path): Result /** * Calculates the SHA-512 checksum of a file and returns it as a hexadecimal string. @@ -57,11 +57,11 @@ interface HashGenerator { * Java Cryptography Architecture (JCA), and returns the hash as a hexadecimal string. * If the file doesn't exist, the function returns null. * - * @param file The file to calculate the SHA-512 checksum from. + * @param filePath The file to calculate the SHA-512 checksum from. * @return The SHA-512 checksum of the file as a hexadecimal string * @throws SecurityException If there is a security error during the process. * * @author https://www.baeldung.com/sha-256-hashing-java */ - suspend fun generateSHA512(file: File): Result + suspend fun generateSHA512(filePath: Path): Result } diff --git a/sync-script/src/main/kotlin/services/hashGenerator/JavaMessageDigestHashGenerator.kt b/sync-script/src/main/kotlin/services/hashGenerator/JavaMessageDigestHashGenerator.kt index 9d5500a..08521df 100644 --- a/sync-script/src/main/kotlin/services/hashGenerator/JavaMessageDigestHashGenerator.kt +++ b/sync-script/src/main/kotlin/services/hashGenerator/JavaMessageDigestHashGenerator.kt @@ -2,8 +2,9 @@ package services.hashGenerator import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import java.io.File +import java.nio.file.Path import java.security.MessageDigest +import kotlin.io.path.inputStream class JavaMessageDigestHashGenerator : HashGenerator { override suspend fun generateMD5(text: String): Result { @@ -17,12 +18,12 @@ class JavaMessageDigestHashGenerator : HashGenerator { } } - override suspend fun generateMD5(file: File): Result { + override suspend fun generateMD5(filePath: Path): Result { return try { val buffer = ByteArray(1024) val md = MessageDigest.getInstance("MD5") - file.inputStream().use { fileInputStream -> + filePath.inputStream().use { fileInputStream -> var bytesRead: Int while (fileInputStream.read(buffer).also { bytesRead = it } != -1) { md.update(buffer, 0, bytesRead) @@ -39,9 +40,9 @@ class JavaMessageDigestHashGenerator : HashGenerator { /** * @author https://www.baeldung.com/sha-256-hashing-java * */ - override suspend fun generateSHA1(file: File): Result { - return try { - file.inputStream().use { inputStream -> + override suspend fun generateSHA1(filePath: Path): Result = + try { + filePath.inputStream().use { inputStream -> val digest = MessageDigest.getInstance("SHA-1") val buffer = ByteArray(1024) var read: Int @@ -63,14 +64,13 @@ class JavaMessageDigestHashGenerator : HashGenerator { e.printStackTrace() Result.failure(e) } - } /** * @author https://www.baeldung.com/sha-256-hashing-java * */ - override suspend fun generateSHA256(file: File): Result { - return try { - file.inputStream().use { inputStream -> + override suspend fun generateSHA256(filePath: Path): Result = + try { + filePath.inputStream().use { inputStream -> val digest = MessageDigest.getInstance("SHA-256") val buffer = ByteArray(1024) var read: Int @@ -92,16 +92,15 @@ class JavaMessageDigestHashGenerator : HashGenerator { e.printStackTrace() Result.failure(e) } - } /** * @author https://www.baeldung.com/sha-256-hashing-java * */ - override suspend fun generateSHA512(file: File): Result { - return try { + override suspend fun generateSHA512(filePath: Path): Result = + try { withContext(Dispatchers.IO) { val digest = MessageDigest.getInstance("SHA-512") - file.inputStream().use { inputStream -> + filePath.inputStream().use { inputStream -> val byteBuffer = ByteArray(1024) var bytesRead: Int while (inputStream.read(byteBuffer).also { bytesRead = it } != -1) { @@ -115,5 +114,4 @@ class JavaMessageDigestHashGenerator : HashGenerator { e.printStackTrace() Result.failure(e) } - } } diff --git a/sync-script/src/main/kotlin/services/hashGenerator/OkioHashGenerator.kt b/sync-script/src/main/kotlin/services/hashGenerator/OkioHashGenerator.kt index d5e458d..68c70c4 100644 --- a/sync-script/src/main/kotlin/services/hashGenerator/OkioHashGenerator.kt +++ b/sync-script/src/main/kotlin/services/hashGenerator/OkioHashGenerator.kt @@ -5,11 +5,11 @@ import okio.HashingSource import okio.blackholeSink import okio.buffer import okio.source -import java.io.File +import java.nio.file.Path class OkioHashGenerator : HashGenerator { - override suspend fun generateMD5(text: String): Result { - return try { + override suspend fun generateMD5(text: String): Result = + try { HashingSink.md5(blackholeSink()).use { hashingSink -> hashingSink.buffer().use { bufferedSink -> bufferedSink.writeUtf8(text) @@ -21,11 +21,10 @@ class OkioHashGenerator : HashGenerator { e.printStackTrace() Result.failure(e) } - } - override suspend fun generateMD5(file: File): Result { - return try { - file.source().use { fileSource -> + override suspend fun generateMD5(filePath: Path): Result = + try { + filePath.source().use { fileSource -> HashingSource.md5(fileSource).use { hashingSource -> hashingSource.buffer().use { source -> source.readAll(blackholeSink()) @@ -37,11 +36,10 @@ class OkioHashGenerator : HashGenerator { e.printStackTrace() Result.failure(e) } - } - override suspend fun generateSHA1(file: File): Result { - return try { - file.source().use { fileSource -> + override suspend fun generateSHA1(filePath: Path): Result = + try { + filePath.source().use { fileSource -> HashingSource.sha1(fileSource).use { hashingSource -> hashingSource.buffer().use { source -> source.readAll(blackholeSink()) @@ -53,11 +51,10 @@ class OkioHashGenerator : HashGenerator { e.printStackTrace() Result.failure(e) } - } - override suspend fun generateSHA256(file: File): Result { - return try { - file.source().use { fileSource -> + override suspend fun generateSHA256(filePath: Path): Result = + try { + filePath.source().use { fileSource -> HashingSource.sha256(fileSource).use { hashingSource -> hashingSource.buffer().use { source -> source.readAll(blackholeSink()) @@ -69,11 +66,10 @@ class OkioHashGenerator : HashGenerator { e.printStackTrace() Result.failure(e) } - } - override suspend fun generateSHA512(file: File): Result { - return try { - file.source().use { fileSource -> + override suspend fun generateSHA512(filePath: Path): Result = + try { + filePath.source().use { fileSource -> HashingSource.sha512(fileSource).use { hashingSource -> hashingSource.buffer().use { source -> source.readAll(blackholeSink()) @@ -85,5 +81,4 @@ class OkioHashGenerator : HashGenerator { e.printStackTrace() Result.failure(e) } - } } diff --git a/sync-script/src/main/kotlin/services/minecraft/MinecraftOptionsManager.kt b/sync-script/src/main/kotlin/services/minecraft/MinecraftOptionsManager.kt index 58bbb1c..6c27818 100644 --- a/sync-script/src/main/kotlin/services/minecraft/MinecraftOptionsManager.kt +++ b/sync-script/src/main/kotlin/services/minecraft/MinecraftOptionsManager.kt @@ -4,21 +4,27 @@ import constants.SyncScriptDotMinecraftFiles import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import org.jetbrains.annotations.VisibleForTesting -import java.io.File +import java.nio.file.Path +import kotlin.io.path.bufferedWriter +import kotlin.io.path.exists +import kotlin.io.path.forEachLine +import kotlin.io.path.name +import kotlin.io.path.pathString +import kotlin.io.path.writeText /** * Helper class for reading [SyncScriptDotMinecraftFiles.Options] file which contains Minecraft settings * for reading and getting the properties or set them if it doesn't exist * */ object MinecraftOptionsManager { - private var optionsFile = SyncScriptDotMinecraftFiles.Options.file + private var optionsFilePath = SyncScriptDotMinecraftFiles.Options.path private val properties: MutableMap = mutableMapOf() private var isLoaded = false @VisibleForTesting - fun setOptionsFileForTests(file: File) { - optionsFile = file + fun setOptionsFilePathForTests(filePath: Path) { + optionsFilePath = filePath } @VisibleForTesting @@ -38,18 +44,20 @@ object MinecraftOptionsManager { } /** - * Load the [properties] from [optionsFile] + * Load the [properties] from [optionsFilePath] * - * @throws IllegalArgumentException If [optionsFile] doesn't exist - * @throws IndexOutOfBoundsException If the text of [optionsFile] is invalid + * @throws IllegalArgumentException If [optionsFilePath] doesn't exist + * @throws IndexOutOfBoundsException If the text of [optionsFilePath] is invalid * */ fun loadPropertiesFromFile(): Result = try { - require(optionsFile.exists()) { "The file ${optionsFile.name} doesn't exist in ${optionsFile.path}" } + require( + optionsFilePath.exists(), + ) { "The file ${optionsFilePath.name} doesn't exist in ${optionsFilePath.pathString}" } if (properties.isNotEmpty()) { properties.clear() } - optionsFile.forEachLine { line -> + optionsFilePath.forEachLine { line -> val (key, value) = line.split(":", limit = 2) val trimmedKey = key.trim() val trimmedValue = value.trim() @@ -72,7 +80,7 @@ object MinecraftOptionsManager { /** * Should call [loadPropertiesFromFile] before calling this - * @IllegalArgumentException If the property doesn't exist in [optionsFile] + * @IllegalArgumentException If the property doesn't exist in [optionsFilePath] * */ fun readProperty(property: Property): Result = try { @@ -82,7 +90,7 @@ object MinecraftOptionsManager { val propertyValue = properties[propertyKey] requireNotNull( propertyValue, - ) { "The key property $propertyKey doesn't exist in the ${optionsFile.name} in ${optionsFile.path}" } + ) { "The key property $propertyKey doesn't exist in the ${optionsFilePath.name} in ${optionsFilePath.pathString}" } println(properties) Result.success(propertyValue) } catch (e: Exception) { @@ -96,7 +104,7 @@ object MinecraftOptionsManager { fun clear() { properties.clear() - optionsFile.writeText(text = "") + optionsFilePath.writeText(text = "") } fun setProperty( @@ -108,7 +116,7 @@ object MinecraftOptionsManager { properties[property.key] = propertyValue - optionsFile.bufferedWriter().use { bufferedWriter -> + optionsFilePath.bufferedWriter().use { bufferedWriter -> properties.forEach { (key, value) -> bufferedWriter.appendLine("$key:$value") } diff --git a/sync-script/src/main/kotlin/syncInfo/models/FileIntegrityExtensions.kt b/sync-script/src/main/kotlin/syncInfo/models/FileIntegrityExtensions.kt index 0e6f49a..c6609a2 100644 --- a/sync-script/src/main/kotlin/syncInfo/models/FileIntegrityExtensions.kt +++ b/sync-script/src/main/kotlin/syncInfo/models/FileIntegrityExtensions.kt @@ -1,31 +1,32 @@ package syncInfo.models import services.hashGenerator.HashGeneratorInstance -import java.io.File +import java.nio.file.Path +import kotlin.io.path.fileSize /** * Support for using one, some or all of the ways for validating a file * @return if none of them are specified, return `null` as unknown * otherwise `true` or `false` * */ -private suspend fun FileIntegrityInfo.validateAll(file: File): Result { +private suspend fun FileIntegrityInfo.validateAll(filePath: Path): Result { return try { val validations = mutableListOf() if (sizeInBytes != null) { - validations.add(sizeInBytes == file.length()) + validations.add(sizeInBytes == filePath.fileSize()) } if (md5 != null) { - validations.add(md5 == HashGeneratorInstance.generateMD5(file).getOrThrow()) + validations.add(md5 == HashGeneratorInstance.generateMD5(filePath).getOrThrow()) } if (sha1 != null) { - validations.add(sha1 == HashGeneratorInstance.generateSHA1(file).getOrThrow()) + validations.add(sha1 == HashGeneratorInstance.generateSHA1(filePath).getOrThrow()) } if (sha256 != null) { - validations.add(sha256 == HashGeneratorInstance.generateSHA256(file).getOrThrow()) + validations.add(sha256 == HashGeneratorInstance.generateSHA256(filePath).getOrThrow()) } if (sha512 != null) { - validations.add(sha512 == HashGeneratorInstance.generateSHA512(file).getOrThrow()) + validations.add(sha512 == HashGeneratorInstance.generateSHA512(filePath).getOrThrow()) } if (validations.isEmpty()) { // If none of the values specified, return null as unknown @@ -42,74 +43,74 @@ private suspend fun FileIntegrityInfo.validateAll(file: File): Result * Only validate the file using the preferred file integrity method/option * */ private suspend fun FileIntegrityInfo.validateOnlyPreferredOption( - file: File, + filePath: Path, preferredFileVerificationOption: PreferredFileVerificationOption, ): Result { return try { val isValidFileIntegrity = when (preferredFileVerificationOption) { PreferredFileVerificationOption.Strong -> - sha512?.let { it == HashGeneratorInstance.generateSHA512(file).getOrThrow() } - ?: sha256?.let { it == HashGeneratorInstance.generateSHA256(file).getOrThrow() } - ?: sha1?.let { it == HashGeneratorInstance.generateSHA1(file).getOrThrow() } - ?: md5?.let { it == HashGeneratorInstance.generateMD5(file).getOrThrow() } - ?: sizeInBytes?.let { it == file.length() } + sha512?.let { it == HashGeneratorInstance.generateSHA512(filePath).getOrThrow() } + ?: sha256?.let { it == HashGeneratorInstance.generateSHA256(filePath).getOrThrow() } + ?: sha1?.let { it == HashGeneratorInstance.generateSHA1(filePath).getOrThrow() } + ?: md5?.let { it == HashGeneratorInstance.generateMD5(filePath).getOrThrow() } + ?: sizeInBytes?.let { it == filePath.fileSize() } PreferredFileVerificationOption.Medium -> - sha256?.let { it == HashGeneratorInstance.generateSHA256(file).getOrThrow() } - ?: sha512?.let { it == HashGeneratorInstance.generateSHA512(file).getOrThrow() } - ?: sha1?.let { it == HashGeneratorInstance.generateSHA1(file).getOrThrow() } - ?: md5?.let { it == HashGeneratorInstance.generateMD5(file).getOrThrow() } - ?: sizeInBytes?.let { it == file.length() } + sha256?.let { it == HashGeneratorInstance.generateSHA256(filePath).getOrThrow() } + ?: sha512?.let { it == HashGeneratorInstance.generateSHA512(filePath).getOrThrow() } + ?: sha1?.let { it == HashGeneratorInstance.generateSHA1(filePath).getOrThrow() } + ?: md5?.let { it == HashGeneratorInstance.generateMD5(filePath).getOrThrow() } + ?: sizeInBytes?.let { it == filePath.fileSize() } PreferredFileVerificationOption.Unsecure -> - md5?.let { it == HashGeneratorInstance.generateMD5(file).getOrThrow() } - ?: sha1?.let { it == HashGeneratorInstance.generateSHA1(file).getOrThrow() } - ?: sha256?.let { it == HashGeneratorInstance.generateSHA256(file).getOrThrow() } - ?: sha512?.let { it == HashGeneratorInstance.generateSHA512(file).getOrThrow() } - ?: sizeInBytes?.let { it == file.length() } + md5?.let { it == HashGeneratorInstance.generateMD5(filePath).getOrThrow() } + ?: sha1?.let { it == HashGeneratorInstance.generateSHA1(filePath).getOrThrow() } + ?: sha256?.let { it == HashGeneratorInstance.generateSHA256(filePath).getOrThrow() } + ?: sha512?.let { it == HashGeneratorInstance.generateSHA512(filePath).getOrThrow() } + ?: sizeInBytes?.let { it == filePath.fileSize() } PreferredFileVerificationOption.LeastUnsecure -> - sizeInBytes?.let { it == file.length() } - ?: md5?.let { it == HashGeneratorInstance.generateMD5(file).getOrThrow() } - ?: sha1?.let { it == HashGeneratorInstance.generateSHA1(file).getOrThrow() } - ?: sha256?.let { it == HashGeneratorInstance.generateSHA256(file).getOrThrow() } - ?: sha512?.let { it == HashGeneratorInstance.generateSHA512(file).getOrThrow() } + sizeInBytes?.let { it == filePath.fileSize() } + ?: md5?.let { it == HashGeneratorInstance.generateMD5(filePath).getOrThrow() } + ?: sha1?.let { it == HashGeneratorInstance.generateSHA1(filePath).getOrThrow() } + ?: sha256?.let { it == HashGeneratorInstance.generateSHA256(filePath).getOrThrow() } + ?: sha512?.let { it == HashGeneratorInstance.generateSHA512(filePath).getOrThrow() } PreferredFileVerificationOption.FileSize -> - sizeInBytes?.let { it == file.length() } - ?: sha512?.let { it == HashGeneratorInstance.generateSHA512(file).getOrThrow() } - ?: sha256?.let { it == HashGeneratorInstance.generateSHA256(file).getOrThrow() } - ?: sha1?.let { it == HashGeneratorInstance.generateSHA1(file).getOrThrow() } - ?: md5?.let { it == HashGeneratorInstance.generateMD5(file).getOrThrow() } + sizeInBytes?.let { it == filePath.fileSize() } + ?: sha512?.let { it == HashGeneratorInstance.generateSHA512(filePath).getOrThrow() } + ?: sha256?.let { it == HashGeneratorInstance.generateSHA256(filePath).getOrThrow() } + ?: sha1?.let { it == HashGeneratorInstance.generateSHA1(filePath).getOrThrow() } + ?: md5?.let { it == HashGeneratorInstance.generateMD5(filePath).getOrThrow() } PreferredFileVerificationOption.Md5 -> - md5?.let { it == HashGeneratorInstance.generateMD5(file).getOrThrow() } - ?: sha512?.let { it == HashGeneratorInstance.generateSHA512(file).getOrThrow() } - ?: sha256?.let { it == HashGeneratorInstance.generateSHA256(file).getOrThrow() } - ?: sha1?.let { it == HashGeneratorInstance.generateSHA1(file).getOrThrow() } - ?: sizeInBytes?.let { it == file.length() } + md5?.let { it == HashGeneratorInstance.generateMD5(filePath).getOrThrow() } + ?: sha512?.let { it == HashGeneratorInstance.generateSHA512(filePath).getOrThrow() } + ?: sha256?.let { it == HashGeneratorInstance.generateSHA256(filePath).getOrThrow() } + ?: sha1?.let { it == HashGeneratorInstance.generateSHA1(filePath).getOrThrow() } + ?: sizeInBytes?.let { it == filePath.fileSize() } PreferredFileVerificationOption.Sha1 -> - sha1?.let { it == HashGeneratorInstance.generateSHA1(file).getOrThrow() } - ?: sha512?.let { it == HashGeneratorInstance.generateSHA512(file).getOrThrow() } - ?: sha256?.let { it == HashGeneratorInstance.generateSHA256(file).getOrThrow() } - ?: md5?.let { it == HashGeneratorInstance.generateMD5(file).getOrThrow() } - ?: sizeInBytes?.let { it == file.length() } + sha1?.let { it == HashGeneratorInstance.generateSHA1(filePath).getOrThrow() } + ?: sha512?.let { it == HashGeneratorInstance.generateSHA512(filePath).getOrThrow() } + ?: sha256?.let { it == HashGeneratorInstance.generateSHA256(filePath).getOrThrow() } + ?: md5?.let { it == HashGeneratorInstance.generateMD5(filePath).getOrThrow() } + ?: sizeInBytes?.let { it == filePath.fileSize() } PreferredFileVerificationOption.Sha256 -> - sha256?.let { it == HashGeneratorInstance.generateSHA256(file).getOrThrow() } - ?: sha512?.let { it == HashGeneratorInstance.generateSHA512(file).getOrThrow() } - ?: sha1?.let { it == HashGeneratorInstance.generateSHA1(file).getOrThrow() } - ?: md5?.let { it == HashGeneratorInstance.generateMD5(file).getOrThrow() } - ?: sizeInBytes?.let { it == file.length() } + sha256?.let { it == HashGeneratorInstance.generateSHA256(filePath).getOrThrow() } + ?: sha512?.let { it == HashGeneratorInstance.generateSHA512(filePath).getOrThrow() } + ?: sha1?.let { it == HashGeneratorInstance.generateSHA1(filePath).getOrThrow() } + ?: md5?.let { it == HashGeneratorInstance.generateMD5(filePath).getOrThrow() } + ?: sizeInBytes?.let { it == filePath.fileSize() } PreferredFileVerificationOption.Sha512 -> - sha512?.let { it == HashGeneratorInstance.generateSHA512(file).getOrThrow() } - ?: sha256?.let { it == HashGeneratorInstance.generateSHA256(file).getOrThrow() } - ?: sha1?.let { it == HashGeneratorInstance.generateSHA1(file).getOrThrow() } - ?: md5?.let { it == HashGeneratorInstance.generateMD5(file).getOrThrow() } - ?: sizeInBytes?.let { it == file.length() } + sha512?.let { it == HashGeneratorInstance.generateSHA512(filePath).getOrThrow() } + ?: sha256?.let { it == HashGeneratorInstance.generateSHA256(filePath).getOrThrow() } + ?: sha1?.let { it == HashGeneratorInstance.generateSHA1(filePath).getOrThrow() } + ?: md5?.let { it == HashGeneratorInstance.generateMD5(filePath).getOrThrow() } + ?: sizeInBytes?.let { it == filePath.fileSize() } } return Result.success(isValidFileIntegrity) } catch (e: Exception) { @@ -123,13 +124,13 @@ private suspend fun FileIntegrityInfo.validateOnlyPreferredOption( * will either validate the file using all the data in [FileIntegrityInfo] or only one of them. * @return true or false if the file integrity is known, null otherwise * */ -suspend fun FileIntegrityInfo.hasValidIntegrity(file: File): Result { +suspend fun FileIntegrityInfo.hasValidIntegrity(filePath: Path): Result { return try { val isValidFileIntegrityResult = SyncInfo.instance.preferredAssetFileVerification?.let { - this.validateOnlyPreferredOption(file = file, preferredFileVerificationOption = it) + this.validateOnlyPreferredOption(filePath = filePath, preferredFileVerificationOption = it) } ?: this.validateAll( - file = file, + filePath = filePath, ) return isValidFileIntegrityResult } catch (e: Exception) { diff --git a/sync-script/src/main/kotlin/syncInfo/models/ModExtensions.kt b/sync-script/src/main/kotlin/syncInfo/models/ModExtensions.kt index 7fb73e4..3cb9f12 100644 --- a/sync-script/src/main/kotlin/syncInfo/models/ModExtensions.kt +++ b/sync-script/src/main/kotlin/syncInfo/models/ModExtensions.kt @@ -3,7 +3,8 @@ package syncInfo.models import config.models.ScriptConfig import utils.getFileNameFromUrlOrError import utils.showErrorMessageAndTerminate -import java.io.File +import java.nio.file.Path +import kotlin.io.path.name import kotlin.system.exitProcess /** @@ -44,11 +45,11 @@ fun Mod.shouldVerifyFileIntegrity(): Boolean = overrideShouldVerifyFileIntegrity ?: SyncInfo.instance.shouldVerifyModFilesIntegrity ?: SyncInfo.instance.shouldVerifyAssetFilesIntegrity -suspend fun Mod.hasValidFileIntegrityOrError(modFile: File): Boolean? = - this.fileIntegrityInfo.hasValidIntegrity(file = modFile).getOrElse { +suspend fun Mod.hasValidFileIntegrityOrError(modFilePath: Path): Boolean? = + this.fileIntegrityInfo.hasValidIntegrity(filePath = modFilePath).getOrElse { showErrorMessageAndTerminate( title = "File Integrity Validation Error ⚠️", - message = "An error occurred while validating the integrity of the mod file (${modFile.name}) \uD83D\uDCC1.", + message = "An error occurred while validating the integrity of the mod file (${modFilePath.name}) \uD83D\uDCC1.", ) // This will never reach due to the previous statement stopping the application exitProcess(0) diff --git a/sync-script/src/main/kotlin/syncService/ModsSyncService.kt b/sync-script/src/main/kotlin/syncService/ModsSyncService.kt index 05c5149..07bfb21 100644 --- a/sync-script/src/main/kotlin/syncService/ModsSyncService.kt +++ b/sync-script/src/main/kotlin/syncService/ModsSyncService.kt @@ -15,15 +15,25 @@ import utils.ExecutionTimer import utils.FileDownloader import utils.calculateProgressByIndex import utils.convertBytesToReadableMegabytesAsString +import utils.deleteExistingOrTerminate import utils.getFileNameFromUrlOrError +import utils.listFilteredPaths import utils.showErrorMessageAndTerminate -import java.io.File +import java.nio.file.Path +import java.nio.file.Paths +import kotlin.io.path.createDirectories +import kotlin.io.path.exists +import kotlin.io.path.extension +import kotlin.io.path.isDirectory +import kotlin.io.path.isHidden +import kotlin.io.path.name +import kotlin.io.path.nameWithoutExtension // TODO: Use JarFile(modFile).manifest.mainAttributes to read the mod name, id and some info to solve the duplicating // mods issue when allowing the user to install other mods class ModsSyncService : SyncService { - private val modsDirectory = SyncScriptDotMinecraftFiles.Mods.file + private val modsDirectoryPath = SyncScriptDotMinecraftFiles.Mods.path companion object { private const val MOD_FILE_EXTENSION = "jar" @@ -72,54 +82,56 @@ class ModsSyncService : SyncService { } private fun validateModsDirectory() { - if (!modsDirectory.exists()) { + if (!modsDirectoryPath.exists()) { println("\uD83D\uDCC1 The mods folder doesn't exist, creating it..") - modsDirectory.mkdirs() + modsDirectoryPath.createDirectories() } - if (!modsDirectory.isDirectory) { + if (!modsDirectoryPath.isDirectory()) { showErrorMessageAndTerminate( title = "❌ Invalid Mods Folder", message = "\uD83D\uDEE0 Mods must be stored in a directory/folder \uD83D\uDCC2 called " + - "`${SyncScriptDotMinecraftFiles.Mods.file.name}`" + + "`${SyncScriptDotMinecraftFiles.Mods.path.name}`" + ", a file was found instead.", ) } } - private fun deleteUnSyncedLocalModFiles(mods: List) { - val localModFiles = - ( - modsDirectory.listFiles() ?: kotlin.run { - showErrorMessageAndTerminate( - title = "📁 File Listing Error", - message = "⚠ Failed to list the files in the mods folder.", - ) - return - } - ).filter { !it.isDirectory && it.extension == MOD_FILE_EXTENSION } - + private suspend fun deleteUnSyncedLocalModFiles(mods: List) { // Get only the mods that are created by the script if the admin allows the player to install other mods /** * The mods to deal with based on [SyncInfo.allowUsingOtherMods] * will or will not remove the mods that are created by the script * */ - val localModFilesToProcess = - if (syncInfo.allowUsingOtherMods) { - localModFiles.filter { isScriptMod(it) } - } else { - localModFiles.toList() - } + val localModFilePathsToProcess = + modsDirectoryPath + .listFilteredPaths { + val isModFileExtension = !it.isDirectory() && !it.isHidden() && it.extension == MOD_FILE_EXTENSION + if (syncInfo.allowUsingOtherMods) { + return@listFilteredPaths isModFileExtension && isScriptModFile(it) + } else { + return@listFilteredPaths isModFileExtension + } + }.getOrElse { + showErrorMessageAndTerminate( + title = "📁 File Listing Error", + message = "⚠ Failed to list the files in the mods folder: ${it.message}", + ) + return + } // Delete the old un-synced mods - val remoteModFileNames: List = mods.map { getModFile(it).name } - for (localModFile in localModFilesToProcess) { - if (localModFile.name !in remoteModFileNames) { - println("❌ Deleting the mod '${localModFile.name}' as it's no longer on the server.") - localModFile.delete() + val remoteModFileNames: List = mods.map { getModFilePath(it).name } + for (localModFilePath in localModFilePathsToProcess) { + if (localModFilePath.name !in remoteModFileNames) { + println("\uD83D\uDEAB Deleting the mod '${localModFilePath.name}' as it's no longer on the server.") + localModFilePath.deleteExistingOrTerminate( + fileEntityType = "mod", + reasonOfDelete = "it's no longer on the server", + ) } } } @@ -129,10 +141,13 @@ class ModsSyncService : SyncService { val currentEnvironmentMods = mods.filter { mod -> if (!mod.shouldSyncOnCurrentEnvironment()) { - val modFile = getModFile(mod) - if (modFile.exists()) { - println("❌ Deleting the mod '${modFile.name}' as it's not needed on the current environment.") - modFile.delete() + val modFilePath = getModFilePath(mod) + if (modFilePath.exists()) { + println("❌ Deleting the mod '${modFilePath.name}' as it's not needed on the current environment.") + modFilePath.deleteExistingOrTerminate( + fileEntityType = "mod", + reasonOfDelete = "it's not required on the current environment", + ) } // Exclude the mod as it's not needed in the current environment return@filter false @@ -151,8 +166,8 @@ class ModsSyncService : SyncService { ): List { return mods.filter { mod -> val modFileName = getFileNameFromUrlOrError(mod.downloadUrl) - val modFile = getModFile(mod) - if (modFile.exists()) { + val modFilePath = getModFilePath(mod) + if (modFilePath.exists()) { if (!mod.shouldVerifyFileIntegrity()) { println("ℹ️ The mod: '$modFileName' is set to not be verified. Skipping to the next mod.") return@filter false @@ -167,7 +182,7 @@ class ModsSyncService : SyncService { detailsText = "Verifying the mod files integrity...", ) - val hasValidModIntegrity = mod.hasValidFileIntegrityOrError(modFile) + val hasValidModIntegrity = mod.hasValidFileIntegrityOrError(modFilePath) if (hasValidModIntegrity == null) { println("❓ The mod: '$modFileName' has an unknown integrity. Skipping to the next mod.") return@filter false @@ -180,7 +195,10 @@ class ModsSyncService : SyncService { "❌ The mod: '$modFileName' has invalid integrity. Deleting the mod " + "and downloading it again.", ) - modFile.delete() + modFilePath.deleteExistingOrTerminate( + fileEntityType = "mod", + reasonOfDelete = "it has invalid file integrity", + ) } // Add this mod to the download list process @@ -195,8 +213,8 @@ class ModsSyncService : SyncService { ) { for ((index, mod) in modsToDownload.withIndex()) { val modFileName = getFileNameFromUrlOrError(mod.downloadUrl) - val modFile = getModFile(mod) - if (modFile.exists()) { + val modFilePath = getModFilePath(mod) + if (modFilePath.exists()) { println("⚠\uFE0F The mod: '$modFileName' already exists.") } @@ -204,7 +222,7 @@ class ModsSyncService : SyncService { FileDownloader( downloadUrl = mod.downloadUrl, - targetFile = modFile, + targetFilePath = modFilePath, progressListener = { downloadedBytes, downloadedProgress, bytesToDownload -> loadingIndicatorDialog?.updateComponentProperties( title = @@ -223,7 +241,7 @@ class ModsSyncService : SyncService { ).downloadFile() // This will always validate newly downloaded mods regardless of the configurations - val isNewlyDownloadedFileHasValidFileIntegrity = mod.hasValidFileIntegrityOrError(modFile) + val isNewlyDownloadedFileHasValidFileIntegrity = mod.hasValidFileIntegrityOrError(modFilePath) if (isNewlyDownloadedFileHasValidFileIntegrity == false) { showErrorMessageAndTerminate( title = "❌ File Integrity Check Failed", @@ -236,18 +254,21 @@ class ModsSyncService : SyncService { } /** - * @return The file that will be used, we use [SyncInfo.modSyncMarker] to support [isScriptMod] + * @return The file that will be used, we use [SyncInfo.modSyncMarker] to support [isScriptModFile] * will be the same file name from the [Mod.downloadUrl] if [SyncInfo.modSyncMarker] is null * - * @see isScriptMod + * @see isScriptModFile * */ - private fun getModFile(mod: Mod): File { + private fun getModFilePath(mod: Mod): Path { val modFileNameWithoutExtension = - File(getFileNameFromUrlOrError(mod.downloadUrl)).nameWithoutExtension - return File( - modsDirectory, - "${modFileNameWithoutExtension}${SyncInfo.instance.modSyncMarker.orEmpty()}.${MOD_FILE_EXTENSION}", - ) + Paths.get(getFileNameFromUrlOrError(mod.downloadUrl)).nameWithoutExtension + val modFileName = + buildString { + append(modFileNameWithoutExtension) + SyncInfo.instance.modSyncMarker?.let { append(it) } + append(".${MOD_FILE_EXTENSION}") + } + return modsDirectoryPath.resolve(modFileName) } /** @@ -255,10 +276,10 @@ class ModsSyncService : SyncService { * it will be identified by [SyncInfo.modSyncMarker] and will always return true * if [SyncInfo.modSyncMarker] is null * - * @see getModFile + * @see getModFilePath * */ - private fun isScriptMod(modFile: File): Boolean = - modFile.name.endsWith( + private fun isScriptModFile(modFilePath: Path): Boolean = + modFilePath.name.endsWith( "${SyncInfo.instance.modSyncMarker.orEmpty()}.${MOD_FILE_EXTENSION}", ) diff --git a/sync-script/src/main/kotlin/syncService/ResourcePacksSyncService.kt b/sync-script/src/main/kotlin/syncService/ResourcePacksSyncService.kt index a5b7ff9..24413dd 100644 --- a/sync-script/src/main/kotlin/syncService/ResourcePacksSyncService.kt +++ b/sync-script/src/main/kotlin/syncService/ResourcePacksSyncService.kt @@ -3,7 +3,7 @@ package syncService import constants.SyncScriptDotMinecraftFiles class ResourcePacksSyncService : SyncService { - private val resourcePacksDirectory = SyncScriptDotMinecraftFiles.ResourcePacks.file + private val resourcePacksDirectoryPath = SyncScriptDotMinecraftFiles.ResourcePacks.path override suspend fun syncData() { TODO("Not yet implemented") diff --git a/sync-script/src/main/kotlin/syncService/ServersSyncService.kt b/sync-script/src/main/kotlin/syncService/ServersSyncService.kt index f60fc85..90cd71c 100644 --- a/sync-script/src/main/kotlin/syncService/ServersSyncService.kt +++ b/sync-script/src/main/kotlin/syncService/ServersSyncService.kt @@ -3,7 +3,7 @@ package syncService import constants.SyncScriptDotMinecraftFiles class ServersSyncService : SyncService { - private val serversFile = SyncScriptDotMinecraftFiles.ServersDat.file + private val serversFilePath = SyncScriptDotMinecraftFiles.ServersDat.path override suspend fun syncData() { TODO("Not yet implemented") diff --git a/sync-script/src/main/kotlin/syncService/ShaderPacksSyncService.kt b/sync-script/src/main/kotlin/syncService/ShaderPacksSyncService.kt index cead6f1..f6f6373 100644 --- a/sync-script/src/main/kotlin/syncService/ShaderPacksSyncService.kt +++ b/sync-script/src/main/kotlin/syncService/ShaderPacksSyncService.kt @@ -3,7 +3,7 @@ package syncService import constants.SyncScriptDotMinecraftFiles class ShaderPacksSyncService : SyncService { - private val shaderPacksDirectory = SyncScriptDotMinecraftFiles.ShaderPacks.file + private val shaderPacksDirectoryPath = SyncScriptDotMinecraftFiles.ShaderPacks.path override suspend fun syncData() { TODO("Not yet implemented") diff --git a/sync-script/src/main/kotlin/utils/FileDownloader.kt b/sync-script/src/main/kotlin/utils/FileDownloader.kt index cedd70c..1b64c38 100644 --- a/sync-script/src/main/kotlin/utils/FileDownloader.kt +++ b/sync-script/src/main/kotlin/utils/FileDownloader.kt @@ -6,12 +6,16 @@ import kotlinx.coroutines.withContext import okhttp3.Request import okio.buffer import okio.sink -import java.io.File -import java.nio.file.Files +import java.nio.file.Path import java.nio.file.StandardCopyOption +import kotlin.io.path.exists +import kotlin.io.path.extension +import kotlin.io.path.isWritable +import kotlin.io.path.nameWithoutExtension +import kotlin.io.path.pathString /** - * A utility class for downloading files from [downloadUrl] to [targetFile] with [progressListener]. + * A utility class for downloading files from [downloadUrl] to [targetFilePath] with [progressListener]. * TODO: Add the option to cancel a download, might need to refactor this class too * * Currently will handle errors internally by showing a error message and close. @@ -20,7 +24,7 @@ import java.nio.file.StandardCopyOption * */ class FileDownloader( private val downloadUrl: String, - private val targetFile: File, + private val targetFilePath: Path, val progressListener: ( downloadedBytes: Long, // in percentage, from 0 to 100 @@ -29,12 +33,12 @@ class FileDownloader( ) -> Unit, ) { suspend fun downloadFile() { - if (targetFile.exists()) { + if (targetFilePath.exists()) { showErrorMessageAndTerminate( title = "📁 File Conflict", message = "Unable to download the file. The destination file already exists. " + - "This might be a bug, delete the file: (${targetFile.path}) as a workaround.", + "This might be a bug, delete the file: (${targetFilePath.pathString}) as a workaround.", ) } val request = @@ -60,28 +64,16 @@ class FileDownloader( // We could use File.createTempFile from JVM, to avoid creating // files on the user system we will handle it manually val tempFile = - SyncScriptDotMinecraftFiles.SyncScriptData.Temp.file.resolve( - "${targetFile.nameWithoutExtension}-${System.currentTimeMillis()}.${targetFile.extension}", + SyncScriptDotMinecraftFiles.SyncScriptData.Temp.path.resolve( + "${targetFilePath.nameWithoutExtension}-${System.currentTimeMillis()}.${targetFilePath.extension}", ) - if (!tempFile.exists()) { - tempFile.parentFile.mkdirs() - } - val wasFileCreated = tempFile.createNewFile() - if (!wasFileCreated) { - showErrorMessageAndTerminate( - title = "📄 File Already Exists", - message = - "⚠️ The temporary file '${tempFile.name}' already exists. We're unable to create it. " + - "This might be a bug," + - " delete the file: ${targetFile.path} as a workaround.", - ) - } - if (!tempFile.canWrite()) { + tempFile.createFileWithParentDirectoriesOrTerminate() + if (!tempFile.isWritable()) { showErrorMessageAndTerminate( title = "🔒 Permission Error", message = "It seems that we don't have the necessary write permission to download" + - " the file: ${tempFile.path}. Double check your permissions and try again.", + " the file: ${tempFile.pathString}. Double check your permissions and try again.", ) } tempFile.sink().buffer().use { sink -> @@ -101,7 +93,11 @@ class FileDownloader( // The use block already calls 'response.closeQuietly()' // Move the downloaded file from the temporary place to where it should - Files.move(tempFile.toPath(), targetFile.toPath(), StandardCopyOption.ATOMIC_MOVE) + tempFile.moveToOrTerminate( + target = targetFilePath, + StandardCopyOption.ATOMIC_MOVE, + fileEntityType = "JAR", + ) } catch (e: Exception) { e.printStackTrace() showErrorMessageAndTerminate( diff --git a/sync-script/src/main/kotlin/utils/FilePathUtils.kt b/sync-script/src/main/kotlin/utils/FilePathUtils.kt new file mode 100644 index 0000000..f7872a0 --- /dev/null +++ b/sync-script/src/main/kotlin/utils/FilePathUtils.kt @@ -0,0 +1,100 @@ +package utils + +import java.nio.file.CopyOption +import java.nio.file.FileAlreadyExistsException +import java.nio.file.Path +import kotlin.io.path.createFile +import kotlin.io.path.createParentDirectories +import kotlin.io.path.deleteExisting +import kotlin.io.path.exists +import kotlin.io.path.moveTo +import kotlin.io.path.name +import kotlin.io.path.pathString + +fun Path.deleteExistingOrTerminate( + fileEntityType: String, + reasonOfDelete: String, +) { + try { + this.deleteExisting() + } catch (e: Exception) { + showErrorMessageAndTerminate( + title = "File Deletion Error ⚠️", + message = "❌ Failed to delete the $fileEntityType file '${this.name}' as $reasonOfDelete: ${e.message}", + ) + } +} + +fun Path.moveToOrTerminate( + target: Path, + overwrite: Boolean = false, + fileEntityType: String, +) { + try { + this.moveTo(target = target, overwrite = overwrite) + } catch (e: Exception) { + showErrorMessageAndTerminate( + title = "File Moving Error ⚠\uFE0F", + message = "❌ Failed to move the $fileEntityType file '${this.pathString}' to '${target.pathString}': ${e.message}", + ) + } +} + +fun Path.moveToOrTerminate( + target: Path, + vararg options: CopyOption, + fileEntityType: String, +) { + try { + this.moveTo(target = target, *options) + } catch (e: Exception) { + showErrorMessageAndTerminate( + title = "File Moving Error ⚠\uFE0F", + message = "❌ Failed to move the $fileEntityType file '${this.pathString}' to '${target.pathString}': ${e.message}", + ) + } +} + +fun Path.createParentDirectoriesIfDoesNotExist() { + if (!parent.exists()) { + try { + createParentDirectories() + } catch (e: Exception) { + showErrorMessageAndTerminate( + title = "File Creation Error ⚠\uFE0F", + message = "❌ Failed to create the directory '${this.name}': $e", + ) + } + } +} + +fun Path.createFileWithParentDirectoriesOrTerminate() { + createParentDirectoriesIfDoesNotExist() + if (!parent.exists()) { + try { + createParentDirectories() + } catch (e: Exception) { + showErrorMessageAndTerminate( + title = "File Creation Error ⚠\uFE0F", + message = "❌ Failed to create the directory '${this.name}': $e", + ) + } + } + try { + createFile() + } catch (e: Exception) { + e.printStackTrace() + if (e is FileAlreadyExistsException) { + showErrorMessageAndTerminate( + title = "📄 File Already Exists", + message = + "⚠️ The file '$name' already exists. We're unable to create it. " + + "This might be a bug, a workaround is to delete '$pathString'.", + ) + } + showErrorMessageAndTerminate( + title = "File Creation Error ⚠\uFE0F", + message = "❌ Failed to create the file '${this.name}': $e", + ) + } +} diff --git a/sync-script/src/main/kotlin/utils/JarUtils.kt b/sync-script/src/main/kotlin/utils/JarUtils.kt index 6741a96..f88ff31 100644 --- a/sync-script/src/main/kotlin/utils/JarUtils.kt +++ b/sync-script/src/main/kotlin/utils/JarUtils.kt @@ -1,6 +1,8 @@ package utils import java.net.URL +import java.nio.file.Path +import kotlin.io.path.toPath /** * @return The current running JAR file, or the JAR URI used to run the application from using `java -jar app.jar` @@ -22,3 +24,10 @@ fun getRunningJarFileAsUrl(): Result { IllegalStateException("The running JAR file or the code source location is null"), ) } + +fun getRunningJarFilePath(): Result = + try { + Result.success(getRunningJarFileAsUrl().getOrThrow().toURI().toPath()) + } catch (e: Exception) { + Result.failure(e) + } diff --git a/sync-script/src/test/kotlin/services/hashGenerator/HashGeneratorTest.kt b/sync-script/src/test/kotlin/services/hashGenerator/HashGeneratorTest.kt index 584a9b1..f1304a1 100644 --- a/sync-script/src/test/kotlin/services/hashGenerator/HashGeneratorTest.kt +++ b/sync-script/src/test/kotlin/services/hashGenerator/HashGeneratorTest.kt @@ -2,12 +2,13 @@ package services.hashGenerator import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.Test -import utils.getResourceAsFileOrThrow +import utils.getResourceAsPathOrThrow +import java.nio.file.Path import kotlin.test.assertEquals import kotlin.test.assertNotEquals class HashGeneratorTest { - private fun getTestFile() = getResourceAsFileOrThrow("Punji-1.20.6-Fabric-1.0.1.jar") + private fun getTestFilePath(): Path = getResourceAsPathOrThrow("Punji-1.20.6-Fabric-1.0.1.jar") private val hashGenerator: HashGenerator = HashGeneratorInstance @@ -28,11 +29,11 @@ class HashGeneratorTest { fun `test generateMD5 from file`() { assertEquals( "b957e0476916680c926a9342231305ce", - runBlocking { hashGenerator.generateMD5(getTestFile()).getOrThrow() }, + runBlocking { hashGenerator.generateMD5(getTestFilePath()).getOrThrow() }, ) assertNotEquals( "invalid-hash-value", - runBlocking { hashGenerator.generateMD5(getTestFile()).getOrThrow() }, + runBlocking { hashGenerator.generateMD5(getTestFilePath()).getOrThrow() }, ) } @@ -40,11 +41,11 @@ class HashGeneratorTest { fun `test generateSHA1`() { assertEquals( "a2f6bd4fb8a9f7b49ac7dda7a67d028227d8389e", - runBlocking { hashGenerator.generateSHA1(getTestFile()).getOrThrow() }, + runBlocking { hashGenerator.generateSHA1(getTestFilePath()).getOrThrow() }, ) assertNotEquals( "invalid-hash-value", - runBlocking { hashGenerator.generateSHA1(getTestFile()).getOrThrow() }, + runBlocking { hashGenerator.generateSHA1(getTestFilePath()).getOrThrow() }, ) } @@ -52,11 +53,11 @@ class HashGeneratorTest { fun `test generateSHA256`() { assertEquals( "ecd6e096b3c54ada81bd9174f3d150116259f51b53a6aae576cc00996ede1f40", - runBlocking { hashGenerator.generateSHA256(getTestFile()).getOrThrow() }, + runBlocking { hashGenerator.generateSHA256(getTestFilePath()).getOrThrow() }, ) assertNotEquals( "invalid-hash-value", - runBlocking { hashGenerator.generateSHA256(getTestFile()).getOrThrow() }, + runBlocking { hashGenerator.generateSHA256(getTestFilePath()).getOrThrow() }, ) } @@ -65,11 +66,11 @@ class HashGeneratorTest { assertEquals( "4e23be8cee226e92dfd0f7ef408219bdb4edfcbcc738d11b6e2ae7b4f84a89c4037130" + "4a84ae6a68074239d743251c45aa1ca3c1caf0d17b97e32c13c3fb7b38", - runBlocking { hashGenerator.generateSHA512(getTestFile()).getOrThrow() }, + runBlocking { hashGenerator.generateSHA512(getTestFilePath()).getOrThrow() }, ) assertNotEquals( "invalid-hash-value", - runBlocking { hashGenerator.generateSHA512(getTestFile()).getOrThrow() }, + runBlocking { hashGenerator.generateSHA512(getTestFilePath()).getOrThrow() }, ) } } diff --git a/sync-script/src/test/kotlin/services/minecraft/MinecraftOptionsManagerTest.kt b/sync-script/src/test/kotlin/services/minecraft/MinecraftOptionsManagerTest.kt index b6b6c46..f8b3d55 100644 --- a/sync-script/src/test/kotlin/services/minecraft/MinecraftOptionsManagerTest.kt +++ b/sync-script/src/test/kotlin/services/minecraft/MinecraftOptionsManagerTest.kt @@ -7,7 +7,15 @@ import kotlinx.serialization.json.Json import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.assertDoesNotThrow import org.junit.jupiter.api.assertThrows -import java.io.File +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import kotlin.io.path.deleteIfExists +import kotlin.io.path.exists +import kotlin.io.path.extension +import kotlin.io.path.nameWithoutExtension +import kotlin.io.path.readText +import kotlin.io.path.writeText import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test @@ -18,25 +26,28 @@ import kotlin.test.assertNull import kotlin.test.assertTrue class MinecraftOptionsManagerTest { - private var testsOptionsFile: File = File("") + companion object { + const val FILE_PATH_THAT_DOES_NOT_EXIST = "non_existent_file.txt" + } + + private var testsOptionsFilePath: Path = Paths.get(FILE_PATH_THAT_DOES_NOT_EXIST) set(value) { - manager.setOptionsFileForTests(value) + manager.setOptionsFilePathForTests(value) field = value } /** - * Using [manager] to update the properties will also update [testsOptionsFile] as we're using [MinecraftOptionsManager.setOptionsFileForTests] + * Using [manager] to update the properties will also update [testsOptionsFilePath] as we're using [MinecraftOptionsManager.setOptionsFilePathForTests] * */ private val manager = MinecraftOptionsManager @BeforeTest fun setUp() { - testsOptionsFile = - File.createTempFile( - SyncScriptDotMinecraftFiles.Options.file.nameWithoutExtension, - ".${SyncScriptDotMinecraftFiles.Options.file.extension}", + testsOptionsFilePath = + Files.createTempFile( + SyncScriptDotMinecraftFiles.Options.path.nameWithoutExtension, + ".${SyncScriptDotMinecraftFiles.Options.path.extension}", ) - testsOptionsFile.deleteOnExit() // To make sure the next test doesn't use the properties from previous test manager.loadPropertiesFromFile().getOrThrow() @@ -44,12 +55,11 @@ class MinecraftOptionsManagerTest { @AfterTest fun cleanUp() { - // Optionally delete the file, it will be already deleted once existing the tests - testsOptionsFile.delete() + testsOptionsFilePath.deleteIfExists() } private fun setOptionsFileText(value: String) { - testsOptionsFile.writeText(value) + testsOptionsFilePath.writeText(value) manager.loadPropertiesFromFile().getOrThrow() } @@ -111,12 +121,12 @@ class MinecraftOptionsManagerTest { @Test fun `should throw exception for file doesn't exist`() { - assertTrue(testsOptionsFile.exists(), "The options file should exist") + assertTrue(testsOptionsFilePath.exists(), "The options file should exist") // Override the file with a file doesn't exist - testsOptionsFile = File("") + testsOptionsFilePath = Paths.get(FILE_PATH_THAT_DOES_NOT_EXIST) - assertFalse(testsOptionsFile.exists(), "The options file exist which it shouldn't") + assertFalse(testsOptionsFilePath.exists(), "The options file exist which it shouldn't") assertThrows { manager.loadPropertiesFromFile().getOrThrow() } @@ -165,9 +175,9 @@ class MinecraftOptionsManagerTest { MinecraftOptionsManager.ResourcePack.BuiltIn("fabric"), MinecraftOptionsManager.ResourcePack.BuiltIn("quilt"), ) - val currentOptionsFileText = testsOptionsFile.readText() + val currentOptionsFileText = testsOptionsFilePath.readText() manager.setResourcePacks(inputResourcePacks).getOrThrow() - val newOptionsFileText = testsOptionsFile.readText() + val newOptionsFileText = testsOptionsFilePath.readText() val outputResourcePacks = manager.readResourcePacks() @@ -209,7 +219,7 @@ class MinecraftOptionsManagerTest { // Adding a new property manager.setResourcePacks(resourcePack) assertEquals( - testsOptionsFile.readText(), + testsOptionsFilePath.readText(), buildString { appendLine(inCompatibleResourcePacksLine) appendLine( @@ -256,9 +266,9 @@ class MinecraftOptionsManagerTest { @Test fun `clearing the properties should clear the file too`() { manager.setResourcePacks(listOf()) - assertTrue(testsOptionsFile.readText().isNotEmpty()) + assertTrue(testsOptionsFilePath.readText().isNotEmpty()) manager.clear() - assertTrue(testsOptionsFile.readText().isEmpty()) + assertTrue(testsOptionsFilePath.readText().isEmpty()) } @Test