feat(database): 更改数据库命名方式.

由于 BotToken 可以更换, 所以旧版命名方式将会有迁移的问题, 故更改为通过 Bot AccountId 来命名数据库.

CLOSED #3
This commit is contained in:
LamGC 2022-03-20 11:56:38 +08:00
parent 0748afaff5
commit 19162dcaef
Signed by: LamGC
GPG Key ID: 6C5AE2A913941E1D
2 changed files with 175 additions and 8 deletions

View File

@ -112,7 +112,7 @@ internal class Launcher : AutoCloseable {
) )
val bot = ScalaBot( val bot = ScalaBot(
BotDBMaker.getBotMaker(account), BotDBMaker.getBotDbInstance(account),
botOption, botOption,
extensionPackageFinders, extensionPackageFinders,
botConfig botConfig

View File

@ -1,23 +1,190 @@
package net.lamgc.scalabot package net.lamgc.scalabot
import com.google.common.io.Files
import mu.KotlinLogging
import net.lamgc.scalabot.util.toHaxString import net.lamgc.scalabot.util.toHaxString
import org.mapdb.DB
import org.mapdb.DBException
import org.mapdb.DBMaker import org.mapdb.DBMaker
import org.telegram.abilitybots.api.db.DBContext import org.telegram.abilitybots.api.db.DBContext
import org.telegram.abilitybots.api.db.MapDBContext import org.telegram.abilitybots.api.db.MapDBContext
import java.io.File
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.security.MessageDigest import java.security.MessageDigest
/**
* 数据库适配器.
*
* 应按照新到旧的顺序放置, 新的适配器应该在上面.
*/
private val adapters = arrayListOf<DbAdapter>(
BotAccountIdDbAdapter, // since [v0.2.0 ~ latest)
BotTokenDbAdapter // since [v0.0.1 ~ v0.2.0)
)
private const val FIELD_DB_VERSION = "::DB_VERSION"
internal object BotDBMaker { internal object BotDBMaker {
fun getBotMaker(botAccount: BotAccount): DBContext { private val logger = KotlinLogging.logger { }
val digest: MessageDigest = MessageDigest.getInstance("SHA-256")
val digestBytes = digest.digest(botAccount.token.toByteArray(StandardCharsets.UTF_8)) fun getBotDbInstance(botAccount: BotAccount): DBContext {
val dbPath = AppPaths.DATA_DB.path + "${digestBytes.toHaxString()}.db" for (adapter in adapters) {
val db = DBMaker.fileDB(dbPath) val botDb = try {
adapter.getBotDb(botAccount, create = false) ?: continue
} catch (e: Exception) {
logger.error(e) { "适配器 ${adapter::class.java} 打开数据库时发生异常." }
continue
}
if (!adapter.dbVersionMatches(botDb)) {
logger.warn {
"数据库版本号与适配器不符. " +
"(Adapter: ${adapter::class.java};(${adapter.dbVersion})," +
" DatabaseVer: ${adapter.getDbVersion(botDb)})"
}
botDb.close()
continue
} else {
if (adapter != adapters[0]) {
logger.debug {
"数据库适配器不是最新的, 正在升级数据库... " +
"(Old: ${adapter::class.java}; New: ${adapters[0]::class.java})"
}
val db = try {
botDb.close()
val newDb = adapters[0].migrateDb(botAccount, adapter)
logger.debug { "数据库版本升级完成." }
newDb
} catch (e: Exception) {
logger.warn(e) { "Bot 数据库版本升级失败, 将继续使用旧版数据库." }
adapter.getBotDb(botAccount, create = false) ?: continue
}
return MapDBContext(db)
}
return MapDBContext(botDb)
}
}
logger.debug { "没有适配器成功打开数据库, 使用最新的适配器创建数据库. (Adapter: ${adapters[0]::class.java})" }
val newDb = adapters[0].getBotDb(botAccount, create = true)
?: throw IllegalStateException("No adapter is available to get the database.")
adapters[0].setDbVersion(newDb, adapters[0].dbVersion)
return MapDBContext(newDb)
}
}
/**
* 数据库适配器.
*
* 用于解决数据库格式更新带来的问题, 通过迁移机制, 将数据库从旧版本迁移到新版本, 或者只通过旧版本适配器访问而不迁移.
* @param dbVersion 数据库格式版本. 格式为: `{格式标识}_{最后使用的版本号}`, 如果为最新版适配器, 则不需要填写最后使用的版本号.
*/
private abstract class DbAdapter(val dbVersion: String) {
/**
* 获取 Bot 专有的 [DBContext].
* @param botAccount Bot 账号信息.
*/
abstract fun getBotDb(botAccount: BotAccount, create: Boolean = false): DB?
/**
* 通过 Bot 账号信息获取数据库文件.
*/
abstract fun getBotDbFile(botAccount: BotAccount): File
/**
* 将旧版数据库迁移到当前版本.
*
* 实现时请注意不要直接修改原数据库, 以防升级过程出错导致无法回退到旧版本.
*/
abstract fun migrateDb(botAccount: BotAccount, oldDbAdapter: DbAdapter): DB
/**
* 数据库版本是否匹配.
*/
open fun dbVersionMatches(db: DB): Boolean {
return getDbVersion(db) == dbVersion
}
fun getDbVersion(db: DB): String? {
if (!db.exists(FIELD_DB_VERSION)) {
return null
}
val dbVersionField = try {
db.atomicString(FIELD_DB_VERSION).open()
} catch (e: DBException.WrongConfiguration) {
return null
}
return dbVersionField.get()
}
fun setDbVersion(db: DB, version: String) {
db.atomicString(FIELD_DB_VERSION).createOrOpen().set(version)
}
}
/**
* 抽象文件数据库适配器.
*
* 只有文件有变化的适配器.
*/
private abstract class FileDbAdapter(
dbVersion: String,
private val fileProvider: (BotAccount) -> File
) : DbAdapter(dbVersion) {
@Suppress("unused")
constructor(dbVersion: String) : this(dbVersion,
{ throw NotImplementedError("When using this constructor, the \"getBotDbFile\" method must be implemented") })
override fun getBotDb(botAccount: BotAccount, create: Boolean): DB? {
val dbFile = getBotDbFile(botAccount)
if (!dbFile.exists() && !create) {
return null
}
return DBMaker.fileDB(dbFile)
.closeOnJvmShutdownWeakReference() .closeOnJvmShutdownWeakReference()
.checksumStoreEnable() .checksumStoreEnable()
.fileChannelEnable() .fileChannelEnable()
.make() .make()
return MapDBContext(db)
} }
override fun getBotDbFile(botAccount: BotAccount): File = fileProvider(botAccount)
override fun migrateDb(botAccount: BotAccount, oldDbAdapter: DbAdapter): DB {
val oldFile = oldDbAdapter.getBotDbFile(botAccount)
val newFile = getBotDbFile(botAccount)
try {
@Suppress("UnstableApiUsage")
Files.copy(oldFile, newFile)
} catch (e: Exception) {
if (newFile.exists()) {
// 删除新文件以防止异常退出后直接读取新文件.
newFile.delete()
}
throw e
}
oldFile.delete()
return getBotDb(botAccount)!!.apply {
setDbVersion(this, this@FileDbAdapter.dbVersion)
}
}
} }
/**
* 使用 Bot Token 中的 Account Id 命名数据库文件名.
*/
private object BotAccountIdDbAdapter : FileDbAdapter("BotAccountId", { botAccount ->
File(AppPaths.DATA_DB.file, "${botAccount.id}.db")
})
/**
* 使用 Bot Token, 经过 Sha256 加密后得到文件名.
*
* **已弃用**: 由于 Token 可以重新生成, Token 改变后数据库文件名也会改变, 故弃用该方法.
*/
private object BotTokenDbAdapter : FileDbAdapter("BotToken_v0.1.0", { botAccount ->
val digest: MessageDigest = MessageDigest.getInstance("SHA-256")
val digestBytes = digest.digest(botAccount.token.toByteArray(StandardCharsets.UTF_8))
File(AppPaths.DATA_DB.file, "${digestBytes.toHaxString()}.db")
})