mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
chore!: migrate to version 2.x
This commit is contained in:
@@ -0,0 +1,746 @@
|
||||
package app.termora.plugins.migration
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.Application.ohMyJson
|
||||
import app.termora.highlight.KeywordHighlight
|
||||
import app.termora.keymap.Keymap
|
||||
import app.termora.keymgr.OhKeyPair
|
||||
import app.termora.macro.Macro
|
||||
import app.termora.snippet.Snippet
|
||||
import app.termora.terminal.CursorStyle
|
||||
import jetbrains.exodus.bindings.StringBinding
|
||||
import jetbrains.exodus.env.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
class Database private constructor(private val env: Environment) : Disposable {
|
||||
companion object {
|
||||
private const val KEYMAP_STORE = "Keymap"
|
||||
private const val HOST_STORE = "Host"
|
||||
private const val SNIPPET_STORE = "Snippet"
|
||||
private const val KEYWORD_HIGHLIGHT_STORE = "KeywordHighlight"
|
||||
private const val MACRO_STORE = "Macro"
|
||||
private const val KEY_PAIR_STORE = "KeyPair"
|
||||
private const val DELETED_DATA_STORE = "DeletedData"
|
||||
private val log = LoggerFactory.getLogger(Database::class.java)
|
||||
|
||||
|
||||
private fun open(dir: File): Database {
|
||||
val config = EnvironmentConfig()
|
||||
// 32MB
|
||||
config.setLogFileSize(1024 * 32)
|
||||
config.setGcEnabled(true)
|
||||
// 5m
|
||||
config.setGcStartIn(5.minutes.inWholeMilliseconds.toInt())
|
||||
val environment = Environments.newInstance(dir, config)
|
||||
return Database(environment)
|
||||
}
|
||||
|
||||
fun getDatabase(): Database {
|
||||
return ApplicationScope.forApplicationScope()
|
||||
.getOrCreate(Database::class) { open(MigrationApplicationRunnerExtension.instance.getDatabaseFile()) }
|
||||
}
|
||||
}
|
||||
|
||||
val properties by lazy { Properties() }
|
||||
val safetyProperties by lazy { SafetyProperties("Setting.SafetyProperties") }
|
||||
val terminal by lazy { Terminal() }
|
||||
val appearance by lazy { Appearance() }
|
||||
val sftp by lazy { SFTP() }
|
||||
val sync by lazy { Sync() }
|
||||
|
||||
private val doorman get() = Doorman.getInstance()
|
||||
|
||||
|
||||
fun getKeymaps(): Collection<Keymap> {
|
||||
val array = env.computeInTransaction { tx ->
|
||||
openCursor<String>(tx, KEYMAP_STORE) { _, value ->
|
||||
value
|
||||
}.values
|
||||
}
|
||||
|
||||
val keymaps = mutableListOf<Keymap>()
|
||||
for (text in array.iterator()) {
|
||||
keymaps.add(Keymap.fromJSON(text) ?: continue)
|
||||
}
|
||||
|
||||
return keymaps
|
||||
}
|
||||
|
||||
fun addKeymap(keymap: Keymap) {
|
||||
env.executeInTransaction {
|
||||
put(it, KEYMAP_STORE, keymap.name, keymap.toJSON())
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Added Keymap: ${keymap.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeKeymap(name: String) {
|
||||
env.executeInTransaction {
|
||||
delete(it, KEYMAP_STORE, name)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Removed Keymap: $name")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun getHosts(): Collection<Host> {
|
||||
val isWorking = doorman.isWorking()
|
||||
return env.computeInTransaction { tx ->
|
||||
openCursor<Host>(tx, HOST_STORE) { _, value ->
|
||||
if (isWorking)
|
||||
ohMyJson.decodeFromString(doorman.decrypt(value))
|
||||
else
|
||||
ohMyJson.decodeFromString(value)
|
||||
}.values
|
||||
}
|
||||
}
|
||||
|
||||
fun removeAllKeyPair() {
|
||||
env.executeInTransaction { tx ->
|
||||
val store = env.openStore(KEY_PAIR_STORE, StoreConfig.WITHOUT_DUPLICATES_WITH_PREFIXING, tx)
|
||||
store.openCursor(tx).use {
|
||||
while (it.next) {
|
||||
it.deleteCurrent()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getKeyPairs(): Collection<OhKeyPair> {
|
||||
val isWorking = doorman.isWorking()
|
||||
return env.computeInTransaction { tx ->
|
||||
openCursor<OhKeyPair>(tx, KEY_PAIR_STORE) { _, value ->
|
||||
if (isWorking)
|
||||
ohMyJson.decodeFromString(doorman.decrypt(value))
|
||||
else
|
||||
ohMyJson.decodeFromString(value)
|
||||
}.values
|
||||
}
|
||||
}
|
||||
|
||||
fun addHost(host: Host) {
|
||||
var text = ohMyJson.encodeToString(host)
|
||||
if (doorman.isWorking()) {
|
||||
text = doorman.encrypt(text)
|
||||
}
|
||||
env.executeInTransaction {
|
||||
put(it, HOST_STORE, host.id, text)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Added Host: ${host.id} , ${host.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeHost(id: String) {
|
||||
env.executeInTransaction {
|
||||
delete(it, HOST_STORE, id)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Removed host: $id")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addDeletedData(deletedData: DeletedData) {
|
||||
val text = ohMyJson.encodeToString(deletedData)
|
||||
env.executeInTransaction {
|
||||
put(it, DELETED_DATA_STORE, deletedData.id, text)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Added DeletedData: ${deletedData.id} , $text")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getDeletedData(): Collection<DeletedData> {
|
||||
return env.computeInTransaction { tx ->
|
||||
openCursor<DeletedData?>(tx, DELETED_DATA_STORE) { _, value ->
|
||||
try {
|
||||
ohMyJson.decodeFromString(value)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}.values.filterNotNull()
|
||||
}
|
||||
}
|
||||
|
||||
fun addSnippet(snippet: Snippet) {
|
||||
var text = ohMyJson.encodeToString(snippet)
|
||||
if (doorman.isWorking()) {
|
||||
text = doorman.encrypt(text)
|
||||
}
|
||||
env.executeInTransaction {
|
||||
put(it, SNIPPET_STORE, snippet.id, text)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Added Snippet: ${snippet.id} , ${snippet.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeSnippet(id: String) {
|
||||
env.executeInTransaction {
|
||||
delete(it, SNIPPET_STORE, id)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Removed snippet: $id")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getSnippets(): Collection<Snippet> {
|
||||
val isWorking = doorman.isWorking()
|
||||
return env.computeInTransaction { tx ->
|
||||
openCursor<Snippet>(tx, SNIPPET_STORE) { _, value ->
|
||||
if (isWorking)
|
||||
ohMyJson.decodeFromString(doorman.decrypt(value))
|
||||
else
|
||||
ohMyJson.decodeFromString(value)
|
||||
}.values
|
||||
}
|
||||
}
|
||||
|
||||
fun getKeywordHighlights(): Collection<KeywordHighlight> {
|
||||
return env.computeInTransaction { tx ->
|
||||
openCursor<KeywordHighlight>(tx, KEYWORD_HIGHLIGHT_STORE) { _, value ->
|
||||
ohMyJson.decodeFromString(value)
|
||||
}.values
|
||||
}
|
||||
}
|
||||
|
||||
fun addKeywordHighlight(keywordHighlight: KeywordHighlight) {
|
||||
val text = ohMyJson.encodeToString(keywordHighlight)
|
||||
env.executeInTransaction {
|
||||
put(it, KEYWORD_HIGHLIGHT_STORE, keywordHighlight.id, text)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Added keyword highlight: ${keywordHighlight.id} , ${keywordHighlight.keyword}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeKeywordHighlight(id: String) {
|
||||
env.executeInTransaction {
|
||||
delete(it, KEYWORD_HIGHLIGHT_STORE, id)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Removed keyword highlight: $id")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getMacros(): Collection<Macro> {
|
||||
return env.computeInTransaction { tx ->
|
||||
openCursor<Macro>(tx, MACRO_STORE) { _, value ->
|
||||
ohMyJson.decodeFromString(value)
|
||||
}.values
|
||||
}
|
||||
}
|
||||
|
||||
fun addMacro(macro: Macro) {
|
||||
val text = ohMyJson.encodeToString(macro)
|
||||
env.executeInTransaction {
|
||||
put(it, MACRO_STORE, macro.id, text)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Added macro: ${macro.id}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeMacro(id: String) {
|
||||
env.executeInTransaction {
|
||||
delete(it, MACRO_STORE, id)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Removed macro: $id")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addKeyPair(key: OhKeyPair) {
|
||||
var text = ohMyJson.encodeToString(key)
|
||||
if (doorman.isWorking()) {
|
||||
text = doorman.encrypt(text)
|
||||
}
|
||||
env.executeInTransaction {
|
||||
put(it, KEY_PAIR_STORE, key.id, text)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Added Key Pair: ${key.id} , ${key.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeKeyPair(id: String) {
|
||||
env.executeInTransaction {
|
||||
delete(it, KEY_PAIR_STORE, id)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Removed Key Pair: $id")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun put(tx: Transaction, name: String, key: String, value: String) {
|
||||
val store = env.openStore(name, StoreConfig.WITHOUT_DUPLICATES_WITH_PREFIXING, tx)
|
||||
val k = StringBinding.stringToEntry(key)
|
||||
val v = StringBinding.stringToEntry(value)
|
||||
store.put(tx, k, v)
|
||||
}
|
||||
|
||||
private fun delete(tx: Transaction, name: String, key: String) {
|
||||
val store = env.openStore(name, StoreConfig.WITHOUT_DUPLICATES_WITH_PREFIXING, tx)
|
||||
val k = StringBinding.stringToEntry(key)
|
||||
store.delete(tx, k)
|
||||
}
|
||||
|
||||
fun getSafetyProperties(): List<SafetyProperties> {
|
||||
return listOf(sync, safetyProperties)
|
||||
}
|
||||
|
||||
private inline fun <reified T> openCursor(
|
||||
tx: Transaction,
|
||||
name: String,
|
||||
callback: (key: String, value: String) -> T
|
||||
): Map<String, T> {
|
||||
val store = env.openStore(name, StoreConfig.WITHOUT_DUPLICATES_WITH_PREFIXING, tx)
|
||||
val map = mutableMapOf<String, T>()
|
||||
store.openCursor(tx).use {
|
||||
while (it.next) {
|
||||
try {
|
||||
val key = StringBinding.entryToString(it.key)
|
||||
map[key] = callback.invoke(
|
||||
key,
|
||||
StringBinding.entryToString(it.value)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
if (log.isWarnEnabled) {
|
||||
log.warn("Decode data failed. data: {}", it.value, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
private fun putString(name: String, map: Map<String, String>) {
|
||||
return env.computeInTransaction {
|
||||
val store = env.openStore(name, StoreConfig.WITHOUT_DUPLICATES_WITH_PREFIXING, it)
|
||||
for ((key, value) in map.entries) {
|
||||
store.put(it, StringBinding.stringToEntry(key), StringBinding.stringToEntry(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getString(name: String, key: String): String? {
|
||||
return env.computeInTransaction {
|
||||
val store = env.openStore(name, StoreConfig.WITHOUT_DUPLICATES_WITH_PREFIXING, it)
|
||||
val value = store.get(it, StringBinding.stringToEntry(key))
|
||||
if (value == null) null else StringBinding.entryToString(value)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
abstract inner class Property(val name: String) {
|
||||
private val properties = Collections.synchronizedMap(mutableMapOf<String, String>())
|
||||
|
||||
init {
|
||||
swingCoroutineScope.launch(Dispatchers.IO) { properties.putAll(getProperties()) }
|
||||
}
|
||||
|
||||
protected open fun getString(key: String): String? {
|
||||
if (properties.containsKey(key)) {
|
||||
return properties[key]
|
||||
}
|
||||
return getString(name, key)
|
||||
}
|
||||
|
||||
open fun getProperties(): Map<String, String> {
|
||||
return env.computeInTransaction { tx ->
|
||||
openCursor<String>(
|
||||
tx,
|
||||
name
|
||||
) { _, value -> value }
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun putString(key: String, value: String) {
|
||||
properties[key] = value
|
||||
putString(name, mapOf(key to value))
|
||||
}
|
||||
|
||||
|
||||
protected abstract inner class PropertyLazyDelegate<T>(protected val initializer: () -> T) :
|
||||
ReadWriteProperty<Any?, T> {
|
||||
private var value: T? = null
|
||||
|
||||
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
|
||||
if (value == null) {
|
||||
val v = getString(property.name)
|
||||
value = if (v == null) {
|
||||
initializer.invoke()
|
||||
} else {
|
||||
convertValue(v)
|
||||
}
|
||||
}
|
||||
|
||||
if (value == null) {
|
||||
value = initializer.invoke()
|
||||
}
|
||||
return value!!
|
||||
}
|
||||
|
||||
abstract fun convertValue(value: String): T
|
||||
|
||||
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
|
||||
this.value = value
|
||||
putString(property.name, value.toString())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected abstract inner class PropertyDelegate<T>(private val defaultValue: T) :
|
||||
PropertyLazyDelegate<T>({ defaultValue })
|
||||
|
||||
|
||||
protected inner class StringPropertyDelegate(defaultValue: String) :
|
||||
PropertyDelegate<String>(defaultValue) {
|
||||
override fun convertValue(value: String): String {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
protected inner class IntPropertyDelegate(defaultValue: Int) :
|
||||
PropertyDelegate<Int>(defaultValue) {
|
||||
override fun convertValue(value: String): Int {
|
||||
return value.toIntOrNull() ?: initializer.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
protected inner class DoublePropertyDelegate(defaultValue: Double) :
|
||||
PropertyDelegate<Double>(defaultValue) {
|
||||
override fun convertValue(value: String): Double {
|
||||
return value.toDoubleOrNull() ?: initializer.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected inner class LongPropertyDelegate(defaultValue: Long) :
|
||||
PropertyDelegate<Long>(defaultValue) {
|
||||
override fun convertValue(value: String): Long {
|
||||
return value.toLongOrNull() ?: initializer.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
protected inner class BooleanPropertyDelegate(defaultValue: Boolean) :
|
||||
PropertyDelegate<Boolean>(defaultValue) {
|
||||
override fun convertValue(value: String): Boolean {
|
||||
return value.toBooleanStrictOrNull() ?: initializer.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
protected open inner class StringPropertyLazyDelegate(initializer: () -> String) :
|
||||
PropertyLazyDelegate<String>(initializer) {
|
||||
override fun convertValue(value: String): String {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected inner class CursorStylePropertyDelegate(defaultValue: CursorStyle) :
|
||||
PropertyDelegate<CursorStyle>(defaultValue) {
|
||||
override fun convertValue(value: String): CursorStyle {
|
||||
return try {
|
||||
CursorStyle.valueOf(value)
|
||||
} catch (_: Exception) {
|
||||
initializer.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected inner class SyncTypePropertyDelegate(defaultValue: SyncType) :
|
||||
PropertyDelegate<SyncType>(defaultValue) {
|
||||
override fun convertValue(value: String): SyncType {
|
||||
try {
|
||||
return SyncType.valueOf(value)
|
||||
} catch (e: Exception) {
|
||||
return initializer.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 终端设置
|
||||
*/
|
||||
inner class Terminal : Property("Setting.Terminal") {
|
||||
|
||||
/**
|
||||
* 字体
|
||||
*/
|
||||
var font by StringPropertyDelegate("JetBrains Mono")
|
||||
|
||||
/**
|
||||
* 默认终端
|
||||
*/
|
||||
var localShell by StringPropertyLazyDelegate { Application.getDefaultShell() }
|
||||
|
||||
/**
|
||||
* 字体大小
|
||||
*/
|
||||
var fontSize by IntPropertyDelegate(14)
|
||||
|
||||
/**
|
||||
* 最大行数
|
||||
*/
|
||||
var maxRows by IntPropertyDelegate(5000)
|
||||
|
||||
/**
|
||||
* 调试模式
|
||||
*/
|
||||
var debug by BooleanPropertyDelegate(false)
|
||||
|
||||
/**
|
||||
* 蜂鸣声
|
||||
*/
|
||||
var beep by BooleanPropertyDelegate(true)
|
||||
|
||||
/**
|
||||
* 超链接
|
||||
*/
|
||||
var hyperlink by BooleanPropertyDelegate(true)
|
||||
|
||||
/**
|
||||
* 光标闪烁
|
||||
*/
|
||||
var cursorBlink by BooleanPropertyDelegate(false)
|
||||
|
||||
/**
|
||||
* 选中复制
|
||||
*/
|
||||
var selectCopy by BooleanPropertyDelegate(false)
|
||||
|
||||
/**
|
||||
* 光标样式
|
||||
*/
|
||||
var cursor by CursorStylePropertyDelegate(CursorStyle.Block)
|
||||
|
||||
/**
|
||||
* 终端断开连接时自动关闭Tab
|
||||
*/
|
||||
var autoCloseTabWhenDisconnected by BooleanPropertyDelegate(false)
|
||||
|
||||
/**
|
||||
* 是否显示悬浮工具栏
|
||||
*/
|
||||
var floatingToolbar by BooleanPropertyDelegate(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用属性
|
||||
*/
|
||||
inner class Properties : Property("Setting.Properties") {
|
||||
public override fun getString(key: String): String? {
|
||||
return super.getString(key)
|
||||
}
|
||||
|
||||
|
||||
fun getString(key: String, defaultValue: String): String {
|
||||
return getString(key) ?: defaultValue
|
||||
}
|
||||
|
||||
public override fun putString(key: String, value: String) {
|
||||
super.putString(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 安全的通用属性
|
||||
*/
|
||||
open inner class SafetyProperties(name: String) : Property(name) {
|
||||
private val doorman get() = Doorman.getInstance()
|
||||
|
||||
public override fun getString(key: String): String? {
|
||||
var value = super.getString(key)
|
||||
if (value != null && doorman.isWorking()) {
|
||||
try {
|
||||
value = doorman.decrypt(value)
|
||||
} catch (e: Exception) {
|
||||
if (log.isWarnEnabled) {
|
||||
log.warn("decryption key: [{}], value: [{}] failed: {}", key, value, e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
|
||||
override fun getProperties(): Map<String, String> {
|
||||
val properties = super.getProperties()
|
||||
val map = mutableMapOf<String, String>()
|
||||
if (doorman.isWorking()) {
|
||||
for ((k, v) in properties) {
|
||||
try {
|
||||
map[k] = doorman.decrypt(v)
|
||||
} catch (e: Exception) {
|
||||
if (log.isWarnEnabled) {
|
||||
log.warn("decryption key: [{}], value: [{}] failed: {}", k, v, e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
map.putAll(properties)
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
fun getString(key: String, defaultValue: String): String {
|
||||
return getString(key) ?: defaultValue
|
||||
}
|
||||
|
||||
public override fun putString(key: String, value: String) {
|
||||
val v = if (doorman.isWorking()) doorman.encrypt(value) else value
|
||||
super.putString(key, v)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 外观
|
||||
*/
|
||||
inner class Appearance : Property("Setting.Appearance") {
|
||||
|
||||
|
||||
/**
|
||||
* 外观
|
||||
*/
|
||||
var theme by StringPropertyDelegate("Light")
|
||||
|
||||
/**
|
||||
* 跟随系统
|
||||
*/
|
||||
var followSystem by BooleanPropertyDelegate(true)
|
||||
var darkTheme by StringPropertyDelegate("Dark")
|
||||
var lightTheme by StringPropertyDelegate("Light")
|
||||
|
||||
/**
|
||||
* 允许后台运行,也就是托盘
|
||||
*/
|
||||
var backgroundRunning by BooleanPropertyDelegate(false)
|
||||
|
||||
/**
|
||||
* 标签关闭前确认
|
||||
*/
|
||||
var confirmTabClose by BooleanPropertyDelegate(false)
|
||||
|
||||
/**
|
||||
* 背景图片的地址
|
||||
*/
|
||||
var backgroundImage by StringPropertyDelegate(StringUtils.EMPTY)
|
||||
|
||||
/**
|
||||
* 语言
|
||||
*/
|
||||
var language by StringPropertyLazyDelegate {
|
||||
I18n.containsLanguage(Locale.getDefault()) ?: Locale.US.toString()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 透明度
|
||||
*/
|
||||
var opacity by DoublePropertyDelegate(1.0)
|
||||
}
|
||||
|
||||
/**
|
||||
* SFTP
|
||||
*/
|
||||
inner class SFTP : Property("Setting.SFTP") {
|
||||
|
||||
|
||||
/**
|
||||
* 编辑命令
|
||||
*/
|
||||
var editCommand by StringPropertyDelegate(StringUtils.EMPTY)
|
||||
|
||||
|
||||
/**
|
||||
* sftp command
|
||||
*/
|
||||
var sftpCommand by StringPropertyDelegate(StringUtils.EMPTY)
|
||||
|
||||
/**
|
||||
* defaultDirectory
|
||||
*/
|
||||
var defaultDirectory by StringPropertyDelegate(StringUtils.EMPTY)
|
||||
|
||||
|
||||
/**
|
||||
* 是否固定在标签栏
|
||||
*/
|
||||
var pinTab by BooleanPropertyDelegate(false)
|
||||
|
||||
/**
|
||||
* 是否保留原始文件时间
|
||||
*/
|
||||
var preserveModificationTime by BooleanPropertyDelegate(false)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步配置
|
||||
*/
|
||||
inner class Sync : SafetyProperties("Setting.Sync") {
|
||||
/**
|
||||
* 同步类型
|
||||
*/
|
||||
var type by SyncTypePropertyDelegate(SyncType.GitHub)
|
||||
|
||||
/**
|
||||
* 范围
|
||||
*/
|
||||
var rangeHosts by BooleanPropertyDelegate(true)
|
||||
var rangeKeyPairs by BooleanPropertyDelegate(true)
|
||||
var rangeSnippets by BooleanPropertyDelegate(true)
|
||||
var rangeKeywordHighlights by BooleanPropertyDelegate(true)
|
||||
var rangeMacros by BooleanPropertyDelegate(true)
|
||||
var rangeKeymap by BooleanPropertyDelegate(true)
|
||||
|
||||
/**
|
||||
* Token
|
||||
*/
|
||||
var token by StringPropertyDelegate(String())
|
||||
|
||||
/**
|
||||
* Gist ID
|
||||
*/
|
||||
var gist by StringPropertyDelegate(String())
|
||||
|
||||
/**
|
||||
* Domain
|
||||
*/
|
||||
var domain by StringPropertyDelegate(String())
|
||||
|
||||
/**
|
||||
* 最后同步时间
|
||||
*/
|
||||
var lastSyncTime by LongPropertyDelegate(0L)
|
||||
|
||||
/**
|
||||
* 同步策略,为空就是默认手动
|
||||
*/
|
||||
var policy by StringPropertyDelegate(StringUtils.EMPTY)
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
IOUtils.closeQuietly(env)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
package app.termora.plugins.migration
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.AES.decodeBase64
|
||||
import app.termora.actions.AnAction
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.terminal.ControlCharacters
|
||||
import cash.z.ecc.android.bip39.Mnemonics
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.extras.FlatSVGIcon
|
||||
import com.formdev.flatlaf.extras.components.FlatButton
|
||||
import com.formdev.flatlaf.extras.components.FlatLabel
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.jdesktop.swingx.JXHyperlink
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.Dimension
|
||||
import java.awt.Window
|
||||
import java.awt.datatransfer.DataFlavor
|
||||
import java.awt.event.KeyAdapter
|
||||
import java.awt.event.KeyEvent
|
||||
import javax.imageio.ImageIO
|
||||
import javax.swing.*
|
||||
import kotlin.math.max
|
||||
|
||||
class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(DoormanDialog::class.java)
|
||||
}
|
||||
|
||||
private val formMargin = "7dlu"
|
||||
private val label = FlatLabel()
|
||||
private val icon = JLabel()
|
||||
private val passwordTextField = OutlinePasswordField()
|
||||
private val tip = FlatLabel()
|
||||
private val safeBtn = FlatButton()
|
||||
|
||||
var isOpened = false
|
||||
|
||||
init {
|
||||
size = Dimension(UIManager.getInt("Dialog.width") - 200, UIManager.getInt("Dialog.height") - 150)
|
||||
isModal = true
|
||||
isResizable = false
|
||||
controlsVisible = false
|
||||
|
||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||
title = I18n.getString("termora.doorman.safe")
|
||||
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_TITLE, false)
|
||||
}
|
||||
|
||||
|
||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||
val sizes = listOf(16, 20, 24, 28, 32, 48, 64)
|
||||
val loader = TermoraFrame::class.java.classLoader
|
||||
val images = sizes.mapNotNull { e ->
|
||||
loader.getResourceAsStream("icons/termora_${e}x${e}.png")?.use { ImageIO.read(it) }
|
||||
}
|
||||
iconImages = images
|
||||
}
|
||||
|
||||
setLocationRelativeTo(null)
|
||||
init()
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
label.text = I18n.getString("termora.doorman.safe")
|
||||
tip.text = I18n.getString("termora.doorman.unlock-data")
|
||||
icon.icon = FlatSVGIcon(Icons.role.name, 80, 80)
|
||||
safeBtn.icon = Icons.unlocked
|
||||
|
||||
|
||||
label.labelType = FlatLabel.LabelType.h2
|
||||
label.horizontalAlignment = SwingConstants.CENTER
|
||||
safeBtn.isFocusable = false
|
||||
tip.foreground = UIManager.getColor("TextField.placeholderForeground")
|
||||
icon.horizontalAlignment = SwingConstants.CENTER
|
||||
|
||||
|
||||
safeBtn.addActionListener { doOKAction() }
|
||||
passwordTextField.addActionListener { doOKAction() }
|
||||
|
||||
var rows = 2
|
||||
val step = 2
|
||||
return FormBuilder.create().debug(false)
|
||||
.layout(
|
||||
FormLayout(
|
||||
"$formMargin, default:grow, 4dlu, pref, $formMargin",
|
||||
"${"0dlu"}, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin"
|
||||
)
|
||||
)
|
||||
.add(icon).xyw(2, rows, 4).apply { rows += step }
|
||||
.add(label).xyw(2, rows, 4).apply { rows += step }
|
||||
.add(passwordTextField).xy(2, rows)
|
||||
.add(safeBtn).xy(4, rows).apply { rows += step }
|
||||
.add(tip).xyw(2, rows, 4, "center, fill").apply { rows += step }
|
||||
.add(JXHyperlink(object : AnAction(I18n.getString("termora.doorman.forget-password")) {
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
val option = OptionPane.showConfirmDialog(
|
||||
this@DoormanDialog, I18n.getString("termora.doorman.forget-password-message"),
|
||||
options = arrayOf(
|
||||
I18n.getString("termora.doorman.have-a-mnemonic"),
|
||||
I18n.getString("termora.doorman.dont-have-a-mnemonic"),
|
||||
),
|
||||
optionType = JOptionPane.YES_NO_OPTION,
|
||||
messageType = JOptionPane.INFORMATION_MESSAGE,
|
||||
initialValue = I18n.getString("termora.doorman.have-a-mnemonic")
|
||||
)
|
||||
if (option == JOptionPane.YES_OPTION) {
|
||||
showMnemonicsDialog()
|
||||
} else if (option == JOptionPane.NO_OPTION) {
|
||||
OptionPane.showMessageDialog(
|
||||
this@DoormanDialog,
|
||||
I18n.getString("termora.doorman.delete-data"),
|
||||
messageType = JOptionPane.WARNING_MESSAGE
|
||||
)
|
||||
Application.browse(MigrationApplicationRunnerExtension.instance.getDatabaseFile().toURI())
|
||||
}
|
||||
}
|
||||
}).apply { isFocusable = false }).xyw(2, rows, 4, "center, fill")
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun showMnemonicsDialog() {
|
||||
val dialog = MnemonicsDialog(this@DoormanDialog)
|
||||
dialog.isVisible = true
|
||||
val entropy = dialog.entropy
|
||||
if (entropy.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val keyBackup = Database.getDatabase()
|
||||
.properties.getString("doorman-key-backup")
|
||||
?: throw IllegalStateException("doorman-key-backup is null")
|
||||
val key = AES.ECB.decrypt(entropy, keyBackup.decodeBase64())
|
||||
Doorman.getInstance().work(key)
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
OptionPane.showMessageDialog(
|
||||
this, I18n.getString("termora.doorman.mnemonic-data-corrupted"),
|
||||
messageType = JOptionPane.ERROR_MESSAGE
|
||||
)
|
||||
passwordTextField.outline = "error"
|
||||
passwordTextField.requestFocus()
|
||||
return
|
||||
}
|
||||
|
||||
isOpened = true
|
||||
super.doOKAction()
|
||||
|
||||
}
|
||||
|
||||
override fun doOKAction() {
|
||||
if (passwordTextField.password.isEmpty()) {
|
||||
passwordTextField.outline = "error"
|
||||
passwordTextField.requestFocus()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
Doorman.getInstance().work(passwordTextField.password)
|
||||
} catch (e: Exception) {
|
||||
if (e is PasswordWrongException) {
|
||||
OptionPane.showMessageDialog(
|
||||
this, I18n.getString("termora.doorman.password-wrong"),
|
||||
messageType = JOptionPane.ERROR_MESSAGE
|
||||
)
|
||||
}
|
||||
passwordTextField.outline = "error"
|
||||
passwordTextField.requestFocus()
|
||||
return
|
||||
}
|
||||
|
||||
isOpened = true
|
||||
|
||||
super.doOKAction()
|
||||
}
|
||||
|
||||
fun open(): Boolean {
|
||||
isModal = true
|
||||
isVisible = true
|
||||
return isOpened
|
||||
}
|
||||
|
||||
|
||||
private class MnemonicsDialog(owner: Window) : DialogWrapper(owner) {
|
||||
|
||||
private val textFields = (1..12).map { PasteTextField(it) }
|
||||
var entropy = byteArrayOf()
|
||||
private set
|
||||
|
||||
init {
|
||||
isModal = true
|
||||
isResizable = true
|
||||
controlsVisible = false
|
||||
title = I18n.getString("termora.doorman.mnemonic.title")
|
||||
init()
|
||||
pack()
|
||||
size = Dimension(max(size.width, UIManager.getInt("Dialog.width") - 250), size.height)
|
||||
setLocationRelativeTo(null)
|
||||
}
|
||||
|
||||
fun getWords(): List<String> {
|
||||
val words = mutableListOf<String>()
|
||||
for (e in textFields) {
|
||||
if (e.text.isBlank()) {
|
||||
return emptyList()
|
||||
}
|
||||
words.add(e.text)
|
||||
}
|
||||
return words
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
val formMargin = "4dlu"
|
||||
val layout = FormLayout(
|
||||
"default:grow, $formMargin, default:grow, $formMargin, default:grow, $formMargin, default:grow",
|
||||
"pref, $formMargin, pref, $formMargin, pref"
|
||||
)
|
||||
|
||||
val builder = FormBuilder.create().padding("0, $formMargin, $formMargin, $formMargin")
|
||||
.layout(layout).debug(false)
|
||||
val iterator = textFields.iterator()
|
||||
for (i in 1..5 step 2) {
|
||||
for (j in 1..7 step 2) {
|
||||
builder.add(iterator.next()).xy(j, i)
|
||||
}
|
||||
}
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
override fun doOKAction() {
|
||||
for (textField in textFields) {
|
||||
if (textField.text.isBlank()) {
|
||||
textField.outline = "error"
|
||||
textField.requestFocusInWindow()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Mnemonics.MnemonicCode(getWords().joinToString(StringUtils.SPACE)).use {
|
||||
it.validate()
|
||||
entropy = it.toEntropy()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
OptionPane.showMessageDialog(
|
||||
this,
|
||||
I18n.getString("termora.doorman.mnemonic.incorrect"),
|
||||
messageType = JOptionPane.ERROR_MESSAGE
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
super.doOKAction()
|
||||
}
|
||||
|
||||
override fun doCancelAction() {
|
||||
entropy = byteArrayOf()
|
||||
super.doCancelAction()
|
||||
}
|
||||
|
||||
private inner class PasteTextField(private val index: Int) : OutlineTextField() {
|
||||
init {
|
||||
addKeyListener(object : KeyAdapter() {
|
||||
override fun keyPressed(e: KeyEvent) {
|
||||
if (e.keyCode == KeyEvent.VK_BACK_SPACE) {
|
||||
if (text.isEmpty() && index != 1) {
|
||||
textFields[index - 2].requestFocusInWindow()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun paste() {
|
||||
if (!toolkit.systemClipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) {
|
||||
return
|
||||
}
|
||||
|
||||
val text = toolkit.systemClipboard.getData(DataFlavor.stringFlavor)?.toString() ?: return
|
||||
if (text.isBlank()) {
|
||||
return
|
||||
}
|
||||
val words = mutableListOf<String>()
|
||||
if (text.count { it == ControlCharacters.SP } > text.count { it == ControlCharacters.LF }) {
|
||||
words.addAll(text.split(StringUtils.SPACE))
|
||||
} else {
|
||||
words.addAll(text.split(ControlCharacters.LF))
|
||||
}
|
||||
val iterator = words.iterator()
|
||||
for (i in index..textFields.size) {
|
||||
if (iterator.hasNext()) {
|
||||
textFields[i - 1].text = iterator.next()
|
||||
textFields[i - 1].requestFocusInWindow()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun createSouthPanel(): JComponent? {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
package app.termora.plugins.migration
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.account.AccountManager
|
||||
import app.termora.account.AccountOwner
|
||||
import app.termora.database.DatabaseManager
|
||||
import app.termora.database.OwnerType
|
||||
import app.termora.highlight.KeywordHighlightManager
|
||||
import app.termora.keymap.KeymapManager
|
||||
import app.termora.keymgr.KeyManager
|
||||
import app.termora.macro.MacroManager
|
||||
import app.termora.snippet.SnippetManager
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.File
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import javax.swing.JOptionPane
|
||||
import javax.swing.SwingUtilities
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class MigrationApplicationRunnerExtension private constructor() : ApplicationRunnerExtension {
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(MigrationApplicationRunnerExtension::class.java)
|
||||
val instance by lazy { MigrationApplicationRunnerExtension() }
|
||||
}
|
||||
|
||||
override fun ready() {
|
||||
val file = getDatabaseFile()
|
||||
if (file.exists().not()) return
|
||||
|
||||
// 如果数据库文件存在,那么需要迁移文件
|
||||
val countDownLatch = CountDownLatch(1)
|
||||
|
||||
SwingUtilities.invokeAndWait {
|
||||
try {
|
||||
// 打开数据
|
||||
openDatabase()
|
||||
|
||||
// 尝试解锁
|
||||
openDoor()
|
||||
|
||||
// 询问是否迁移
|
||||
if (askMigrate()) {
|
||||
|
||||
// 迁移
|
||||
migrate()
|
||||
|
||||
// 移动到旧的目录
|
||||
moveOldDirectory()
|
||||
|
||||
// 重启
|
||||
restart()
|
||||
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
} finally {
|
||||
countDownLatch.countDown()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
countDownLatch.await()
|
||||
|
||||
}
|
||||
|
||||
private fun openDoor() {
|
||||
if (Doorman.getInstance().isWorking()) {
|
||||
if (DoormanDialog(null).open().not()) {
|
||||
Disposer.dispose(TermoraFrameManager.getInstance())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun openDatabase() {
|
||||
try {
|
||||
// 初始化数据库
|
||||
Database.getDatabase()
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
JOptionPane.showMessageDialog(
|
||||
null, "Unable to open database",
|
||||
I18n.getString("termora.title"), JOptionPane.ERROR_MESSAGE
|
||||
)
|
||||
exitProcess(1)
|
||||
}
|
||||
}
|
||||
|
||||
private fun migrate() {
|
||||
val database = Database.getDatabase()
|
||||
val accountManager = AccountManager.getInstance()
|
||||
val databaseManager = DatabaseManager.getInstance()
|
||||
val ownerId = accountManager.getAccountId()
|
||||
val hostManager = HostManager.getInstance()
|
||||
val snippetManager = SnippetManager.getInstance()
|
||||
val macroManager = MacroManager.getInstance()
|
||||
val keymapManager = KeymapManager.getInstance()
|
||||
val keyManager = KeyManager.getInstance()
|
||||
val highlightManager = KeywordHighlightManager.getInstance()
|
||||
val accountOwner = AccountOwner(
|
||||
id = accountManager.getAccountId(),
|
||||
name = accountManager.getEmail(),
|
||||
type = OwnerType.User
|
||||
)
|
||||
|
||||
for (host in database.getHosts()) {
|
||||
if (host.deleted) continue
|
||||
hostManager.addHost(host.copy(ownerId = accountManager.getAccountId(), ownerType = OwnerType.User.name))
|
||||
}
|
||||
|
||||
for (snippet in database.getSnippets()) {
|
||||
if (snippet.deleted) continue
|
||||
snippetManager.addSnippet(snippet)
|
||||
}
|
||||
|
||||
for (macro in database.getMacros()) {
|
||||
macroManager.addMacro(macro)
|
||||
}
|
||||
|
||||
for (keymap in database.getKeymaps()) {
|
||||
keymapManager.addKeymap(keymap)
|
||||
}
|
||||
|
||||
for (keypair in database.getKeyPairs()) {
|
||||
keyManager.addOhKeyPair(keypair, accountOwner)
|
||||
}
|
||||
|
||||
for (e in database.getKeywordHighlights()) {
|
||||
highlightManager.addKeywordHighlight(e, accountOwner)
|
||||
}
|
||||
|
||||
val list = listOf(
|
||||
database.sync,
|
||||
database.properties,
|
||||
database.terminal,
|
||||
database.sftp,
|
||||
database.appearance,
|
||||
)
|
||||
|
||||
for (e in list) {
|
||||
for (k in e.getProperties()) {
|
||||
databaseManager.setSetting(e.name + "." + k.key, k.value)
|
||||
}
|
||||
}
|
||||
|
||||
for (e in database.safetyProperties.getProperties()) {
|
||||
databaseManager.setSetting(database.properties.name + "." + e.key, e.value)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private fun askMigrate(): Boolean {
|
||||
|
||||
if (MigrationDialog(null).open()) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 移动到旧的目录
|
||||
moveOldDirectory()
|
||||
|
||||
// 重启
|
||||
restart()
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private fun moveOldDirectory() {
|
||||
// 关闭数据库
|
||||
Disposer.dispose(Database.getDatabase())
|
||||
|
||||
val file = getDatabaseFile()
|
||||
FileUtils.moveDirectory(
|
||||
file,
|
||||
FileUtils.getFile(file.parentFile, file.name + "-old-" + System.currentTimeMillis())
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
private fun restart() {
|
||||
|
||||
// 重启
|
||||
TermoraRestarter.getInstance().scheduleRestart(null, ask = false)
|
||||
|
||||
// 退出程序
|
||||
Disposer.dispose(TermoraFrameManager.getInstance())
|
||||
}
|
||||
|
||||
|
||||
fun getDatabaseFile(): File {
|
||||
return FileUtils.getFile(Application.getBaseDataDir(), "storage")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package app.termora.plugins.migration
|
||||
|
||||
import app.termora.*
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.extras.FlatSVGIcon
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.jdesktop.swingx.JXEditorPane
|
||||
import java.awt.Dimension
|
||||
import java.awt.Window
|
||||
import java.awt.event.WindowAdapter
|
||||
import java.awt.event.WindowEvent
|
||||
import javax.imageio.ImageIO
|
||||
import javax.swing.*
|
||||
import javax.swing.event.HyperlinkEvent
|
||||
|
||||
class MigrationDialog(owner: Window?) : DialogWrapper(owner) {
|
||||
|
||||
private var isOpened = false
|
||||
|
||||
init {
|
||||
size = Dimension(UIManager.getInt("Dialog.width") - 200, UIManager.getInt("Dialog.height") - 150)
|
||||
isModal = true
|
||||
isResizable = false
|
||||
controlsVisible = false
|
||||
escapeDispose = false
|
||||
|
||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||
title = StringUtils.EMPTY
|
||||
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_TITLE, false)
|
||||
}
|
||||
|
||||
|
||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||
val sizes = listOf(16, 20, 24, 28, 32, 48, 64)
|
||||
val loader = TermoraFrame::class.java.classLoader
|
||||
val images = sizes.mapNotNull { e ->
|
||||
loader.getResourceAsStream("icons/termora_${e}x${e}.png")?.use { ImageIO.read(it) }
|
||||
}
|
||||
iconImages = images
|
||||
}
|
||||
|
||||
setLocationRelativeTo(null)
|
||||
init()
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
var rows = 2
|
||||
val step = 2
|
||||
val formMargin = "7dlu"
|
||||
val icon = JLabel()
|
||||
icon.horizontalAlignment = SwingConstants.CENTER
|
||||
icon.icon = FlatSVGIcon(Icons.newUI.name, 80, 80)
|
||||
|
||||
val editorPane = JXEditorPane()
|
||||
editorPane.contentType = "text/html"
|
||||
editorPane.text = MigrationI18n.getString("termora.plugins.migration.message")
|
||||
editorPane.isEditable = false
|
||||
editorPane.addHyperlinkListener {
|
||||
if (it.eventType == HyperlinkEvent.EventType.ACTIVATED) {
|
||||
Application.browse(it.url.toURI())
|
||||
}
|
||||
}
|
||||
editorPane.background = DynamicColor("window")
|
||||
val scrollPane = JScrollPane(editorPane)
|
||||
scrollPane.border = BorderFactory.createEmptyBorder()
|
||||
scrollPane.preferredSize = Dimension(Int.MAX_VALUE, 225)
|
||||
|
||||
addWindowListener(object : WindowAdapter() {
|
||||
override fun windowOpened(e: WindowEvent) {
|
||||
removeWindowListener(this)
|
||||
SwingUtilities.invokeLater { scrollPane.verticalScrollBar.value = 0 }
|
||||
}
|
||||
})
|
||||
|
||||
return FormBuilder.create().debug(false)
|
||||
.layout(
|
||||
FormLayout(
|
||||
"$formMargin, default:grow, 4dlu, pref, $formMargin",
|
||||
"${"0dlu"}, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin"
|
||||
)
|
||||
)
|
||||
.add(icon).xyw(2, rows, 4).apply { rows += step }
|
||||
.add(scrollPane).xyw(2, rows, 4).apply { rows += step }
|
||||
.build()
|
||||
}
|
||||
|
||||
|
||||
fun open(): Boolean {
|
||||
isModal = true
|
||||
isVisible = true
|
||||
return isOpened
|
||||
}
|
||||
|
||||
override fun doOKAction() {
|
||||
isOpened = true
|
||||
super.doOKAction()
|
||||
}
|
||||
|
||||
override fun doCancelAction() {
|
||||
isOpened = false
|
||||
super.doCancelAction()
|
||||
}
|
||||
|
||||
override fun createOkAction(): AbstractAction {
|
||||
return OkAction(MigrationI18n.getString("termora.plugins.migration.migrate"))
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package app.termora.plugins.migration
|
||||
|
||||
import app.termora.NamedI18n
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
object MigrationI18n : NamedI18n("i18n/messages") {
|
||||
private val log = LoggerFactory.getLogger(MigrationI18n::class.java)
|
||||
|
||||
override fun getLogger(): Logger {
|
||||
return log
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package app.termora.plugins.migration
|
||||
|
||||
import app.termora.ApplicationRunnerExtension
|
||||
import app.termora.DynamicIcon
|
||||
import app.termora.I18n
|
||||
import app.termora.Icons
|
||||
import app.termora.plugin.Extension
|
||||
import app.termora.plugin.ExtensionSupport
|
||||
import app.termora.plugin.Plugin
|
||||
|
||||
class MigrationPlugin : Plugin {
|
||||
private val support = ExtensionSupport()
|
||||
|
||||
init {
|
||||
support.addExtension(ApplicationRunnerExtension::class.java) { MigrationApplicationRunnerExtension.instance }
|
||||
}
|
||||
|
||||
override fun getAuthor(): String {
|
||||
return "TermoraDev"
|
||||
}
|
||||
|
||||
|
||||
override fun getName(): String {
|
||||
return "Migration"
|
||||
}
|
||||
|
||||
|
||||
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||
return support.getExtensions(clazz)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package app.termora.plugins.migration
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import javax.crypto.SecretKeyFactory
|
||||
import javax.crypto.spec.PBEKeySpec
|
||||
import kotlin.time.measureTime
|
||||
|
||||
object PBKDF2 {
|
||||
|
||||
private const val ALGORITHM = "PBKDF2WithHmacSHA512"
|
||||
private val log = LoggerFactory.getLogger(PBKDF2::class.java)
|
||||
|
||||
fun generateSecret(
|
||||
password: CharArray,
|
||||
salt: ByteArray,
|
||||
iterationCount: Int = 150000,
|
||||
keyLength: Int = 256
|
||||
): ByteArray {
|
||||
val bytes: ByteArray
|
||||
val time = measureTime {
|
||||
bytes = SecretKeyFactory.getInstance(ALGORITHM)
|
||||
.generateSecret(PBEKeySpec(password, salt, iterationCount, keyLength))
|
||||
.encoded
|
||||
}
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Secret generated $time")
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
|
||||
fun hash(slat: ByteArray, password: CharArray, iterationCount: Int, keyLength: Int): ByteArray {
|
||||
val spec = PBEKeySpec(password, slat, iterationCount, keyLength)
|
||||
val secretKeyFactory = SecretKeyFactory.getInstance(ALGORITHM)
|
||||
return secretKeyFactory.generateSecret(spec).encoded
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package app.termora.plugins.migration
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.AES.decodeBase64
|
||||
import app.termora.AES.encodeBase64String
|
||||
|
||||
class PasswordWrongException : RuntimeException()
|
||||
|
||||
class Doorman private constructor() : Disposable {
|
||||
private val properties get() = Database.getDatabase().properties
|
||||
private var key = byteArrayOf()
|
||||
|
||||
companion object {
|
||||
fun getInstance(): Doorman {
|
||||
return ApplicationScope.forApplicationScope().getOrCreate(Doorman::class) { Doorman() }
|
||||
}
|
||||
}
|
||||
|
||||
fun isWorking(): Boolean {
|
||||
return properties.getString("doorman", "false").toBoolean()
|
||||
}
|
||||
|
||||
fun encrypt(text: String): String {
|
||||
checkIsWorking()
|
||||
return AES.ECB.encrypt(key, text.toByteArray()).encodeBase64String()
|
||||
}
|
||||
|
||||
|
||||
fun decrypt(text: String): String {
|
||||
checkIsWorking()
|
||||
return AES.ECB.decrypt(key, text.decodeBase64()).decodeToString()
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回钥匙
|
||||
*/
|
||||
fun work(password: CharArray): ByteArray {
|
||||
if (key.isNotEmpty()) {
|
||||
throw IllegalStateException("Working")
|
||||
}
|
||||
return work(convertKey(password))
|
||||
}
|
||||
|
||||
fun work(key: ByteArray): ByteArray {
|
||||
val verify = properties.getString("doorman-verify")
|
||||
if (verify == null) {
|
||||
properties.putString(
|
||||
"doorman-verify",
|
||||
AES.ECB.encrypt(key, factor()).encodeBase64String()
|
||||
)
|
||||
} else {
|
||||
try {
|
||||
if (!AES.ECB.decrypt(key, verify.decodeBase64()).contentEquals(factor())) {
|
||||
throw PasswordWrongException()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
throw PasswordWrongException()
|
||||
}
|
||||
}
|
||||
|
||||
this.key = key
|
||||
properties.putString("doorman", "true")
|
||||
|
||||
return this.key
|
||||
}
|
||||
|
||||
|
||||
private fun convertKey(password: CharArray): ByteArray {
|
||||
return PBKDF2.generateSecret(password, factor())
|
||||
}
|
||||
|
||||
|
||||
private fun checkIsWorking() {
|
||||
if (key.isEmpty() || !isWorking()) {
|
||||
throw UnsupportedOperationException("Doorman is not working")
|
||||
}
|
||||
}
|
||||
|
||||
private fun factor(): ByteArray {
|
||||
return Application.getName().toByteArray()
|
||||
}
|
||||
|
||||
fun test(password: CharArray): Boolean {
|
||||
checkIsWorking()
|
||||
return key.contentEquals(convertKey(password))
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
key = byteArrayOf()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package app.termora.plugins.migration
|
||||
enum class SyncType {
|
||||
GitLab,
|
||||
GitHub,
|
||||
Gitee,
|
||||
WebDAV,
|
||||
}
|
||||
24
plugins/migration/src/main/resources/META-INF/plugin.xml
Normal file
24
plugins/migration/src/main/resources/META-INF/plugin.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<termora-plugin>
|
||||
|
||||
<id>migration</id>
|
||||
|
||||
<name>Migration</name>
|
||||
|
||||
<version>${projectVersion}</version>
|
||||
|
||||
<!-- since: >=xxx , or >xxx -->
|
||||
<!-- until: <=xxx , or <xxx -->
|
||||
<termora-version since=">=${rootProjectVersion}" until=""/>
|
||||
|
||||
<entry>app.termora.plugins.migration.MigrationPlugin</entry>
|
||||
|
||||
<descriptions>
|
||||
<description>Migrate version 1.x configuration files to 2.x</description>
|
||||
<description language="zh_CN">将 1.x 版本的配置文件迁移到 2.x</description>
|
||||
<description language="zh_TW">將 1.x 版本的設定檔移轉到 2.x</description>
|
||||
</descriptions>
|
||||
|
||||
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
|
||||
|
||||
|
||||
</termora-plugin>
|
||||
19
plugins/migration/src/main/resources/META-INF/pluginIcon.svg
Normal file
19
plugins/migration/src/main/resources/META-INF/pluginIcon.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_659_75852)">
|
||||
<path d="M7.49998 0.523674C8.50002 5.49999 10.5 7.49999 15.554 8.5C10.5 9.49999 8.5 11.5 7.50005 16.4763C6.50002 11.5 4.50002 9.49999 -0.553986 8.49998C4.5 7.49999 6.5 5.49999 7.49998 0.523674Z" fill="url(#paint0_linear_659_75852)"/>
|
||||
<path d="M12.9933 4.90705C14.0451 4.90705 14.8979 4.05433 14.8979 3.00245C14.8979 1.95056 14.0451 1.09784 12.9933 1.09784C11.9414 1.09784 11.0886 1.95056 11.0886 3.00245C11.0886 4.05433 11.9414 4.90705 12.9933 4.90705Z" fill="url(#paint1_linear_659_75852)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_659_75852" x1="7.50002" y1="0.523674" x2="7.50002" y2="16.4763" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#3573F0"/>
|
||||
<stop offset="1" stop-color="#EA33EC"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_659_75852" x1="7.50002" y1="0.523674" x2="7.50002" y2="16.4763" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#3573F0"/>
|
||||
<stop offset="1" stop-color="#EA33EC"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_659_75852">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,9 @@
|
||||
termora.plugins.migration.message=<html> \
|
||||
<h1 align="center">2.0 is ready.</h1> \
|
||||
<br/> \
|
||||
<h3>1. The storage structure has been updated. Existing data needs to be migrated. Just click <font color="#3573F0">“Migrate”</font> to complete the process.</h3> \
|
||||
<h3>2. The <font color="#3573F0">Sync feature</font> is now provided as a plugin. If needed, please <font color="#EA33EC">manually install</font> it from Settings.</h3> \
|
||||
<h3>3. The <font color="#3573F0">Data Encryption</font> feature has been <font color="#EA33EC">removed</font> (local data will now be stored with basic encryption). Please ensure your device is in a trusted environment.</h3> \
|
||||
<h3 align="center">📎 For more information, please see: <a href="https://github.com/TermoraDev/termora/issues/645">TermoraDev/termora/issues/645</a></h3> \
|
||||
</html>
|
||||
termora.plugins.migration.migrate=Migrate
|
||||
@@ -0,0 +1,9 @@
|
||||
termora.plugins.migration.message=<html> \
|
||||
<h1 align="center">2.0 已就绪。</h1> \
|
||||
<br/> \
|
||||
<h3>1. 存储结构已更新,需迁移现有数据。只需点击 <font color="#3573F0">“迁移”</font> 即可完成操作。</h3> \
|
||||
<h3>2. <font color="#3573F0">同步功能</font> 现作为插件提供,如需使用,请前往设置中 <font color="#EA33EC">手动安装</font>。</h3> \
|
||||
<h3>3. <font color="#3573F0">数据加密</font> 功能已被 <font color="#EA33EC">移除</font>(本地数据将以简单加密方式存储),请确保你的设备处于可信环境中。</h3> \
|
||||
<h3 align="center">📎 更多信息请查看:<a href="https://github.com/TermoraDev/termora/issues/645">TermoraDev/termora/issues/645</a></h3> \
|
||||
</html>
|
||||
termora.plugins.migration.migrate=迁移
|
||||
@@ -0,0 +1,9 @@
|
||||
termora.plugins.migration.message=<html> \
|
||||
<h1 align="center">2.0 已準備就緒。</h1> \
|
||||
<br/> \
|
||||
<h3>1. 儲存結構已更新,需要遷移現有資料。只需點擊 <font color="#3573F0">「遷移」</font> 即可完成操作。</h3> \
|
||||
<h3>2. <font color="#3573F0">同步功能</font> 現以外掛形式提供,如需使用,請至設定中 <font color="#EA33EC">手動安裝</font>。</h3> \
|
||||
<h3>3. <font color="#3573F0">資料加密</font> 功能已被 <font color="#EA33EC">移除</font>(本機資料將以簡易加密方式儲存),請確保你的裝置處於可信環境中。</h3> \
|
||||
<h3 align="center">📎 更多資訊請參見:<a href="https://github.com/TermoraDev/termora/issues/645">TermoraDev/termora/issues/645</a></h3> \
|
||||
</html>
|
||||
termora.plugins.migration.migrate=遷移
|
||||
Reference in New Issue
Block a user