diff options
| author | Linnea Gräf <nea@nea.moe> | 2025-09-28 19:48:11 +0200 |
|---|---|---|
| committer | Linnea Gräf <nea@nea.moe> | 2025-10-13 18:26:42 +0200 |
| commit | 9b6c8f21f86385c993c0e34ba4b31348cae200cd (patch) | |
| tree | d310ffaf3e98af40c64d9895771026dd7ee4c56d | |
| parent | e8a42a056cb0209f8bbeb35dd74f42e5c2d8bd62 (diff) | |
| download | Firmament-9b6c8f21f86385c993c0e34ba4b31348cae200cd.tar.gz Firmament-9b6c8f21f86385c993c0e34ba4b31348cae200cd.tar.bz2 Firmament-9b6c8f21f86385c993c0e34ba4b31348cae200cd.zip | |
fix: improve config backups
11 files changed, 122 insertions, 97 deletions
diff --git a/src/main/java/moe/nea/firmament/util/data/ManagedConfig.kt b/src/main/java/moe/nea/firmament/util/data/ManagedConfig.kt index b441b02..4d22d71 100644 --- a/src/main/java/moe/nea/firmament/util/data/ManagedConfig.kt +++ b/src/main/java/moe/nea/firmament/util/data/ManagedConfig.kt @@ -110,6 +110,11 @@ abstract class ManagedConfig( } } + override fun explicitDefaultLoad() { + val empty = JsonObject(mapOf()) + sortedOptions.forEach { it.load(empty) } + } + override fun loadFrom(key: Unit, jsonObject: JsonObject) { val unprefixed = jsonObject[name]?.jsonObject ?: JsonObject(mapOf()) sortedOptions.forEach { diff --git a/src/main/kotlin/Firmament.kt b/src/main/kotlin/Firmament.kt index 04af5bc..795619e 100644 --- a/src/main/kotlin/Firmament.kt +++ b/src/main/kotlin/Firmament.kt @@ -60,7 +60,6 @@ object Firmament { val DEBUG = System.getProperty("firmament.debug") == "true" val DATA_DIR: Path = Path.of(".firmament").also { Files.createDirectories(it) } - val CONFIG_DIR: Path = Path.of("config/firmament").also { Files.createDirectories(it) } val logger: Logger = LogManager.getLogger("Firmament") private val metadata: ModMetadata by lazy { FabricLoader.getInstance().getModContainer(MOD_ID).orElseThrow().metadata diff --git a/src/main/kotlin/features/inventory/SlotLocking.kt b/src/main/kotlin/features/inventory/SlotLocking.kt index bae6a5e..903b184 100644 --- a/src/main/kotlin/features/inventory/SlotLocking.kt +++ b/src/main/kotlin/features/inventory/SlotLocking.kt @@ -79,9 +79,9 @@ object SlotLocking { val currentWorldData get() = if (SBData.skyblockLocation == SkyBlockIsland.RIFT) - DConfig.data?.rift + DConfig.data.rift else - DConfig.data?.overworld + DConfig.data.overworld @Serializable data class BoundSlot( diff --git a/src/main/kotlin/features/world/FirmWaypointManager.kt b/src/main/kotlin/features/world/FirmWaypointManager.kt index d18483c..41c3007 100644 --- a/src/main/kotlin/features/world/FirmWaypointManager.kt +++ b/src/main/kotlin/features/world/FirmWaypointManager.kt @@ -16,11 +16,11 @@ import moe.nea.firmament.util.ClipboardUtils import moe.nea.firmament.util.FirmFormatters import moe.nea.firmament.util.MC import moe.nea.firmament.util.TemplateUtil -import moe.nea.firmament.util.data.MultiFileDataHolder +import moe.nea.firmament.util.data.DataHolder import moe.nea.firmament.util.tr object FirmWaypointManager { - object DataHolder : MultiFileDataHolder<FirmWaypoints>(serializer(), "waypoints") + object DConfig : DataHolder<MutableMap<String, FirmWaypoints>>(serializer(), "waypoints", ::mutableMapOf) val SHARE_PREFIX = "FIRM_WAYPOINTS/" val ENCODED_SHARE_PREFIX = TemplateUtil.getPrefixComparisonSafeBase64Encoding(SHARE_PREFIX) @@ -100,7 +100,7 @@ object FirmWaypointManager { } thenLiteral("save") { thenArgument("name", StringArgumentType.string()) { name -> - suggestsList { DataHolder.list().keys } + suggestsList { DConfig.data.keys } thenExecute { val waypoints = Waypoints.useNonEmptyWaypoints() if (waypoints == null) { @@ -109,8 +109,8 @@ object FirmWaypointManager { } waypoints.id = get(name) val exportableWaypoints = createExportableCopy(waypoints) - DataHolder.insert(get(name), exportableWaypoints) - DataHolder.save() + DConfig.data[get(name)] = exportableWaypoints + DConfig.markDirty() source.sendFeedback(tr("firmament.command.waypoint.saved", "Saved waypoints locally as ${get(name)}. Use /firm waypoints load to load them again.")) } @@ -118,10 +118,10 @@ object FirmWaypointManager { } thenLiteral("load") { thenArgument("name", StringArgumentType.string()) { name -> - suggestsList { DataHolder.list().keys } + suggestsList { DConfig.data.keys } thenExecute { val name = get(name) - val waypoints = DataHolder.list()[name] + val waypoints = DConfig.data[name] if (waypoints == null) { source.sendError( tr("firmament.command.waypoint.nosaved", diff --git a/src/main/kotlin/gui/config/storage/ConfigLoadContext.kt b/src/main/kotlin/gui/config/storage/ConfigLoadContext.kt index 59ca71e..4a06ec6 100644 --- a/src/main/kotlin/gui/config/storage/ConfigLoadContext.kt +++ b/src/main/kotlin/gui/config/storage/ConfigLoadContext.kt @@ -3,7 +3,10 @@ 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.ExperimentalPathApi +import kotlin.io.path.OnErrorResult import kotlin.io.path.Path +import kotlin.io.path.copyToRecursively import kotlin.io.path.createParentDirectories import kotlin.io.path.writeText import moe.nea.firmament.Firmament @@ -11,6 +14,9 @@ import moe.nea.firmament.Firmament data class ConfigLoadContext( val loadId: String, ) : AutoCloseable { + val backupPath = Path("backups").resolve(Firmament.MOD_ID) + .resolve("config-$loadId") + .toAbsolutePath() val logFile = Path("logs") .resolve(Firmament.MOD_ID) .resolve("config-$loadId.log") @@ -74,4 +80,19 @@ data class ConfigLoadContext( } } } + + @OptIn(ExperimentalPathApi::class) + fun createBackup(folder: Path, string: String) { + val backupDestination = backupPath.resolve("$string-${System.currentTimeMillis()}") + logError("Creating backup of $folder in $backupDestination") + folder.copyToRecursively( + backupDestination.createParentDirectories(), + onError = { source: Path, target: Path, exception: Exception -> + logError("Failed to copy subtree $source to $target", exception) + OnErrorResult.SKIP_SUBTREE + }, + followLinks = false, + overwrite = false + ) + } } diff --git a/src/main/kotlin/gui/config/storage/FirmamentConfigLoader.kt b/src/main/kotlin/gui/config/storage/FirmamentConfigLoader.kt index f8e3104..a408136 100644 --- a/src/main/kotlin/gui/config/storage/FirmamentConfigLoader.kt +++ b/src/main/kotlin/gui/config/storage/FirmamentConfigLoader.kt @@ -13,6 +13,7 @@ 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.SBData.NULL_UUID import moe.nea.firmament.util.data.IConfigProvider import moe.nea.firmament.util.data.IDataHolder import moe.nea.firmament.util.data.ProfileKeyedConfig @@ -46,12 +47,15 @@ object FirmamentConfigLoader { loadConfigFromData(configData, Unit, ConfigStorageClass.CONFIG) val storageData = FirstLevelSplitJsonFolder(loadContext, storageFolder).load() loadConfigFromData(storageData, Unit, ConfigStorageClass.STORAGE) - val profileData = - profilePath.listDirectoryEntries() - .filter { it.isDirectory() } - .associate { + var profileData = + profilePath.takeIf { it.exists() } + ?.listDirectoryEntries() + ?.filter { it.isDirectory() } + ?.associate { UUID.fromString(it.name) to FirstLevelSplitJsonFolder(loadContext, it).load() } + if (profileData.isNullOrEmpty()) + profileData = mapOf(NULL_UUID to JsonObject(mapOf())) profileData.forEach { (key, value) -> loadConfigFromData(value, key, ConfigStorageClass.PROFILE) } @@ -60,12 +64,17 @@ object FirmamentConfigLoader { fun <T> loadConfigFromData( configData: JsonObject, - key: T, + key: T?, storageClass: ConfigStorageClass ) { for (holder in allConfigs) { if (holder.storageClass == storageClass) { - (holder as IDataHolder<T>).loadFrom(key, configData) + val h = (holder as IDataHolder<T>) + if (key == null) { + h.explicitDefaultLoad() + } else { + h.loadFrom(key, configData) + } } } } @@ -120,6 +129,7 @@ object FirmamentConfigLoader { FirstLevelSplitJsonFolder(context, profilePath.resolve(profileId.toString())) ) } + writeConfigVersion() } } @@ -170,16 +180,24 @@ object FirmamentConfigLoader { FirstLevelSplitJsonFolder(loadContext, it) ) } - configVersionFile.writeText("$currentConfigVersion ${tagLines.random()}") + writeConfigVersion() } } + fun writeConfigVersion() { + configVersionFile.writeText("$currentConfigVersion ${tagLines.random()}") + } + private fun updateOneConfig( loadContext: ConfigLoadContext, startVersion: Int, storageClass: ConfigStorageClass, firstLevelSplitJsonFolder: FirstLevelSplitJsonFolder ) { + if (startVersion == currentConfigVersion) { + loadContext.logDebug("Skipping upgrade to ") + return + } loadContext.logInfo("Starting upgrade from at ${firstLevelSplitJsonFolder.folder} ($storageClass) to $startVersion") var data = firstLevelSplitJsonFolder.load() for (nextVersion in (startVersion + 1)..currentConfigVersion) { diff --git a/src/main/kotlin/gui/config/storage/FirstLevelSplitJsonFolder.kt b/src/main/kotlin/gui/config/storage/FirstLevelSplitJsonFolder.kt index ff544d5..3c672bf 100644 --- a/src/main/kotlin/gui/config/storage/FirstLevelSplitJsonFolder.kt +++ b/src/main/kotlin/gui/config/storage/FirstLevelSplitJsonFolder.kt @@ -8,7 +8,9 @@ import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.decodeFromStream import kotlinx.serialization.json.encodeToStream +import kotlin.io.path.createDirectories import kotlin.io.path.deleteExisting +import kotlin.io.path.exists import kotlin.io.path.inputStream import kotlin.io.path.listDirectoryEntries import kotlin.io.path.nameWithoutExtension @@ -19,23 +21,41 @@ class FirstLevelSplitJsonFolder( val context: ConfigLoadContext, val folder: Path ) { + + var hasCreatedBackup = false + + fun backup(cause: String) { + if (hasCreatedBackup) return + hasCreatedBackup = true + context.createBackup(folder, cause) + } + 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!") } + if (!folder.exists()) + return JsonObject(mapOf()) + return try { + folder.listDirectoryEntries("*.json") + .mapNotNull(::loadIndividualFile) + .toMap() + .let(::JsonObject) + .also { context.logInfo("FLSJF from $folder - Voller Erfolg!") } + } catch (ex: Exception) { + context.logError("Could not load files from $folder", ex) + backup("failed-load") + JsonObject(mapOf()) + } } fun loadIndividualFile(path: Path): Pair<String, JsonElement>? { - context.logInfo("Loading partial file from $path") + context.logDebug("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) + backup("failed-load") null } } @@ -43,6 +63,10 @@ class FirstLevelSplitJsonFolder( fun save(value: JsonObject) { context.logInfo("Saving FLSJF to $folder") context.logDebug("Current value:\n$value") + if (!folder.exists()) { + context.logInfo("Creating folder $folder") + folder.createDirectories() + } val entries = folder.listDirectoryEntries("*.json") .toMutableList() for ((name, element) in value) { @@ -55,7 +79,7 @@ class FirstLevelSplitJsonFolder( context.logInfo("Deleting additional files.") for (path in entries) { context.logInfo("Deleting $path") -// context.backup(path) + backup("save-deletion") try { path.deleteExisting() } catch (ex: Exception) { @@ -68,7 +92,7 @@ class FirstLevelSplitJsonFolder( fun saveIndividualFile(name: String, element: JsonElement): Path? { try { - context.logInfo("Saving partial file with name $name") + context.logDebug("Saving partial file with name $name") val path = folder.resolve("$name.json") context.ensureWritable(path) path.outputStream().use { @@ -77,6 +101,7 @@ class FirstLevelSplitJsonFolder( return path } catch (ex: Exception) { context.logError("Could not save $name with value $element", ex) + backup("failed-save") return null } } diff --git a/src/main/kotlin/gui/config/storage/LegacyImporter.kt b/src/main/kotlin/gui/config/storage/LegacyImporter.kt index d06afcc..c1f8b90 100644 --- a/src/main/kotlin/gui/config/storage/LegacyImporter.kt +++ b/src/main/kotlin/gui/config/storage/LegacyImporter.kt @@ -32,6 +32,7 @@ object LegacyImporter { ) fun importFromLegacy() { + if (!configFolder.exists()) return configFolder.moveTo(backupPath) configFolder.createDirectories() @@ -50,7 +51,8 @@ object LegacyImporter { } backupPath.resolve("profiles") - .forEachDirectoryEntry { category -> + .takeIf { it.exists() } + ?.forEachDirectoryEntry { category -> category.forEachDirectoryEntry { profile -> copyIf( profile, @@ -61,6 +63,6 @@ object LegacyImporter { } } - configVersionFile.writeText(legacyConfigVersion.toString()) + configVersionFile.writeText("$legacyConfigVersion LEGACY") } } diff --git a/src/main/kotlin/util/data/IDataHolder.kt b/src/main/kotlin/util/data/IDataHolder.kt index 541fc1b..de6dff8 100644 --- a/src/main/kotlin/util/data/IDataHolder.kt +++ b/src/main/kotlin/util/data/IDataHolder.kt @@ -21,6 +21,7 @@ sealed class IDataHolder<T> { abstract fun keys(): Collection<T> abstract fun saveTo(key: T): JsonObject abstract fun loadFrom(key: T, jsonObject: JsonObject) + abstract fun explicitDefaultLoad() abstract fun clear() abstract val storageClass: ConfigStorageClass } @@ -28,18 +29,21 @@ sealed class IDataHolder<T> { open class ProfileKeyedConfig<T>( val prefix: String, val serializer: KSerializer<T>, - val default: () -> T, + val default: () -> T & Any, ) : IDataHolder<UUID>() { override val storageClass: ConfigStorageClass get() = ConfigStorageClass.PROFILE private var _data: MutableMap<UUID, T>? = null - val data - get() = _data!!.let { map -> - map[SBData.profileIdOrNil] - ?: default().also { map[SBData.profileIdOrNil] = it } - } ?: error("Config $this not loaded — forgot to register?") + val data: T & Any + get() { + val map = _data ?: error("Config $this not loaded — forgot to register?") + map[SBData.profileIdOrNil]?.let { return it } + val newValue = default() + map[SBData.profileIdOrNil] = newValue + return newValue + } override fun keys(): Collection<UUID> { return _data!!.keys @@ -53,13 +57,22 @@ open class ProfileKeyedConfig<T>( } override fun loadFrom(key: UUID, jsonObject: JsonObject) { - (_data ?: mutableMapOf<UUID, T>().also { _data = it })[key] = + var map = _data + if (map == null) { + map = mutableMapOf() + _data = map + } + map[key] = jsonObject[prefix] ?.let { Firmament.json.decodeFromJsonElement(serializer, it) } ?: default() } + override fun explicitDefaultLoad() { + _data = mutableMapOf() + } + override fun clear() { _data = null } @@ -79,6 +92,10 @@ abstract class GenericConfig<T>( return listOf(Unit) } + override fun explicitDefaultLoad() { + _data = default() + } + open fun onLoad() { } diff --git a/src/main/kotlin/util/data/MultiFileDataHolder.kt b/src/main/kotlin/util/data/MultiFileDataHolder.kt deleted file mode 100644 index 209f780..0000000 --- a/src/main/kotlin/util/data/MultiFileDataHolder.kt +++ /dev/null @@ -1,62 +0,0 @@ -package moe.nea.firmament.util.data - -import kotlinx.serialization.KSerializer -import kotlin.io.path.createDirectories -import kotlin.io.path.deleteExisting -import kotlin.io.path.exists -import kotlin.io.path.extension -import kotlin.io.path.listDirectoryEntries -import kotlin.io.path.nameWithoutExtension -import kotlin.io.path.readText -import kotlin.io.path.writeText -import moe.nea.firmament.Firmament - -abstract class MultiFileDataHolder<T>( - val dataSerializer: KSerializer<T>, - val configName: String -) { // TODO: abstract this + ProfileSpecificDataHolder - val configDirectory = Firmament.CONFIG_DIR.resolve(configName) - private var allData = readValues() - protected fun readValues(): MutableMap<String, T> { - if (!configDirectory.exists()) { - configDirectory.createDirectories() - } - val profileFiles = configDirectory.listDirectoryEntries() - return profileFiles - .filter { it.extension == "json" } - .mapNotNull { - try { - it.nameWithoutExtension to Firmament.json.decodeFromString(dataSerializer, it.readText()) - } catch (e: Exception) { /* Expecting IOException and SerializationException, but Kotlin doesn't allow multi catches*/ - Firmament.logger.error( - "Exception during loading of multi file data holder $it ($configName). This will reset that profiles config.", - e - ) - null - } - }.toMap().toMutableMap() - } - - fun save() { - if (!configDirectory.exists()) { - configDirectory.createDirectories() - } - val c = allData - configDirectory.listDirectoryEntries().forEach { - if (it.nameWithoutExtension !in c.mapKeys { it.toString() }) { - it.deleteExisting() - } - } - c.forEach { (name, value) -> - val f = configDirectory.resolve("$name.json") - f.writeText(Firmament.json.encodeToString(dataSerializer, value)) - } - } - - fun list(): Map<String, T> = allData - val validPathRegex = "[a-zA-Z0-9_][a-zA-Z0-9\\-_.]*".toPattern() - fun insert(name: String, value: T) { - require(validPathRegex.matcher(name).matches()) { "Not a valid name: $name" } - allData[name] = value - } -} diff --git a/src/main/kotlin/util/data/ProfileSpecificDataHolder.kt b/src/main/kotlin/util/data/ProfileSpecificDataHolder.kt index 3922c34..853ba7d 100644 --- a/src/main/kotlin/util/data/ProfileSpecificDataHolder.kt +++ b/src/main/kotlin/util/data/ProfileSpecificDataHolder.kt @@ -5,5 +5,5 @@ import kotlinx.serialization.KSerializer abstract class ProfileSpecificDataHolder<S>( dataSerializer: KSerializer<S>, configName: String, - configDefault: () -> S + configDefault: () -> S & Any ) : ProfileKeyedConfig<S>(configName, dataSerializer, configDefault) |
