diff options
Diffstat (limited to 'src/main/kotlin/gui/config/storage')
5 files changed, 423 insertions, 0 deletions
diff --git a/src/main/kotlin/gui/config/storage/ConfigLoadContext.kt b/src/main/kotlin/gui/config/storage/ConfigLoadContext.kt new file mode 100644 index 0000000..59afaa1 --- /dev/null +++ b/src/main/kotlin/gui/config/storage/ConfigLoadContext.kt @@ -0,0 +1,65 @@ +package moe.nea.firmament.gui.config.storage + +import java.io.PrintWriter +import java.nio.file.Path +import org.apache.commons.io.output.StringBuilderWriter +import kotlin.io.path.Path +import kotlin.io.path.createParentDirectories +import kotlin.io.path.writeText +import moe.nea.firmament.Firmament + +data class ConfigLoadContext( + val loadId: String, +) : AutoCloseable { + val logFile = Path("logs") + .resolve(Firmament.MOD_ID) + .resolve("config-$loadId.log") + .toAbsolutePath() + val logBuffer = StringBuilder() + + var shouldSaveLogBuffer = false + fun markShouldSaveLogBuffer() { + shouldSaveLogBuffer = true + } + + fun logDebug(message: String) { + logBuffer.append("[DEBUG] ").append(message).appendLine() + } + + fun logInfo(message: String) { + Firmament.logger.info("[ConfigUpgrade] $message") + logBuffer.append("[INFO] ").append(message).appendLine() + } + + fun logError(message: String, exception: Throwable) { + markShouldSaveLogBuffer() + Firmament.logger.error("[ConfigUpgrade] $message", exception) + logBuffer.append("[ERROR] ").append(message).appendLine() + PrintWriter(StringBuilderWriter(logBuffer)).use { + exception.printStackTrace(it) + } + logBuffer.appendLine() + } + + fun logError(message: String) { + markShouldSaveLogBuffer() + Firmament.logger.error("[ConfigUpgrade] $message") + logBuffer.append("[ERROR] ").append(message).appendLine() + } + + fun ensureWritable(path: Path) { + path.createParentDirectories() + } + + override fun close() { + logInfo("Closing out config load.") + if (shouldSaveLogBuffer) { + try { + ensureWritable(logFile) + logFile.writeText(logBuffer.toString()) + } catch (ex: Exception) { + logError("Could not save config load log", ex) + } + } + } +} diff --git a/src/main/kotlin/gui/config/storage/ConfigStorageClass.kt b/src/main/kotlin/gui/config/storage/ConfigStorageClass.kt new file mode 100644 index 0000000..8258fe7 --- /dev/null +++ b/src/main/kotlin/gui/config/storage/ConfigStorageClass.kt @@ -0,0 +1,8 @@ +package moe.nea.firmament.gui.config.storage + +enum class ConfigStorageClass { // TODO: make this encode type info somehow + PROFILE, + STORAGE, + CONFIG, +} + diff --git a/src/main/kotlin/gui/config/storage/FirmamentConfigLoader.kt b/src/main/kotlin/gui/config/storage/FirmamentConfigLoader.kt new file mode 100644 index 0000000..22cba2c --- /dev/null +++ b/src/main/kotlin/gui/config/storage/FirmamentConfigLoader.kt @@ -0,0 +1,204 @@ +package moe.nea.firmament.gui.config.storage + +import java.util.UUID +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonObject +import kotlin.io.path.Path +import kotlin.io.path.exists +import kotlin.io.path.forEachDirectoryEntry +import kotlin.io.path.isDirectory +import kotlin.io.path.listDirectoryEntries +import kotlin.io.path.name +import kotlin.io.path.readText +import kotlin.io.path.writeText +import moe.nea.firmament.util.data.IConfigProvider +import moe.nea.firmament.util.data.IDataHolder +import moe.nea.firmament.util.data.ProfileKeyedConfig +import moe.nea.firmament.util.json.intoGson +import moe.nea.firmament.util.json.intoKotlinJson + +object FirmamentConfigLoader { + val currentConfigVersion = 1000 + val configFolder = Path("config/firmament") + .toAbsolutePath() + val storageFolder = configFolder.resolve("storage") + val profilePath = configFolder.resolve("profiles") + val tagLines = listOf( + "<- your config version here", + "I'm a teapot", + "mail.example.com ESMTP", + "Apples" + ) + val configVersionFile = configFolder.resolve("config.version") + + fun loadConfig() { + if (configFolder.exists()) { + if (!configVersionFile.exists()) { + LegacyImporter.importFromLegacy() + } + updateConfigs() + } + + ConfigLoadContext("load-${System.currentTimeMillis()}").use { loadContext -> + val configData = FirstLevelSplitJsonFolder(loadContext, configFolder).load() + loadConfigFromData(configData, Unit, ConfigStorageClass.CONFIG) + val storageData = FirstLevelSplitJsonFolder(loadContext, storageFolder).load() + loadConfigFromData(storageData, Unit, ConfigStorageClass.STORAGE) + val profileData = + profilePath.listDirectoryEntries() + .filter { it.isDirectory() } + .associate { + UUID.fromString(it.name) to FirstLevelSplitJsonFolder(loadContext, it).load() + } + profileData.forEach { (key, value) -> + loadConfigFromData(value, key, ConfigStorageClass.PROFILE) + } + } + } + + fun <T> loadConfigFromData( + configData: JsonObject, + key: T, + storageClass: ConfigStorageClass + ) { + for (holder in allConfigs) { + if (holder.storageClass == storageClass) { + (holder as IDataHolder<T>).loadFrom(key, configData) + } + } + } + + fun <T> collectConfigFromData( + key: T, + storageClass: ConfigStorageClass, + ): JsonObject { + var json = JsonObject(mapOf()) + for (holder in allConfigs) { + if (holder.storageClass == storageClass) { + json = mergeJson(json, (holder as IDataHolder<T>).saveTo(key)) + } + } + return json + } + + fun <T> saveStorage( + storageClass: ConfigStorageClass, + key: T, + firstLevelSplitJsonFolder: FirstLevelSplitJsonFolder, + ) { + firstLevelSplitJsonFolder.save( + collectConfigFromData(key, storageClass) + ) + } + + fun collectAllProfileIds(): Set<UUID> { + return allConfigs + .filter { it.storageClass == ConfigStorageClass.PROFILE } + .flatMapTo(mutableSetOf()) { + (it as ProfileKeyedConfig<*>).keys() + } + } + + fun saveAll() { + ConfigLoadContext("load-${System.currentTimeMillis()}").use { context -> + saveStorage( + ConfigStorageClass.CONFIG, + Unit, + FirstLevelSplitJsonFolder(context, configFolder) + ) + saveStorage( + ConfigStorageClass.STORAGE, + Unit, + FirstLevelSplitJsonFolder(context, storageFolder) + ) + collectAllProfileIds().forEach { profileId -> + saveStorage( + ConfigStorageClass.PROFILE, + profileId, + FirstLevelSplitJsonFolder(context, profilePath.resolve(profileId.toString())) + ) + } + } + } + + fun mergeJson(a: JsonObject, b: JsonObject): JsonObject { + fun mergeInner(a: JsonElement?, b: JsonElement?): JsonElement { + if (a == null) + return b!! + if (b == null) + return a + a as JsonObject + b as JsonObject + return buildJsonObject { + (a.keys + b.keys) + .forEach { + put(it, mergeInner(a[it], b[it])) + } + } + } + return mergeInner(a, b) as JsonObject + } + + val allConfigs: List<IDataHolder<*>> = IConfigProvider.providers.allValidInstances.flatMap { it.configs } + + fun updateConfigs() { + val startVersion = configVersionFile.readText() + .substringBefore(' ') + .trim() + .toInt() + ConfigLoadContext("update-from-$startVersion-to-$currentConfigVersion-${System.currentTimeMillis()}") + .use { loadContext -> + updateOneConfig( + loadContext, + startVersion, + ConfigStorageClass.CONFIG, + FirstLevelSplitJsonFolder(loadContext, configFolder) + ) + updateOneConfig( + loadContext, + startVersion, + ConfigStorageClass.STORAGE, + FirstLevelSplitJsonFolder(loadContext, storageFolder) + ) + profilePath.forEachDirectoryEntry { + updateOneConfig( + loadContext, + startVersion, + ConfigStorageClass.PROFILE, + FirstLevelSplitJsonFolder(loadContext, it) + ) + } + configVersionFile.writeText("$currentConfigVersion ${tagLines.random()}") + } + } + + private fun updateOneConfig( + loadContext: ConfigLoadContext, + startVersion: Int, + storageClass: ConfigStorageClass, + firstLevelSplitJsonFolder: FirstLevelSplitJsonFolder + ) { + loadContext.logInfo("Starting upgrade from at ${firstLevelSplitJsonFolder.folder} ($storageClass) to $startVersion") + var data = firstLevelSplitJsonFolder.load() + for (nextVersion in (startVersion + 1)..currentConfigVersion) { + data = updateOneConfigOnce(nextVersion, storageClass, data) + } + firstLevelSplitJsonFolder.save(data) + } + + private fun updateOneConfigOnce( + nextVersion: Int, + storageClass: ConfigStorageClass, + data: JsonObject + ): JsonObject { + return ConfigFixEvent.publish(ConfigFixEvent(storageClass, nextVersion, data.intoGson().asJsonObject)) + .data.intoKotlinJson().jsonObject + } + + fun markDirty(holder: IDataHolder<*>) { + TODO("Not yet implemented") + } + +} diff --git a/src/main/kotlin/gui/config/storage/FirstLevelSplitJsonFolder.kt b/src/main/kotlin/gui/config/storage/FirstLevelSplitJsonFolder.kt new file mode 100644 index 0000000..ff544d5 --- /dev/null +++ b/src/main/kotlin/gui/config/storage/FirstLevelSplitJsonFolder.kt @@ -0,0 +1,83 @@ +@file:OptIn(ExperimentalSerializationApi::class) + +package moe.nea.firmament.gui.config.storage + +import java.nio.file.Path +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.decodeFromStream +import kotlinx.serialization.json.encodeToStream +import kotlin.io.path.deleteExisting +import kotlin.io.path.inputStream +import kotlin.io.path.listDirectoryEntries +import kotlin.io.path.nameWithoutExtension +import kotlin.io.path.outputStream +import moe.nea.firmament.Firmament + +class FirstLevelSplitJsonFolder( + val context: ConfigLoadContext, + val folder: Path +) { + fun load(): JsonObject { + context.logInfo("Loading FLSJF from $folder") + return folder.listDirectoryEntries("*.json") + .mapNotNull(::loadIndividualFile) + .toMap() + .let(::JsonObject) + .also { context.logInfo("FLSJF from $folder - Voller Erfolg!") } + } + + fun loadIndividualFile(path: Path): Pair<String, JsonElement>? { + context.logInfo("Loading partial file from $path") + return try { + path.inputStream().use { + path.nameWithoutExtension to Firmament.json.decodeFromStream(JsonElement.serializer(), it) + } + } catch (ex: Exception) { + context.logError("Could not load file from $path", ex) + null + } + } + + fun save(value: JsonObject) { + context.logInfo("Saving FLSJF to $folder") + context.logDebug("Current value:\n$value") + val entries = folder.listDirectoryEntries("*.json") + .toMutableList() + for ((name, element) in value) { + val path = saveIndividualFile(name, element) + if (path != null) { + entries.remove(path) + } + } + if (entries.isNotEmpty()) { + context.logInfo("Deleting additional files.") + for (path in entries) { + context.logInfo("Deleting $path") +// context.backup(path) + try { + path.deleteExisting() + } catch (ex: Exception) { + context.logError("Could not delete $path", ex) + } + } + } + context.logInfo("FLSJF to $folder - Voller Erfolg!") + } + + fun saveIndividualFile(name: String, element: JsonElement): Path? { + try { + context.logInfo("Saving partial file with name $name") + val path = folder.resolve("$name.json") + context.ensureWritable(path) + path.outputStream().use { + Firmament.json.encodeToStream(JsonElement.serializer(), element, it) + } + return path + } catch (ex: Exception) { + context.logError("Could not save $name with value $element", ex) + return null + } + } +} diff --git a/src/main/kotlin/gui/config/storage/LegacyImporter.kt b/src/main/kotlin/gui/config/storage/LegacyImporter.kt new file mode 100644 index 0000000..8915c17 --- /dev/null +++ b/src/main/kotlin/gui/config/storage/LegacyImporter.kt @@ -0,0 +1,63 @@ +package moe.nea.firmament.gui.config.storage + +import java.nio.file.Path +import javax.xml.namespace.QName +import kotlin.io.path.Path +import kotlin.io.path.copyTo +import kotlin.io.path.copyToRecursively +import kotlin.io.path.createDirectories +import kotlin.io.path.createParentDirectories +import kotlin.io.path.exists +import kotlin.io.path.forEachDirectoryEntry +import kotlin.io.path.listDirectoryEntries +import kotlin.io.path.moveTo +import kotlin.io.path.name +import kotlin.io.path.nameWithoutExtension +import kotlin.io.path.writeText +import moe.nea.firmament.gui.config.storage.FirmamentConfigLoader.configFolder +import moe.nea.firmament.gui.config.storage.FirmamentConfigLoader.configVersionFile +import moe.nea.firmament.gui.config.storage.FirmamentConfigLoader.storageFolder + +object LegacyImporter { + val legacyConfigVersion = 995 + val backupPath = configFolder.resolveSibling("firmament-legacy-config-${System.currentTimeMillis()}") + + fun copyIf(from: Path, to: Path) { + if (from.exists()) { + to.createParentDirectories() + from.copyTo(to) + } + } + + fun importFromLegacy() { + configFolder.moveTo(backupPath) + configFolder.createDirectories() + + copyIf( + backupPath.resolve("inventory-buttons.json"), + storageFolder.resolve("inventory-buttons.json") + ) + + backupPath.listDirectoryEntries("*.json") + .forEach { path -> + val name = path.name + if (name == "inventory-buttons.json") + return@forEach + path.copyTo(configFolder.resolve(name)) + } + + backupPath.resolve("profiles") + .forEachDirectoryEntry { category -> + category.forEachDirectoryEntry { profile -> + copyIf( + profile, + FirmamentConfigLoader.profilePath + .resolve(profile.nameWithoutExtension) + .resolve(category.name + ".json") + ) + } + } + + configVersionFile.writeText(legacyConfigVersion.toString()) + } +} |
