All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.pixeloutlaw.minecraft.spigot.config.migration.ConfigMigrator.kt Maven / Gradle / Ivy

The newest version!
/*
 * This file is part of MythicDrops, licensed under the MIT License.
 *
 * Copyright (C) 2021 Richard Harrah
 *
 * Permission is hereby granted, free of charge,
 * to any person obtaining a copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
 * and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or
 * substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
 * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
 * OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package io.pixeloutlaw.minecraft.spigot.config.migration

import io.pixeloutlaw.kindling.Log
import io.pixeloutlaw.minecraft.spigot.config.FileAwareYamlConfiguration
import io.pixeloutlaw.minecraft.spigot.config.VersionedFileAwareYamlConfiguration
import io.pixeloutlaw.minecraft.spigot.config.migration.models.ConfigMigration
import io.pixeloutlaw.minecraft.spigot.config.migration.models.NamedConfigMigration
import io.pixeloutlaw.minecraft.spigot.klob.Glob
import java.io.File
import java.nio.file.Path

/**
 * Migrates Spigot YAML configurations across versions.
 *
 * @property dataFolder data folder for a plugin
 * @property backupOnMigrate should a backup file be created when migrating?
 */
internal abstract class ConfigMigrator
    @JvmOverloads
    constructor(
        private val dataFolder: File,
        private val backupOnMigrate: Boolean = true
    ) {
        /**
         * Migrations that this instance will run through in the given order.
         */
        abstract val namedConfigMigrations: List

        /**
         * Attempts to run all the migrations that it is aware of. Will not copy configurations from resources
         * into [dataFolder]. Iterates through [ConfigMigration] sorted by name from [namedConfigMigrations] in order,
         * finding any files that match [ConfigMigration.fileGlobs] with a matching [ConfigMigration.fromVersion], and
         * running applicable [io.pixeloutlaw.minecraft.spigot.config.migration.models.ConfigMigrationStep]s.
         */
        fun migrate() {
            Log.info("Beginning migration process")
            for (namedConfigMigration in namedConfigMigrations) {
                runMigration(namedConfigMigration)
            }
            Log.info("Finished migration process!")
        }

        fun writeYamlFromResourcesIfNotExists(resource: String) {
            val targetFile = dataFolder.toPath().resolve(resource).toFile()
            if (targetFile.exists() || !targetFile.parentFile.exists() && !targetFile.parentFile.mkdirs()) {
                return
            }
            try {
                val text = javaClass.classLoader?.getResource(resource)?.readText()
                if (text != null) {
                    FileAwareYamlConfiguration(targetFile).apply {
                        loadFromString(text)
                        save()
                    }
                } else {
                    Log.warn("Unable to write resource because text was unreadable: $resource")
                }
            } catch (expected: Throwable) {
                Log.warn("Unable to write resource: $resource", expected)
                return
            }
        }

        // suppress spread operator warning because Glob has no other interface we can use
        @Suppress("SpreadOperator")
        private fun runMigration(namedConfigMigration: NamedConfigMigration) {
            Log.debug("> Running migration: ${namedConfigMigration.migrationName}")
            val configMigration = namedConfigMigration.configMigration
            Log.debug("=> Looking for files in dataFolder matching ${configMigration.fileGlobs}")
            val matchingPaths =
                Glob
                    .from(*configMigration.fileGlobs.toTypedArray())
                    .iterate(dataFolder.toPath())
                    .asSequence()
                    .toList()
            Log.debug("=> Found matching files: ${matchingPaths.joinToString(", ")}")
            Log.debug("=> Loading matched files to check their versions")
            val yamlConfigurations =
                matchingPaths
                    .map {
                        val pathToFile = it.toFile()
                        VersionedFileAwareYamlConfiguration(
                            pathToFile
                        )
                    }.filter {
                        Log.verbose(
                            """
                    ==> Checking if ${it.fileName} (${it.version}) has a version matching ${configMigration.fromVersion}
                """.trimLog()
                        )
                        it.version == configMigration.fromVersion
                    }
            Log.debug("=> Found configurations matching versions: ${yamlConfigurations.map { it.fileName }}")
            Log.debug("=> Running migration over matching configurations")
            runMigrationOverConfigurations(yamlConfigurations, configMigration)
            Log.debug("=> Finished running migration over matching configurations!")
            Log.debug("> Finished running migration!")
        }

        private fun runMigrationOverConfigurations(
            yamlConfigurations: List,
            configMigration: ConfigMigration
        ) {
            for (yamlConfiguration in yamlConfigurations) {
                handleBackups(configMigration, yamlConfiguration)
                if (handleOverwrites(configMigration, yamlConfiguration, dataFolder.toPath())) {
                    Log.verbose(
                        """
                    |=> Canceling migration for ${yamlConfiguration.fileName} as it has been overwritten!
                    """.trimLog()
                    )
                    continue
                }
                Log.verbose("==> Running migration steps over ${yamlConfiguration.fileName}")
                for (step in configMigration.configMigrationSteps) {
                    step.migrate(yamlConfiguration)
                }
                Log.verbose(
                    """
                |==> Setting configuration version to target version:
                |configuration=${yamlConfiguration.fileName} version=${configMigration.toVersion}
                """.trimLog()
                )
                yamlConfiguration.version = configMigration.toVersion
                Log.verbose("==> Saving configuration")
                yamlConfiguration.save()
                Log.verbose("==> Finished running migration steps!")
            }
        }

        private fun handleBackups(
            configMigration: ConfigMigration,
            yamlConfiguration: VersionedFileAwareYamlConfiguration
        ) {
            if (configMigration.createBackup && backupOnMigrate) {
                val lastDot = yamlConfiguration.fileName.lastIndexOf(".")
                val backupFilename =
                    yamlConfiguration.fileName.substring(
                        0,
                        lastDot
                    ) + "_${
                        yamlConfiguration.version.toString().replace(
                            ".",
                            "_"
                        )
                    }" + yamlConfiguration.fileName.substring(lastDot) + ".backup"
                Log.debug("==> Creating backup of file as $backupFilename")
                try {
                    yamlConfiguration.file?.let {
                        it.copyTo(File(it.parentFile, backupFilename), overwrite = true)
                    }
                } catch (expected: Exception) {
                    Log.error("Unable to create a backup of a config!", expected)
                }
            }
        }

        // if this returns true, we should skip the migration as we're just overwriting the existing file
        private fun handleOverwrites(
            configMigration: ConfigMigration,
            yamlConfiguration: VersionedFileAwareYamlConfiguration,
            pathToDataFolder: Path
        ): Boolean {
            val pathToYamlFile =
                yamlConfiguration.file
                    ?.toPath()
                    ?.toAbsolutePath()
                    ?.normalize()
            if (!configMigration.overwrite || pathToYamlFile == null) {
                return false
            }
            val normalizedPathToDataFolder = pathToDataFolder.toAbsolutePath().normalize()
            val relativizedPathToYamlFile = normalizedPathToDataFolder.relativize(pathToYamlFile)
            val pathToResource = relativizedPathToYamlFile.toString().replace("\\", "/")
            try {
                val resourceContents = javaClass.classLoader?.getResource(pathToResource)?.readText() ?: ""
                yamlConfiguration.file?.writeText(resourceContents)
                Log.debug(
                    """
                |==> Overwrote contents of ${yamlConfiguration.fileName} with
                |contents of (hopefully) same file from plugin!
                """.trimLog()
                )
            } catch (expected: Exception) {
                Log.error("Unable to overwrite a config!", expected)
            }
            return true
        }

        /**
         * Trims the margins from the string and replaces newlines with a space.
         */
        private fun String.trimLog(): String = this.trimMargin().replace("\n", " ")
    }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy