mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-15 18:02:58 +08:00
feat: improve sync (#429)
This commit is contained in:
@@ -30,6 +30,7 @@ class Database private constructor(private val env: Environment) : Disposable {
|
||||
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)
|
||||
|
||||
|
||||
@@ -142,6 +143,37 @@ class Database private constructor(private val env: Environment) : Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
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()) {
|
||||
@@ -155,6 +187,14 @@ class Database private constructor(private val env: Environment) : Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
52
src/main/kotlin/app/termora/DeleteDataManager.kt
Normal file
52
src/main/kotlin/app/termora/DeleteDataManager.kt
Normal file
@@ -0,0 +1,52 @@
|
||||
package app.termora
|
||||
|
||||
/**
|
||||
* 仅标记
|
||||
*/
|
||||
class DeleteDataManager private constructor() {
|
||||
companion object {
|
||||
fun getInstance(): DeleteDataManager {
|
||||
return ApplicationScope.forApplicationScope().getOrCreate(DeleteDataManager::class) { DeleteDataManager() }
|
||||
}
|
||||
}
|
||||
|
||||
private val data = mutableMapOf<String, DeletedData>()
|
||||
private val database get() = Database.getDatabase()
|
||||
|
||||
fun removeHost(id: String, deleteDate: Long = System.currentTimeMillis()) {
|
||||
addDeletedData(DeletedData(id, "Host", deleteDate))
|
||||
}
|
||||
|
||||
fun removeKeymap(id: String, deleteDate: Long = System.currentTimeMillis()) {
|
||||
addDeletedData(DeletedData(id, "Keymap", deleteDate))
|
||||
}
|
||||
|
||||
fun removeKeyPair(id: String, deleteDate: Long = System.currentTimeMillis()) {
|
||||
addDeletedData(DeletedData(id, "KeyPair", deleteDate))
|
||||
}
|
||||
|
||||
fun removeKeywordHighlight(id: String, deleteDate: Long = System.currentTimeMillis()) {
|
||||
addDeletedData(DeletedData(id, "KeywordHighlight", deleteDate))
|
||||
}
|
||||
|
||||
fun removeMacro(id: String, deleteDate: Long = System.currentTimeMillis()) {
|
||||
addDeletedData(DeletedData(id, "Macro", deleteDate))
|
||||
}
|
||||
|
||||
fun removeSnippet(id: String, deleteDate: Long = System.currentTimeMillis()) {
|
||||
addDeletedData(DeletedData(id, "Snippet", deleteDate))
|
||||
}
|
||||
|
||||
private fun addDeletedData(deletedData: DeletedData) {
|
||||
if (data.containsKey(deletedData.id)) return
|
||||
data[deletedData.id] = deletedData
|
||||
database.addDeletedData(deletedData)
|
||||
}
|
||||
|
||||
fun getDeletedData(): List<DeletedData> {
|
||||
if (data.isEmpty()) {
|
||||
data.putAll(database.getDeletedData().associateBy { it.id })
|
||||
}
|
||||
return data.values.sortedBy { it.deleteDate }
|
||||
}
|
||||
}
|
||||
@@ -214,6 +214,27 @@ data class EncryptedHost(
|
||||
var updateDate: Long = 0L,
|
||||
)
|
||||
|
||||
/**
|
||||
* 被删除的数据
|
||||
*/
|
||||
@Serializable
|
||||
data class DeletedData(
|
||||
/**
|
||||
* 被删除的 ID
|
||||
*/
|
||||
val id: String = StringUtils.EMPTY,
|
||||
|
||||
/**
|
||||
* 数据类型:Host、Keymap、KeyPair、KeywordHighlight、Macro、Snippet
|
||||
*/
|
||||
val type: String = StringUtils.EMPTY,
|
||||
|
||||
/**
|
||||
* 被删除的时间
|
||||
*/
|
||||
val deleteDate: Long,
|
||||
)
|
||||
|
||||
|
||||
@Serializable
|
||||
data class Host(
|
||||
|
||||
@@ -16,14 +16,20 @@ class HostManager private constructor() {
|
||||
*/
|
||||
fun addHost(host: Host) {
|
||||
assertEventDispatchThread()
|
||||
database.addHost(host)
|
||||
if (host.deleted) {
|
||||
hosts.entries.removeIf { it.value.id == host.id || it.value.parentId == host.id }
|
||||
removeHost(host.id)
|
||||
} else {
|
||||
database.addHost(host)
|
||||
hosts[host.id] = host
|
||||
}
|
||||
}
|
||||
|
||||
fun removeHost(id: String) {
|
||||
hosts.entries.removeIf { it.value.id == id || it.value.parentId == id }
|
||||
database.removeHost(id)
|
||||
DeleteDataManager.getInstance().removeHost(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* 第一次调用从数据库中获取,后续从缓存中获取
|
||||
*/
|
||||
|
||||
@@ -59,11 +59,11 @@ import java.io.File
|
||||
import java.net.URI
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.*
|
||||
import java.util.function.Consumer
|
||||
import javax.swing.*
|
||||
import javax.swing.event.DocumentEvent
|
||||
import javax.swing.event.PopupMenuEvent
|
||||
import javax.swing.event.PopupMenuListener
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
|
||||
class SettingsOptionsPane : OptionsPane() {
|
||||
@@ -79,7 +79,6 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(SettingsOptionsPane::class.java)
|
||||
private val localShells by lazy { loadShells() }
|
||||
var pulled = false
|
||||
|
||||
private fun loadShells(): List<String> {
|
||||
val shells = mutableListOf<String>()
|
||||
@@ -568,10 +567,9 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
val tokenTextField = OutlinePasswordField(255)
|
||||
val gistTextField = OutlineTextField(255)
|
||||
val domainTextField = OutlineTextField(255)
|
||||
val uploadConfigButton = JButton(I18n.getString("termora.settings.sync.push"), Icons.upload)
|
||||
val syncConfigButton = JButton(I18n.getString("termora.settings.sync"), Icons.download)
|
||||
val exportConfigButton = JButton(I18n.getString("termora.settings.sync.export"), Icons.export)
|
||||
val importConfigButton = JButton(I18n.getString("termora.settings.sync.import"), Icons.import)
|
||||
val downloadConfigButton = JButton(I18n.getString("termora.settings.sync.pull"), Icons.download)
|
||||
val lastSyncTimeLabel = JLabel()
|
||||
val sync get() = database.sync
|
||||
val hostsCheckBox = JCheckBox(I18n.getString("termora.welcome.my-hosts"))
|
||||
@@ -591,16 +589,8 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
private fun initEvents() {
|
||||
downloadConfigButton.addActionListener {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
pushOrPull(false)
|
||||
}
|
||||
}
|
||||
|
||||
uploadConfigButton.addActionListener {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
pushOrPull(true)
|
||||
}
|
||||
syncConfigButton.addActionListener {
|
||||
GlobalScope.launch(Dispatchers.IO) { sync() }
|
||||
}
|
||||
|
||||
typeComboBox.addItemListener {
|
||||
@@ -686,17 +676,41 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
|
||||
}
|
||||
|
||||
private suspend fun sync() {
|
||||
if (!pushOrPull(false)) return
|
||||
if (!pushOrPull(true)) return
|
||||
|
||||
withContext(Dispatchers.Swing) {
|
||||
if (hostsCheckBox.isSelected) {
|
||||
for (window in TermoraFrameManager.getInstance().getWindows()) {
|
||||
visit(window.rootPane) {
|
||||
if (it is NewHostTree) it.refreshNode()
|
||||
}
|
||||
}
|
||||
}
|
||||
OptionPane.showMessageDialog(owner, message = I18n.getString("termora.settings.sync.done"))
|
||||
}
|
||||
}
|
||||
|
||||
private fun visit(c: JComponent, consumer: Consumer<JComponent>) {
|
||||
for (e in c.components) {
|
||||
if (e is JComponent) {
|
||||
consumer.accept(e)
|
||||
visit(e, consumer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshButtons() {
|
||||
sync.rangeKeyPairs = keysCheckBox.isSelected
|
||||
sync.rangeHosts = hostsCheckBox.isSelected
|
||||
sync.rangeSnippets = snippetsCheckBox.isSelected
|
||||
sync.rangeKeywordHighlights = keywordHighlightsCheckBox.isSelected
|
||||
|
||||
downloadConfigButton.isEnabled = keysCheckBox.isSelected || hostsCheckBox.isSelected
|
||||
syncConfigButton.isEnabled = keysCheckBox.isSelected || hostsCheckBox.isSelected
|
||||
|| keywordHighlightsCheckBox.isSelected
|
||||
uploadConfigButton.isEnabled = downloadConfigButton.isEnabled
|
||||
exportConfigButton.isEnabled = downloadConfigButton.isEnabled
|
||||
importConfigButton.isEnabled = downloadConfigButton.isEnabled
|
||||
exportConfigButton.isEnabled = syncConfigButton.isEnabled
|
||||
importConfigButton.isEnabled = syncConfigButton.isEnabled
|
||||
}
|
||||
|
||||
private fun export() {
|
||||
@@ -1022,8 +1036,11 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true 同步成功
|
||||
*/
|
||||
@Suppress("DuplicatedCode")
|
||||
private suspend fun pushOrPull(push: Boolean) {
|
||||
private suspend fun pushOrPull(push: Boolean): Boolean {
|
||||
|
||||
if (typeComboBox.selectedItem == SyncType.GitLab) {
|
||||
if (domainTextField.text.isBlank()) {
|
||||
@@ -1031,7 +1048,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
domainTextField.outline = "error"
|
||||
domainTextField.requestFocusInWindow()
|
||||
}
|
||||
return
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1040,7 +1057,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
tokenTextField.outline = "error"
|
||||
tokenTextField.requestFocusInWindow()
|
||||
}
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
if (gistTextField.text.isBlank() && !push) {
|
||||
@@ -1048,39 +1065,13 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
gistTextField.outline = "error"
|
||||
gistTextField.requestFocusInWindow()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 没有拉取过 && 是推送 && gistId 不为空
|
||||
if (!pulled && push && gistTextField.text.isNotBlank()) {
|
||||
val code = withContext(Dispatchers.Swing) {
|
||||
// 提示第一次推送
|
||||
OptionPane.showConfirmDialog(
|
||||
owner,
|
||||
I18n.getString("termora.settings.sync.push-warning"),
|
||||
messageType = JOptionPane.WARNING_MESSAGE,
|
||||
optionType = JOptionPane.YES_NO_CANCEL_OPTION,
|
||||
options = arrayOf(
|
||||
uploadConfigButton.text,
|
||||
downloadConfigButton.text,
|
||||
I18n.getString("termora.cancel")
|
||||
),
|
||||
initialValue = I18n.getString("termora.cancel")
|
||||
)
|
||||
}
|
||||
when (code) {
|
||||
-1, JOptionPane.CANCEL_OPTION -> return
|
||||
JOptionPane.NO_OPTION -> pushOrPull(false) // pull
|
||||
JOptionPane.YES_OPTION -> pulled = true // force push
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Swing) {
|
||||
exportConfigButton.isEnabled = false
|
||||
importConfigButton.isEnabled = false
|
||||
downloadConfigButton.isEnabled = false
|
||||
uploadConfigButton.isEnabled = false
|
||||
syncConfigButton.isEnabled = false
|
||||
typeComboBox.isEnabled = false
|
||||
gistTextField.isEnabled = false
|
||||
tokenTextField.isEnabled = false
|
||||
@@ -1091,12 +1082,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
hostsCheckBox.isEnabled = false
|
||||
snippetsCheckBox.isEnabled = false
|
||||
domainTextField.isEnabled = false
|
||||
|
||||
if (push) {
|
||||
uploadConfigButton.text = "${I18n.getString("termora.settings.sync.push")}..."
|
||||
} else {
|
||||
downloadConfigButton.text = "${I18n.getString("termora.settings.sync.pull")}..."
|
||||
}
|
||||
syncConfigButton.text = "${I18n.getString("termora.settings.sync")}..."
|
||||
}
|
||||
|
||||
val syncConfig = getSyncConfig()
|
||||
@@ -1113,10 +1099,9 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
|
||||
// 恢复状态
|
||||
withContext(Dispatchers.Swing) {
|
||||
downloadConfigButton.isEnabled = true
|
||||
syncConfigButton.isEnabled = true
|
||||
exportConfigButton.isEnabled = true
|
||||
importConfigButton.isEnabled = true
|
||||
uploadConfigButton.isEnabled = true
|
||||
keysCheckBox.isEnabled = true
|
||||
hostsCheckBox.isEnabled = true
|
||||
snippetsCheckBox.isEnabled = true
|
||||
@@ -1127,11 +1112,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
tokenTextField.isEnabled = true
|
||||
domainTextField.isEnabled = true
|
||||
keywordHighlightsCheckBox.isEnabled = true
|
||||
if (push) {
|
||||
uploadConfigButton.text = I18n.getString("termora.settings.sync.push")
|
||||
} else {
|
||||
downloadConfigButton.text = I18n.getString("termora.settings.sync.pull")
|
||||
}
|
||||
syncConfigButton.text = I18n.getString("termora.settings.sync")
|
||||
}
|
||||
|
||||
// 如果失败,提示错误
|
||||
@@ -1151,10 +1132,8 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
withContext(Dispatchers.Swing) {
|
||||
OptionPane.showMessageDialog(owner, message, messageType = JOptionPane.ERROR_MESSAGE)
|
||||
}
|
||||
} else {
|
||||
// pulled
|
||||
if (!pulled) pulled = !push
|
||||
|
||||
} else {
|
||||
withContext(Dispatchers.Swing) {
|
||||
val now = System.currentTimeMillis()
|
||||
sync.lastSyncTime = now
|
||||
@@ -1163,14 +1142,10 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
if (push && gistTextField.text.isBlank()) {
|
||||
gistTextField.text = syncResult.map { it.config }.getOrDefault(syncConfig).gistId
|
||||
}
|
||||
OptionPane.showMessageDialog(
|
||||
owner,
|
||||
message = I18n.getString("termora.settings.sync.done"),
|
||||
duration = 1500.milliseconds,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return syncResult.isSuccess
|
||||
|
||||
}
|
||||
|
||||
@@ -1327,11 +1302,10 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
// Sync buttons
|
||||
.add(
|
||||
FormBuilder.create()
|
||||
.layout(FormLayout("pref, 2dlu, pref, 2dlu, pref, 2dlu, pref", "pref"))
|
||||
.add(uploadConfigButton).xy(1, 1)
|
||||
.add(downloadConfigButton).xy(3, 1)
|
||||
.add(exportConfigButton).xy(5, 1)
|
||||
.add(importConfigButton).xy(7, 1)
|
||||
.layout(FormLayout("pref, 2dlu, pref, 2dlu, pref", "pref"))
|
||||
.add(syncConfigButton).xy(1, 1)
|
||||
.add(exportConfigButton).xy(3, 1)
|
||||
.add(importConfigButton).xy(5, 1)
|
||||
.build()
|
||||
).xy(3, rows, "center, fill").apply { rows += step }
|
||||
.add(lastSyncTimeLabel).xy(3, rows, "center, fill").apply { rows += step }
|
||||
|
||||
@@ -197,6 +197,7 @@ open class SimpleTree : JXTree() {
|
||||
}
|
||||
|
||||
override fun importData(support: TransferSupport): Boolean {
|
||||
if (!support.isDrop) return false
|
||||
val dropLocation = support.dropLocation as? JTree.DropLocation ?: return false
|
||||
val node = dropLocation.path.lastPathComponent as? SimpleTreeNode<*> ?: return false
|
||||
val nodes = (support.transferable.getTransferData(MoveNodeTransferable.dataFlavor) as? List<*>)
|
||||
@@ -277,7 +278,7 @@ open class SimpleTree : JXTree() {
|
||||
|
||||
protected open fun onRenamed(node: SimpleTreeNode<*>, text: String) {}
|
||||
|
||||
protected open fun refreshNode(node: SimpleTreeNode<*>) {
|
||||
open fun refreshNode(node: SimpleTreeNode<*> = model.root) {
|
||||
val state = TreeUtils.saveExpansionState(tree)
|
||||
val rows = selectionRows
|
||||
|
||||
|
||||
@@ -62,5 +62,10 @@ data class KeywordHighlight(
|
||||
/**
|
||||
* 排序
|
||||
*/
|
||||
val sort: Long = System.currentTimeMillis()
|
||||
val sort: Long = System.currentTimeMillis(),
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
val updateDate: Long = System.currentTimeMillis(),
|
||||
)
|
||||
@@ -2,6 +2,7 @@ package app.termora.highlight
|
||||
|
||||
import app.termora.ApplicationScope
|
||||
import app.termora.Database
|
||||
import app.termora.DeleteDataManager
|
||||
import app.termora.TerminalPanelFactory
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
@@ -38,6 +39,7 @@ class KeywordHighlightManager private constructor() {
|
||||
database.removeKeywordHighlight(id)
|
||||
keywordHighlights.remove(id)
|
||||
TerminalPanelFactory.getInstance().repaintAll()
|
||||
DeleteDataManager.getInstance().removeKeywordHighlight(id)
|
||||
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Keyword highlighter removed. {}", id)
|
||||
|
||||
@@ -89,6 +89,7 @@ class KeymapManager private constructor() : Disposable {
|
||||
fun removeKeymap(name: String) {
|
||||
keymaps.remove(name)
|
||||
database.removeKeymap(name)
|
||||
DeleteDataManager.getInstance().removeKeymap(name)
|
||||
}
|
||||
|
||||
private inner class KeymapKeyEventDispatcher : KeyEventDispatcher {
|
||||
|
||||
@@ -2,6 +2,7 @@ package app.termora.keymgr
|
||||
|
||||
import app.termora.ApplicationScope
|
||||
import app.termora.Database
|
||||
import app.termora.DeleteDataManager
|
||||
|
||||
class KeyManager private constructor() {
|
||||
companion object {
|
||||
@@ -29,6 +30,7 @@ class KeyManager private constructor() {
|
||||
fun removeOhKeyPair(id: String) {
|
||||
keyPairs.removeIf { it.id == id }
|
||||
database.removeKeyPair(id)
|
||||
DeleteDataManager.getInstance().removeKeyPair(id)
|
||||
}
|
||||
|
||||
fun getOhKeyPairs(): List<OhKeyPair> {
|
||||
@@ -39,9 +41,4 @@ class KeyManager private constructor() {
|
||||
return keyPairs.findLast { it.id == id }
|
||||
}
|
||||
|
||||
fun removeAll() {
|
||||
keyPairs.clear()
|
||||
database.removeAllKeyPair()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,6 +15,7 @@ data class OhKeyPair(
|
||||
val remark: String,
|
||||
val length: Int,
|
||||
val sort: Long,
|
||||
val updateDate: Long = System.currentTimeMillis(),
|
||||
) {
|
||||
companion object {
|
||||
val empty = OhKeyPair(String(), String(), String(), String(), String(), String(), 0, 0)
|
||||
|
||||
@@ -19,6 +19,11 @@ data class Macro(
|
||||
* 越大越靠前
|
||||
*/
|
||||
val sort: Long = System.currentTimeMillis(),
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
val updateDate: Long = System.currentTimeMillis(),
|
||||
) {
|
||||
val macroByteArray by lazy { macro.decodeBase64() }
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package app.termora.macro
|
||||
|
||||
import app.termora.ApplicationScope
|
||||
import app.termora.Database
|
||||
import app.termora.DeleteDataManager
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
/**
|
||||
@@ -38,6 +39,7 @@ class MacroManager private constructor() {
|
||||
fun removeMacro(id: String) {
|
||||
database.removeMacro(id)
|
||||
macros.remove(id)
|
||||
DeleteDataManager.getInstance().removeMacro(id)
|
||||
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Removed macro $id")
|
||||
|
||||
@@ -2,6 +2,7 @@ package app.termora.snippet
|
||||
|
||||
import app.termora.ApplicationScope
|
||||
import app.termora.Database
|
||||
import app.termora.DeleteDataManager
|
||||
import app.termora.assertEventDispatchThread
|
||||
|
||||
|
||||
@@ -20,14 +21,20 @@ class SnippetManager private constructor() {
|
||||
*/
|
||||
fun addSnippet(snippet: Snippet) {
|
||||
assertEventDispatchThread()
|
||||
database.addSnippet(snippet)
|
||||
if (snippet.deleted) {
|
||||
snippets.entries.removeIf { it.value.id == snippet.id || it.value.parentId == snippet.id }
|
||||
removeSnippet(snippet.id)
|
||||
} else {
|
||||
database.addSnippet(snippet)
|
||||
snippets[snippet.id] = snippet
|
||||
}
|
||||
}
|
||||
|
||||
fun removeSnippet(id: String) {
|
||||
snippets.entries.removeIf { it.value.id == id || it.value.parentId == id }
|
||||
database.removeSnippet(id)
|
||||
DeleteDataManager.getInstance().removeSnippet(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* 第一次调用从数据库中获取,后续从缓存中获取
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package app.termora.sync
|
||||
|
||||
import app.termora.Application.ohMyJson
|
||||
import app.termora.DeletedData
|
||||
import app.termora.ResponseException
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
@@ -26,46 +27,51 @@ abstract class GitSyncer : SafetySyncer() {
|
||||
}
|
||||
|
||||
val gistResponse = parsePullResponse(response, config)
|
||||
val deletedData = mutableListOf<DeletedData>()
|
||||
|
||||
// DeletedData
|
||||
gistResponse.gists.findLast { it.filename == "DeletedData" }
|
||||
?.let { deletedData.addAll(decodeDeletedData(it.content, config)) }
|
||||
|
||||
// decode hosts
|
||||
if (config.ranges.contains(SyncRange.Hosts)) {
|
||||
gistResponse.gists.findLast { it.filename == "Hosts" }?.let {
|
||||
decodeHosts(it.content, config)
|
||||
decodeHosts(it.content, deletedData.filter { e -> e.type == "Host" }, config)
|
||||
}
|
||||
}
|
||||
|
||||
// decode keys
|
||||
if (config.ranges.contains(SyncRange.KeyPairs)) {
|
||||
gistResponse.gists.findLast { it.filename == "KeyPairs" }?.let {
|
||||
decodeKeys(it.content, config)
|
||||
decodeKeys(it.content, deletedData.filter { e -> e.type == "KeyPair" }, config)
|
||||
}
|
||||
}
|
||||
|
||||
// decode keyword highlights
|
||||
if (config.ranges.contains(SyncRange.KeywordHighlights)) {
|
||||
gistResponse.gists.findLast { it.filename == "KeywordHighlights" }?.let {
|
||||
decodeKeywordHighlights(it.content, config)
|
||||
decodeKeywordHighlights(it.content, deletedData.filter { e -> e.type == "KeywordHighlight" }, config)
|
||||
}
|
||||
}
|
||||
|
||||
// decode macros
|
||||
if (config.ranges.contains(SyncRange.Macros)) {
|
||||
gistResponse.gists.findLast { it.filename == "Macros" }?.let {
|
||||
decodeMacros(it.content, config)
|
||||
decodeMacros(it.content, deletedData.filter { e -> e.type == "Macro" }, config)
|
||||
}
|
||||
}
|
||||
|
||||
// decode keymaps
|
||||
if (config.ranges.contains(SyncRange.Macros)) {
|
||||
gistResponse.gists.findLast { it.filename == "Keymaps" }?.let {
|
||||
decodeKeymaps(it.content, config)
|
||||
decodeKeymaps(it.content, deletedData.filter { e -> e.type == "Keymap" }, config)
|
||||
}
|
||||
}
|
||||
|
||||
// decode Snippets
|
||||
if (config.ranges.contains(SyncRange.Snippets)) {
|
||||
gistResponse.gists.findLast { it.filename == "Snippets" }?.let {
|
||||
decodeSnippets(it.content, config)
|
||||
decodeSnippets(it.content, deletedData.filter { e -> e.type == "Snippet" }, config)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +85,11 @@ abstract class GitSyncer : SafetySyncer() {
|
||||
|
||||
|
||||
override fun push(config: SyncConfig): GistResponse {
|
||||
|
||||
if (log.isInfoEnabled) {
|
||||
log.info("Type: ${config.type} , Gist: ${config.gistId} Push...")
|
||||
}
|
||||
|
||||
val gistFiles = mutableListOf<GistFile>()
|
||||
// aes key
|
||||
val key = ArrayUtils.subarray(config.token.padEnd(16, '0').toByteArray(), 0, 16)
|
||||
@@ -142,9 +153,21 @@ abstract class GitSyncer : SafetySyncer() {
|
||||
throw IllegalArgumentException("No gist files found")
|
||||
}
|
||||
|
||||
val deletedData = encodeDeletedData(config)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Push DeletedData: {}", deletedData)
|
||||
}
|
||||
gistFiles.add(GistFile("DeletedData", deletedData))
|
||||
|
||||
val request = newPushRequestBuilder(gistFiles, config).build()
|
||||
|
||||
return parsePushResponse(httpClient.newCall(request).execute(), config)
|
||||
try {
|
||||
return parsePushResponse(httpClient.newCall(request).execute(), config)
|
||||
} finally {
|
||||
if (log.isInfoEnabled) {
|
||||
log.info("Type: ${config.type} , Gist: ${config.gistId} Pushed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open fun parsePullResponse(response: Response, config: SyncConfig): GistResponse {
|
||||
|
||||
@@ -34,8 +34,9 @@ abstract class SafetySyncer : Syncer {
|
||||
protected val macroManager get() = MacroManager.getInstance()
|
||||
protected val keymapManager get() = KeymapManager.getInstance()
|
||||
protected val snippetManager get() = SnippetManager.getInstance()
|
||||
protected val deleteDataManager get() = DeleteDataManager.getInstance()
|
||||
|
||||
protected fun decodeHosts(text: String, config: SyncConfig) {
|
||||
protected fun decodeHosts(text: String, deletedData: List<DeletedData>, config: SyncConfig) {
|
||||
// aes key
|
||||
val key = getKey(config)
|
||||
val encryptedHosts = ohMyJson.decodeFromString<List<EncryptedHost>>(text)
|
||||
@@ -44,9 +45,9 @@ abstract class SafetySyncer : Syncer {
|
||||
for (encryptedHost in encryptedHosts) {
|
||||
val oldHost = hosts[encryptedHost.id]
|
||||
|
||||
// 如果一样,则无需配置
|
||||
// 如果本地的修改时间大于云端时间,那么跳过
|
||||
if (oldHost != null) {
|
||||
if (oldHost.updateDate == encryptedHost.updateDate) {
|
||||
if (oldHost.updateDate >= encryptedHost.updateDate) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -83,7 +84,6 @@ abstract class SafetySyncer : Syncer {
|
||||
creatorId = encryptedHost.creatorId.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
||||
createDate = encryptedHost.createDate,
|
||||
updateDate = encryptedHost.updateDate,
|
||||
deleted = encryptedHost.deleted
|
||||
)
|
||||
SwingUtilities.invokeLater { hostManager.addHost(host) }
|
||||
} catch (e: Exception) {
|
||||
@@ -93,6 +93,12 @@ abstract class SafetySyncer : Syncer {
|
||||
}
|
||||
}
|
||||
|
||||
SwingUtilities.invokeLater {
|
||||
deletedData.forEach {
|
||||
hostManager.removeHost(it.id)
|
||||
deleteDataManager.removeHost(it.id, it.deleteDate)
|
||||
}
|
||||
}
|
||||
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Decode hosts: {}", text)
|
||||
@@ -120,7 +126,6 @@ abstract class SafetySyncer : Syncer {
|
||||
encryptedHost.tunnelings =
|
||||
ohMyJson.encodeToString(host.tunnelings).aesCBCEncrypt(key, iv).encodeBase64String()
|
||||
encryptedHost.sort = host.sort
|
||||
encryptedHost.deleted = host.deleted
|
||||
encryptedHost.parentId = host.parentId.aesCBCEncrypt(key, iv).encodeBase64String()
|
||||
encryptedHost.ownerId = host.ownerId.aesCBCEncrypt(key, iv).encodeBase64String()
|
||||
encryptedHost.creatorId = host.creatorId.aesCBCEncrypt(key, iv).encodeBase64String()
|
||||
@@ -133,7 +138,18 @@ abstract class SafetySyncer : Syncer {
|
||||
|
||||
}
|
||||
|
||||
protected fun decodeSnippets(text: String, config: SyncConfig) {
|
||||
protected fun encodeDeletedData(config: SyncConfig): String {
|
||||
return ohMyJson.encodeToString(deleteDataManager.getDeletedData())
|
||||
}
|
||||
|
||||
protected fun decodeDeletedData(text: String, config: SyncConfig): List<DeletedData> {
|
||||
val deletedData = ohMyJson.decodeFromString<List<DeletedData>>(text).toMutableList()
|
||||
// 和本地融合
|
||||
deletedData.addAll(deleteDataManager.getDeletedData())
|
||||
return deletedData
|
||||
}
|
||||
|
||||
protected fun decodeSnippets(text: String, deletedData: List<DeletedData>, config: SyncConfig) {
|
||||
// aes key
|
||||
val key = getKey(config)
|
||||
val encryptedSnippets = ohMyJson.decodeFromString<List<Snippet>>(text)
|
||||
@@ -144,7 +160,7 @@ abstract class SafetySyncer : Syncer {
|
||||
|
||||
// 如果一样,则无需配置
|
||||
if (oldHost != null) {
|
||||
if (oldHost.updateDate == encryptedSnippet.updateDate) {
|
||||
if (oldHost.updateDate >= encryptedSnippet.updateDate) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -165,9 +181,15 @@ abstract class SafetySyncer : Syncer {
|
||||
}
|
||||
}
|
||||
|
||||
SwingUtilities.invokeLater {
|
||||
deletedData.forEach {
|
||||
snippetManager.removeSnippet(it.id)
|
||||
deleteDataManager.removeSnippet(it.id, it.deleteDate)
|
||||
}
|
||||
}
|
||||
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Decode hosts: {}", text)
|
||||
log.debug("Decode Snippets: {}", text)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,12 +210,20 @@ abstract class SafetySyncer : Syncer {
|
||||
|
||||
}
|
||||
|
||||
protected fun decodeKeys(text: String, config: SyncConfig) {
|
||||
protected fun decodeKeys(text: String, deletedData: List<DeletedData>, config: SyncConfig) {
|
||||
// aes key
|
||||
val key = getKey(config)
|
||||
val encryptedKeys = ohMyJson.decodeFromString<List<OhKeyPair>>(text)
|
||||
val keys = keyManager.getOhKeyPairs().associateBy { it.id }
|
||||
|
||||
for (encryptedKey in encryptedKeys) {
|
||||
val k = keys[encryptedKey.id]
|
||||
if (k != null) {
|
||||
if (k.updateDate > encryptedKey.updateDate) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// aes iv
|
||||
val iv = getIv(encryptedKey.id)
|
||||
@@ -215,6 +245,13 @@ abstract class SafetySyncer : Syncer {
|
||||
}
|
||||
}
|
||||
|
||||
SwingUtilities.invokeLater {
|
||||
deletedData.forEach {
|
||||
keyManager.removeOhKeyPair(it.id)
|
||||
deleteDataManager.removeKeyPair(it.id, it.deleteDate)
|
||||
}
|
||||
}
|
||||
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Decode keys: {}", text)
|
||||
}
|
||||
@@ -240,12 +277,20 @@ abstract class SafetySyncer : Syncer {
|
||||
return ohMyJson.encodeToString(encryptedKeys)
|
||||
}
|
||||
|
||||
protected fun decodeKeywordHighlights(text: String, config: SyncConfig) {
|
||||
protected fun decodeKeywordHighlights(text: String, deletedData: List<DeletedData>, config: SyncConfig) {
|
||||
// aes key
|
||||
val key = getKey(config)
|
||||
val encryptedKeywordHighlights = ohMyJson.decodeFromString<List<KeywordHighlight>>(text)
|
||||
val keywordHighlights = keywordHighlightManager.getKeywordHighlights().associateBy { it.id }
|
||||
|
||||
for (e in encryptedKeywordHighlights) {
|
||||
val keywordHighlight = keywordHighlights[e.id]
|
||||
if (keywordHighlight != null) {
|
||||
if (keywordHighlight.updateDate >= e.updateDate) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// aes iv
|
||||
val iv = getIv(e.id)
|
||||
@@ -262,6 +307,13 @@ abstract class SafetySyncer : Syncer {
|
||||
}
|
||||
}
|
||||
|
||||
SwingUtilities.invokeLater {
|
||||
deletedData.forEach {
|
||||
keywordHighlightManager.removeKeywordHighlight(it.id)
|
||||
deleteDataManager.removeKeywordHighlight(it.id, it.deleteDate)
|
||||
}
|
||||
}
|
||||
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Decode KeywordHighlight: {}", text)
|
||||
}
|
||||
@@ -281,12 +333,19 @@ abstract class SafetySyncer : Syncer {
|
||||
return ohMyJson.encodeToString(keywordHighlights)
|
||||
}
|
||||
|
||||
protected fun decodeMacros(text: String, config: SyncConfig) {
|
||||
protected fun decodeMacros(text: String, deletedData: List<DeletedData>, config: SyncConfig) {
|
||||
// aes key
|
||||
val key = getKey(config)
|
||||
val encryptedMacros = ohMyJson.decodeFromString<List<Macro>>(text)
|
||||
|
||||
val macros = macroManager.getMacros().associateBy { it.id }
|
||||
for (e in encryptedMacros) {
|
||||
val macro = macros[e.id]
|
||||
if (macro != null) {
|
||||
if (macro.updateDate >= e.updateDate) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// aes iv
|
||||
val iv = getIv(e.id)
|
||||
@@ -303,6 +362,13 @@ abstract class SafetySyncer : Syncer {
|
||||
}
|
||||
}
|
||||
|
||||
SwingUtilities.invokeLater {
|
||||
deletedData.forEach {
|
||||
macroManager.removeMacro(it.id)
|
||||
deleteDataManager.removeMacro(it.id, it.deleteDate)
|
||||
}
|
||||
}
|
||||
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Decode Macros: {}", text)
|
||||
}
|
||||
@@ -322,12 +388,19 @@ abstract class SafetySyncer : Syncer {
|
||||
return ohMyJson.encodeToString(macros)
|
||||
}
|
||||
|
||||
protected fun decodeKeymaps(text: String, config: SyncConfig) {
|
||||
protected fun decodeKeymaps(text: String, deletedData: List<DeletedData>, config: SyncConfig) {
|
||||
|
||||
for (keymap in ohMyJson.decodeFromString<List<JsonObject>>(text).mapNotNull { Keymap.fromJSON(it) }) {
|
||||
keymapManager.addKeymap(keymap)
|
||||
}
|
||||
|
||||
SwingUtilities.invokeLater {
|
||||
deletedData.forEach {
|
||||
keymapManager.removeKeymap(it.id)
|
||||
deleteDataManager.removeKeymap(it.id, it.deleteDate)
|
||||
}
|
||||
}
|
||||
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Decode Keymaps: {}", text)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package app.termora.sync
|
||||
|
||||
import app.termora.Application.ohMyJson
|
||||
import app.termora.ApplicationScope
|
||||
import app.termora.DeletedData
|
||||
import app.termora.PBKDF2
|
||||
import app.termora.ResponseException
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
@@ -27,6 +28,9 @@ class WebDAVSyncer private constructor() : SafetySyncer() {
|
||||
override fun pull(config: SyncConfig): GistResponse {
|
||||
val response = httpClient.newCall(newRequestBuilder(config).get().build()).execute()
|
||||
if (!response.isSuccessful) {
|
||||
if (response.code == 404) {
|
||||
return GistResponse(config, emptyList())
|
||||
}
|
||||
throw ResponseException(response.code, response)
|
||||
}
|
||||
|
||||
@@ -34,46 +38,48 @@ class WebDAVSyncer private constructor() : SafetySyncer() {
|
||||
?: throw ResponseException(response.code, response)
|
||||
|
||||
val json = ohMyJson.decodeFromString<JsonObject>(text)
|
||||
val deletedData = mutableListOf<DeletedData>()
|
||||
json["DeletedData"]?.jsonPrimitive?.content?.let { deletedData.addAll(decodeDeletedData(it, config)) }
|
||||
|
||||
// decode hosts
|
||||
if (config.ranges.contains(SyncRange.Hosts)) {
|
||||
json["Hosts"]?.jsonPrimitive?.content?.let {
|
||||
decodeHosts(it, config)
|
||||
decodeHosts(it, deletedData.filter { e -> e.type == "Host" }, config)
|
||||
}
|
||||
}
|
||||
|
||||
// decode KeyPairs
|
||||
if (config.ranges.contains(SyncRange.KeyPairs)) {
|
||||
json["KeyPairs"]?.jsonPrimitive?.content?.let {
|
||||
decodeKeys(it, config)
|
||||
decodeKeys(it, deletedData.filter { e -> e.type == "KeyPair" }, config)
|
||||
}
|
||||
}
|
||||
|
||||
// decode Highlights
|
||||
if (config.ranges.contains(SyncRange.KeywordHighlights)) {
|
||||
json["KeywordHighlights"]?.jsonPrimitive?.content?.let {
|
||||
decodeKeywordHighlights(it, config)
|
||||
decodeKeywordHighlights(it, deletedData.filter { e -> e.type == "KeywordHighlight" }, config)
|
||||
}
|
||||
}
|
||||
|
||||
// decode Macros
|
||||
if (config.ranges.contains(SyncRange.Macros)) {
|
||||
json["Macros"]?.jsonPrimitive?.content?.let {
|
||||
decodeMacros(it, config)
|
||||
decodeMacros(it, deletedData.filter { e -> e.type == "Macro" }, config)
|
||||
}
|
||||
}
|
||||
|
||||
// decode Keymaps
|
||||
if (config.ranges.contains(SyncRange.Keymap)) {
|
||||
json["Keymaps"]?.jsonPrimitive?.content?.let {
|
||||
decodeKeymaps(it, config)
|
||||
decodeKeymaps(it, deletedData.filter { e -> e.type == "Keymap" }, config)
|
||||
}
|
||||
}
|
||||
|
||||
// decode Snippets
|
||||
if (config.ranges.contains(SyncRange.Snippets)) {
|
||||
json["Snippets"]?.jsonPrimitive?.content?.let {
|
||||
decodeSnippets(it, config)
|
||||
decodeSnippets(it, deletedData.filter { e -> e.type == "Snippet" }, config)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,6 +143,13 @@ class WebDAVSyncer private constructor() : SafetySyncer() {
|
||||
}
|
||||
put("Keymaps", keymapsContent)
|
||||
}
|
||||
|
||||
// deletedData
|
||||
val deletedData = encodeDeletedData(config)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Push DeletedData: {}", deletedData)
|
||||
}
|
||||
put("DeletedData", deletedData)
|
||||
}
|
||||
|
||||
val response = httpClient.newCall(
|
||||
|
||||
@@ -76,9 +76,6 @@ termora.settings.terminal.auto-close-tab=Auto Close Tab
|
||||
termora.settings.terminal.auto-close-tab-description=Automatically close the tab when the terminal is disconnected normally
|
||||
|
||||
termora.settings.sync=Sync
|
||||
termora.settings.sync.push=Push
|
||||
termora.settings.sync.push-warning=Pushing will overwrite the configuration. It is recommended to pull before pushing
|
||||
termora.settings.sync.pull=Pull
|
||||
termora.settings.sync.done=Synchronized data successfully
|
||||
termora.settings.sync.export=${termora.keymgr.export}
|
||||
termora.settings.sync.import=${termora.keymgr.import}
|
||||
|
||||
@@ -82,9 +82,6 @@ termora.settings.terminal.auto-close-tab-description=当终端正常断开连接
|
||||
|
||||
|
||||
termora.settings.sync=同步
|
||||
termora.settings.sync.push=推送
|
||||
termora.settings.sync.push-warning=推送将覆盖已有配置,建议先拉取再推送
|
||||
termora.settings.sync.pull=拉取
|
||||
termora.settings.sync.export-done=导出成功
|
||||
termora.settings.sync.export-encrypt=输入密码加密文件 (可选)
|
||||
termora.settings.sync.export-done-open-folder=导出成功,是否需要打开所在文件夹?
|
||||
|
||||
@@ -93,9 +93,6 @@ termora.settings.terminal.auto-close-tab=自動關閉標籤
|
||||
termora.settings.terminal.auto-close-tab-description=當終端正常斷開連線時自動關閉標籤頁
|
||||
|
||||
termora.settings.sync=同步
|
||||
termora.settings.sync.push=推送
|
||||
termora.settings.sync.push-warning=推送將覆蓋先前的配置,建議先拉取再推送
|
||||
termora.settings.sync.pull=拉取
|
||||
termora.settings.sync.export-done=匯出成功
|
||||
termora.settings.sync.export-encrypt=輸入密碼加密檔案 (可選)
|
||||
termora.settings.sync.export-done-open-folder=匯出成功,是否需要打開所在資料夾?
|
||||
|
||||
Reference in New Issue
Block a user