diff --git a/src/main/kotlin/de/darkatra/vrising/discord/migration/DatabaseHelper.kt b/src/main/kotlin/de/darkatra/vrising/discord/migration/DatabaseHelper.kt index eb7234f..6681eaa 100644 --- a/src/main/kotlin/de/darkatra/vrising/discord/migration/DatabaseHelper.kt +++ b/src/main/kotlin/de/darkatra/vrising/discord/migration/DatabaseHelper.kt @@ -3,8 +3,15 @@ package de.darkatra.vrising.discord.migration import org.dizitart.no2.Nitrite import org.dizitart.no2.collection.Document import org.dizitart.no2.collection.NitriteId +import org.dizitart.no2.common.Constants +import org.dizitart.no2.common.meta.Attributes import org.dizitart.no2.store.NitriteMap fun Nitrite.getNitriteMap(name: String): NitriteMap { return store.openMap(name, NitriteId::class.java, Document::class.java) } + +fun Nitrite.listAllCollectionNames(): List { + return store.openMap(Constants.META_MAP_NAME, String::class.java, Attributes::class.java).keys() + .filter { key -> !key.startsWith("\$nitrite") } +} diff --git a/src/main/kotlin/de/darkatra/vrising/discord/persistence/DatabaseConfiguration.kt b/src/main/kotlin/de/darkatra/vrising/discord/persistence/DatabaseConfiguration.kt index 30e4605..97070f2 100644 --- a/src/main/kotlin/de/darkatra/vrising/discord/persistence/DatabaseConfiguration.kt +++ b/src/main/kotlin/de/darkatra/vrising/discord/persistence/DatabaseConfiguration.kt @@ -2,17 +2,28 @@ package de.darkatra.vrising.discord.persistence import de.darkatra.vrising.discord.BotProperties import de.darkatra.vrising.discord.migration.SchemaEntityConverter +import de.darkatra.vrising.discord.migration.getNitriteMap +import de.darkatra.vrising.discord.migration.listAllCollectionNames import de.darkatra.vrising.discord.persistence.model.converter.ErrorEntityConverter import de.darkatra.vrising.discord.persistence.model.converter.PlayerActivityFeedEntityConverter import de.darkatra.vrising.discord.persistence.model.converter.PvpKillFeedEntityConverter import de.darkatra.vrising.discord.persistence.model.converter.ServerEntityConverter import de.darkatra.vrising.discord.persistence.model.converter.StatusMonitorEntityConverter import org.dizitart.no2.Nitrite +import org.dizitart.no2.NitriteBuilder +import org.dizitart.no2.exceptions.NitriteIOException import org.dizitart.no2.mvstore.MVStoreModule +import org.dizitart.no2.store.StoreModule +import org.slf4j.LoggerFactory import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import java.nio.file.Files import java.nio.file.Path +import kotlin.io.path.absolutePathString +import kotlin.io.path.copyTo +import kotlin.io.path.deleteIfExists +import kotlin.io.path.exists @Configuration @EnableConfigurationProperties(BotProperties::class) @@ -22,12 +33,63 @@ class DatabaseConfiguration( companion object { + private val logger by lazy { LoggerFactory.getLogger(DatabaseConfiguration::class.java) } + fun buildNitriteDatabase(databaseFile: Path, username: String? = null, password: String? = null): Nitrite { - val storeModule = MVStoreModule.withConfig() - .filePath(databaseFile.toAbsolutePath().toFile()) - .compress(true) - .build() + // version 2.12.0 introduced database encryption at rest. the following code attempts to perform the migration if necessary + return try { + // try to open the database with encryption + getNitriteBuilder(getStoreModule(databaseFile, password)).openOrCreate(username, password) + } catch (e: NitriteIOException) { + + // if the automated migration was aborted while writing the files to disc, restore the backup + val unencryptedDatabaseBackupFile = Path.of(System.getProperty("java.io.tmpdir")).resolve("v-rising-bot.db.unencrypted") + if (unencryptedDatabaseBackupFile.exists()) { + logger.info("Found an unencrypted backup of the database at: ${unencryptedDatabaseBackupFile.absolutePathString()}") + unencryptedDatabaseBackupFile.copyTo(databaseFile, overwrite = true) + logger.info("Successfully restored the backup. Will re-attempt the migration.") + } + + logger.info("Attempting to encrypt the bot database with the provided database password.") + + // retry opening the database without encryption if we encounter an error + val unencryptedDatabase = try { + getNitriteBuilder(getStoreModule(databaseFile, null)).openOrCreate(username, password) + } catch (e2: NitriteIOException) { + // if we also can't open the database without encryption, throw the initial error as it probably isn't related to encryption + throw e + } + + unencryptedDatabaseBackupFile.deleteIfExists() + + // create an encrypted copy of the existing database + val tempDatabaseFile = Files.createTempFile("v-rising-bot", ".db") + + val encryptedDatabase = getNitriteBuilder(getStoreModule(tempDatabaseFile, password)).openOrCreate(username, password) + for (collectionName in unencryptedDatabase.listAllCollectionNames()) { + + val oldCollection = unencryptedDatabase.getNitriteMap(collectionName) + val newCollection = encryptedDatabase.getNitriteMap(collectionName) + + oldCollection.values().forEach { document -> newCollection.put(document.id, document) } + } + unencryptedDatabase.close() + encryptedDatabase.close() + + databaseFile.copyTo(unencryptedDatabaseBackupFile) + tempDatabaseFile.copyTo(databaseFile, overwrite = true) + + unencryptedDatabaseBackupFile.deleteIfExists() + tempDatabaseFile.deleteIfExists() + + getNitriteBuilder(getStoreModule(databaseFile, password)).openOrCreate(username, password).also { + logger.info("Successfully encrypted the database.") + } + } + } + + private fun getNitriteBuilder(storeModule: StoreModule): NitriteBuilder { return Nitrite.builder() .loadModule(storeModule) @@ -38,7 +100,15 @@ class DatabaseConfiguration( .registerEntityConverter(PvpKillFeedEntityConverter()) .registerEntityConverter(ServerEntityConverter()) .registerEntityConverter(StatusMonitorEntityConverter()) - .openOrCreate(username, password) + } + + private fun getStoreModule(databaseFile: Path, password: String?): MVStoreModule { + + return MVStoreModule.withConfig() + .filePath(databaseFile.toAbsolutePath().toFile()) + .encryptionKey(password?.let(String::toCharArray)) + .compress(true) + .build() } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 388f6f8..7322acd 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -9,6 +9,9 @@ logging: level: root: info com.ibasco.agql: warn + # nitrite is logging some of the exceptions before throwing - disable all nitrite logs since we already log all exceptions + nitrite: off + nitrite-mvstore: off pattern: console: "%clr(%d{yyyy-MM-dd'T'HH:mm:ss.SSSXXX}){faint} %clr(%5p) %clr(${PID:-}){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m %mdc%n%wEx"