aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLinnea Gräf <nea@nea.moe>2025-09-28 19:48:11 +0200
committerLinnea Gräf <nea@nea.moe>2025-10-13 18:26:42 +0200
commit9b6c8f21f86385c993c0e34ba4b31348cae200cd (patch)
treed310ffaf3e98af40c64d9895771026dd7ee4c56d
parente8a42a056cb0209f8bbeb35dd74f42e5c2d8bd62 (diff)
downloadFirmament-9b6c8f21f86385c993c0e34ba4b31348cae200cd.tar.gz
Firmament-9b6c8f21f86385c993c0e34ba4b31348cae200cd.tar.bz2
Firmament-9b6c8f21f86385c993c0e34ba4b31348cae200cd.zip
fix: improve config backups
-rw-r--r--src/main/java/moe/nea/firmament/util/data/ManagedConfig.kt5
-rw-r--r--src/main/kotlin/Firmament.kt1
-rw-r--r--src/main/kotlin/features/inventory/SlotLocking.kt4
-rw-r--r--src/main/kotlin/features/world/FirmWaypointManager.kt14
-rw-r--r--src/main/kotlin/gui/config/storage/ConfigLoadContext.kt21
-rw-r--r--src/main/kotlin/gui/config/storage/FirmamentConfigLoader.kt32
-rw-r--r--src/main/kotlin/gui/config/storage/FirstLevelSplitJsonFolder.kt41
-rw-r--r--src/main/kotlin/gui/config/storage/LegacyImporter.kt6
-rw-r--r--src/main/kotlin/util/data/IDataHolder.kt31
-rw-r--r--src/main/kotlin/util/data/MultiFileDataHolder.kt62
-rw-r--r--src/main/kotlin/util/data/ProfileSpecificDataHolder.kt2
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)